docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -61,6 +61,7 @@ func ComputeAppHash(a *App) string {
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.Tags))
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesFunctions))
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesTypes))
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesModules))
|
||||
fmt.Fprintf(h, "|%s|%s|%s|%s|%s|%s", a.Framework, a.EntryPoint, a.Documentation, a.Notes, a.DirPath, a.RepoURL)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
@@ -73,10 +74,22 @@ func ComputeAnalysisHash(a *Analysis) string {
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.Tags))
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesFunctions))
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesTypes))
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesModules))
|
||||
fmt.Fprintf(h, "|%s|%s|%s|%s|%s|%s", a.Framework, a.EntryPoint, a.Documentation, a.Notes, a.DirPath, a.RepoURL)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// ComputeModuleHash computes a deterministic hash of all content fields of a Module.
|
||||
func ComputeModuleHash(m *Module) string {
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%s|%s|%s|%s|%s",
|
||||
m.ID, m.Name, m.Version, m.Lang, m.Description)
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(m.Members))
|
||||
fmt.Fprintf(h, "|%s", marshalStrings(m.Tags))
|
||||
fmt.Fprintf(h, "|%s|%s|%s|%s", m.DirPath, m.RepoURL, m.Documentation, m.Notes)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// ComputeProjectHash computes a deterministic hash of all content fields of a Project.
|
||||
func ComputeProjectHash(p *Project) string {
|
||||
h := sha256.New()
|
||||
@@ -98,7 +111,7 @@ func ComputeVaultHash(v *Vault) string {
|
||||
|
||||
// LoadTimestamps reads existing id → {created_at, updated_at, content_hash} from all tables.
|
||||
// Called before Purge so we can preserve dates across reindexing.
|
||||
func (db *DB) LoadTimestamps() (funcs, types, apps, analysis, projects, vaults map[string]timestampRecord, err error) {
|
||||
func (db *DB) LoadTimestamps() (funcs, types, apps, analysis, projects, vaults, modules map[string]timestampRecord, err error) {
|
||||
funcs, err = loadTable(db, "functions")
|
||||
if err != nil {
|
||||
return
|
||||
@@ -120,6 +133,10 @@ func (db *DB) LoadTimestamps() (funcs, types, apps, analysis, projects, vaults m
|
||||
return
|
||||
}
|
||||
vaults, err = loadTable(db, "vaults")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
modules, err = loadTable(db, "modules")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+59
-2
@@ -16,6 +16,7 @@ type IndexResult struct {
|
||||
Analysis int
|
||||
Projects int
|
||||
Vaults int
|
||||
Modules int
|
||||
UnitTests int
|
||||
ValidationErrors []string
|
||||
Warnings []string
|
||||
@@ -31,7 +32,7 @@ type IndexResult struct {
|
||||
// directories (e.g. python/functions/, python/types/).
|
||||
func Index(db *DB, root string) (*IndexResult, error) {
|
||||
// Load existing timestamps before purging so we can preserve created_at
|
||||
oldFuncs, oldTypes, oldApps, oldAnalysis, oldProjects, oldVaults, err := db.LoadTimestamps()
|
||||
oldFuncs, oldTypes, oldApps, oldAnalysis, oldProjects, oldVaults, oldModules, err := db.LoadTimestamps()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading timestamps: %w", err)
|
||||
}
|
||||
@@ -62,6 +63,20 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Discover module directories (modules/<name>/) — each may contain function .md
|
||||
// files alongside the module.md. Module entrypoint .md files (e.g. data_table.md)
|
||||
// live in their module dir; types still live in types/ to keep cross-module reuse.
|
||||
modRoot := filepath.Join(root, "modules")
|
||||
if fi, err := os.Stat(modRoot); err == nil && fi.IsDir() {
|
||||
modEntries, _ := os.ReadDir(modRoot)
|
||||
for _, me := range modEntries {
|
||||
if !me.IsDir() {
|
||||
continue
|
||||
}
|
||||
funcDirs = append(funcDirs, filepath.Join(modRoot, me.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
for _, dir := range funcDirs {
|
||||
walkMD(dir, func(path string) {
|
||||
f, err := ParseFunctionMD(path, root)
|
||||
@@ -146,6 +161,31 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse modules from modules/*/module.md
|
||||
var modules []*Module
|
||||
modulesDir := filepath.Join(root, "modules")
|
||||
if fi, err := os.Stat(modulesDir); err == nil && fi.IsDir() {
|
||||
modEntries, _ := os.ReadDir(modulesDir)
|
||||
for _, me := range modEntries {
|
||||
if !me.IsDir() {
|
||||
continue
|
||||
}
|
||||
modMD := filepath.Join(modulesDir, me.Name(), "module.md")
|
||||
if _, err := os.Stat(modMD); err != nil {
|
||||
continue
|
||||
}
|
||||
m, err := ParseModuleMD(modMD, root)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", modMD, err))
|
||||
continue
|
||||
}
|
||||
if m.DirPath == "" {
|
||||
m.DirPath = filepath.Join("modules", me.Name())
|
||||
}
|
||||
modules = append(modules, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse projects from projects/*/project.md
|
||||
var projects []*Project
|
||||
var vaults []*Vault
|
||||
@@ -347,6 +387,19 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
||||
result.Vaults++
|
||||
}
|
||||
|
||||
for _, m := range modules {
|
||||
m.ContentHash = ComputeModuleHash(m)
|
||||
applyTimestamps(&m.CreatedAt, &m.UpdatedAt, m.ContentHash, oldModules[m.ID], now)
|
||||
if err := db.InsertModule(m); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("insert module %s: %v", m.ID, err))
|
||||
continue
|
||||
}
|
||||
if err := emitModuleVersionHeader(m, root); err != nil {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("module %s: codegen version header: %v", m.ID, err))
|
||||
}
|
||||
result.Modules++
|
||||
}
|
||||
|
||||
// Extract unit tests from test files of tested functions
|
||||
if err := db.PurgeUnitTests(); err != nil {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("purging unit_tests: %v", err))
|
||||
@@ -437,7 +490,8 @@ func applyTimestamps(createdAt, updatedAt *time.Time, newHash string, old timest
|
||||
}
|
||||
}
|
||||
|
||||
// walkMD walks a directory recursively and calls fn for each .md file found.
|
||||
// walkMD walks a directory recursively and calls fn for each .md file found,
|
||||
// skipping module.md (which is parsed separately as a Module entry).
|
||||
func walkMD(dir string, fn func(path string)) {
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
return
|
||||
@@ -446,6 +500,9 @@ func walkMD(dir string, fn func(path string)) {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
|
||||
return nil
|
||||
}
|
||||
if filepath.Base(path) == "module.md" {
|
||||
return nil
|
||||
}
|
||||
fn(path)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Modules: reusable cohesive units (e.g. data_table) versioned with semver.
|
||||
-- A module groups a set of related registry functions/types under a single
|
||||
-- versioned artefact that apps opt into via uses_modules in app.md.
|
||||
--
|
||||
-- Modules son datos vivos: fn sync los replica entre PCs igual que apps/proposals.
|
||||
-- Aunque la fuente es modules/*/module.md (parseable), conservamos created_at /
|
||||
-- updated_at de forma persistente para mantener historico cross-PC.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS modules (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT NOT NULL DEFAULT '0.0.0',
|
||||
lang TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
members TEXT NOT NULL DEFAULT '[]',
|
||||
tags TEXT NOT NULL DEFAULT '[]',
|
||||
dir_path TEXT NOT NULL DEFAULT '',
|
||||
repo_url TEXT NOT NULL DEFAULT '',
|
||||
documentation TEXT NOT NULL DEFAULT '',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
content_hash TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS modules_fts USING fts5(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
members,
|
||||
documentation,
|
||||
notes,
|
||||
content='modules',
|
||||
content_rowid='rowid'
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS modules_ai AFTER INSERT ON modules BEGIN
|
||||
INSERT INTO modules_fts(rowid, id, name, description, tags, members, documentation, notes)
|
||||
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.members, new.documentation, new.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS modules_ad AFTER DELETE ON modules BEGIN
|
||||
INSERT INTO modules_fts(modules_fts, rowid, id, name, description, tags, members, documentation, notes)
|
||||
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.members, old.documentation, old.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS modules_au AFTER UPDATE ON modules BEGIN
|
||||
INSERT INTO modules_fts(modules_fts, rowid, id, name, description, tags, members, documentation, notes)
|
||||
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.members, old.documentation, old.notes);
|
||||
INSERT INTO modules_fts(rowid, id, name, description, tags, members, documentation, notes)
|
||||
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.members, new.documentation, new.notes);
|
||||
END;
|
||||
|
||||
-- uses_modules en apps/analysis: lista declarativa de modulos consumidos.
|
||||
ALTER TABLE apps ADD COLUMN uses_modules TEXT NOT NULL DEFAULT '[]';
|
||||
ALTER TABLE analysis ADD COLUMN uses_modules TEXT NOT NULL DEFAULT '[]';
|
||||
@@ -113,6 +113,7 @@ type App struct {
|
||||
Tags []string `json:"tags"`
|
||||
UsesFunctions []string `json:"uses_functions"`
|
||||
UsesTypes []string `json:"uses_types"`
|
||||
UsesModules []string `json:"uses_modules"`
|
||||
Framework string `json:"framework"`
|
||||
EntryPoint string `json:"entry_point"`
|
||||
Documentation string `json:"documentation"`
|
||||
@@ -135,6 +136,7 @@ type Analysis struct {
|
||||
Tags []string `json:"tags"`
|
||||
UsesFunctions []string `json:"uses_functions"`
|
||||
UsesTypes []string `json:"uses_types"`
|
||||
UsesModules []string `json:"uses_modules"`
|
||||
Framework string `json:"framework"`
|
||||
EntryPoint string `json:"entry_point"`
|
||||
Documentation string `json:"documentation"`
|
||||
@@ -147,6 +149,27 @@ type Analysis struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Module represents an entry in the modules table.
|
||||
// A module groups related registry functions/types under a single versioned
|
||||
// artefact that apps opt into via uses_modules in app.md. Living data: kept
|
||||
// in sync across PCs via fn sync.
|
||||
type Module struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Lang string `json:"lang"`
|
||||
Description string `json:"description"`
|
||||
Members []string `json:"members"`
|
||||
Tags []string `json:"tags"`
|
||||
DirPath string `json:"dir_path"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
Documentation string `json:"documentation"`
|
||||
Notes string `json:"notes"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ProposalKind classifies a proposal.
|
||||
type ProposalKind string
|
||||
|
||||
@@ -241,3 +264,9 @@ type PcLocation struct {
|
||||
func GenerateID(name, lang, domain string) string {
|
||||
return name + "_" + lang + "_" + domain
|
||||
}
|
||||
|
||||
// GenerateModuleID builds the module canonical ID: {name}_{lang}.
|
||||
// Modules are language-scoped but domain-agnostic; they live at modules/<name>/.
|
||||
func GenerateModuleID(name, lang string) string {
|
||||
return name + "_" + lang
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// emitModuleVersionHeader writes <dir_path>/version_generated.h for a module.
|
||||
// The header exposes constants the C++ side can read:
|
||||
//
|
||||
// FN_MODULE_<NAME>_VERSION (const char*)
|
||||
// FN_MODULE_<NAME>_NAME (const char*)
|
||||
// FN_MODULE_<NAME>_DESCRIPTION (const char*)
|
||||
//
|
||||
// For C++ modules only (lang == "cpp"). For other langs, returns nil silently.
|
||||
// Idempotent: only rewrites when content differs.
|
||||
func emitModuleVersionHeader(m *Module, root string) error {
|
||||
if m.Lang != "cpp" {
|
||||
return nil
|
||||
}
|
||||
if m.DirPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
upper := strings.ToUpper(m.Name)
|
||||
macroPrefix := "FN_MODULE_" + upper
|
||||
guard := macroPrefix + "_VERSION_GENERATED_H"
|
||||
|
||||
body := fmt.Sprintf(`// Auto-generated by `+"`fn index`"+` — do not edit.
|
||||
// Module: %s
|
||||
// Source of truth: modules/%s/module.md
|
||||
#ifndef %s
|
||||
#define %s
|
||||
|
||||
#define %s_NAME %q
|
||||
#define %s_VERSION %q
|
||||
#define %s_DESCRIPTION %q
|
||||
|
||||
#endif // %s
|
||||
`, m.Name, m.Name,
|
||||
guard, guard,
|
||||
macroPrefix, m.Name,
|
||||
macroPrefix, m.Version,
|
||||
macroPrefix, m.Description,
|
||||
guard)
|
||||
|
||||
headerPath := filepath.Join(root, m.DirPath, "version_generated.h")
|
||||
|
||||
// Idempotent: skip write when content already matches.
|
||||
if existing, err := os.ReadFile(headerPath); err == nil && string(existing) == body {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(headerPath), 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir %s: %w", filepath.Dir(headerPath), err)
|
||||
}
|
||||
return os.WriteFile(headerPath, []byte(body), 0o644)
|
||||
}
|
||||
@@ -82,6 +82,7 @@ type rawApp struct {
|
||||
Tags []string `yaml:"tags"`
|
||||
UsesFunctions []string `yaml:"uses_functions"`
|
||||
UsesTypes []string `yaml:"uses_types"`
|
||||
UsesModules []string `yaml:"uses_modules"`
|
||||
Framework string `yaml:"framework"`
|
||||
EntryPoint string `yaml:"entry_point"`
|
||||
DirPath string `yaml:"dir_path"`
|
||||
@@ -97,12 +98,25 @@ type rawAnalysis struct {
|
||||
Tags []string `yaml:"tags"`
|
||||
UsesFunctions []string `yaml:"uses_functions"`
|
||||
UsesTypes []string `yaml:"uses_types"`
|
||||
UsesModules []string `yaml:"uses_modules"`
|
||||
Framework string `yaml:"framework"`
|
||||
EntryPoint string `yaml:"entry_point"`
|
||||
DirPath string `yaml:"dir_path"`
|
||||
RepoURL string `yaml:"repo_url"`
|
||||
}
|
||||
|
||||
// rawModule mirrors the YAML frontmatter of a module.md file.
|
||||
type rawModule struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Lang string `yaml:"lang"`
|
||||
Description string `yaml:"description"`
|
||||
Members []string `yaml:"members"`
|
||||
Tags []string `yaml:"tags"`
|
||||
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"`
|
||||
@@ -320,6 +334,7 @@ func ParseAppMD(path string, root string) (*App, error) {
|
||||
Tags: raw.Tags,
|
||||
UsesFunctions: raw.UsesFunctions,
|
||||
UsesTypes: raw.UsesTypes,
|
||||
UsesModules: raw.UsesModules,
|
||||
Framework: raw.Framework,
|
||||
EntryPoint: raw.EntryPoint,
|
||||
Documentation: sections.documentation,
|
||||
@@ -366,6 +381,7 @@ func ParseAnalysisMD(path string, root string) (*Analysis, error) {
|
||||
Tags: raw.Tags,
|
||||
UsesFunctions: raw.UsesFunctions,
|
||||
UsesTypes: raw.UsesTypes,
|
||||
UsesModules: raw.UsesModules,
|
||||
Framework: raw.Framework,
|
||||
EntryPoint: raw.EntryPoint,
|
||||
Documentation: sections.documentation,
|
||||
@@ -377,6 +393,55 @@ func ParseAnalysisMD(path string, root string) (*Analysis, error) {
|
||||
return an, nil
|
||||
}
|
||||
|
||||
// ParseModuleMD parses a module .md file into a Module.
|
||||
func ParseModuleMD(path string, root string) (*Module, 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 rawModule
|
||||
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.Lang == "" {
|
||||
return nil, fmt.Errorf("%s: lang is required", path)
|
||||
}
|
||||
if raw.Description == "" {
|
||||
return nil, fmt.Errorf("%s: description is required", path)
|
||||
}
|
||||
if raw.Version == "" {
|
||||
raw.Version = "0.0.0"
|
||||
}
|
||||
|
||||
sections := extractSections(body)
|
||||
|
||||
m := &Module{
|
||||
ID: GenerateModuleID(raw.Name, raw.Lang),
|
||||
Name: raw.Name,
|
||||
Version: raw.Version,
|
||||
Lang: raw.Lang,
|
||||
Description: raw.Description,
|
||||
Members: raw.Members,
|
||||
Tags: raw.Tags,
|
||||
DirPath: raw.DirPath,
|
||||
RepoURL: raw.RepoURL,
|
||||
Documentation: sections.documentation,
|
||||
Notes: sections.notes,
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ParseProjectMD parses a project .md file into a Project.
|
||||
func ParseProjectMD(path string, root string) (*Project, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
+133
-12
@@ -307,12 +307,12 @@ func (db *DB) InsertApp(a *App) error {
|
||||
INSERT OR REPLACE INTO apps (
|
||||
id, name, lang, domain, description, tags,
|
||||
uses_functions, uses_types, framework, entry_point,
|
||||
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url, project_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url, project_id, uses_modules
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
|
||||
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
|
||||
a.Documentation, a.Notes, a.DirPath, a.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
|
||||
a.RepoURL, a.ProjectID,
|
||||
a.RepoURL, a.ProjectID, marshalStrings(a.UsesModules),
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -372,14 +372,14 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
|
||||
var result []App
|
||||
for rows.Next() {
|
||||
var a App
|
||||
var tagsJSON, usesFnJSON, usesTypJSON string
|
||||
var tagsJSON, usesFnJSON, usesTypJSON, usesModJSON string
|
||||
var createdAt, updatedAt string
|
||||
|
||||
err := rows.Scan(
|
||||
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
||||
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
||||
&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt, &a.ContentHash,
|
||||
&a.RepoURL, &a.ProjectID,
|
||||
&a.RepoURL, &a.ProjectID, &usesModJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning app: %w", err)
|
||||
@@ -388,6 +388,7 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
|
||||
a.Tags = unmarshalStrings(tagsJSON)
|
||||
a.UsesFunctions = unmarshalStrings(usesFnJSON)
|
||||
a.UsesTypes = unmarshalStrings(usesTypJSON)
|
||||
a.UsesModules = unmarshalStrings(usesModJSON)
|
||||
a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
|
||||
@@ -396,7 +397,7 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Purge deletes all data from functions, types, apps, analysis, projects and vaults. Used before re-indexing.
|
||||
// Purge deletes all data from functions, types, apps, analysis, projects, vaults and modules. Used before re-indexing.
|
||||
func (db *DB) Purge() error {
|
||||
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
||||
return err
|
||||
@@ -413,7 +414,10 @@ func (db *DB) Purge() error {
|
||||
if _, err := db.conn.Exec("DELETE FROM projects"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.conn.Exec("DELETE FROM vaults")
|
||||
if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.conn.Exec("DELETE FROM modules")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -458,6 +462,10 @@ func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs, localProjectIDs map[
|
||||
if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Modules: always purge and re-insert from modules/*/module.md
|
||||
if _, err := db.conn.Exec("DELETE FROM modules"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -481,12 +489,12 @@ func (db *DB) InsertAnalysis(a *Analysis) error {
|
||||
INSERT OR REPLACE INTO analysis (
|
||||
id, name, lang, domain, description, tags,
|
||||
uses_functions, uses_types, framework, entry_point,
|
||||
documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at, project_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at, project_id, uses_modules
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
|
||||
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
|
||||
a.Documentation, a.Notes, a.RepoURL, a.DirPath, a.ContentHash,
|
||||
a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339), a.ProjectID,
|
||||
a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339), a.ProjectID, marshalStrings(a.UsesModules),
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -556,14 +564,14 @@ func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis
|
||||
var result []Analysis
|
||||
for rows.Next() {
|
||||
var a Analysis
|
||||
var tagsJSON, usesFnJSON, usesTypJSON string
|
||||
var tagsJSON, usesFnJSON, usesTypJSON, usesModJSON string
|
||||
var createdAt, updatedAt string
|
||||
|
||||
err := rows.Scan(
|
||||
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
||||
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
||||
&a.Documentation, &a.Notes, &a.RepoURL, &a.DirPath, &a.ContentHash,
|
||||
&createdAt, &updatedAt, &a.ProjectID,
|
||||
&createdAt, &updatedAt, &a.ProjectID, &usesModJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning analysis: %w", err)
|
||||
@@ -572,6 +580,7 @@ func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis
|
||||
a.Tags = unmarshalStrings(tagsJSON)
|
||||
a.UsesFunctions = unmarshalStrings(usesFnJSON)
|
||||
a.UsesTypes = unmarshalStrings(usesTypJSON)
|
||||
a.UsesModules = unmarshalStrings(usesModJSON)
|
||||
a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
|
||||
@@ -1223,3 +1232,115 @@ func (db *DB) AllProposals() ([]Proposal, error) {
|
||||
func (db *DB) AllVaults() ([]Vault, error) {
|
||||
return db.SearchVaults("", "")
|
||||
}
|
||||
|
||||
// --- Module CRUD ---
|
||||
|
||||
// InsertModule inserts or replaces a module entry.
|
||||
func (db *DB) InsertModule(m *Module) error {
|
||||
now := time.Now().UTC()
|
||||
if m.CreatedAt.IsZero() {
|
||||
m.CreatedAt = now
|
||||
}
|
||||
if m.UpdatedAt.IsZero() {
|
||||
m.UpdatedAt = now
|
||||
}
|
||||
if m.ID == "" {
|
||||
m.ID = GenerateModuleID(m.Name, m.Lang)
|
||||
}
|
||||
|
||||
_, err := db.conn.Exec(`
|
||||
INSERT OR REPLACE INTO modules (
|
||||
id, name, version, lang, description, members, tags,
|
||||
dir_path, repo_url, documentation, notes, content_hash, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
m.ID, m.Name, m.Version, m.Lang, m.Description,
|
||||
marshalStrings(m.Members), marshalStrings(m.Tags),
|
||||
m.DirPath, m.RepoURL, m.Documentation, m.Notes, m.ContentHash,
|
||||
m.CreatedAt.Format(time.RFC3339), m.UpdatedAt.Format(time.RFC3339),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetModule returns a single module by ID.
|
||||
func (db *DB) GetModule(id string) (*Module, error) {
|
||||
rows, err := db.conn.Query("SELECT * FROM modules WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := scanModules(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf("module %q not found", id)
|
||||
}
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
// SearchModules performs FTS search on modules with optional filters.
|
||||
func (db *DB) SearchModules(query, lang string) ([]Module, error) {
|
||||
where := []string{}
|
||||
args := []any{}
|
||||
|
||||
if query != "" {
|
||||
where = append(where, "m.id IN (SELECT id FROM modules_fts WHERE modules_fts MATCH ?)")
|
||||
args = append(args, query)
|
||||
}
|
||||
if lang != "" {
|
||||
where = append(where, "m.lang = ?")
|
||||
args = append(args, lang)
|
||||
}
|
||||
|
||||
sql := "SELECT * FROM modules m"
|
||||
if len(where) > 0 {
|
||||
sql += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
sql += " ORDER BY m.name"
|
||||
|
||||
rows, err := db.conn.Query(sql, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search modules: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanModules(rows)
|
||||
}
|
||||
|
||||
// ListAllModules returns all module entries.
|
||||
func (db *DB) ListAllModules() ([]Module, error) {
|
||||
return db.SearchModules("", "")
|
||||
}
|
||||
|
||||
// AllModules returns all modules (for sync export).
|
||||
func (db *DB) AllModules() ([]Module, error) {
|
||||
return db.SearchModules("", "")
|
||||
}
|
||||
|
||||
func scanModules(rows interface{ Next() bool; Scan(...any) error }) ([]Module, error) {
|
||||
var result []Module
|
||||
for rows.Next() {
|
||||
var m Module
|
||||
var membersJSON, tagsJSON string
|
||||
var createdAt, updatedAt string
|
||||
|
||||
err := rows.Scan(
|
||||
&m.ID, &m.Name, &m.Version, &m.Lang, &m.Description,
|
||||
&membersJSON, &tagsJSON,
|
||||
&m.DirPath, &m.RepoURL, &m.Documentation, &m.Notes, &m.ContentHash,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning module: %w", err)
|
||||
}
|
||||
|
||||
m.Members = unmarshalStrings(membersJSON)
|
||||
m.Tags = unmarshalStrings(tagsJSON)
|
||||
m.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
m.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
|
||||
result = append(result, m)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user