// c14 — adapter: builds a package-directory dependency DAG for a Go // module rooted at a given path, then computes graphDepth via the // pure helper in b32_graph_depth_polyglot.ts. // // Module-granularity per /sama/v2 §5 (operational) — see also the // comment at the top of b32_graph_depth_polyglot.ts. The TS metric // works at file level because TS module ≈ file. Go's natural unit // is the PACKAGE DIRECTORY: multiple .go files in one directory // share their package (and therefore their imports). graphDepth // here = longest path through the package-directory dependency // graph, restricted to intra-module edges (imports that begin with // the module's import path). // // Algorithm: // 1. Read /go.mod, parse `module ` to get module path. // 2. Walk every non-test .go file under , skipping .git/, // vendor/, node_modules/. // 3. For each file, parse its `import "..."` or `import (...)` // block; collect all imported package paths. // 4. For each import that starts with `/`, derive // the imported directory (repo-relative) by stripping the // module path prefix. // 5. The importing file's package directory = its repo-relative // directory. Add edge (importing-dir → imported-dir). // 6. Deduplicate edges (multiple files in one dir importing the // same target = one edge in the package DAG). // 7. Pass to computeGraphDepth. import { readdirSync, readFileSync, statSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { computeGraphDepth, type GraphDepthResult, } from "./b32_graph_depth_polyglot.ts"; const SKIPPED_DIRS: ReadonlySet = new Set([ ".git", "vendor", "node_modules", ]); interface GoFile { // Repo-relative path, e.g. "dive/filetree/comparer.go". path: string; content: string; } const collectGoFiles = (root: string): GoFile[] => { const out: GoFile[] = []; const walk = (absDir: string, relDir: string): void => { let entries: ReturnType; try { entries = readdirSync(absDir, { withFileTypes: true }); } catch { return; } for (const e of entries) { if (e.isDirectory()) { if (e.name.startsWith(".")) continue; if (SKIPPED_DIRS.has(e.name)) continue; const sub = resolve(absDir, e.name); const subRel = relDir === "" ? e.name : `${relDir}/${e.name}`; walk(sub, subRel); continue; } if (!e.isFile()) continue; if (!e.name.endsWith(".go")) continue; if (e.name.endsWith("_test.go")) continue; const abs = resolve(absDir, e.name); const repoPath = relDir === "" ? e.name : `${relDir}/${e.name}`; out.push({ path: repoPath, content: readFileSync(abs, "utf8") }); } }; walk(resolve(root), ""); return out; }; // Parse `module ` from go.mod content. export const parseGoModulePath = (gomod: string): string => { // go.mod is line-oriented; the directive is `module ` near // the top, possibly preceded by comments. const lines = gomod.split("\n"); for (const raw of lines) { const line = raw.replace(/\/\/.*$/, "").trim(); if (line.startsWith("module ")) { // "module github.com/wagoodman/dive" or "module \"...\"" const rest = line.slice(7).trim(); if (rest.startsWith('"') && rest.endsWith('"')) { return rest.slice(1, -1); } return rest; } } throw new Error("go.mod does not declare a `module` path"); }; // Extract every import path string from one .go file. Handles both // single-line `import "x"` and the block form `import (...)` with // possibly an alias before the string. Strips line comments first // (// ...) so a commented-out import doesn't false-positive. export const collectGoImports = (content: string): string[] => { // Drop // line comments and /* block comments */. const stripped = content .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/\/\/[^\n]*/g, ""); const out: string[] = []; // Single-line: import "..." or import alias "..." const singleRe = /^\s*import\s+(?:[A-Za-z_][\w]*\s+)?"([^"]+)"\s*$/gm; let m: RegExpExecArray | null; while ((m = singleRe.exec(stripped)) !== null) { if (m[1]) out.push(m[1]); } // Block form: import ( ... ) const blockRe = /^\s*import\s*\(\s*([\s\S]*?)\s*\)/gm; while ((m = blockRe.exec(stripped)) !== null) { const body = m[1] ?? ""; const lineRe = /^\s*(?:[A-Za-z_][\w]*\s+)?"([^"]+)"\s*$/gm; let lm: RegExpExecArray | null; while ((lm = lineRe.exec(body)) !== null) { if (lm[1]) out.push(lm[1]); } } return out; }; export interface GoGraphDepthResult extends GraphDepthResult { language: "go"; modulePath: string; } export const computeGoGraphDepth = (repoRoot: string): GoGraphDepthResult => { const root = resolve(repoRoot); const rootStat = statSync(root); if (!rootStat.isDirectory()) { throw new Error(`expected a directory, got: ${repoRoot}`); } const gomodPath = resolve(root, "go.mod"); const gomod = readFileSync(gomodPath, "utf8"); const modulePath = parseGoModulePath(gomod); const files = collectGoFiles(root); // Edges: (importing-dir, imported-dir). Deduplicate per-package. const nodeSet = new Set(); const edgeSet = new Set(); // key = "from→to" const edges: Array<[string, string]> = []; for (const f of files) { const fromDir = dirname(f.path); nodeSet.add(fromDir); for (const imp of collectGoImports(f.content)) { if (imp === modulePath) { // Root-package import (rare). Represented as repo-root dir "". const toDir = ""; nodeSet.add(toDir); const key = `${fromDir}${toDir}`; if (fromDir !== toDir && !edgeSet.has(key)) { edgeSet.add(key); edges.push([fromDir, toDir]); } continue; } if (imp.startsWith(modulePath + "/")) { const toDir = imp.slice(modulePath.length + 1); nodeSet.add(toDir); const key = `${fromDir}${toDir}`; if (fromDir !== toDir && !edgeSet.has(key)) { edgeSet.add(key); edges.push([fromDir, toDir]); } } } } // A pure helper that has no IMPORTED-only directories will declare // them too via the nodeSet (we added every imported intra-module // package). Good — the depth count includes leaves that don't // themselves import anything intra-module. const result = computeGraphDepth({ nodes: [...nodeSet], edges, }); return { ...result, language: "go", modulePath, }; };