package cockpit import ( "errors" "fmt" "os" "strings" "time" "github.com/ajhahnde/eeco/internal/config" ) // GenerateAll emits an aggregate target's single shared artifact for the whole // playbook set, reversibly. The uniform safety gate runs on EVERY playbook in // the set first: a forbidden write-git verb in any one refuses the whole emit // (nothing written, no ledger churn), naming the offending playbook — advisory // targets do not relax the invariant. The artifact dir is cfg.UserDir (never // pruned), so off can never remove the private tree. Re-emitting unchanged // bytes is a byte-idempotent no-op. func GenerateAll(cfg *config.Config, set []Playbook, target string) (GenerateResult, error) { r, ok := rendererFor(target) if !ok { return GenerateResult{}, unknownTargetErr(target) } agg, ok := isAggregate(r) if !ok { return GenerateResult{}, fmt.Errorf("target %q is per-playbook; use Generate", target) } for _, pb := range set { if hits := ScanAllowlistForWriteGitVerbs(composeAllowedTools(pb), pb.Intent.forbiddenVerbs()); len(hits) > 0 { return GenerateResult{}, fmt.Errorf( "refusing to emit %s: playbook %q has forbidden write-git verb(s) in allowlist: %s", target, pb.Name, strings.Join(hits, ", ")) } } content, err := agg.RenderAll(set) if err != nil { return GenerateResult{}, err } dst, err := userArtifactPath(cfg, agg.AggRelPath()) if err != nil { return GenerateResult{}, err } newSHA := sha256hex(content) l, err := loadLedger(cfg) if err != nil { return GenerateResult{}, err } priorIdx := l.findAgg(target) hasPrior := priorIdx >= 0 && l.Records[priorIdx].Installed var prior record if priorIdx >= 0 { prior = l.Records[priorIdx] } if hasPrior && prior.SHA256 == newSHA { if onDisk, rerr := os.ReadFile(dst); rerr == nil && sha256hex(onDisk) == newSHA { return GenerateResult{Path: dst, Action: "already current", Fidelity: fidelityOf(r)}, nil } } backup := prior.Backup if !hasPrior { // The aggregate dir is cfg.UserDir, which always exists and is never // pruned, so only a pre-existing foreign file needs backing up. if existing, rerr := os.ReadFile(dst); rerr == nil { bp, berr := backupExisting(cfg, target, "_all", existing) if berr != nil { return GenerateResult{}, berr } backup = bp } else if !errors.Is(rerr, os.ErrNotExist) { return GenerateResult{}, fmt.Errorf("inspect %s: %w", dst, rerr) } } if err := writeFileAtomic(dst, content, 0o644); err != nil { return GenerateResult{}, err } l.upsertAgg(record{ Installed: true, Target: target, Playbook: "", // aggregate: keyed on target alone Path: dst, SHA256: newSHA, Backup: backup, Created: false, // dir is cfg.UserDir — never created, never pruned At: time.Now().UTC().Format(time.RFC3339), }) if err := saveLedger(cfg, l); err != nil { return GenerateResult{}, err } action := "generated" switch { case hasPrior: action = "regenerated" case backup != "": action = "updated" } return GenerateResult{Path: dst, Action: action, Backup: backup, Fidelity: fidelityOf(r)}, nil } // VerifyAll checks an aggregate target's on-disk artifact against the bytes // freshly rendered for set, then runs the self-consistency safety check on the // on-disk bytes (S4) — the advisory analog of the enforced allowlist scan, // since these targets have no answer key. It never mutates anything. func VerifyAll(cfg *config.Config, set []Playbook, target string) (VerifyResult, error) { r, ok := rendererFor(target) if !ok { return VerifyResult{}, unknownTargetErr(target) } agg, ok := isAggregate(r) if !ok { return VerifyResult{}, fmt.Errorf("target %q is per-playbook; use Verify", target) } desired, err := agg.RenderAll(set) if err != nil { return VerifyResult{}, err } dst, err := userArtifactPath(cfg, agg.AggRelPath()) if err != nil { return VerifyResult{}, err } onDisk, rerr := os.ReadFile(dst) if errors.Is(rerr, os.ErrNotExist) { return VerifyResult{Clean: false, Detail: fmt.Sprintf("%s: not emitted (run `eeco cockpit generate`)", target)}, nil } if rerr != nil { return VerifyResult{}, fmt.Errorf("read %s: %w", dst, rerr) } if sha256hex(onDisk) != sha256hex(desired) { return VerifyResult{Clean: false, Detail: fmt.Sprintf( "%s: drifted (hand-edited; run `eeco cockpit generate` to restore)", target)}, nil } sc := checkSelfConsistencyBytes(onDisk, set) if !sc.OK { return VerifyResult{Clean: false, Detail: fmt.Sprintf( "%s: self-consistency FAILED: %s", target, strings.Join(sc.Notes, "; "))}, nil } return VerifyResult{Clean: true, Detail: fmt.Sprintf("%s: clean (advisory — not harness-enforced)", target)}, nil } // OffAll removes an aggregate target's shared artifact, sha-gated and // reversible. A hand-edited file (on-disk sha != recorded sha) is left // untouched. It restores a backed-up foreign file or removes eeco's artifact, // then clears the target-only record. It NEVER prunes a directory, so it can // never remove cfg.UserDir (the private tree). func OffAll(cfg *config.Config, target string) (OffResult, error) { r, ok := rendererFor(target) if !ok { return OffResult{}, unknownTargetErr(target) } agg, ok := isAggregate(r) if !ok { return OffResult{}, fmt.Errorf("target %q is per-playbook; use Off", target) } dst, err := userArtifactPath(cfg, agg.AggRelPath()) if err != nil { return OffResult{}, err } l, err := loadLedger(cfg) if err != nil { return OffResult{}, err } i := l.findAgg(target) if i < 0 || !l.Records[i].Installed { return OffResult{Changed: false, Message: fmt.Sprintf("%s: not emitted", target)}, nil } rec := l.Records[i] onDisk, rerr := os.ReadFile(dst) if errors.Is(rerr, os.ErrNotExist) { l.clearAgg(target) if err := saveLedger(cfg, l); err != nil { return OffResult{}, err } return OffResult{Changed: true, Message: fmt.Sprintf("%s: already removed; ledger cleared", target)}, nil } if rerr != nil { return OffResult{}, fmt.Errorf("read %s: %w", dst, rerr) } if sha256hex(onDisk) != rec.SHA256 { return OffResult{Changed: false, Message: fmt.Sprintf("%s: edited since generate — left untouched", target)}, nil } if rec.Backup != "" { // Restore the pre-eeco file by atomically replacing eeco's artifact, so // the path is never absent mid-restore; an unreadable backup falls back // to removing the artifact. if bb, berr := os.ReadFile(rec.Backup); berr == nil { if werr := writeFileAtomic(dst, bb, 0o644); werr != nil { return OffResult{}, werr } } else if remErr := os.Remove(dst); remErr != nil { return OffResult{}, fmt.Errorf("remove %s: %w", dst, remErr) } } else if err := os.Remove(dst); err != nil { return OffResult{}, fmt.Errorf("remove %s: %w", dst, err) } l.clearAgg(target) if err := saveLedger(cfg, l); err != nil { return OffResult{}, err } return OffResult{Changed: true, Message: fmt.Sprintf("%s: removed (reversed)", target)}, nil }