package registry import ( "bytes" "encoding/json" "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"` // Params schema Params []rawParam `yaml:"params"` Output string `yaml:"output"` // 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"` } // 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"` 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"` RepoURL string `yaml:"repo_url"` } // rawAnalysis mirrors the YAML frontmatter of an analysis .md file. type rawAnalysis 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"` RepoURL string `yaml:"repo_url"` } // rawProject mirrors the YAML frontmatter of a project .md file. type rawProject struct { Name string `yaml:"name"` Description string `yaml:"description"` Tags []string `yaml:"tags"` RepoURL string `yaml:"repo_url"` } // rawVaultFile mirrors the YAML of a vault.yaml manifest file. type rawVaultFile struct { Vaults []rawVaultEntry `yaml:"vaults"` } // rawVaultEntry describes a single vault in vault.yaml. type rawVaultEntry struct { Name string `yaml:"name"` Description string `yaml:"description"` Path string `yaml:"path"` Tags []string `yaml:"tags"` } // 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, } // 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 { 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, RepoURL: raw.RepoURL, } return a, nil } // ParseAnalysisMD parses an analysis .md file into an Analysis. func ParseAnalysisMD(path string, root string) (*Analysis, 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 rawAnalysis 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) an := &Analysis{ 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, RepoURL: raw.RepoURL, } return an, nil } // ParseProjectMD parses a project .md file into a Project. func ParseProjectMD(path string, root string) (*Project, 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 rawProject 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) p := &Project{ ID: raw.Name, Name: raw.Name, Description: raw.Description, Tags: raw.Tags, RepoURL: raw.RepoURL, DirPath: filepath.Join("projects", raw.Name), Documentation: sections.documentation, Notes: sections.notes, } return p, nil } // ParseVaultYAML parses a vault.yaml manifest into a slice of Vaults. // projectID is the owning project ID, or "" for registry-level vaults. // vaultsDir is the directory containing vault.yaml (used to detect symlinks). func ParseVaultYAML(path string, projectID string, vaultsDir string) ([]*Vault, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } var raw rawVaultFile if err := yaml.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("parsing YAML in %s: %w", path, err) } var vaults []*Vault for _, rv := range raw.Vaults { if rv.Name == "" { continue } suffix := projectID if suffix == "" { suffix = "registry" } id := rv.Name + "_" + suffix // Detect if the vault entry on disk is a symlink isSymlink := false vaultPath := rv.Path entryPath := filepath.Join(vaultsDir, rv.Name) if fi, err := os.Lstat(entryPath); err == nil { if fi.Mode()&os.ModeSymlink != 0 { isSymlink = true if target, err := os.Readlink(entryPath); err == nil && vaultPath == "" { vaultPath = target } } } vaults = append(vaults, &Vault{ ID: id, Name: rv.Name, ProjectID: projectID, Description: rv.Description, Path: vaultPath, Symlink: isSymlink, Tags: rv.Tags, }) } return vaults, 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 }