diff --git a/registry/hash.go b/registry/hash.go index ab23fa87..83b28102 100644 --- a/registry/hash.go +++ b/registry/hash.go @@ -37,6 +37,7 @@ func ComputeFunctionHash(f *Function) string { fmt.Fprintf(h, "|%s", marshalStrings(f.Variant)) fmt.Fprintf(h, "|%s|%s|%s", f.Notes, f.Documentation, f.Code) fmt.Fprintf(h, "|%s|%s|%s", f.SourceRepo, f.SourceLicense, f.SourceFile) + fmt.Fprintf(h, "|%s", f.ParamsSchema) return fmt.Sprintf("%x", h.Sum(nil)) } diff --git a/registry/indexer.go b/registry/indexer.go index a38422a4..6970a098 100644 --- a/registry/indexer.go +++ b/registry/indexer.go @@ -238,6 +238,8 @@ func Index(db *DB, root string) (*IndexResult, error) { } // Post-insert: warn about file_path entries that don't exist on disk + // and functions missing params_schema + missingParams := 0 for _, f := range functions { if f.FilePath != "" { abs := filepath.Join(root, f.FilePath) @@ -251,6 +253,12 @@ func Index(db *DB, root string) (*IndexResult, error) { result.Warnings = append(result.Warnings, fmt.Sprintf("%s: test_file_path %q not found", f.ID, f.TestFilePath)) } } + if f.ParamsSchema == "" { + missingParams++ + } + } + if missingParams > 0 { + result.Warnings = append(result.Warnings, fmt.Sprintf("%d functions missing params_schema (run 'fn check params' to list)", missingParams)) } for _, t := range types { if t.FilePath != "" { diff --git a/registry/migrations/009_params_schema.sql b/registry/migrations/009_params_schema.sql new file mode 100644 index 00000000..97aa53b5 --- /dev/null +++ b/registry/migrations/009_params_schema.sql @@ -0,0 +1,51 @@ +-- Add params_schema to functions: JSON with semantic descriptions of inputs/outputs. +-- Format: {"params":[{"name":"x","desc":"..."}],"output":"..."} +-- Enables agent reasoning about function composability. + +ALTER TABLE functions ADD COLUMN params_schema TEXT NOT NULL DEFAULT ''; + +-- Rebuild FTS for functions: add params_schema +DROP TRIGGER IF EXISTS functions_ai; +DROP TRIGGER IF EXISTS functions_ad; +DROP TRIGGER IF EXISTS functions_au; + +INSERT INTO functions_fts(functions_fts) VALUES('rebuild'); +DROP TABLE IF EXISTS functions_fts; + +CREATE VIRTUAL TABLE functions_fts USING fts5( + id, + name, + description, + tags, + signature, + domain, + example, + notes, + documentation, + code, + params_schema, + content='functions', + content_rowid='rowid' +); + +-- Populate FTS from existing data +INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema) +SELECT rowid, id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema +FROM functions; + +CREATE TRIGGER functions_ai AFTER INSERT ON functions BEGIN + INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema) + VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain, new.example, new.notes, new.documentation, new.code, new.params_schema); +END; + +CREATE TRIGGER functions_ad AFTER DELETE ON functions BEGIN + INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema) + VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain, old.example, old.notes, old.documentation, old.code, old.params_schema); +END; + +CREATE TRIGGER functions_au AFTER UPDATE ON functions BEGIN + INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema) + VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain, old.example, old.notes, old.documentation, old.code, old.params_schema); + INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema) + VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain, new.example, new.notes, new.documentation, new.code, new.params_schema); +END; diff --git a/registry/models.go b/registry/models.go index 0f1eae68..a6c4c038 100644 --- a/registry/models.go +++ b/registry/models.go @@ -50,6 +50,7 @@ type Function struct { Notes string `json:"notes"` Documentation string `json:"documentation"` Code string `json:"code"` + ParamsSchema string `json:"params_schema"` Tested bool `json:"tested"` Tests []string `json:"tests"` TestFilePath string `json:"test_file_path"` diff --git a/registry/parser.go b/registry/parser.go index 50a4f862..8f46526e 100644 --- a/registry/parser.go +++ b/registry/parser.go @@ -2,6 +2,7 @@ package registry import ( "bytes" + "encoding/json" "fmt" "os" "path/filepath" @@ -32,6 +33,10 @@ type rawFunction struct { TestFilePath string `yaml:"test_file_path"` FilePath string `yaml:"file_path"` + // Params schema + Params []rawParam `yaml:"params"` + Output string `yaml:"output"` + // Source attribution SourceRepo string `yaml:"source_repo"` SourceLicense string `yaml:"source_license"` @@ -45,6 +50,12 @@ type rawFunction struct { Variant []string `yaml:"variant"` } +// rawParam describes a function parameter with semantic meaning. +type rawParam struct { + Name string `yaml:"name" json:"name"` + Desc string `yaml:"desc" json:"desc"` +} + // rawType mirrors the YAML frontmatter of a type .md file. type rawType struct { Name string `yaml:"name"` @@ -175,6 +186,17 @@ func ParseFunctionMD(path string, root string) (*Function, error) { SourceFile: raw.SourceFile, } + // Serialize params + output to JSON for params_schema column + if len(raw.Params) > 0 || raw.Output != "" { + schema := struct { + Params []rawParam `json:"params,omitempty"` + Output string `json:"output,omitempty"` + }{Params: raw.Params, Output: raw.Output} + if b, err := json.Marshal(schema); err == nil { + f.ParamsSchema = string(b) + } + } + if root != "" && raw.FilePath != "" { codePath := filepath.Join(root, raw.FilePath) if codeData, err := os.ReadFile(codePath); err == nil { diff --git a/registry/store.go b/registry/store.go index b2d8d102..246097ba 100644 --- a/registry/store.go +++ b/registry/store.go @@ -86,7 +86,8 @@ func (db *DB) InsertFunction(f *Function) error { tests, test_file_path, file_path, content_hash, created_at, updated_at, props, emits, has_state, framework, variant, notes, documentation, code, - source_repo, source_license, source_file + source_repo, source_license, source_file, + params_schema ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @@ -94,7 +95,8 @@ func (db *DB) InsertFunction(f *Function) error { ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ? + ?, ?, ?, + ? )`, f.ID, f.Name, string(f.Kind), f.Lang, f.Domain, f.Version, string(f.Purity), f.Signature, f.Description, marshalStrings(f.Tags), marshalStrings(f.UsesFunctions), marshalStrings(f.UsesTypes), marshalStrings(f.Returns), @@ -103,6 +105,7 @@ func (db *DB) InsertFunction(f *Function) error { marshalProps(f.Props), marshalStrings(f.Emits), hasState, f.Framework, marshalStrings(f.Variant), f.Notes, f.Documentation, f.Code, f.SourceRepo, f.SourceLicense, f.SourceFile, + f.ParamsSchema, ) return err } @@ -559,6 +562,7 @@ func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Functio &propsJSON, &emitsJSON, &hasState, &f.Framework, &variantJSON, &f.Notes, &f.Documentation, &f.Code, &f.ContentHash, &f.SourceRepo, &f.SourceLicense, &f.SourceFile, + &f.ParamsSchema, ) if err != nil { return nil, fmt.Errorf("scanning function: %w", err)