Jobmaker-LdPost/shared/state/state.go
Ricardo Carneiro ea532659b0 feat: pipeline inicial ldpost-squad (6 agentes)
Pipeline completo de publicação no LinkedIn:
evaluator → redator → editor → art → director → publisher

- Seed com 37 posts em _sugestoes.md
- Sorteio de formato com N=3 bloqueados (format-history)
- Reciclagem mensal de posts com rotação de formato
- Revisão via Telegram com chat livre (Gemini 2.5 Flash)
- Publicação via LinkedIn API (OAuth2)
- Makefile com targets para Windows/Linux/ARM64

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:55:39 -03:00

170 lines
5.5 KiB
Go

package state
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// ─── Status constants ─────────────────────────────────────────────────────────
const (
StatusWaitingEvaluator = "waiting_evaluator"
StatusWaitingRedator = "waiting_redator"
StatusWaitingEditor = "waiting_editor"
StatusWaitingArt = "waiting_art"
StatusWaitingDirector = "waiting_director"
StatusWaitingPublisher = "waiting_publisher"
StatusPublished = "published"
StatusRejected = "rejected"
EtapaDone = "done"
EtapaPending = "pending"
EtapaWaiting = "waiting"
)
// ─── Schema ───────────────────────────────────────────────────────────────────
type Etapas struct {
Evaluator string `json:"evaluator"`
Redator string `json:"redator"`
Editor string `json:"editor"`
Art string `json:"art"`
Director string `json:"director"`
Publisher string `json:"publisher"`
}
type Aprovacao struct {
Aprovado bool `json:"aprovado"`
Timestamp time.Time `json:"timestamp,omitempty"`
Ciclos int `json:"ciclos,omitempty"`
}
type Aprovacoes struct {
Tema Aprovacao `json:"tema"`
Texto Aprovacao `json:"texto"`
Imagens Aprovacao `json:"imagens"`
Final Aprovacao `json:"final,omitempty"`
}
type PostState struct {
Slug string `json:"slug"`
Categoria string `json:"categoria"`
Status string `json:"status"`
Formato string `json:"formato"`
TemaEscolhido string `json:"tema_escolhido"`
TrendReferencia string `json:"trend_referencia"`
FunilTag string `json:"funil_tag,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Etapas Etapas `json:"etapas"`
Aprovacoes Aprovacoes `json:"aprovacoes"`
PollingActive bool `json:"polling_active,omitempty"`
DirectorMessageID int `json:"director_message_id,omitempty"`
LinkedInPostID string `json:"linkedin_post_id,omitempty"`
PublishedAt *time.Time `json:"published_at,omitempty"`
}
// ─── I/O ──────────────────────────────────────────────────────────────────────
// LoadState reads work/state.json relative to postPath.
// postPath is the post root: <workspace>/<categoria>/<slug>
func LoadState(postPath string) (*PostState, error) {
path := stateFilePath(postPath)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("ler state.json (%s): %w", path, err)
}
var s PostState
if err := json.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("parsear state.json: %w", err)
}
return &s, nil
}
// SaveState writes work/state.json atomically (tmp + rename).
// Updates UpdatedAt automatically.
func SaveState(postPath string, s *PostState) error {
s.UpdatedAt = time.Now()
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return fmt.Errorf("serializar state: %w", err)
}
workDir := filepath.Join(postPath, "work")
if err := os.MkdirAll(workDir, 0755); err != nil {
return fmt.Errorf("criar work/: %w", err)
}
path := stateFilePath(postPath)
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil {
return fmt.Errorf("escrever tmp: %w", err)
}
if err := os.Rename(tmp, path); err != nil {
os.Remove(tmp)
return fmt.Errorf("rename tmp→state.json: %w", err)
}
return nil
}
// ─── Methods ──────────────────────────────────────────────────────────────────
// SetEtapa updates a specific agent's step status and bumps UpdatedAt.
func (s *PostState) SetEtapa(agente, status string) {
s.UpdatedAt = time.Now()
switch agente {
case "evaluator":
s.Etapas.Evaluator = status
case "redator":
s.Etapas.Redator = status
case "editor":
s.Etapas.Editor = status
case "art":
s.Etapas.Art = status
case "director":
s.Etapas.Director = status
case "publisher":
s.Etapas.Publisher = status
}
}
// IsStatus reports whether the current Status matches expected.
func (s *PostState) IsStatus(expected string) bool {
return s.Status == expected
}
// ─── Constructor ──────────────────────────────────────────────────────────────
func NewPostState(slug, categoria, formato, tema, trend string) *PostState {
now := time.Now()
return &PostState{
Slug: slug,
Categoria: categoria,
Status: StatusWaitingEvaluator,
Formato: formato,
TemaEscolhido: tema,
TrendReferencia: trend,
CreatedAt: now,
UpdatedAt: now,
Etapas: Etapas{
Evaluator: EtapaPending,
Redator: EtapaPending,
Editor: EtapaPending,
Art: EtapaPending,
Director: EtapaPending,
Publisher: EtapaPending,
},
}
}
// ─── Internal ─────────────────────────────────────────────────────────────────
func stateFilePath(postPath string) string {
return filepath.Join(postPath, "work", "state.json")
}