c72ae15429
Sistema de extracción de funciones desde repos externos. Agrega campos source_repo, source_license y source_file en functions y types (migración 006). Incluye manifest sources/sources.yaml, regla sources.md, parser con campos de atribución, y template actualizado con los nuevos campos. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
338 lines
9.6 KiB
Go
338 lines
9.6 KiB
Go
package registry
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"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"`
|
|
|
|
// Source attribution
|
|
SourceRepo string `yaml:"source_repo"`
|
|
SourceLicense string `yaml:"source_license"`
|
|
SourceFile string `yaml:"source_file"`
|
|
|
|
// 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"`
|
|
SourceRepo string `yaml:"source_repo"`
|
|
SourceLicense string `yaml:"source_license"`
|
|
SourceFile string `yaml:"source_file"`
|
|
}
|
|
|
|
// rawApp mirrors the YAML frontmatter of an app .md file.
|
|
type rawApp struct {
|
|
Name string `yaml:"name"`
|
|
Lang string `yaml:"lang"`
|
|
Domain string `yaml:"domain"`
|
|
Description string `yaml:"description"`
|
|
Tags []string `yaml:"tags"`
|
|
UsesFunctions []string `yaml:"uses_functions"`
|
|
UsesTypes []string `yaml:"uses_types"`
|
|
Framework string `yaml:"framework"`
|
|
EntryPoint string `yaml:"entry_point"`
|
|
DirPath string `yaml:"dir_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.
|
|
// root is the registry root directory, used to resolve file_path for code reading.
|
|
func ParseFunctionMD(path string, root 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)
|
|
}
|
|
|
|
sections := extractSections(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,
|
|
Example: sections.example,
|
|
Notes: sections.notes,
|
|
Documentation: sections.documentation,
|
|
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,
|
|
SourceRepo: raw.SourceRepo,
|
|
SourceLicense: raw.SourceLicense,
|
|
SourceFile: raw.SourceFile,
|
|
}
|
|
|
|
if root != "" && raw.FilePath != "" {
|
|
codePath := filepath.Join(root, raw.FilePath)
|
|
if codeData, err := os.ReadFile(codePath); err == nil {
|
|
f.Code = string(codeData)
|
|
}
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
// ParseTypeMD parses a type .md file into a Type.
|
|
// root is the registry root directory, used to resolve file_path for code reading.
|
|
func ParseTypeMD(path string, root string) (*Type, 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 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)
|
|
}
|
|
|
|
sections := extractSections(body)
|
|
|
|
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,
|
|
SourceRepo: raw.SourceRepo,
|
|
SourceLicense: raw.SourceLicense,
|
|
SourceFile: raw.SourceFile,
|
|
Examples: sections.example,
|
|
Notes: sections.notes,
|
|
Documentation: sections.documentation,
|
|
FilePath: raw.FilePath,
|
|
}
|
|
|
|
if root != "" && raw.FilePath != "" {
|
|
codePath := filepath.Join(root, raw.FilePath)
|
|
if codeData, err := os.ReadFile(codePath); err == nil {
|
|
t.Code = string(codeData)
|
|
}
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// ParseAppMD parses an app .md file into an App.
|
|
func ParseAppMD(path string, root string) (*App, 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 rawApp
|
|
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)
|
|
}
|
|
|
|
sections := extractSections(body)
|
|
|
|
a := &App{
|
|
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
|
|
Name: raw.Name,
|
|
Lang: raw.Lang,
|
|
Domain: raw.Domain,
|
|
Description: raw.Description,
|
|
Tags: raw.Tags,
|
|
UsesFunctions: raw.UsesFunctions,
|
|
UsesTypes: raw.UsesTypes,
|
|
Framework: raw.Framework,
|
|
EntryPoint: raw.EntryPoint,
|
|
Documentation: sections.documentation,
|
|
Notes: sections.notes,
|
|
DirPath: raw.DirPath,
|
|
}
|
|
|
|
return a, nil
|
|
}
|
|
|
|
// bodySections holds the extracted sections from a .md body.
|
|
type bodySections struct {
|
|
example string // content under ## Ejemplo
|
|
notes string // content under ## Notas
|
|
documentation string // everything else
|
|
}
|
|
|
|
// extractSections splits the markdown body into named sections.
|
|
// Known sections (## Ejemplo, ## Notas) are extracted separately.
|
|
// All other content (including unknown ## headings) goes into documentation.
|
|
func extractSections(body []byte) bodySections {
|
|
lines := strings.Split(string(body), "\n")
|
|
var s bodySections
|
|
|
|
type section struct {
|
|
name string
|
|
lines []string
|
|
}
|
|
|
|
var current *section
|
|
var sections []section
|
|
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "## ") {
|
|
if current != nil {
|
|
sections = append(sections, *current)
|
|
}
|
|
current = §ion{name: trimmed}
|
|
continue
|
|
}
|
|
if current != nil {
|
|
current.lines = append(current.lines, line)
|
|
} else {
|
|
// Content before any ## heading goes to documentation
|
|
sections = append(sections, section{name: "_preamble", lines: []string{line}})
|
|
}
|
|
}
|
|
if current != nil {
|
|
sections = append(sections, *current)
|
|
}
|
|
|
|
var docParts []string
|
|
for _, sec := range sections {
|
|
content := strings.TrimSpace(strings.Join(sec.lines, "\n"))
|
|
if content == "" && sec.name == "_preamble" {
|
|
continue
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(sec.name, "## Ejemplo"):
|
|
s.example = content
|
|
case strings.HasPrefix(sec.name, "## Notas"):
|
|
s.notes = content
|
|
case sec.name == "_preamble":
|
|
docParts = append(docParts, content)
|
|
default:
|
|
// Unknown sections go to documentation with their heading
|
|
docParts = append(docParts, sec.name+"\n\n"+content)
|
|
}
|
|
}
|
|
s.documentation = strings.TrimSpace(strings.Join(docParts, "\n\n"))
|
|
|
|
return s
|
|
}
|