package infra import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) // AppDriftEntry describes an app and its module drift state relative to the registry. type AppDriftEntry struct { AppName string `json:"app_name"` AppID string `json:"app_id"` DirPath string `json:"dir_path"` BuildKind string `json:"build_kind"` // "windows-deployed" | "windows-build" | "linux-build" | "" BinaryPath string `json:"binary_path"` // path inspected (or modules_generated.cpp) LinkedModules map[string]string `json:"linked_modules"` // module name -> embedded version RegistryModules map[string]string `json:"registry_modules"` // module name -> registry version Stale []string `json:"stale"` // modules with drift (linked != registry) Status string `json:"status"` // "ok" | "drift" | "no-build" } // AuditAppDrift scans all C++ apps in the registry, reads the module versions // embedded in their compiled artifacts, and compares them against the versions // declared in modules/*/module.md. // // Binary inspection priority (first found wins): // 1. windows-deployed: //.exe — PE binary search // 2. windows-build: cpp/build/windows/apps//_modules_generated.cpp // 3. linux-build: cpp/build/linux/apps//_modules_generated.cpp // // If windowsDeployRoot is empty, only cpp/build paths are checked. // registryRoot is the absolute path to the fn_registry root. func AuditAppDrift(registryRoot string, windowsDeployRoot string) ([]AppDriftEntry, error) { // 1. Read registry module versions from modules/*/module.md registryMods, err := readRegistryModules(registryRoot) if err != nil { return nil, fmt.Errorf("audit_app_drift: read registry modules: %w", err) } // 2. Find all C++ app.md files. appDirs, err := findAppDirs(registryRoot) if err != nil { return nil, fmt.Errorf("audit_app_drift: find app dirs: %w", err) } var result []AppDriftEntry for _, dir := range appDirs { appMDPath := filepath.Join(dir, "app.md") meta, err := readCppAppMeta(appMDPath) if err != nil || meta.Lang != "cpp" { continue } entry := AppDriftEntry{ AppName: meta.Name, AppID: meta.ID, DirPath: meta.DirPath, LinkedModules: map[string]string{}, RegistryModules: registryMods, } // 3. Find artifact and extract linked module versions. kind, artifactPath, linked := resolveLinkedVersions( meta.Name, registryRoot, windowsDeployRoot, ) entry.BuildKind = kind entry.BinaryPath = artifactPath entry.LinkedModules = linked if kind == "" || linked == nil { entry.Status = "no-build" result = append(result, entry) continue } // 4. Compute drift: only for modules that are both linked AND in registry. var stale []string for modName, linkedVer := range linked { regVer, inRegistry := registryMods[modName] if !inRegistry { continue } if linkedVer != regVer { stale = append(stale, modName) } } sort.Strings(stale) entry.Stale = stale if len(stale) > 0 { entry.Status = "drift" } else { entry.Status = "ok" } result = append(result, entry) } sort.Slice(result, func(i, j int) bool { return result[i].AppID < result[j].AppID }) return result, nil } // readRegistryModules reads modules/*/module.md and returns name -> version map. // Uses stdlib only: reads files line by line, extracts name/version with regex. func readRegistryModules(root string) (map[string]string, error) { modulesDir := filepath.Join(root, "modules") entries, err := os.ReadDir(modulesDir) if err != nil { if os.IsNotExist(err) { return map[string]string{}, nil } return nil, err } out := map[string]string{} for _, e := range entries { if !e.IsDir() { continue } mdPath := filepath.Join(modulesDir, e.Name(), "module.md") name, version, err := parseModuleMD(mdPath) if err != nil || name == "" || version == "" { continue } out[name] = version } return out, nil } var ( moduleNameRE = regexp.MustCompile(`(?m)^name:\s*(.+?)\s*$`) moduleVersionRE = regexp.MustCompile(`(?m)^version:\s*(.+?)\s*$`) ) // parseModuleMD extracts name and version from a module.md frontmatter using // simple line-by-line regex (no external YAML deps). func parseModuleMD(path string) (name, version string, err error) { data, err := os.ReadFile(path) if err != nil { return "", "", err } // Restrict to frontmatter only (between --- markers). fm := extractFrontmatter(string(data)) if m := moduleNameRE.FindStringSubmatch(fm); len(m) > 1 { name = strings.TrimSpace(strings.Trim(m[1], `"`)) } if m := moduleVersionRE.FindStringSubmatch(fm); len(m) > 1 { version = strings.TrimSpace(strings.Trim(m[1], `"`)) } return name, version, nil } // extractFrontmatter returns the YAML block between the first --- pair, or the // full content if no markers found. func extractFrontmatter(content string) string { if !strings.HasPrefix(content, "---") { return content } rest := content[3:] // Skip optional newline after opening --- if strings.HasPrefix(rest, "\n") { rest = rest[1:] } end := strings.Index(rest, "\n---") if end < 0 { return rest } return rest[:end] } // cppAppMeta holds the fields we need from an app.md for C++ apps. type cppAppMeta struct { Name string ID string Lang string Domain string DirPath string } var ( appLangRE = regexp.MustCompile(`(?m)^lang:\s*(.+?)\s*$`) appNameRE = regexp.MustCompile(`(?m)^name:\s*(.+?)\s*$`) appDomainRE = regexp.MustCompile(`(?m)^domain:\s*(.+?)\s*$`) appDirPathRE = regexp.MustCompile(`(?m)^dir_path:\s*"?(.+?)"?\s*$`) ) // readCppAppMeta extracts name, lang, domain and dir_path from an app.md. func readCppAppMeta(path string) (cppAppMeta, error) { data, err := os.ReadFile(path) if err != nil { return cppAppMeta{}, err } fm := extractFrontmatter(string(data)) var meta cppAppMeta if m := appNameRE.FindStringSubmatch(fm); len(m) > 1 { meta.Name = strings.TrimSpace(strings.Trim(m[1], `"`)) } if m := appLangRE.FindStringSubmatch(fm); len(m) > 1 { meta.Lang = strings.TrimSpace(strings.Trim(m[1], `"`)) } if m := appDomainRE.FindStringSubmatch(fm); len(m) > 1 { meta.Domain = strings.TrimSpace(strings.Trim(m[1], `"`)) } if m := appDirPathRE.FindStringSubmatch(fm); len(m) > 1 { meta.DirPath = strings.TrimSpace(strings.Trim(m[1], `"`)) } // Derive ID: name_lang_domain meta.ID = meta.Name if meta.Lang != "" { meta.ID += "_" + meta.Lang } if meta.Domain != "" { meta.ID += "_" + meta.Domain } return meta, nil } // resolveLinkedVersions finds the best artifact for a given app and extracts // module name->version from it. // // Priority: windows-deployed > windows-build > linux-build. // Returns ("", "", nil) when no artifact is found. func resolveLinkedVersions( appName string, registryRoot string, windowsDeployRoot string, ) (kind string, artifactPath string, linked map[string]string) { // 1. Windows deployed exe. if windowsDeployRoot != "" { exePath := filepath.Join(windowsDeployRoot, appName, appName+".exe") if _, err := os.Stat(exePath); err == nil { if mods := extractModulesFromBinary(exePath); len(mods) > 0 { return "windows-deployed", exePath, mods } } } // 2. Windows build modules_generated.cpp. winGenCPP := filepath.Join( registryRoot, "cpp", "build", "windows", "apps", appName, appName+"_modules_generated.cpp", ) if _, err := os.Stat(winGenCPP); err == nil { if mods := extractModulesFromGeneratedCPP(winGenCPP); len(mods) > 0 { return "windows-build", winGenCPP, mods } } // 3. Linux build modules_generated.cpp. linuxGenCPP := filepath.Join( registryRoot, "cpp", "build", "linux", "apps", appName, appName+"_modules_generated.cpp", ) if _, err := os.Stat(linuxGenCPP); err == nil { if mods := extractModulesFromGeneratedCPP(linuxGenCPP); len(mods) > 0 { return "linux-build", linuxGenCPP, mods } } return "", "", nil } // binaryModuleRE matches the pattern \x00\x00 as it // appears in PE (Windows) binaries compiled from the generated ModuleInfo struct. // Group 1 = module name, group 2 = version string. var binaryModuleRE = regexp.MustCompile(`([a-z][a-z0-9_]{1,63})\x00(\d+\.\d+\.\d+)\x00`) // extractModulesFromBinary scans a PE binary for the contiguous pattern // \x00\x00 that the MSVC/MinGW compiler emits for the // ModuleInfo struct string literals. Returns nil when no matches are found. // // Note: ELF (Linux) binaries may not have this contiguous layout — use // extractModulesFromGeneratedCPP instead for Linux builds. func extractModulesFromBinary(path string) map[string]string { data, err := os.ReadFile(path) if err != nil { return nil } out := map[string]string{} for _, m := range binaryModuleRE.FindAllSubmatch(data, -1) { if len(m) < 3 { continue } name := string(m[1]) ver := string(m[2]) if _, seen := out[name]; !seen { out[name] = ver } } if len(out) == 0 { return nil } return out } // generatedModuleRE matches lines like: { "data_table", "1.5.0", "..." } var generatedModuleRE = regexp.MustCompile(`\{\s*"([a-z][a-z0-9_]*)"\s*,\s*"(\d+\.\d+\.\d+)"`) // extractModulesFromGeneratedCPP parses a *_modules_generated.cpp file and // returns module name -> version. This is the authoritative source for Linux // builds and a reliable fallback for Windows builds. func extractModulesFromGeneratedCPP(path string) map[string]string { data, err := os.ReadFile(path) if err != nil { return nil } out := map[string]string{} for _, m := range generatedModuleRE.FindAllSubmatch(data, -1) { if len(m) < 3 { continue } name := string(m[1]) ver := string(m[2]) if _, seen := out[name]; !seen { out[name] = ver } } if len(out) == 0 { return nil } return out }