49eecd0c87
Añade campos documentation, notes y code a functions y types. El parser extrae el contenido del .md y el código fuente del archivo referenciado en file_path. El indexer los almacena en SQLite y los incluye en FTS5 para búsqueda sobre código y documentación. Nueva migración 003_documentation.sql para añadir las columnas.
300 lines
6.2 KiB
Go
300 lines
6.2 KiB
Go
package registry
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
const functionMD = `---
|
|
name: filter_slice
|
|
kind: function
|
|
lang: go
|
|
domain: core
|
|
version: "1.0.0"
|
|
purity: pure
|
|
signature: "func FilterSlice[T any](xs []T, pred func(T) bool) []T"
|
|
description: "Filtra un slice aplicando un predicado sin mutar el original."
|
|
tags: [slice, functional, generic]
|
|
uses_functions: []
|
|
uses_types: []
|
|
returns: []
|
|
returns_optional: false
|
|
error_type: ""
|
|
imports: []
|
|
tested: false
|
|
tests: []
|
|
test_file_path: ""
|
|
file_path: "functions/core/filter_slice.go"
|
|
---
|
|
|
|
## Ejemplo
|
|
|
|
` + "```go" + `
|
|
evens := FilterSlice([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })
|
|
` + "```" + `
|
|
`
|
|
|
|
const typeMD = `---
|
|
name: ohlcv
|
|
lang: go
|
|
domain: finance
|
|
version: "1.0.0"
|
|
algebraic: product
|
|
definition: |
|
|
type OHLCV struct {
|
|
Open float64
|
|
High float64
|
|
Low float64
|
|
Close float64
|
|
Volume float64
|
|
}
|
|
description: "Vela de mercado con precios OHLCV."
|
|
tags: [finance, market, candle]
|
|
uses_types: []
|
|
file_path: "types/finance/ohlcv.go"
|
|
---
|
|
|
|
## Notas
|
|
|
|
Tipo producto.
|
|
`
|
|
|
|
const componentMD = `---
|
|
name: DataTable
|
|
kind: component
|
|
lang: typescript
|
|
domain: core
|
|
version: "1.0.0"
|
|
purity: impure
|
|
signature: "DataTable<T>(props: { data: T[] }): JSX.Element"
|
|
description: "Tabla de datos generica."
|
|
tags: [table, ui]
|
|
uses_functions: []
|
|
uses_types: []
|
|
returns: []
|
|
returns_optional: false
|
|
error_type: ""
|
|
imports: [react]
|
|
tested: false
|
|
tests: []
|
|
test_file_path: ""
|
|
file_path: "frontend/functions/ui/data_table.tsx"
|
|
props:
|
|
- name: data
|
|
type: "T[]"
|
|
required: true
|
|
description: "Array de datos"
|
|
emits: [onRowClick]
|
|
has_state: true
|
|
framework: react
|
|
variant: [default, compact]
|
|
---
|
|
`
|
|
|
|
func writeTempFile(t *testing.T, dir, name, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
os.MkdirAll(filepath.Dir(path), 0o755)
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestParseFunctionMD(t *testing.T) {
|
|
path := writeTempFile(t, t.TempDir(), "filter_slice.md", functionMD)
|
|
|
|
f, err := ParseFunctionMD(path, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if f.ID != "filter_slice_go_core" {
|
|
t.Errorf("id: got %q", f.ID)
|
|
}
|
|
if f.Kind != KindFunction {
|
|
t.Errorf("kind: got %q", f.Kind)
|
|
}
|
|
if f.Purity != PurityPure {
|
|
t.Errorf("purity: got %q", f.Purity)
|
|
}
|
|
if len(f.Tags) != 3 {
|
|
t.Errorf("tags: got %d, want 3", len(f.Tags))
|
|
}
|
|
if f.Example == "" {
|
|
t.Error("example should be extracted from body")
|
|
}
|
|
}
|
|
|
|
func TestParseTypeMD(t *testing.T) {
|
|
path := writeTempFile(t, t.TempDir(), "ohlcv.md", typeMD)
|
|
|
|
typ, err := ParseTypeMD(path, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if typ.ID != "ohlcv_go_finance" {
|
|
t.Errorf("id: got %q", typ.ID)
|
|
}
|
|
if typ.Algebraic != AlgebraicProduct {
|
|
t.Errorf("algebraic: got %q", typ.Algebraic)
|
|
}
|
|
if typ.Definition == "" {
|
|
t.Error("definition should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestParseComponentMD(t *testing.T) {
|
|
path := writeTempFile(t, t.TempDir(), "DataTable.md", componentMD)
|
|
|
|
f, err := ParseFunctionMD(path, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if f.Kind != KindComponent {
|
|
t.Errorf("kind: got %q", f.Kind)
|
|
}
|
|
if f.Framework != "react" {
|
|
t.Errorf("framework: got %q", f.Framework)
|
|
}
|
|
if len(f.Props) != 1 {
|
|
t.Errorf("props: got %d, want 1", len(f.Props))
|
|
}
|
|
if f.HasState == nil || !*f.HasState {
|
|
t.Error("has_state should be true")
|
|
}
|
|
if len(f.Emits) != 1 || f.Emits[0] != "onRowClick" {
|
|
t.Errorf("emits: got %v", f.Emits)
|
|
}
|
|
}
|
|
|
|
func TestParseMissingFrontmatter(t *testing.T) {
|
|
path := writeTempFile(t, t.TempDir(), "bad.md", "# No frontmatter here\n")
|
|
|
|
_, err := ParseFunctionMD(path, "")
|
|
if err == nil {
|
|
t.Error("expected error for missing frontmatter")
|
|
}
|
|
}
|
|
|
|
func TestIndexFullCycle(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
// Create function .md
|
|
writeTempFile(t, root, "functions/core/filter_slice.md", functionMD)
|
|
// Create type .md
|
|
writeTempFile(t, root, "types/finance/ohlcv.md", typeMD)
|
|
|
|
dbPath := filepath.Join(root, "registry.db")
|
|
db, err := Open(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
result, err := Index(db, root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if result.Functions != 1 {
|
|
t.Errorf("functions indexed: got %d, want 1", result.Functions)
|
|
}
|
|
if result.Types != 1 {
|
|
t.Errorf("types indexed: got %d, want 1", result.Types)
|
|
}
|
|
if len(result.Errors) != 0 {
|
|
t.Errorf("unexpected errors: %v", result.Errors)
|
|
}
|
|
if len(result.ValidationErrors) != 0 {
|
|
t.Errorf("unexpected validation errors: %v", result.ValidationErrors)
|
|
}
|
|
|
|
// Verify searchable
|
|
fns, err := db.SearchFunctions("filter", "", "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(fns) != 1 {
|
|
t.Errorf("search 'filter': got %d, want 1", len(fns))
|
|
}
|
|
|
|
ts, err := db.SearchTypes("ohlcv", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(ts) != 1 {
|
|
t.Errorf("search 'ohlcv': got %d, want 1", len(ts))
|
|
}
|
|
|
|
// Re-index should be idempotent
|
|
result2, err := Index(db, root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result2.Functions != 1 || result2.Types != 1 {
|
|
t.Error("re-index should produce same counts")
|
|
}
|
|
}
|
|
|
|
const invalidPipelineMD = `---
|
|
name: bad_pipeline
|
|
kind: pipeline
|
|
lang: go
|
|
domain: core
|
|
version: "1.0.0"
|
|
purity: pure
|
|
description: "Pipeline puro sin uses_functions — debe fallar."
|
|
tags: []
|
|
uses_functions: []
|
|
uses_types: []
|
|
returns: []
|
|
returns_optional: false
|
|
error_type: ""
|
|
imports: []
|
|
tested: false
|
|
tests: []
|
|
test_file_path: ""
|
|
file_path: "functions/pipelines/bad.go"
|
|
---
|
|
`
|
|
|
|
func TestIndexRejectsInvalid(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
// Valid function
|
|
writeTempFile(t, root, "functions/core/filter_slice.md", functionMD)
|
|
// Invalid pipeline (pure + empty uses_functions)
|
|
writeTempFile(t, root, "functions/pipelines/bad.md", invalidPipelineMD)
|
|
// Valid type
|
|
writeTempFile(t, root, "types/finance/ohlcv.md", typeMD)
|
|
|
|
dbPath := filepath.Join(root, "registry.db")
|
|
db, err := Open(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
result, err := Index(db, root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Valid entries should be indexed
|
|
if result.Functions != 1 {
|
|
t.Errorf("functions: got %d, want 1 (only the valid one)", result.Functions)
|
|
}
|
|
if result.Types != 1 {
|
|
t.Errorf("types: got %d, want 1", result.Types)
|
|
}
|
|
|
|
// Invalid pipeline should produce validation error
|
|
if len(result.ValidationErrors) == 0 {
|
|
t.Error("expected validation errors for invalid pipeline")
|
|
}
|
|
}
|