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:
@@ -0,0 +1,225 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ModuleDriftCheck describes per-app drift between app.md uses_modules and
|
||||
// CMakeLists.txt fn_module_* link calls.
|
||||
type ModuleDriftCheck struct {
|
||||
AppID string `json:"app_id"`
|
||||
AppMD string `json:"app_md"`
|
||||
CMakeLists string `json:"cmake_lists"`
|
||||
Declared []string `json:"declared"` // module IDs from uses_modules
|
||||
Linked []string `json:"linked"` // module names from fn_module_<name>
|
||||
MissingLinks []string `json:"missing_links"` // declared but not linked
|
||||
ExtraLinks []string `json:"extra_links"` // linked but not declared
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
|
||||
var (
|
||||
cmakeLinkRE = regexp.MustCompile(`\bfn_module_([a-z0-9_]+)\b`)
|
||||
)
|
||||
|
||||
// AuditModulesDrift scans apps/*/app.md, projects/*/apps/*/app.md, cpp/apps/*/app.md
|
||||
// and compares uses_modules in the frontmatter against fn_module_<name> link calls
|
||||
// in the adjacent CMakeLists.txt.
|
||||
//
|
||||
// An app is OK when:
|
||||
// - It has no CMakeLists.txt (non-C++ app) — drift check N/A; skipped silently.
|
||||
// - declared (modulo `_<lang>` suffix) == linked.
|
||||
func AuditModulesDrift(root string) ([]ModuleDriftCheck, error) {
|
||||
candidates, err := findAppDirs(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []ModuleDriftCheck
|
||||
for _, dir := range candidates {
|
||||
appMD := filepath.Join(dir, "app.md")
|
||||
cmakeLists := filepath.Join(dir, "CMakeLists.txt")
|
||||
|
||||
if _, err := os.Stat(cmakeLists); err != nil {
|
||||
// Non-C++ app or app without CMakeLists. Skip drift check.
|
||||
continue
|
||||
}
|
||||
|
||||
declared, appID, err := readUsesModules(appMD)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
linked, err := readLinkedModules(cmakeLists)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Normalize declared (module IDs like "data_table_cpp") to module names
|
||||
// for comparison with link target names ("fn_module_data_table" -> "data_table").
|
||||
declaredNames := make([]string, 0, len(declared))
|
||||
for _, d := range declared {
|
||||
declaredNames = append(declaredNames, stripLangSuffix(d))
|
||||
}
|
||||
|
||||
missing := diffStrings(declaredNames, linked)
|
||||
extra := diffStrings(linked, declaredNames)
|
||||
|
||||
relMD, _ := filepath.Rel(root, appMD)
|
||||
relCM, _ := filepath.Rel(root, cmakeLists)
|
||||
|
||||
result = append(result, ModuleDriftCheck{
|
||||
AppID: appID,
|
||||
AppMD: relMD,
|
||||
CMakeLists: relCM,
|
||||
Declared: declaredNames,
|
||||
Linked: linked,
|
||||
MissingLinks: missing,
|
||||
ExtraLinks: extra,
|
||||
OK: len(missing) == 0 && len(extra) == 0,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool { return result[i].AppID < result[j].AppID })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// findAppDirs returns directories that contain an app.md file:
|
||||
// - <root>/apps/*/
|
||||
// - <root>/projects/*/apps/*/
|
||||
func findAppDirs(root string) ([]string, error) {
|
||||
var dirs []string
|
||||
|
||||
// <root>/apps/*/
|
||||
appsRoot := filepath.Join(root, "apps")
|
||||
if entries, err := os.ReadDir(appsRoot); err == nil {
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
candidate := filepath.Join(appsRoot, e.Name())
|
||||
if _, err := os.Stat(filepath.Join(candidate, "app.md")); err == nil {
|
||||
dirs = append(dirs, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <root>/projects/*/apps/*/
|
||||
projectsRoot := filepath.Join(root, "projects")
|
||||
if projEntries, err := os.ReadDir(projectsRoot); err == nil {
|
||||
for _, pe := range projEntries {
|
||||
if !pe.IsDir() {
|
||||
continue
|
||||
}
|
||||
projAppsDir := filepath.Join(projectsRoot, pe.Name(), "apps")
|
||||
if appEntries, err := os.ReadDir(projAppsDir); err == nil {
|
||||
for _, ae := range appEntries {
|
||||
if !ae.IsDir() {
|
||||
continue
|
||||
}
|
||||
candidate := filepath.Join(projAppsDir, ae.Name())
|
||||
if _, err := os.Stat(filepath.Join(candidate, "app.md")); err == nil {
|
||||
dirs = append(dirs, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dirs, nil
|
||||
}
|
||||
|
||||
type appFrontmatter struct {
|
||||
Name string `yaml:"name"`
|
||||
Lang string `yaml:"lang"`
|
||||
Domain string `yaml:"domain"`
|
||||
UsesModules []string `yaml:"uses_modules"`
|
||||
}
|
||||
|
||||
func readUsesModules(path string) (modules []string, appID string, err error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// Extract YAML frontmatter between leading "---" markers.
|
||||
if !strings.HasPrefix(string(data), "---") {
|
||||
return nil, "", fmt.Errorf("missing frontmatter in %s", path)
|
||||
}
|
||||
rest := string(data)[4:]
|
||||
end := strings.Index(rest, "\n---")
|
||||
if end < 0 {
|
||||
return nil, "", fmt.Errorf("missing closing --- in %s", path)
|
||||
}
|
||||
fm := rest[:end]
|
||||
|
||||
var raw appFrontmatter
|
||||
if err := yaml.Unmarshal([]byte(fm), &raw); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if raw.Name == "" {
|
||||
return nil, "", fmt.Errorf("no name in %s", path)
|
||||
}
|
||||
id := raw.Name
|
||||
if raw.Lang != "" {
|
||||
id += "_" + raw.Lang
|
||||
}
|
||||
if raw.Domain != "" {
|
||||
id += "_" + raw.Domain
|
||||
}
|
||||
return raw.UsesModules, id, nil
|
||||
}
|
||||
|
||||
func readLinkedModules(cmakePath string) ([]string, error) {
|
||||
data, err := os.ReadFile(cmakePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches := cmakeLinkRE.FindAllStringSubmatch(string(data), -1)
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, m := range matches {
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
if !seen[m[1]] {
|
||||
seen[m[1]] = true
|
||||
out = append(out, m[1])
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// stripLangSuffix removes a trailing _<lang> suffix for matching purposes.
|
||||
// "data_table_cpp" -> "data_table".
|
||||
func stripLangSuffix(id string) string {
|
||||
for _, suf := range []string{"_cpp", "_py", "_ts", "_bash", "_go"} {
|
||||
if strings.HasSuffix(id, suf) {
|
||||
return id[:len(id)-len(suf)]
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// diffStrings returns elements in a that are not in b.
|
||||
func diffStrings(a, b []string) []string {
|
||||
bset := map[string]bool{}
|
||||
for _, x := range b {
|
||||
bset[x] = true
|
||||
}
|
||||
var out []string
|
||||
for _, x := range a {
|
||||
if !bset[x] {
|
||||
out = append(out, x)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: audit_modules_drift
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "AuditModulesDrift(root string) ([]ModuleDriftCheck, error)"
|
||||
description: "Detecta drift entre app.md uses_modules y CMakeLists.txt fn_module_<name> link calls. Para cada app C++ con CMakeLists.txt: parsea uses_modules + regex sobre target_link_libraries. Devuelve por-app: declared/linked/missing/extra/OK."
|
||||
tags: [audit, modules, cmake, drift, doctor, cpp]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- gopkg.in/yaml.v3
|
||||
file_path: "functions/infra/audit_modules_drift.go"
|
||||
params:
|
||||
- name: root
|
||||
desc: "Raiz del repositorio fn_registry. Se escanean apps/*, projects/*/apps/*."
|
||||
output: "Slice de ModuleDriftCheck (uno por app C++ con CMakeLists.txt). Apps sin CMakeLists son saltadas."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
import "fn-registry/functions/infra"
|
||||
|
||||
checks, err := infra.AuditModulesDrift("/home/lucas/fn_registry")
|
||||
if err != nil { panic(err) }
|
||||
for _, c := range checks {
|
||||
if !c.OK {
|
||||
fmt.Printf("DRIFT %s: missing=%v extra=%v\n", c.AppID, c.MissingLinks, c.ExtraLinks)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tambien expuesto via CLI:
|
||||
|
||||
```bash
|
||||
fn doctor modules # tabla legible
|
||||
fn doctor modules --json # JSON para agentes
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras anadir/quitar un modulo a la app:
|
||||
- Verifica que el `uses_modules` del `app.md` y `target_link_libraries(... PRIVATE fn_module_*)` del CMakeLists.txt coinciden.
|
||||
- Tras renombrar un modulo, detecta apps que quedaron con la version antigua.
|
||||
- Como gate en `/full-git-push` antes de mergear cambios de modulos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Apps sin `CMakeLists.txt` (Python, bash, etc.) se saltan — el drift check no aplica.
|
||||
- Modulos IDs en `uses_modules` llevan sufijo `_<lang>` (ej. `data_table_cpp`); los link targets son `fn_module_<name>` (sin sufijo). La funcion strippa el sufijo antes de comparar.
|
||||
- Regex acepta `fn_module_<name>` en cualquier parte del CMakeLists — comentarios incluidos. Si un comentario referencia un modulo no usado, se reporta como `extra_links` (falso positivo aceptable).
|
||||
Reference in New Issue
Block a user