merge: quick/fase3-parser-indexer — parser YAML, indexer y templates

This commit is contained in:
2026-03-28 02:07:21 +01:00
9 changed files with 675 additions and 1 deletions
+48
View File
@@ -0,0 +1,48 @@
---
name: DataTable
kind: component
lang: typescript
domain: core
version: "1.0.0"
purity: impure
signature: "DataTable<T>(props: { data: T[]; columns: ColumnDef<T>[]; onRowClick?: (row: T) => void }): JSX.Element"
description: "Tabla de datos generica con soporte para columnas configurables y click en fila."
tags: [table, component, generic, ui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "functions/components/DataTable.tsx"
props:
- name: data
type: "T[]"
required: true
description: "Array de datos a renderizar"
- name: columns
type: "ColumnDef<T>[]"
required: true
description: "Definicion de columnas"
- name: onRowClick
type: "(row: T) => void"
required: false
description: "Callback al hacer click en una fila"
emits: [onRowClick]
has_state: true
framework: react
variant: [default, compact, striped]
---
## Ejemplo
```tsx
<DataTable data={users} columns={cols} onRowClick={handleClick} />
```
## Notas
Componente con estado interno para manejar seleccion y scroll.
+32
View File
@@ -0,0 +1,32 @@
---
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 })
// evens = [2, 4]
```
## Notas
Funcion pura generica. No muta el slice original — crea uno nuevo.
+31
View File
@@ -0,0 +1,31 @@
---
name: tick_to_ohlcv
kind: pipeline
lang: go
domain: finance
version: "1.0.0"
purity: impure
signature: "func TickToOHLCV(ctx context.Context, symbol string, interval time.Duration) ([]OHLCV, error)"
description: "Pipeline que obtiene ticks de un exchange y los agrega en velas OHLCV."
tags: [pipeline, finance, ohlcv, ticks]
uses_functions: [fetch_ticks_go_io, aggregate_ohlcv_go_finance]
uses_types: [ohlcv_go_finance, tick_go_finance]
returns: [ohlcv_go_finance]
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "functions/pipelines/tick_to_ohlcv.go"
---
## Ejemplo
```go
candles, err := TickToOHLCV(ctx, "BTCUSDT", 5*time.Minute)
```
## Notas
Pipeline impuro: orquesta fetch_ticks (IO) y aggregate_ohlcv (pura).
+23
View File
@@ -0,0 +1,23 @@
---
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 de apertura, maximo, minimo, cierre y volumen."
tags: [finance, market, candle]
uses_types: []
file_path: "types/finance/ohlcv.go"
---
## Notas
Tipo producto — todos los campos siempre presentes. Modela una vela de mercado.
+4 -1
View File
@@ -2,4 +2,7 @@ module fn-registry
go 1.22.2
require github.com/mattn/go-sqlite3 v1.14.37
require (
github.com/mattn/go-sqlite3 v1.14.37
gopkg.in/yaml.v3 v3.0.1
)
+4
View File
@@ -1,2 +1,6 @@
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+87
View File
@@ -0,0 +1,87 @@
package registry
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// IndexResult holds stats from an indexing run.
type IndexResult struct {
Functions int
Types int
Errors []string
}
// Index walks the registry root, parses all .md files, and populates the database.
// It purges existing data first to ensure a clean rebuild.
func Index(db *DB, root string) (*IndexResult, error) {
if err := db.Purge(); err != nil {
return nil, fmt.Errorf("purging database: %w", err)
}
result := &IndexResult{}
// Index functions
functionsDir := filepath.Join(root, "functions")
if _, err := os.Stat(functionsDir); err == nil {
err := filepath.Walk(functionsDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() || !strings.HasSuffix(path, ".md") {
return nil
}
f, err := ParseFunctionMD(path)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", path, err))
return nil
}
if err := db.InsertFunction(f); err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", f.ID, err))
return nil
}
result.Functions++
return nil
})
if err != nil {
return nil, fmt.Errorf("walking functions: %w", err)
}
}
// Index types
typesDir := filepath.Join(root, "types")
if _, err := os.Stat(typesDir); err == nil {
err := filepath.Walk(typesDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() || !strings.HasSuffix(path, ".md") {
return nil
}
t, err := ParseTypeMD(path)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", path, err))
return nil
}
if err := db.InsertType(t); err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", t.ID, err))
return nil
}
result.Types++
return nil
})
if err != nil {
return nil, fmt.Errorf("walking types: %w", err)
}
}
return result, nil
}
+208
View File
@@ -0,0 +1,208 @@
package registry
import (
"bytes"
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
// rawFunction mirrors the YAML frontmatter of a function .md file.
type rawFunction struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Lang string `yaml:"lang"`
Domain string `yaml:"domain"`
Version string `yaml:"version"`
Purity string `yaml:"purity"`
Signature string `yaml:"signature"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
UsesFunctions []string `yaml:"uses_functions"`
UsesTypes []string `yaml:"uses_types"`
Returns []string `yaml:"returns"`
ReturnsOptional bool `yaml:"returns_optional"`
ErrorType string `yaml:"error_type"`
Imports []string `yaml:"imports"`
Tested bool `yaml:"tested"`
Tests []string `yaml:"tests"`
TestFilePath string `yaml:"test_file_path"`
FilePath string `yaml:"file_path"`
// Component fields
Props []PropDef `yaml:"props"`
Emits []string `yaml:"emits"`
HasState *bool `yaml:"has_state"`
Framework string `yaml:"framework"`
Variant []string `yaml:"variant"`
}
// rawType mirrors the YAML frontmatter of a type .md file.
type rawType struct {
Name string `yaml:"name"`
Lang string `yaml:"lang"`
Domain string `yaml:"domain"`
Version string `yaml:"version"`
Algebraic string `yaml:"algebraic"`
Definition string `yaml:"definition"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
UsesTypes []string `yaml:"uses_types"`
FilePath string `yaml:"file_path"`
}
// extractFrontmatter splits a .md file into YAML frontmatter and body.
func extractFrontmatter(data []byte) ([]byte, []byte, error) {
content := data
if !bytes.HasPrefix(content, []byte("---\n")) && !bytes.HasPrefix(content, []byte("---\r\n")) {
return nil, nil, fmt.Errorf("missing opening --- in frontmatter")
}
// Skip opening ---
rest := content[4:]
idx := bytes.Index(rest, []byte("\n---"))
if idx < 0 {
return nil, nil, fmt.Errorf("missing closing --- in frontmatter")
}
fm := rest[:idx]
body := rest[idx+4:] // skip \n---
return fm, body, nil
}
// ParseFunctionMD parses a function .md file into a Function.
func ParseFunctionMD(path string) (*Function, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
fm, body, err := extractFrontmatter(data)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
var raw rawFunction
if err := yaml.Unmarshal(fm, &raw); err != nil {
return nil, fmt.Errorf("parsing YAML in %s: %w", path, err)
}
if raw.Name == "" {
return nil, fmt.Errorf("%s: name is required", path)
}
if raw.Kind == "" {
return nil, fmt.Errorf("%s: kind is required", path)
}
if raw.Description == "" {
return nil, fmt.Errorf("%s: description is required", path)
}
example := extractExample(body)
f := &Function{
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
Name: raw.Name,
Kind: Kind(raw.Kind),
Lang: raw.Lang,
Domain: raw.Domain,
Version: raw.Version,
Purity: Purity(raw.Purity),
Signature: raw.Signature,
Description: raw.Description,
Tags: raw.Tags,
UsesFunctions: raw.UsesFunctions,
UsesTypes: raw.UsesTypes,
Returns: raw.Returns,
ReturnsOptional: raw.ReturnsOptional,
ErrorType: raw.ErrorType,
Imports: raw.Imports,
Tested: raw.Tested,
Tests: raw.Tests,
TestFilePath: raw.TestFilePath,
FilePath: raw.FilePath,
Props: raw.Props,
Emits: raw.Emits,
HasState: raw.HasState,
Framework: raw.Framework,
Variant: raw.Variant,
}
if example != "" && f.Example == "" {
f.Example = example
}
return f, nil
}
// ParseTypeMD parses a type .md file into a Type.
func ParseTypeMD(path string) (*Type, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
fm, _, err := extractFrontmatter(data)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
var raw rawType
if err := yaml.Unmarshal(fm, &raw); err != nil {
return nil, fmt.Errorf("parsing YAML in %s: %w", path, err)
}
if raw.Name == "" {
return nil, fmt.Errorf("%s: name is required", path)
}
if raw.Description == "" {
return nil, fmt.Errorf("%s: description is required", path)
}
t := &Type{
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
Name: raw.Name,
Lang: raw.Lang,
Domain: raw.Domain,
Version: raw.Version,
Algebraic: Algebraic(raw.Algebraic),
Definition: strings.TrimSpace(raw.Definition),
Description: raw.Description,
Tags: raw.Tags,
UsesTypes: raw.UsesTypes,
FilePath: raw.FilePath,
}
return t, nil
}
// extractExample pulls the first code block after an "## Ejemplo" heading.
func extractExample(body []byte) string {
lines := strings.Split(string(body), "\n")
inExample := false
inCode := false
var code []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "## Ejemplo") {
inExample = true
continue
}
if inExample && !inCode && strings.HasPrefix(trimmed, "```") {
inCode = true
continue
}
if inCode {
if strings.HasPrefix(trimmed, "```") {
return strings.Join(code, "\n")
}
code = append(code, line)
}
if inExample && !inCode && strings.HasPrefix(trimmed, "##") {
break
}
}
return ""
}
+238
View File
@@ -0,0 +1,238 @@
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: "functions/components/DataTable.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)
}
// 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")
}
}