feat(lib): migrate to Core Embed system with Result returns
- All public functions return core.Result instead of (string, error) - Mount scopes basedir — no path prefix needed in ReadString calls - Add Bundle struct replacing (string, map, error) anti-pattern - listDir takes *core.Embed not embed.FS - ListTasks/ListPersonas use FS() + BaseDirectory() for WalkDir - Remove bytes, os, text/template imports - 22 tests: Prompt, Task, TaskBundle, Flow, Persona, Template, List functions, ExtractWorkspace (Good/Bad patterns) Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
3c5e6d6498
commit
ede5d6f561
3 changed files with 300 additions and 119 deletions
218
pkg/lib/lib.go
218
pkg/lib/lib.go
|
|
@ -14,104 +14,134 @@
|
|||
//
|
||||
// Usage:
|
||||
//
|
||||
// prompt, _ := lib.Prompt("coding")
|
||||
// task, _ := lib.Task("code/review")
|
||||
// persona, _ := lib.Persona("secops/developer")
|
||||
// flow, _ := lib.Flow("go")
|
||||
// r := lib.Prompt("coding") // r.Value.(string)
|
||||
// r := lib.Task("code/review") // r.Value.(string)
|
||||
// r := lib.Persona("secops/dev") // r.Value.(string)
|
||||
// r := lib.Flow("go") // r.Value.(string)
|
||||
// lib.ExtractWorkspace("default", "/tmp/ws", data)
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
//go:embed prompt/*.md
|
||||
var promptFS embed.FS
|
||||
//go:embed all:prompt
|
||||
var promptFiles embed.FS
|
||||
|
||||
//go:embed all:task
|
||||
var taskFS embed.FS
|
||||
var taskFiles embed.FS
|
||||
|
||||
//go:embed flow/*.md
|
||||
var flowFS embed.FS
|
||||
//go:embed all:flow
|
||||
var flowFiles embed.FS
|
||||
|
||||
//go:embed persona
|
||||
var personaFS embed.FS
|
||||
//go:embed all:persona
|
||||
var personaFiles embed.FS
|
||||
|
||||
//go:embed all:workspace
|
||||
var workspaceFS embed.FS
|
||||
var workspaceFiles embed.FS
|
||||
|
||||
var (
|
||||
promptFS = mustMount(promptFiles, "prompt")
|
||||
taskFS = mustMount(taskFiles, "task")
|
||||
flowFS = mustMount(flowFiles, "flow")
|
||||
personaFS = mustMount(personaFiles, "persona")
|
||||
workspaceFS = mustMount(workspaceFiles, "workspace")
|
||||
)
|
||||
|
||||
func mustMount(fsys embed.FS, basedir string) *core.Embed {
|
||||
r := core.Mount(fsys, basedir)
|
||||
if !r.OK {
|
||||
panic(r.Value)
|
||||
}
|
||||
return r.Value.(*core.Embed)
|
||||
}
|
||||
|
||||
// --- Prompts ---
|
||||
|
||||
// Template tries Prompt then Task (backwards compat).
|
||||
func Template(slug string) (string, error) {
|
||||
if content, err := Prompt(slug); err == nil {
|
||||
return content, nil
|
||||
//
|
||||
// r := lib.Template("coding")
|
||||
// if r.OK { content := r.Value.(string) }
|
||||
func Template(slug string) core.Result {
|
||||
if r := Prompt(slug); r.OK {
|
||||
return r
|
||||
}
|
||||
return Task(slug)
|
||||
}
|
||||
|
||||
func Prompt(slug string) (string, error) {
|
||||
data, err := promptFS.ReadFile("prompt/" + slug + ".md")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
// Prompt reads a system prompt by slug.
|
||||
//
|
||||
// r := lib.Prompt("coding")
|
||||
// if r.OK { content := r.Value.(string) }
|
||||
func Prompt(slug string) core.Result {
|
||||
return promptFS.ReadString(slug + ".md")
|
||||
}
|
||||
|
||||
func Task(slug string) (string, error) {
|
||||
// Task reads a structured task plan by slug. Tries .md, .yaml, .yml.
|
||||
//
|
||||
// r := lib.Task("code/review")
|
||||
// if r.OK { content := r.Value.(string) }
|
||||
func Task(slug string) core.Result {
|
||||
for _, ext := range []string{".md", ".yaml", ".yml"} {
|
||||
data, err := taskFS.ReadFile("task/" + slug + ext)
|
||||
if err == nil {
|
||||
return string(data), nil
|
||||
if r := taskFS.ReadString(slug + ext); r.OK {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return "", fs.ErrNotExist
|
||||
return core.Result{Value: fs.ErrNotExist}
|
||||
}
|
||||
|
||||
func TaskBundle(slug string) (string, map[string]string, error) {
|
||||
main, err := Task(slug)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
// Bundle holds a task's main content plus companion files.
|
||||
//
|
||||
// r := lib.TaskBundle("code/review")
|
||||
// if r.OK { b := r.Value.(lib.Bundle) }
|
||||
type Bundle struct {
|
||||
Main string
|
||||
Files map[string]string
|
||||
}
|
||||
|
||||
// TaskBundle reads a task and its companion files.
|
||||
//
|
||||
// r := lib.TaskBundle("code/review")
|
||||
// if r.OK { b := r.Value.(lib.Bundle) }
|
||||
func TaskBundle(slug string) core.Result {
|
||||
main := Task(slug)
|
||||
if !main.OK {
|
||||
return main
|
||||
}
|
||||
bundleDir := "task/" + slug
|
||||
entries, err := fs.ReadDir(taskFS, bundleDir)
|
||||
if err != nil {
|
||||
return main, nil, nil
|
||||
b := Bundle{Main: main.Value.(string), Files: make(map[string]string)}
|
||||
r := taskFS.ReadDir(slug)
|
||||
if !r.OK {
|
||||
return core.Result{Value: b, OK: true}
|
||||
}
|
||||
bundle := make(map[string]string)
|
||||
for _, e := range entries {
|
||||
for _, e := range r.Value.([]fs.DirEntry) {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
data, err := taskFS.ReadFile(bundleDir + "/" + e.Name())
|
||||
if err == nil {
|
||||
bundle[e.Name()] = string(data)
|
||||
if fr := taskFS.ReadString(slug + "/" + e.Name()); fr.OK {
|
||||
b.Files[e.Name()] = fr.Value.(string)
|
||||
}
|
||||
}
|
||||
return main, bundle, nil
|
||||
return core.Result{Value: b, OK: true}
|
||||
}
|
||||
|
||||
func Flow(slug string) (string, error) {
|
||||
data, err := flowFS.ReadFile("flow/" + slug + ".md")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
// Flow reads a build/release workflow by slug.
|
||||
//
|
||||
// r := lib.Flow("go")
|
||||
// if r.OK { content := r.Value.(string) }
|
||||
func Flow(slug string) core.Result {
|
||||
return flowFS.ReadString(slug + ".md")
|
||||
}
|
||||
|
||||
func Persona(path string) (string, error) {
|
||||
data, err := personaFS.ReadFile("persona/" + path + ".md")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
// Persona reads a domain/role persona by path.
|
||||
//
|
||||
// r := lib.Persona("secops/developer")
|
||||
// if r.OK { content := r.Value.(string) }
|
||||
func Persona(path string) core.Result {
|
||||
return personaFS.ReadString(path + ".md")
|
||||
}
|
||||
|
||||
// --- Workspace Templates ---
|
||||
|
|
@ -138,67 +168,36 @@ type WorkspaceData struct {
|
|||
// ExtractWorkspace creates an agent workspace from a template.
|
||||
// Template names: "default", "security", "review".
|
||||
func ExtractWorkspace(tmplName, targetDir string, data *WorkspaceData) error {
|
||||
wsDir := "workspace/" + tmplName
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||
return err
|
||||
r := workspaceFS.Sub(tmplName)
|
||||
if !r.OK {
|
||||
if err, ok := r.Value.(error); ok {
|
||||
return err
|
||||
}
|
||||
return core.E("ExtractWorkspace", "template not found: "+tmplName, nil)
|
||||
}
|
||||
|
||||
return fs.WalkDir(workspaceFS, wsDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
result := core.Extract(r.Value.(*core.Embed).FS(), targetDir, data)
|
||||
if !result.OK {
|
||||
if err, ok := result.Value.(error); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get relative path from template root
|
||||
rel, err := filepath.Rel(wsDir, path)
|
||||
if err != nil || rel == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, rel)
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(targetPath, 0755)
|
||||
}
|
||||
|
||||
content, err := fs.ReadFile(workspaceFS, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process .tmpl files through text/template
|
||||
outputName := filepath.Base(targetPath)
|
||||
if core.HasSuffix(outputName, ".tmpl") {
|
||||
outputName = core.TrimSuffix(outputName, ".tmpl")
|
||||
targetPath = filepath.Join(filepath.Dir(targetPath), outputName)
|
||||
tmpl, err := template.New(outputName).Parse(string(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return err
|
||||
}
|
||||
content = buf.Bytes()
|
||||
}
|
||||
|
||||
return os.WriteFile(targetPath, content, 0644)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- List Functions ---
|
||||
|
||||
func ListPrompts() []string { return listDir(promptFS, "prompt") }
|
||||
func ListFlows() []string { return listDir(flowFS, "flow") }
|
||||
func ListWorkspaces() []string { return listDir(workspaceFS, "workspace") }
|
||||
func ListPrompts() []string { return listDir(promptFS) }
|
||||
func ListFlows() []string { return listDir(flowFS) }
|
||||
func ListWorkspaces() []string { return listDir(workspaceFS) }
|
||||
|
||||
func ListTasks() []string {
|
||||
var slugs []string
|
||||
fs.WalkDir(taskFS, "task", func(path string, d fs.DirEntry, err error) error {
|
||||
base := taskFS.BaseDirectory()
|
||||
fs.WalkDir(taskFS.FS(), base, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel := core.TrimPrefix(path, "task/")
|
||||
rel := core.TrimPrefix(path, base+"/")
|
||||
ext := filepath.Ext(rel)
|
||||
slugs = append(slugs, core.TrimSuffix(rel, ext))
|
||||
return nil
|
||||
|
|
@ -208,12 +207,13 @@ func ListTasks() []string {
|
|||
|
||||
func ListPersonas() []string {
|
||||
var paths []string
|
||||
fs.WalkDir(personaFS, "persona", func(path string, d fs.DirEntry, err error) error {
|
||||
base := personaFS.BaseDirectory()
|
||||
fs.WalkDir(personaFS.FS(), base, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if core.HasSuffix(path, ".md") {
|
||||
rel := core.TrimPrefix(path, "persona/")
|
||||
rel := core.TrimPrefix(path, base+"/")
|
||||
rel = core.TrimSuffix(rel, ".md")
|
||||
paths = append(paths, rel)
|
||||
}
|
||||
|
|
@ -222,13 +222,13 @@ func ListPersonas() []string {
|
|||
return paths
|
||||
}
|
||||
|
||||
func listDir(fsys embed.FS, dir string) []string {
|
||||
entries, err := fsys.ReadDir(dir)
|
||||
if err != nil {
|
||||
func listDir(emb *core.Embed) []string {
|
||||
r := emb.ReadDir(".")
|
||||
if !r.OK {
|
||||
return nil
|
||||
}
|
||||
var slugs []string
|
||||
for _, e := range entries {
|
||||
for _, e := range r.Value.([]fs.DirEntry) {
|
||||
name := e.Name()
|
||||
if e.IsDir() {
|
||||
slugs = append(slugs, name)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,202 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- Prompt ---
|
||||
|
||||
func TestPrompt_Good(t *testing.T) {
|
||||
r := Prompt("coding")
|
||||
if !r.OK {
|
||||
t.Fatal("Prompt('coding') returned !OK")
|
||||
}
|
||||
if r.Value.(string) == "" {
|
||||
t.Error("Prompt('coding') returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrompt_Bad(t *testing.T) {
|
||||
r := Prompt("nonexistent-slug")
|
||||
if r.OK {
|
||||
t.Error("Prompt('nonexistent-slug') should return !OK")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Task ---
|
||||
|
||||
func TestTask_Good_Yaml(t *testing.T) {
|
||||
r := Task("bug-fix")
|
||||
if !r.OK {
|
||||
t.Fatal("Task('bug-fix') returned !OK")
|
||||
}
|
||||
if r.Value.(string) == "" {
|
||||
t.Error("Task('bug-fix') returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTask_Good_Md(t *testing.T) {
|
||||
r := Task("code/review")
|
||||
if !r.OK {
|
||||
t.Fatal("Task('code/review') returned !OK")
|
||||
}
|
||||
if r.Value.(string) == "" {
|
||||
t.Error("Task('code/review') returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTask_Bad(t *testing.T) {
|
||||
r := Task("nonexistent-slug")
|
||||
if r.OK {
|
||||
t.Error("Task('nonexistent-slug') should return !OK")
|
||||
}
|
||||
if r.Value != fs.ErrNotExist {
|
||||
t.Error("Task('nonexistent-slug') should return fs.ErrNotExist")
|
||||
}
|
||||
}
|
||||
|
||||
// --- TaskBundle ---
|
||||
|
||||
func TestTaskBundle_Good(t *testing.T) {
|
||||
r := TaskBundle("code/review")
|
||||
if !r.OK {
|
||||
t.Fatal("TaskBundle('code/review') returned !OK")
|
||||
}
|
||||
b := r.Value.(Bundle)
|
||||
if b.Main == "" {
|
||||
t.Error("Bundle.Main is empty")
|
||||
}
|
||||
if len(b.Files) == 0 {
|
||||
t.Error("Bundle.Files is empty — expected companion files")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskBundle_Bad(t *testing.T) {
|
||||
r := TaskBundle("nonexistent")
|
||||
if r.OK {
|
||||
t.Error("TaskBundle('nonexistent') should return !OK")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Flow ---
|
||||
|
||||
func TestFlow_Good(t *testing.T) {
|
||||
r := Flow("go")
|
||||
if !r.OK {
|
||||
t.Fatal("Flow('go') returned !OK")
|
||||
}
|
||||
if r.Value.(string) == "" {
|
||||
t.Error("Flow('go') returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Persona ---
|
||||
|
||||
func TestPersona_Good(t *testing.T) {
|
||||
// Use first persona from list to avoid hardcoding
|
||||
personas := ListPersonas()
|
||||
if len(personas) == 0 {
|
||||
t.Skip("no personas found")
|
||||
}
|
||||
r := Persona(personas[0])
|
||||
if !r.OK {
|
||||
t.Fatalf("Persona(%q) returned !OK", personas[0])
|
||||
}
|
||||
if r.Value.(string) == "" {
|
||||
t.Errorf("Persona(%q) returned empty string", personas[0])
|
||||
}
|
||||
}
|
||||
|
||||
// --- Template ---
|
||||
|
||||
func TestTemplate_Good_Prompt(t *testing.T) {
|
||||
r := Template("coding")
|
||||
if !r.OK {
|
||||
t.Fatal("Template('coding') returned !OK")
|
||||
}
|
||||
if r.Value.(string) == "" {
|
||||
t.Error("Template('coding') returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplate_Good_TaskFallback(t *testing.T) {
|
||||
r := Template("bug-fix")
|
||||
if !r.OK {
|
||||
t.Fatal("Template('bug-fix') returned !OK — should fall through to Task")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplate_Bad(t *testing.T) {
|
||||
r := Template("nonexistent-slug")
|
||||
if r.OK {
|
||||
t.Error("Template('nonexistent-slug') should return !OK")
|
||||
}
|
||||
}
|
||||
|
||||
// --- List Functions ---
|
||||
|
||||
func TestListPrompts(t *testing.T) {
|
||||
prompts := ListPrompts()
|
||||
if len(prompts) == 0 {
|
||||
t.Error("ListPrompts() returned empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTasks(t *testing.T) {
|
||||
tasks := ListTasks()
|
||||
if len(tasks) == 0 {
|
||||
t.Fatal("ListTasks() returned empty")
|
||||
}
|
||||
// Verify nested paths are included (e.g., "code/review")
|
||||
found := false
|
||||
for _, s := range tasks {
|
||||
if s == "code/review" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("ListTasks() missing nested path 'code/review'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPersonas(t *testing.T) {
|
||||
personas := ListPersonas()
|
||||
if len(personas) == 0 {
|
||||
t.Error("ListPersonas() returned empty")
|
||||
}
|
||||
// Should have nested paths like "code/go"
|
||||
hasNested := false
|
||||
for _, p := range personas {
|
||||
if len(p) > 0 && filepath.Dir(p) != "." {
|
||||
hasNested = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasNested {
|
||||
t.Error("ListPersonas() has no nested paths")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFlows(t *testing.T) {
|
||||
flows := ListFlows()
|
||||
if len(flows) == 0 {
|
||||
t.Error("ListFlows() returned empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListWorkspaces(t *testing.T) {
|
||||
workspaces := ListWorkspaces()
|
||||
if len(workspaces) == 0 {
|
||||
t.Error("ListWorkspaces() returned empty")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ExtractWorkspace ---
|
||||
|
||||
func TestExtractWorkspace_CreatesFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
data := &WorkspaceData{Repo: "test-repo", Task: "test task"}
|
||||
|
|
@ -15,7 +206,6 @@ func TestExtractWorkspace_CreatesFiles(t *testing.T) {
|
|||
t.Fatalf("ExtractWorkspace failed: %v", err)
|
||||
}
|
||||
|
||||
// Check top-level template files exist
|
||||
for _, name := range []string{"CODEX.md", "CLAUDE.md", "PROMPT.md", "TODO.md", "CONTEXT.md"} {
|
||||
path := filepath.Join(dir, name)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
|
|
@ -33,19 +223,16 @@ func TestExtractWorkspace_CreatesSubdirectories(t *testing.T) {
|
|||
t.Fatalf("ExtractWorkspace failed: %v", err)
|
||||
}
|
||||
|
||||
// Check .core/reference/ directory exists with files
|
||||
refDir := filepath.Join(dir, ".core", "reference")
|
||||
if _, err := os.Stat(refDir); os.IsNotExist(err) {
|
||||
t.Fatalf(".core/reference/ directory not created")
|
||||
}
|
||||
|
||||
// Check AX spec exists
|
||||
axSpec := filepath.Join(refDir, "RFC-025-AGENT-EXPERIENCE.md")
|
||||
if _, err := os.Stat(axSpec); os.IsNotExist(err) {
|
||||
t.Errorf("AX spec not extracted: %s", axSpec)
|
||||
}
|
||||
|
||||
// Check Core source files exist
|
||||
entries, err := os.ReadDir(refDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read reference dir: %v", err)
|
||||
|
|
@ -61,7 +248,6 @@ func TestExtractWorkspace_CreatesSubdirectories(t *testing.T) {
|
|||
t.Error("no .go files in .core/reference/")
|
||||
}
|
||||
|
||||
// Check docs subdirectory
|
||||
docsDir := filepath.Join(refDir, "docs")
|
||||
if _, err := os.Stat(docsDir); os.IsNotExist(err) {
|
||||
t.Errorf(".core/reference/docs/ not created")
|
||||
|
|
@ -77,7 +263,6 @@ func TestExtractWorkspace_TemplateSubstitution(t *testing.T) {
|
|||
t.Fatalf("ExtractWorkspace failed: %v", err)
|
||||
}
|
||||
|
||||
// TODO.md should contain the task
|
||||
content, err := os.ReadFile(filepath.Join(dir, "TODO.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read TODO.md: %v", err)
|
||||
|
|
|
|||
4
pkg/lib/workspace/default/.gitignore
vendored
4
pkg/lib/workspace/default/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
|||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
.core/
|
||||
Loading…
Add table
Reference in a new issue