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:
2026-04-12 17:29:41 +02:00
parent 1a3e77b0d5
commit 54e62ecb91
8 changed files with 647 additions and 19 deletions
+134 -5
View File
@@ -14,6 +14,8 @@ type IndexResult struct {
Types int
Apps int
Analysis int
Projects int
Vaults int
UnitTests int
ValidationErrors []string
Warnings []string
@@ -29,7 +31,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, err := db.LoadTimestamps()
oldFuncs, oldTypes, oldApps, oldAnalysis, oldProjects, oldVaults, err := db.LoadTimestamps()
if err != nil {
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
localAppIDs := make(map[string]bool)
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
localAnalysisIDs := make(map[string]bool)
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)
if err := db.PurgeLocalOnly(localAppIDs, localAnalysisIDs); err != nil {
// Parse projects from projects/*/project.md
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)
}
@@ -204,6 +309,30 @@ func Index(db *DB, root string) (*IndexResult, error) {
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
if err := db.PurgeUnitTests(); err != nil {
result.Warnings = append(result.Warnings, fmt.Sprintf("purging unit_tests: %v", err))