Go 108 lines
// Package playbooks is eeco's shipped, neutral cockpit-playbook library:
// one embedded JSON source per AI procedure, the single reviewable source
// of truth for the playbooks eeco emits as harness config. It mirrors
// internal/prompts (embedded sources + a registry), but its unit is a
// cockpit.Playbook rather than a text template.
//
// Dependency direction (no cycle): this package imports internal/cockpit
// for the Playbook type; cockpit never imports playbooks. cmd/eeco wires
// the two — playbooks.Get(name) feeds cockpit.Generate.
//
// At C1 the library ships one source: handover. The general and
// parameterized playbooks (commit, doc-drift, …) migrate as additive C2
// slices.
package playbooks
import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"sort"
"strings"
"github.com/ajhahnde/eeco/internal/cockpit"
)
//go:embed data/*.json
var dataFS embed.FS
// entry pairs a parsed Playbook with the raw JSON an operator audits via
// `eeco cockpit show`.
type entry struct {
pb cockpit.Playbook
raw string
}
// registry is built once at package load. A malformed shipped source
// panics here on purpose — a playbook source is a build-time artifact, not
// runtime input, so a parse failure must surface immediately (the
// internal/prompts precedent).
var registry = mustLoad()
func mustLoad() map[string]entry {
files, err := fs.ReadDir(dataFS, "data")
if err != nil {
panic("playbooks: read data dir: " + err.Error())
}
reg := make(map[string]entry, len(files))
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), ".json") {
continue
}
body, err := dataFS.ReadFile("data/" + f.Name())
if err != nil {
panic("playbooks: read source " + f.Name() + ": " + err.Error())
}
var pb cockpit.Playbook
if err := json.Unmarshal(body, &pb); err != nil {
panic(fmt.Sprintf("playbooks: parse %s: %v", f.Name(), err))
}
name := strings.TrimSuffix(f.Name(), ".json")
if pb.Name != name {
panic(fmt.Sprintf("playbooks: %s declares name %q (must match file)", f.Name(), pb.Name))
}
reg[name] = entry{pb: pb, raw: string(body)}
}
return reg
}
// Names returns every available playbook name, sorted.
func Names() []string {
out := make([]string, 0, len(registry))
for n := range registry {
out = append(out, n)
}
sort.Strings(out)
return out
}
// All returns every registered Playbook, ordered by Name (mirroring Names),
// so the aggregate renderers receive a deterministic set.
func All() []cockpit.Playbook {
out := make([]cockpit.Playbook, 0, len(registry))
for _, n := range Names() {
out = append(out, registry[n].pb)
}
return out
}
// Get returns the parsed Playbook for name.
func Get(name string) (cockpit.Playbook, error) {
e, ok := registry[name]
if !ok {
return cockpit.Playbook{}, fmt.Errorf("unknown playbook %q (known: %s)", name, strings.Join(Names(), ", "))
}
return e.pb, nil
}
// Raw returns the canonical JSON source for name — the text an operator
// audits via `eeco cockpit show`.
func Raw(name string) (string, error) {
e, ok := registry[name]
if !ok {
return "", fmt.Errorf("unknown playbook %q (known: %s)", name, strings.Join(Names(), ", "))
}
return e.raw, nil
}