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:
+134
-5
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user