feat: soporte projects y vaults en registry
Añade tablas projects y vaults a registry.db con FTS5, modelos Go,
parser de project.md y vault.yaml, CRUD completo en store, hashing
determinista, validación, y soporte en el indexer para escanear
projects/{name}/ con sus apps, analysis y vaults anidados.
Migration 010 crea las tablas, triggers FTS5, y columna project_id
en apps/analysis. El indexer preserva records remotos (repo_url) al
reindexar, igual que apps/analysis.
This commit is contained in:
BIN
Binary file not shown.
+28
-1
@@ -77,9 +77,28 @@ func ComputeAnalysisHash(a *Analysis) string {
|
|||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
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()
|
||||||
|
fmt.Fprintf(h, "%s|%s|%s",
|
||||||
|
p.ID, p.Name, p.Description)
|
||||||
|
fmt.Fprintf(h, "|%s", marshalStrings(p.Tags))
|
||||||
|
fmt.Fprintf(h, "|%s|%s|%s|%s", p.RepoURL, p.DirPath, p.Documentation, p.Notes)
|
||||||
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeVaultHash computes a deterministic hash of all content fields of a Vault.
|
||||||
|
func ComputeVaultHash(v *Vault) string {
|
||||||
|
h := sha256.New()
|
||||||
|
fmt.Fprintf(h, "%s|%s|%s|%s|%s|%t",
|
||||||
|
v.ID, v.Name, v.ProjectID, v.Description, v.Path, v.Symlink)
|
||||||
|
fmt.Fprintf(h, "|%s", marshalStrings(v.Tags))
|
||||||
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
// LoadTimestamps reads existing id → {created_at, updated_at, content_hash} from all tables.
|
// LoadTimestamps reads existing id → {created_at, updated_at, content_hash} from all tables.
|
||||||
// Called before Purge so we can preserve dates across reindexing.
|
// Called before Purge so we can preserve dates across reindexing.
|
||||||
func (db *DB) LoadTimestamps() (funcs, types, apps, analysis map[string]timestampRecord, err error) {
|
func (db *DB) LoadTimestamps() (funcs, types, apps, analysis, projects, vaults map[string]timestampRecord, err error) {
|
||||||
funcs, err = loadTable(db, "functions")
|
funcs, err = loadTable(db, "functions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -93,6 +112,14 @@ func (db *DB) LoadTimestamps() (funcs, types, apps, analysis map[string]timestam
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
analysis, err = loadTable(db, "analysis")
|
analysis, err = loadTable(db, "analysis")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
projects, err = loadTable(db, "projects")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vaults, err = loadTable(db, "vaults")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+134
-5
@@ -14,6 +14,8 @@ type IndexResult struct {
|
|||||||
Types int
|
Types int
|
||||||
Apps int
|
Apps int
|
||||||
Analysis int
|
Analysis int
|
||||||
|
Projects int
|
||||||
|
Vaults int
|
||||||
UnitTests int
|
UnitTests int
|
||||||
ValidationErrors []string
|
ValidationErrors []string
|
||||||
Warnings []string
|
Warnings []string
|
||||||
@@ -29,7 +31,7 @@ type IndexResult struct {
|
|||||||
// directories (e.g. python/functions/, python/types/).
|
// directories (e.g. python/functions/, python/types/).
|
||||||
func Index(db *DB, root string) (*IndexResult, error) {
|
func Index(db *DB, root string) (*IndexResult, error) {
|
||||||
// Load existing timestamps before purging so we can preserve created_at
|
// Load existing timestamps before purging so we can preserve created_at
|
||||||
oldFuncs, oldTypes, oldApps, oldAnalysis, err := db.LoadTimestamps()
|
oldFuncs, oldTypes, oldApps, oldAnalysis, oldProjects, oldVaults, err := db.LoadTimestamps()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading timestamps: %w", err)
|
return nil, fmt.Errorf("loading timestamps: %w", err)
|
||||||
}
|
}
|
||||||
@@ -82,7 +84,7 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse apps from apps/*/app.md
|
// Parse apps from apps/*/app.md (standalone apps, no project)
|
||||||
var apps []*App
|
var apps []*App
|
||||||
localAppIDs := make(map[string]bool)
|
localAppIDs := make(map[string]bool)
|
||||||
appsDir := filepath.Join(root, "apps")
|
appsDir := filepath.Join(root, "apps")
|
||||||
@@ -106,7 +108,7 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse analysis from analysis/*/analysis.md
|
// Parse analysis from analysis/*/analysis.md (standalone, no project)
|
||||||
var analyses []*Analysis
|
var analyses []*Analysis
|
||||||
localAnalysisIDs := make(map[string]bool)
|
localAnalysisIDs := make(map[string]bool)
|
||||||
analysisDir := filepath.Join(root, "analysis")
|
analysisDir := filepath.Join(root, "analysis")
|
||||||
@@ -130,8 +132,111 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selective purge: preserve remote-only apps/analysis (have repo_url, not cloned locally)
|
// Parse projects from projects/*/project.md
|
||||||
if err := db.PurgeLocalOnly(localAppIDs, localAnalysisIDs); err != nil {
|
var projects []*Project
|
||||||
|
var vaults []*Vault
|
||||||
|
localProjectIDs := make(map[string]bool)
|
||||||
|
projectsDir := filepath.Join(root, "projects")
|
||||||
|
if fi, err := os.Stat(projectsDir); err == nil && fi.IsDir() {
|
||||||
|
projEntries, _ := os.ReadDir(projectsDir)
|
||||||
|
for _, pe := range projEntries {
|
||||||
|
if !pe.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
projName := pe.Name()
|
||||||
|
projDir := filepath.Join(projectsDir, projName)
|
||||||
|
|
||||||
|
// Parse project.md
|
||||||
|
projMD := filepath.Join(projDir, "project.md")
|
||||||
|
if _, err := os.Stat(projMD); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, err := ParseProjectMD(projMD, root)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", projMD, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
projects = append(projects, p)
|
||||||
|
localProjectIDs[p.ID] = true
|
||||||
|
|
||||||
|
// Parse project apps from projects/{name}/apps/*/app.md
|
||||||
|
projAppsDir := filepath.Join(projDir, "apps")
|
||||||
|
if fi, err := os.Stat(projAppsDir); err == nil && fi.IsDir() {
|
||||||
|
appEntries, _ := os.ReadDir(projAppsDir)
|
||||||
|
for _, ae := range appEntries {
|
||||||
|
if !ae.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
appMD := filepath.Join(projAppsDir, ae.Name(), "app.md")
|
||||||
|
if _, err := os.Stat(appMD); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a, err := ParseAppMD(appMD, root)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", appMD, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.ProjectID = p.ID
|
||||||
|
if a.DirPath == "" {
|
||||||
|
a.DirPath = filepath.Join("projects", projName, "apps", ae.Name())
|
||||||
|
}
|
||||||
|
apps = append(apps, a)
|
||||||
|
localAppIDs[a.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse project analysis from projects/{name}/analysis/*/analysis.md
|
||||||
|
projAnalysisDir := filepath.Join(projDir, "analysis")
|
||||||
|
if fi, err := os.Stat(projAnalysisDir); err == nil && fi.IsDir() {
|
||||||
|
anEntries, _ := os.ReadDir(projAnalysisDir)
|
||||||
|
for _, ane := range anEntries {
|
||||||
|
if !ane.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
anMD := filepath.Join(projAnalysisDir, ane.Name(), "analysis.md")
|
||||||
|
if _, err := os.Stat(anMD); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
an, err := ParseAnalysisMD(anMD, root)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", anMD, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
an.ProjectID = p.ID
|
||||||
|
if an.DirPath == "" {
|
||||||
|
an.DirPath = filepath.Join("projects", projName, "analysis", ane.Name())
|
||||||
|
}
|
||||||
|
analyses = append(analyses, an)
|
||||||
|
localAnalysisIDs[an.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse project vaults from projects/{name}/vaults/vault.yaml
|
||||||
|
projVaultYAML := filepath.Join(projDir, "vaults", "vault.yaml")
|
||||||
|
if _, err := os.Stat(projVaultYAML); err == nil {
|
||||||
|
vs, err := ParseVaultYAML(projVaultYAML, p.ID, filepath.Join(projDir, "vaults"))
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", projVaultYAML, err))
|
||||||
|
} else {
|
||||||
|
vaults = append(vaults, vs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse registry-level vaults from vaults/vault.yaml
|
||||||
|
registryVaultYAML := filepath.Join(root, "vaults", "vault.yaml")
|
||||||
|
if _, err := os.Stat(registryVaultYAML); err == nil {
|
||||||
|
vs, err := ParseVaultYAML(registryVaultYAML, "", filepath.Join(root, "vaults"))
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", registryVaultYAML, err))
|
||||||
|
} else {
|
||||||
|
vaults = append(vaults, vs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selective purge: preserve remote-only apps/analysis/projects (have repo_url, not cloned locally)
|
||||||
|
if err := db.PurgeLocalOnly(localAppIDs, localAnalysisIDs, localProjectIDs); err != nil {
|
||||||
return nil, fmt.Errorf("purging database: %w", err)
|
return nil, fmt.Errorf("purging database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +309,30 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
|||||||
result.Analysis++
|
result.Analysis++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, p := range projects {
|
||||||
|
if verr := ValidateProject(p); verr != nil {
|
||||||
|
result.ValidationErrors = append(result.ValidationErrors, verr.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.ContentHash = ComputeProjectHash(p)
|
||||||
|
applyTimestamps(&p.CreatedAt, &p.UpdatedAt, p.ContentHash, oldProjects[p.ID], now)
|
||||||
|
if err := db.InsertProject(p); err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("insert project %s: %v", p.ID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.Projects++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range vaults {
|
||||||
|
v.ContentHash = ComputeVaultHash(v)
|
||||||
|
applyTimestamps(&v.CreatedAt, &v.UpdatedAt, v.ContentHash, oldVaults[v.ID], now)
|
||||||
|
if err := db.InsertVault(v); err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("insert vault %s: %v", v.ID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.Vaults++
|
||||||
|
}
|
||||||
|
|
||||||
// Extract unit tests from test files of tested functions
|
// Extract unit tests from test files of tested functions
|
||||||
if err := db.PurgeUnitTests(); err != nil {
|
if err := db.PurgeUnitTests(); err != nil {
|
||||||
result.Warnings = append(result.Warnings, fmt.Sprintf("purging unit_tests: %v", err))
|
result.Warnings = append(result.Warnings, fmt.Sprintf("purging unit_tests: %v", err))
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- Projects: agrupan apps, analysis y vaults bajo un tema comun.
|
||||||
|
-- Vaults: almacenes de datos (symlinks o directorios) asociados a proyectos o al registry.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
tags TEXT NOT NULL DEFAULT '[]',
|
||||||
|
repo_url TEXT NOT NULL DEFAULT '',
|
||||||
|
dir_path 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 projects_fts USING fts5(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
documentation,
|
||||||
|
notes,
|
||||||
|
content='projects',
|
||||||
|
content_rowid='rowid'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS projects_ai AFTER INSERT ON projects BEGIN
|
||||||
|
INSERT INTO projects_fts(rowid, id, name, description, tags, documentation, notes)
|
||||||
|
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.documentation, new.notes);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS projects_ad AFTER DELETE ON projects BEGIN
|
||||||
|
INSERT INTO projects_fts(projects_fts, rowid, id, name, description, tags, documentation, notes)
|
||||||
|
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.documentation, old.notes);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS projects_au AFTER UPDATE ON projects BEGIN
|
||||||
|
INSERT INTO projects_fts(projects_fts, rowid, id, name, description, tags, documentation, notes)
|
||||||
|
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.documentation, old.notes);
|
||||||
|
INSERT INTO projects_fts(rowid, id, name, description, tags, documentation, notes)
|
||||||
|
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.documentation, new.notes);
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Vaults: almacenes de datos trackados en el registry.
|
||||||
|
CREATE TABLE IF NOT EXISTS vaults (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
project_id TEXT NOT NULL DEFAULT '',
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
path TEXT NOT NULL DEFAULT '',
|
||||||
|
symlink INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tags TEXT NOT NULL DEFAULT '[]',
|
||||||
|
content_hash TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Columna project_id en apps y analysis para vincular a un proyecto.
|
||||||
|
ALTER TABLE apps ADD COLUMN project_id TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE analysis ADD COLUMN project_id TEXT NOT NULL DEFAULT '';
|
||||||
@@ -120,6 +120,7 @@ type App struct {
|
|||||||
DirPath string `json:"dir_path"`
|
DirPath string `json:"dir_path"`
|
||||||
ContentHash string `json:"content_hash"`
|
ContentHash string `json:"content_hash"`
|
||||||
RepoURL string `json:"repo_url"`
|
RepoURL string `json:"repo_url"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -141,6 +142,7 @@ type Analysis struct {
|
|||||||
RepoURL string `json:"repo_url"`
|
RepoURL string `json:"repo_url"`
|
||||||
DirPath string `json:"dir_path"`
|
DirPath string `json:"dir_path"`
|
||||||
ContentHash string `json:"content_hash"`
|
ContentHash string `json:"content_hash"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -193,6 +195,35 @@ type UnitTest struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Project groups apps, analysis and vaults under a common theme.
|
||||||
|
type Project struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
RepoURL string `json:"repo_url"`
|
||||||
|
DirPath string `json:"dir_path"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vault is a data store (symlink or directory) associated with a project or the registry.
|
||||||
|
type Vault struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Symlink bool `json:"symlink"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
ContentHash string `json:"content_hash"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateID builds the canonical ID: {name}_{lang}_{domain}
|
// GenerateID builds the canonical ID: {name}_{lang}_{domain}
|
||||||
func GenerateID(name, lang, domain string) string {
|
func GenerateID(name, lang, domain string) string {
|
||||||
return name + "_" + lang + "_" + domain
|
return name + "_" + lang + "_" + domain
|
||||||
|
|||||||
@@ -103,6 +103,27 @@ type rawAnalysis struct {
|
|||||||
RepoURL string `yaml:"repo_url"`
|
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.
|
// extractFrontmatter splits a .md file into YAML frontmatter and body.
|
||||||
func extractFrontmatter(data []byte) ([]byte, []byte, error) {
|
func extractFrontmatter(data []byte) ([]byte, []byte, error) {
|
||||||
content := data
|
content := data
|
||||||
@@ -356,6 +377,99 @@ func ParseAnalysisMD(path string, root string) (*Analysis, error) {
|
|||||||
return an, nil
|
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.
|
// bodySections holds the extracted sections from a .md body.
|
||||||
type bodySections struct {
|
type bodySections struct {
|
||||||
example string // content under ## Ejemplo
|
example string // content under ## Ejemplo
|
||||||
|
|||||||
+255
-13
@@ -291,12 +291,12 @@ func (db *DB) InsertApp(a *App) error {
|
|||||||
INSERT OR REPLACE INTO apps (
|
INSERT OR REPLACE INTO apps (
|
||||||
id, name, lang, domain, description, tags,
|
id, name, lang, domain, description, tags,
|
||||||
uses_functions, uses_types, framework, entry_point,
|
uses_functions, uses_types, framework, entry_point,
|
||||||
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url
|
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url, project_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
|
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
|
||||||
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
|
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.Documentation, a.Notes, a.DirPath, a.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
|
||||||
a.RepoURL,
|
a.RepoURL, a.ProjectID,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -363,7 +363,7 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
|
|||||||
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
||||||
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
||||||
&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt, &a.ContentHash,
|
&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt, &a.ContentHash,
|
||||||
&a.RepoURL,
|
&a.RepoURL, &a.ProjectID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("scanning app: %w", err)
|
return nil, fmt.Errorf("scanning app: %w", err)
|
||||||
@@ -380,7 +380,7 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purge deletes all data from functions, types, apps and analysis. Used before re-indexing.
|
// Purge deletes all data from functions, types, apps, analysis, projects and vaults. Used before re-indexing.
|
||||||
func (db *DB) Purge() error {
|
func (db *DB) Purge() error {
|
||||||
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -391,13 +391,19 @@ func (db *DB) Purge() error {
|
|||||||
if _, err := db.conn.Exec("DELETE FROM apps"); err != nil {
|
if _, err := db.conn.Exec("DELETE FROM apps"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := db.conn.Exec("DELETE FROM analysis")
|
if _, err := db.conn.Exec("DELETE FROM analysis"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.conn.Exec("DELETE FROM projects"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := db.conn.Exec("DELETE FROM vaults")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurgeLocalOnly deletes functions, types, and only locally-present apps/analysis.
|
// PurgeLocalOnly deletes functions, types, and only locally-present apps/analysis/projects/vaults.
|
||||||
// Remote-only records (repo_url set, not in localAppIDs/localAnalysisIDs) are preserved.
|
// Remote-only records (repo_url set, not in local ID maps) are preserved.
|
||||||
func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs map[string]bool) error {
|
func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs, localProjectIDs map[string]bool) error {
|
||||||
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -423,6 +429,19 @@ func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs map[string]bool) erro
|
|||||||
if _, err := db.conn.Exec("DELETE FROM analysis WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
|
if _, err := db.conn.Exec("DELETE FROM analysis WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Projects: delete locally-scanned, preserve remote-only
|
||||||
|
for id := range localProjectIDs {
|
||||||
|
if _, err := db.conn.Exec("DELETE FROM projects WHERE id = ?", id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := db.conn.Exec("DELETE FROM projects WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Vaults: always purge and re-insert from vault.yaml
|
||||||
|
if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,12 +465,12 @@ func (db *DB) InsertAnalysis(a *Analysis) error {
|
|||||||
INSERT OR REPLACE INTO analysis (
|
INSERT OR REPLACE INTO analysis (
|
||||||
id, name, lang, domain, description, tags,
|
id, name, lang, domain, description, tags,
|
||||||
uses_functions, uses_types, framework, entry_point,
|
uses_functions, uses_types, framework, entry_point,
|
||||||
documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at
|
documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at, project_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
|
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
|
||||||
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
|
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
|
||||||
a.Documentation, a.Notes, a.RepoURL, a.DirPath, a.ContentHash,
|
a.Documentation, a.Notes, a.RepoURL, a.DirPath, a.ContentHash,
|
||||||
a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
|
a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339), a.ProjectID,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -528,7 +547,7 @@ func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis
|
|||||||
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
||||||
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
||||||
&a.Documentation, &a.Notes, &a.RepoURL, &a.DirPath, &a.ContentHash,
|
&a.Documentation, &a.Notes, &a.RepoURL, &a.DirPath, &a.ContentHash,
|
||||||
&createdAt, &updatedAt,
|
&createdAt, &updatedAt, &a.ProjectID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("scanning analysis: %w", err)
|
return nil, fmt.Errorf("scanning analysis: %w", err)
|
||||||
@@ -545,6 +564,229 @@ func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Project CRUD ---
|
||||||
|
|
||||||
|
// InsertProject inserts or replaces a project entry.
|
||||||
|
func (db *DB) InsertProject(p *Project) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if p.CreatedAt.IsZero() {
|
||||||
|
p.CreatedAt = now
|
||||||
|
}
|
||||||
|
if p.UpdatedAt.IsZero() {
|
||||||
|
p.UpdatedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.conn.Exec(`
|
||||||
|
INSERT OR REPLACE INTO projects (
|
||||||
|
id, name, description, tags, repo_url, dir_path,
|
||||||
|
documentation, notes, content_hash, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
p.ID, p.Name, p.Description, marshalStrings(p.Tags), p.RepoURL, p.DirPath,
|
||||||
|
p.Documentation, p.Notes, p.ContentHash,
|
||||||
|
p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject returns a single project by ID.
|
||||||
|
func (db *DB) GetProject(id string) (*Project, error) {
|
||||||
|
rows, err := db.conn.Query("SELECT * FROM projects WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
ps, err := scanProjects(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ps) == 0 {
|
||||||
|
return nil, fmt.Errorf("project %q not found", id)
|
||||||
|
}
|
||||||
|
return &ps[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchProjects performs FTS search on projects.
|
||||||
|
func (db *DB) SearchProjects(query string) ([]Project, error) {
|
||||||
|
where := []string{}
|
||||||
|
args := []any{}
|
||||||
|
|
||||||
|
if query != "" {
|
||||||
|
where = append(where, "p.id IN (SELECT id FROM projects_fts WHERE projects_fts MATCH ?)")
|
||||||
|
args = append(args, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := "SELECT * FROM projects p"
|
||||||
|
if len(where) > 0 {
|
||||||
|
sql += " WHERE " + strings.Join(where, " AND ")
|
||||||
|
}
|
||||||
|
sql += " ORDER BY p.name"
|
||||||
|
|
||||||
|
rows, err := db.conn.Query(sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("search projects: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
return scanProjects(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllProjects returns all project entries.
|
||||||
|
func (db *DB) ListAllProjects() ([]Project, error) {
|
||||||
|
return db.SearchProjects("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectApps returns all apps belonging to a project.
|
||||||
|
func (db *DB) GetProjectApps(projectID string) ([]App, error) {
|
||||||
|
rows, err := db.conn.Query("SELECT * FROM apps WHERE project_id = ? ORDER BY name", projectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return scanApps(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectAnalysis returns all analysis entries belonging to a project.
|
||||||
|
func (db *DB) GetProjectAnalysis(projectID string) ([]Analysis, error) {
|
||||||
|
rows, err := db.conn.Query("SELECT * FROM analysis WHERE project_id = ? ORDER BY name", projectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return scanAnalysis(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanProjects(rows interface{ Next() bool; Scan(...any) error }) ([]Project, error) {
|
||||||
|
var result []Project
|
||||||
|
for rows.Next() {
|
||||||
|
var p Project
|
||||||
|
var tagsJSON string
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&p.ID, &p.Name, &p.Description, &tagsJSON, &p.RepoURL, &p.DirPath,
|
||||||
|
&p.Documentation, &p.Notes, &p.ContentHash, &createdAt, &updatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning project: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Tags = unmarshalStrings(tagsJSON)
|
||||||
|
p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||||
|
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vault CRUD ---
|
||||||
|
|
||||||
|
// InsertVault inserts or replaces a vault entry.
|
||||||
|
func (db *DB) InsertVault(v *Vault) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if v.CreatedAt.IsZero() {
|
||||||
|
v.CreatedAt = now
|
||||||
|
}
|
||||||
|
if v.UpdatedAt.IsZero() {
|
||||||
|
v.UpdatedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
sym := 0
|
||||||
|
if v.Symlink {
|
||||||
|
sym = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.conn.Exec(`
|
||||||
|
INSERT OR REPLACE INTO vaults (
|
||||||
|
id, name, project_id, description, path, symlink, tags,
|
||||||
|
content_hash, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
v.ID, v.Name, v.ProjectID, v.Description, v.Path, sym, marshalStrings(v.Tags),
|
||||||
|
v.ContentHash, v.CreatedAt.Format(time.RFC3339), v.UpdatedAt.Format(time.RFC3339),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVault returns a single vault by ID.
|
||||||
|
func (db *DB) GetVault(id string) (*Vault, error) {
|
||||||
|
rows, err := db.conn.Query("SELECT * FROM vaults WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
vs, err := scanVaults(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(vs) == 0 {
|
||||||
|
return nil, fmt.Errorf("vault %q not found", id)
|
||||||
|
}
|
||||||
|
return &vs[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchVaults performs search on vaults with optional project filter.
|
||||||
|
func (db *DB) SearchVaults(query, projectID string) ([]Vault, error) {
|
||||||
|
where := []string{}
|
||||||
|
args := []any{}
|
||||||
|
|
||||||
|
if query != "" {
|
||||||
|
where = append(where, "name LIKE ? OR description LIKE ?")
|
||||||
|
q := "%" + query + "%"
|
||||||
|
args = append(args, q, q)
|
||||||
|
}
|
||||||
|
if projectID != "" {
|
||||||
|
where = append(where, "project_id = ?")
|
||||||
|
args = append(args, projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := "SELECT * FROM vaults"
|
||||||
|
if len(where) > 0 {
|
||||||
|
sql += " WHERE " + strings.Join(where, " AND ")
|
||||||
|
}
|
||||||
|
sql += " ORDER BY name"
|
||||||
|
|
||||||
|
rows, err := db.conn.Query(sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("search vaults: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
return scanVaults(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectVaults returns all vaults belonging to a project.
|
||||||
|
func (db *DB) GetProjectVaults(projectID string) ([]Vault, error) {
|
||||||
|
return db.SearchVaults("", projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanVaults(rows interface{ Next() bool; Scan(...any) error }) ([]Vault, error) {
|
||||||
|
var result []Vault
|
||||||
|
for rows.Next() {
|
||||||
|
var v Vault
|
||||||
|
var tagsJSON string
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
var sym int
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&v.ID, &v.Name, &v.ProjectID, &v.Description, &v.Path, &sym, &tagsJSON,
|
||||||
|
&v.ContentHash, &createdAt, &updatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Symlink = sym == 1
|
||||||
|
v.Tags = unmarshalStrings(tagsJSON)
|
||||||
|
v.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
v.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||||
|
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Function, error) {
|
func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Function, error) {
|
||||||
var result []Function
|
var result []Function
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
|||||||
@@ -237,6 +237,29 @@ func ValidateAnalysis(a *Analysis, knownFunctions, knownTypes map[string]bool) *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateProject checks integrity rules for projects.
|
||||||
|
func ValidateProject(p *Project) *ValidationError {
|
||||||
|
var errs []string
|
||||||
|
|
||||||
|
if p.ID == "" {
|
||||||
|
errs = append(errs, "id is required")
|
||||||
|
}
|
||||||
|
if p.Name == "" {
|
||||||
|
errs = append(errs, "name is required")
|
||||||
|
}
|
||||||
|
if p.Description == "" {
|
||||||
|
errs = append(errs, "description is required")
|
||||||
|
}
|
||||||
|
if p.DirPath != "" && strings.HasPrefix(p.DirPath, "/") {
|
||||||
|
errs = append(errs, "dir_path must be relative to registry root")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return &ValidationError{ID: p.ID, Errors: errs}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateType checks integrity rules for types.
|
// ValidateType checks integrity rules for types.
|
||||||
func ValidateType(t *Type, knownTypes map[string]bool) *ValidationError {
|
func ValidateType(t *Type, knownTypes map[string]bool) *ValidationError {
|
||||||
var errs []string
|
var errs []string
|
||||||
|
|||||||
Reference in New Issue
Block a user