mason-context 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mason-audit.js +1 -0
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-mcp.js +1 -1
- package/package.json +1 -1
package/dist/mason-audit.js
CHANGED
|
@@ -231,6 +231,7 @@ function normalizePathToken(token) {
|
|
|
231
231
|
if (t.includes(":")) return null;
|
|
232
232
|
const normalized = t.replace(/\/+$/, "");
|
|
233
233
|
if (!normalized) return null;
|
|
234
|
+
if (normalized.split("/").some((seg) => /^\.+$/.test(seg))) return null;
|
|
234
235
|
if (normalized.includes("/")) return normalized;
|
|
235
236
|
return ROOT_FILE_NAMES.has(normalized) ? normalized : null;
|
|
236
237
|
}
|
package/dist/mason-audit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audit/cli.ts","../src/audit/audit.ts","../src/drift/drift.ts","../src/snapshot/snapshot.ts","../src/mcp/sampler.ts","../src/test-map.ts","../src/audit/docs.ts","../src/audit/tree.ts","../src/audit/claims.ts","../src/audit/git.ts","../src/audit/types.ts","../src/audit/checks/deleted-reference.ts","../src/audit/checks/new-module.ts","../src/audit/checks/stale-count.ts","../src/audit/checks/dead-command.ts","../src/audit/checks/deps-changed.ts","../src/decisions/drift.ts","../src/decisions/decisions.ts","../src/context/lexical.ts","../src/audit/checks/decision-anchor.ts","../src/audit/checks/index.ts","../bin/mason-audit.ts"],"sourcesContent":["import path from \"node:path\";\nimport { computeAudit } from \"./audit.js\";\nimport { ALL_CHECKS } from \"./types.js\";\nimport type { AuditIssue, AuditReport, CheckName } from \"./types.js\";\n\nexport const USAGE = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]\n\nAudits the repo's AI context files (CLAUDE.md, .claude/CLAUDE.md, AGENTS.md)\nagainst repo reality: referenced paths that no longer exist, undocumented\nmodules, stale counts, dead npm scripts, and manifests newer than the doc.\nDeterministic: no LLM call, no network – safe for CI. Works on any repo with\na context file; no Mason setup required.\n\nOptions:\n --dir <path> Project root to audit (default: current directory)\n --json Print the full audit report as JSON (additive-only schema)\n --fix-prompt When issues exist, print a work order for ANY coding agent\n (Claude, Codex, Gemini, ...) – pipe it to your agent CLI to\n close the loop. Prints the clean summary when there are none.\n --checks <list> Comma-separated subset of checks to run (default: all):\n ${ALL_CHECKS.join(\", \")}\n --help Show this help\n\nExit codes:\n 0 no issues (advisories may still be present)\n 1 provable issues found\n 2 error (no context file, not a git repository, bad arguments)`;\n\nexport interface AuditCliIo {\n out: (line: string) => void;\n err: (line: string) => void;\n}\n\ninterface ParsedArgs {\n dir: string;\n json: boolean;\n fixPrompt: boolean;\n help: boolean;\n checks: CheckName[] | undefined;\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const parsed: ParsedArgs = {\n dir: process.cwd(),\n json: false,\n fixPrompt: false,\n help: false,\n checks: undefined,\n };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === \"--json\") {\n parsed.json = true;\n } else if (arg === \"--fix-prompt\") {\n parsed.fixPrompt = true;\n } else if (arg === \"--help\" || arg === \"-h\") {\n parsed.help = true;\n } else if (arg === \"--dir\") {\n const value = argv[++i];\n if (!value) throw new Error(\"--dir requires a path argument\");\n parsed.dir = value;\n } else if (arg === \"--checks\") {\n const value = argv[++i];\n if (!value) throw new Error(\"--checks requires a comma-separated list\");\n const names = value.split(\",\").map((n) => n.trim()).filter(Boolean);\n for (const name of names) {\n if (!ALL_CHECKS.includes(name as CheckName)) {\n throw new Error(\n `Unknown check: ${name} (valid: ${ALL_CHECKS.join(\", \")})`\n );\n }\n }\n parsed.checks = names as CheckName[];\n } else if (!arg.startsWith(\"-\") && parsed.dir === process.cwd()) {\n parsed.dir = arg;\n } else {\n throw new Error(`Unknown argument: ${arg}`);\n }\n }\n return parsed;\n}\n\nfunction issueLine(issue: AuditIssue): string {\n const where =\n issue.anchor.line !== null ? `line ${issue.anchor.line}` : \"doc-level\";\n const likely = issue.confidence === \"likely\" ? \" (likely)\" : \"\";\n return ` [${issue.type}]${likely} ${where}: ${issue.message}`;\n}\n\nexport function formatAuditSummary(report: AuditReport): string {\n const lines: string[] = [];\n\n for (const doc of report.docs) {\n const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);\n const committed = doc.lastCommit\n ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}`\n : \"untracked\";\n if (docIssues.length === 0) {\n lines.push(`${doc.path} – clean (${committed})`);\n continue;\n }\n lines.push(\n `${doc.path} – ${docIssues.length} issue${docIssues.length === 1 ? \"\" : \"s\"} (${committed})`\n );\n for (const issue of docIssues) lines.push(issueLine(issue));\n }\n\n // Issues anchored outside the docs list (defensive; new-module anchors to\n // the primary doc, so this should stay empty).\n const docPaths = new Set(report.docs.map((d) => d.path));\n for (const issue of report.issues) {\n if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));\n }\n\n if (report.advisories.length > 0) {\n lines.push(\"Advisories (do not affect the exit code):\");\n for (const advisory of report.advisories) {\n lines.push(` [${advisory.type}] ${advisory.anchor.doc}: ${advisory.message}`);\n }\n }\n if (report.skippedChecks.length > 0) {\n for (const skip of report.skippedChecks) {\n lines.push(` [skipped] ${skip.check}: ${skip.reason}`);\n }\n }\n\n lines.push(\n report.clean\n ? `Context files are clean (${report.docs.length} doc${report.docs.length === 1 ? \"\" : \"s\"} audited).`\n : `${report.issues.length} issue${report.issues.length === 1 ? \"\" : \"s\"} across ${report.docs.length} doc${report.docs.length === 1 ? \"\" : \"s\"}.`\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * Provider-neutral work order: any coding agent can execute it. The evidence\n * is deterministic; the agent's job is judgment scoped to exactly these\n * claims – never a free-form doc rewrite.\n */\nexport function formatFixPrompt(report: AuditReport): string {\n const flaggedDocs = [...new Set(report.issues.map((i) => i.anchor.doc))];\n const lines: string[] = [];\n lines.push(\n \"The AI context files in this repository contain claims that are provably out of date. Fix ONLY the flagged claims. Work autonomously; do not ask questions.\"\n );\n lines.push(\"\");\n lines.push(\"RULES:\");\n lines.push(\n `- Edit ONLY these files: ${flaggedDocs.join(\", \")}. Never modify source code, configs, or anything else – the docs must be brought to match the code, not the other way around.`\n );\n lines.push(\n \"- Keep diffs minimal: change the smallest span that makes each claim true.\"\n );\n lines.push(\n \"- Never invent content. Every replacement must be grounded in the evidence below or in files you read from this repository.\"\n );\n lines.push(\n \"- deleted-reference: if evidence shows renamedTo, update the path; otherwise remove the reference, or rephrase to past tense if the sentence is about history. Deleted paths inside directory trees: delete the tree line.\"\n );\n lines.push(\n \"- stale-count: replace the number with the actual count from the evidence.\"\n );\n lines.push(\n \"- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention.\"\n );\n lines.push(\n \"- new-module: add a one-line factual mention of the directory where sibling modules are described; read the directory's files first and describe only what you verified.\"\n );\n lines.push(\n \"- Do NOT touch anything listed under ADVISORIES – list them in your summary for human review instead.\"\n );\n lines.push(\"\");\n lines.push(\"AUDIT REPORT (deterministic, computed against git HEAD):\");\n lines.push(\n JSON.stringify(\n { issues: report.issues, advisories: report.advisories },\n null,\n 2\n )\n );\n lines.push(\"\");\n lines.push(\n \"Finish by summarizing each edit and citing the evidence item it resolves.\"\n );\n return lines.join(\"\\n\");\n}\n\nexport async function runAuditCli(\n argv: string[],\n io: AuditCliIo = {\n out: (line) => process.stdout.write(`${line}\\n`),\n err: (line) => process.stderr.write(`${line}\\n`),\n }\n): Promise<number> {\n let args: ParsedArgs;\n try {\n args = parseArgs(argv);\n if (args.json && args.fixPrompt) {\n throw new Error(\"--json and --fix-prompt are mutually exclusive\");\n }\n } catch (error) {\n io.err(error instanceof Error ? error.message : String(error));\n io.err(USAGE);\n return 2;\n }\n\n if (args.help) {\n io.out(USAGE);\n return 0;\n }\n\n const rootDir = path.resolve(args.dir);\n const report = await computeAudit(rootDir, { checks: args.checks });\n\n if (!report) {\n io.err(\n `No CLAUDE.md, .claude/CLAUDE.md, or AGENTS.md found in ${rootDir}.`\n );\n return 2;\n }\n\n if (!report.gitAvailable) {\n io.err(\n `Could not determine git HEAD in ${rootDir} – not a git repository, or git is unavailable.`\n );\n return 2;\n }\n\n if (args.fixPrompt) {\n io.out(\n report.clean ? formatAuditSummary(report) : formatFixPrompt(report)\n );\n return report.clean ? 0 : 1;\n }\n\n if (args.json) {\n io.out(JSON.stringify(report, null, 2));\n } else {\n io.out(formatAuditSummary(report));\n }\n return report.clean ? 0 : 1;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getChangesWithStatus } from \"../drift/drift.js\";\nimport type { FileChange } from \"../drift/drift.js\";\nimport { getCurrentGitHash } from \"../snapshot/snapshot.js\";\nimport { discoverDocs } from \"./docs.js\";\nimport { ALL_CHECKS } from \"./types.js\";\nimport type { AuditReport, CheckName } from \"./types.js\";\nimport { CHECKS } from \"./checks/index.js\";\nimport type { CheckContext } from \"./checks/index.js\";\n\nexport interface AuditOptions {\n /** Subset of checks to run; defaults to all. */\n checks?: CheckName[];\n}\n\n/**\n * Audit the repo's context files (CLAUDE.md, .claude/CLAUDE.md, AGENTS.md)\n * against repo reality. Fully deterministic — git, filesystem, and lexical\n * extraction only; no LLM, no network. Returns null when no context file\n * exists.\n */\nexport async function computeAudit(\n rootDir: string,\n options: AuditOptions = {}\n): Promise<AuditReport | null> {\n const resolvedRoot = path.resolve(rootDir);\n const docs = await discoverDocs(resolvedRoot);\n if (docs.length === 0) return null;\n\n const headHash = await getCurrentGitHash(resolvedRoot);\n const report: AuditReport = {\n version: 1,\n root: resolvedRoot,\n gitAvailable: headHash !== \"unknown\",\n docs: docs.map((d) => ({\n path: d.path,\n lastCommit: d.lastCommit,\n dirty: d.dirty,\n lineCount: d.lineCount,\n })),\n decisionsChecked: false,\n issues: [],\n advisories: [],\n skippedChecks: [],\n clean: true,\n };\n\n // Without git the provability gates cannot run — the caller treats this\n // as an error rather than silently degrading precision.\n if (!report.gitAvailable) return report;\n\n const changesSinceDoc = new Map<string, FileChange[] | null>();\n for (const doc of docs) {\n changesSinceDoc.set(\n doc.path,\n doc.lastCommit\n ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash)\n : null\n );\n }\n\n let decisionsPresent = false;\n try {\n await fs.access(path.join(resolvedRoot, \".mason\", \"decisions\"));\n decisionsPresent = true;\n } catch {\n // No decision store — the check stays dark (zero-setup path).\n }\n report.decisionsChecked = decisionsPresent;\n\n const ctx: CheckContext = {\n root: resolvedRoot,\n docs,\n headHash,\n changesSinceDoc,\n decisionsPresent,\n };\n\n const selected = options.checks ?? ALL_CHECKS;\n for (const name of ALL_CHECKS) {\n if (!selected.includes(name)) continue;\n const { issues, advisories, skipped } = await CHECKS[name](ctx);\n report.issues.push(...issues);\n report.advisories.push(...advisories);\n report.skippedChecks.push(...skipped);\n }\n\n report.clean = report.issues.length === 0;\n return report;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport {\n loadSnapshot,\n getCurrentGitHash,\n listSourceFiles,\n} from \"../snapshot/snapshot.js\";\nimport type { Snapshot } from \"../snapshot/snapshot.js\";\n\nconst exec = promisify(execFile);\n\n// Incremental refresh stops paying off once a large share of the map is\n// touched — but small absolute counts are always cheap to refresh in place,\n// so both thresholds must be exceeded before recommending a full rebuild.\nconst FULL_REBUILD_FRACTION = 0.4;\nconst FULL_REBUILD_MIN_CHANGED_MAPPED_FILES = 10;\n\nexport type ChangeStatus = \"added\" | \"modified\" | \"deleted\" | \"renamed\";\n\nexport interface FileChange {\n status: ChangeStatus;\n /** Current path (the new path for renames). */\n path: string;\n /** Pre-rename path, only present for renames. */\n previousPath?: string;\n}\n\nexport type DriftRecommendation = \"up-to-date\" | \"incremental\" | \"full-rebuild\";\n\nexport interface DriftReport {\n stale: boolean;\n snapshotHash: string;\n headHash: string;\n /** Commits between the snapshot and HEAD; null when history is unavailable. */\n commitsBehind: number | null;\n /**\n * False when the snapshot commit is unreachable (shallow clone, rewritten\n * history) — staleFeatures/unmappedFiles/renames cannot be computed then.\n */\n historyAvailable: boolean;\n /** Current paths of every file changed since the snapshot. */\n changedFiles: string[];\n /** Stale feature name → the mapped files that changed under it. */\n staleFeatures: Record<string, string[]>;\n /** Stale flow name → the chain files that changed under it. */\n staleFlows: Record<string, string[]>;\n totalFeatures: number;\n totalFlows: number;\n /** New source files not referenced by any feature or flow. */\n unmappedFiles: string[];\n /** Files referenced by the map that no longer exist on disk. */\n ghostFiles: string[];\n renames: Array<{ from: string; to: string }>;\n recommendation: DriftRecommendation;\n}\n\nexport async function getChangesWithStatus(\n resolvedRoot: string,\n fromHash: string\n): Promise<FileChange[] | null> {\n if (!fromHash || fromHash === \"unknown\") return null;\n try {\n const { stdout } = await exec(\n \"git\",\n [\"diff\", \"--name-status\", \"-M\", fromHash, \"HEAD\"],\n { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }\n );\n\n const changes: FileChange[] = [];\n for (const line of stdout.split(\"\\n\")) {\n if (!line.trim()) continue;\n const parts = line.split(\"\\t\");\n // Mason's own metadata changes on every save — never count it as drift.\n if (parts.some((p) => p.startsWith(\".mason/\"))) continue;\n const code = parts[0];\n if (code.startsWith(\"R\") && parts.length >= 3) {\n changes.push({\n status: \"renamed\",\n path: parts[2],\n previousPath: parts[1],\n });\n } else if (code.startsWith(\"C\") && parts.length >= 3) {\n // A copy leaves the original in place — only the new path is a change.\n changes.push({ status: \"added\", path: parts[2] });\n } else if (code === \"A\" && parts.length >= 2) {\n changes.push({ status: \"added\", path: parts[1] });\n } else if (code === \"D\" && parts.length >= 2) {\n changes.push({ status: \"deleted\", path: parts[1] });\n } else if (parts.length >= 2) {\n // M, T (typechange), and anything unrecognized count as modified.\n changes.push({ status: \"modified\", path: parts[1] });\n }\n }\n return changes;\n } catch {\n return null;\n }\n}\n\nasync function countCommitsBehind(\n resolvedRoot: string,\n fromHash: string\n): Promise<number | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"rev-list\", \"--count\", `${fromHash}..HEAD`],\n { cwd: resolvedRoot }\n );\n const count = Number.parseInt(stdout.trim(), 10);\n return Number.isNaN(count) ? null : count;\n } catch {\n return null;\n }\n}\n\nfunction collectMappedFiles(snapshot: Snapshot): Set<string> {\n const mappedFiles = new Set<string>();\n for (const feature of Object.values(snapshot.features)) {\n for (const f of feature.files) mappedFiles.add(f);\n for (const t of feature.tests ?? []) mappedFiles.add(t);\n }\n for (const flow of Object.values(snapshot.flows)) {\n for (const f of flow.chain) mappedFiles.add(f);\n }\n return mappedFiles;\n}\n\nasync function findGhostFiles(\n resolvedRoot: string,\n mappedFiles: Set<string>\n): Promise<string[]> {\n const ghosts: string[] = [];\n for (const file of mappedFiles) {\n try {\n await fs.access(path.join(resolvedRoot, file));\n } catch {\n ghosts.push(file);\n }\n }\n return ghosts.sort();\n}\n\n/**\n * Compare the concept map against HEAD and report feature-level drift.\n * Fully deterministic — git + filesystem only, no LLM involved.\n * Returns null when no snapshot exists.\n */\nexport async function computeDrift(\n rootDir: string\n): Promise<DriftReport | null> {\n const resolvedRoot = path.resolve(rootDir);\n const snapshot = await loadSnapshot(resolvedRoot);\n if (!snapshot) return null;\n\n const headHash = await getCurrentGitHash(resolvedRoot);\n const totalFeatures = Object.keys(snapshot.features).length;\n const totalFlows = Object.keys(snapshot.flows).length;\n\n // Each entry is only verified as of its refreshedHash (falling back to the\n // top-level gitHash), so drift is evaluated per distinct hash — a partially\n // refreshed map can be fresh at the top level and still hold stale entries.\n const hashFor = (entry: { refreshedHash?: string }): string =>\n entry.refreshedHash ?? snapshot.gitHash;\n\n const distinctHashes = new Set<string>([snapshot.gitHash]);\n for (const feature of Object.values(snapshot.features)) {\n distinctHashes.add(hashFor(feature));\n }\n for (const flow of Object.values(snapshot.flows)) {\n distinctHashes.add(hashFor(flow));\n }\n distinctHashes.delete(\"unknown\");\n\n const staleHashes =\n headHash === \"unknown\"\n ? []\n : [...distinctHashes].filter((h) => h !== headHash);\n const stale = staleHashes.length > 0;\n\n const report: DriftReport = {\n stale,\n snapshotHash: snapshot.gitHash,\n headHash,\n commitsBehind: stale ? null : 0,\n historyAvailable: true,\n changedFiles: [],\n staleFeatures: {},\n staleFlows: {},\n totalFeatures,\n totalFlows,\n unmappedFiles: [],\n ghostFiles: [],\n renames: [],\n recommendation: \"up-to-date\",\n };\n\n if (!stale) return report;\n\n const mappedFiles = collectMappedFiles(snapshot);\n report.ghostFiles = await findGhostFiles(resolvedRoot, mappedFiles);\n\n const changesByHash = new Map<string, FileChange[]>();\n const touchedByHash = new Map<string, Set<string>>();\n for (const hash of staleHashes) {\n const changes = await getChangesWithStatus(resolvedRoot, hash);\n if (changes === null) {\n // One unreachable base commit is enough to make per-entry drift\n // uncomputable — we know the map is stale but not how.\n report.historyAvailable = false;\n report.recommendation = \"full-rebuild\";\n return report;\n }\n changesByHash.set(hash, changes);\n // Every path a change touches, old and new — an entry referencing either\n // side of a rename is stale.\n const touched = new Set<string>();\n for (const change of changes) {\n touched.add(change.path);\n if (change.previousPath) touched.add(change.previousPath);\n }\n touchedByHash.set(hash, touched);\n }\n\n // The oldest verification state in the map is the honest answer to \"how\n // far behind is this snapshot\".\n const commitCounts = await Promise.all(\n staleHashes.map((hash) => countCommitsBehind(resolvedRoot, hash))\n );\n const validCounts = commitCounts.filter((c): c is number => c !== null);\n report.commitsBehind =\n validCounts.length > 0 ? Math.max(...validCounts) : null;\n\n const emptySet = new Set<string>();\n const touchedFor = (entry: { refreshedHash?: string }): Set<string> =>\n touchedByHash.get(hashFor(entry)) ?? emptySet;\n\n for (const [name, feature] of Object.entries(snapshot.features)) {\n const touched = touchedFor(feature);\n const hits = [...feature.files, ...(feature.tests ?? [])].filter((f) =>\n touched.has(f)\n );\n if (hits.length > 0) report.staleFeatures[name] = [...new Set(hits)];\n }\n for (const [name, flow] of Object.entries(snapshot.flows)) {\n const touched = touchedFor(flow);\n const hits = flow.chain.filter((f) => touched.has(f));\n if (hits.length > 0) report.staleFlows[name] = [...new Set(hits)];\n }\n\n const allChanges = [...changesByHash.values()].flat();\n report.changedFiles = [...new Set(allChanges.map((c) => c.path))].sort();\n\n // New source files (added, or the new side of a rename) missing from the map.\n const sourceFileSet = new Set(await listSourceFiles(resolvedRoot));\n const newPaths = allChanges\n .filter((c) => c.status === \"added\" || c.status === \"renamed\")\n .map((c) => c.path);\n report.unmappedFiles = [...new Set(newPaths)]\n .filter((p) => sourceFileSet.has(p) && !mappedFiles.has(p))\n .sort();\n\n const renameKeys = new Set<string>();\n for (const change of allChanges) {\n if (change.status !== \"renamed\" || !change.previousPath) continue;\n const key = `${change.previousPath}\u0000${change.path}`;\n if (renameKeys.has(key)) continue;\n renameKeys.add(key);\n report.renames.push({ from: change.previousPath, to: change.path });\n }\n\n const changedMapped = new Set<string>([\n ...Object.values(report.staleFeatures).flat(),\n ...Object.values(report.staleFlows).flat(),\n ]);\n const changedFraction =\n mappedFiles.size > 0 ? changedMapped.size / mappedFiles.size : 0;\n report.recommendation =\n changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES &&\n changedFraction > FULL_REBUILD_FRACTION\n ? \"full-rebuild\"\n : \"incremental\";\n\n return report;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport fg from \"fast-glob\";\nimport { readFullFile } from \"../mcp/sampler.js\";\nimport { buildTestMap } from \"../test-map.js\";\n\nconst exec = promisify(execFile);\n\nexport interface FeatureEntry {\n description: string;\n files: string[];\n tests?: string[];\n /**\n * Commit this entry was last verified against. Entries updated by an\n * incremental save carry HEAD here; untouched entries keep the hash the\n * map had before the save, so drift stays visible per entry. Absent means\n * \"as of the snapshot's top-level gitHash\".\n */\n refreshedHash?: string;\n /**\n * Whether this is a user-facing capability or internal infrastructure\n * (DI wiring, config loading, logging, provider/transport plumbing).\n * Capabilities are published to product-facing docs (Confluence);\n * infrastructure stays in the AI concept map only. Defaults to \"capability\"\n * when absent (older snapshots) or unrecognized — see normalizeFeatureType.\n */\n type?: \"capability\" | \"infrastructure\";\n /**\n * When an assistant last confirmed this entry's files actually implement\n * the claimed feature (verify_snapshot flow). Absent on older snapshots\n * and never-verified entries. Drift checks freshness against git; this\n * checks the map was CORRECT in the first place.\n */\n verifiedAt?: string;\n /** Set when verification judged the entry wrong — re-map it. */\n verificationFailed?: boolean;\n verificationNote?: string;\n}\n\nexport type FeatureType = \"capability\" | \"infrastructure\";\n\n/**\n * Coerce an arbitrary type value to a known classification. Anything that\n * isn't explicitly \"infrastructure\" defaults to \"capability\" — so older\n * snapshots and unclassified entries are treated as user-facing (published),\n * never silently hidden.\n */\nexport function normalizeFeatureType(value: unknown): FeatureType {\n return value === \"infrastructure\" ? \"infrastructure\" : \"capability\";\n}\n\nexport interface FlowEntry {\n description: string;\n chain: string[];\n /** See FeatureEntry.refreshedHash. */\n refreshedHash?: string;\n /** See FeatureEntry.verifiedAt / verificationFailed. */\n verifiedAt?: string;\n verificationFailed?: boolean;\n verificationNote?: string;\n}\n\nexport interface Snapshot {\n version: 2;\n createdAt: string;\n updatedAt: string;\n gitHash: string;\n features: Record<string, FeatureEntry>;\n flows: Record<string, FlowEntry>;\n}\n\nfunction snapshotDir(rootDir: string): string {\n return path.join(rootDir, \".mason\");\n}\n\nfunction snapshotPath(rootDir: string): string {\n return path.join(snapshotDir(rootDir), \"snapshot.json\");\n}\n\nexport async function loadSnapshot(rootDir: string): Promise<Snapshot | null> {\n try {\n const raw = await fs.readFile(snapshotPath(rootDir), \"utf-8\");\n const parsed = JSON.parse(raw);\n // Skip v1 snapshots — they're the old per-file format\n if (parsed.version !== 2) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nexport async function saveSnapshot(\n rootDir: string,\n snapshot: Snapshot\n): Promise<void> {\n await fs.mkdir(snapshotDir(rootDir), { recursive: true });\n await fs.writeFile(\n snapshotPath(rootDir),\n JSON.stringify(snapshot, null, 2),\n \"utf-8\"\n );\n}\n\nexport async function getCurrentGitHash(rootDir: string): Promise<string> {\n try {\n const { stdout } = await exec(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd: rootDir,\n });\n return stdout.trim();\n } catch {\n return \"unknown\";\n }\n}\n\nexport const SOURCE_GLOB =\n \"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}\";\nexport const SOURCE_IGNORE = [\n \"**/node_modules/**\", \"**/dist/**\", \"**/build/**\", \"**/.gradle/**\",\n \"**/target/**\", \"**/.git/**\", \"**/vendor/**\", \"**/__pycache__/**\",\n \"**/venv/**\", \"**/.venv/**\", \"**/*.min.*\", \"**/*.map\",\n \"**/generated/**\", \"**/R.java\", \"**/BuildConfig.java\",\n];\n\nexport const DEFAULT_BATCH_SIZE = 50;\nconst SKELETON_CHARS = 500;\nconst DEEP_SAMPLE_CHARS = 1500;\nconst DEEP_SAMPLES_PER_BATCH = 3;\n\nexport interface SnapshotBatch {\n offset: number;\n batchSize: number;\n nextOffset: number | null;\n totalFiles: number;\n skeletons: Array<{ path: string; content: string }>;\n samples: Array<{ path: string; content: string }>;\n testPairs: Array<{ test: string; source: string; confidence: string }>;\n}\n\nexport async function listSourceFiles(resolvedRoot: string): Promise<string[]> {\n const all = await fg(SOURCE_GLOB, {\n cwd: resolvedRoot,\n ignore: SOURCE_IGNORE,\n });\n // Deterministic order so the same offset always returns the same batch.\n return [...all].sort();\n}\n\nexport async function prepareSnapshotBatch(\n rootDir: string,\n offset: number,\n batchSize: number = DEFAULT_BATCH_SIZE,\n scopeFiles?: string[]\n): Promise<SnapshotBatch> {\n const resolvedRoot = path.resolve(rootDir);\n let allFiles = await listSourceFiles(resolvedRoot);\n if (scopeFiles) {\n // Intersect with the real source list: keeps ignore rules and path safety,\n // and silently drops scope entries that no longer exist on disk. An empty\n // scope stays empty — it must not fall back to walking the whole project.\n const scopeSet = new Set(scopeFiles);\n allFiles = allFiles.filter((f) => scopeSet.has(f));\n }\n const totalFiles = allFiles.length;\n const safeOffset = Math.max(0, Math.min(offset, totalFiles));\n const batchPaths = allFiles.slice(safeOffset, safeOffset + batchSize);\n\n const skeletons: Array<{ path: string; content: string }> = [];\n for (const filePath of batchPaths) {\n const full = await readFullFile(resolvedRoot, filePath);\n if (full) {\n skeletons.push({\n path: full.path,\n content: full.content.slice(0, SKELETON_CHARS),\n });\n }\n }\n\n // Pick a few files from this batch to read deeply for grounding. Spread\n // evenly across the batch so the deep samples represent the batch's range.\n const samples: Array<{ path: string; content: string }> = [];\n if (skeletons.length > 0) {\n const step = Math.max(1, Math.floor(skeletons.length / DEEP_SAMPLES_PER_BATCH));\n for (let i = 0; i < skeletons.length && samples.length < DEEP_SAMPLES_PER_BATCH; i += step) {\n const full = await readFullFile(resolvedRoot, skeletons[i].path);\n if (full) {\n samples.push({\n path: full.path,\n content: full.content.slice(0, DEEP_SAMPLE_CHARS),\n });\n }\n }\n }\n\n // Only include test pairs that involve files in this batch — keeps the\n // appendix relevant and small.\n const batchPathSet = new Set(batchPaths);\n const allTestPairs = (await buildTestMap(resolvedRoot)).paired;\n const testPairs = allTestPairs.filter(\n (p) => batchPathSet.has(p.test) || batchPathSet.has(p.source)\n );\n\n const nextOffset =\n safeOffset + batchSize >= totalFiles ? null : safeOffset + batchSize;\n\n return {\n offset: safeOffset,\n batchSize,\n nextOffset,\n totalFiles,\n skeletons,\n samples,\n testPairs,\n };\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport fg from \"fast-glob\";\n\nconst exec = promisify(execFile);\n\nconst SOURCE_EXTENSIONS = [\n \"ts\", \"tsx\", \"js\", \"jsx\", \"mts\", \"mjs\",\n \"kt\", \"kts\", \"java\",\n \"py\",\n \"go\",\n \"rs\",\n \"swift\",\n \"rb\",\n \"cs\", \"cpp\", \"c\", \"h\",\n \"dart\",\n];\n\nconst CONFIG_FILES = [\n // Build & project config\n \"package.json\",\n \"tsconfig.json\",\n \"build.gradle.kts\",\n \"build.gradle\",\n \"settings.gradle.kts\",\n \"settings.gradle\",\n \"Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"Gemfile\",\n \"*.csproj\",\n // Version catalogs & dependency locks\n \"gradle/libs.versions.toml\",\n // Code quality & formatting\n \".editorconfig\",\n \".eslintrc.*\",\n \"eslint.config.*\",\n \".prettierrc\",\n \"rustfmt.toml\",\n \".swiftlint.yml\",\n // CI/CD\n \".github/workflows/*.yml\",\n \".gitlab-ci.yml\",\n \"Jenkinsfile\",\n // Containerization\n \"Dockerfile\",\n \"docker-compose.yml\",\n \"docker-compose.yaml\",\n];\n\nconst ENTRY_POINT_PATTERNS = [\n \"src/main.*\",\n \"src/index.*\",\n \"src/app.*\",\n \"main.*\",\n \"index.*\",\n \"app.*\",\n \"App.*\",\n \"**/Main.kt\",\n \"**/Application.kt\",\n \"**/main.py\",\n \"**/main.go\",\n \"**/main.rs\",\n \"**/lib.rs\",\n \"**/Program.cs\",\n];\n\n// Filename patterns that reveal architectural patterns and conventions.\n// These are language-agnostic — the suffixes appear across ecosystems.\n// Ordered by architectural importance — most distinctive patterns first.\nconst ARCHITECTURAL_PATTERNS = [\n // State/data flow\n { glob: \"**/*ViewModel.*\", category: \"state\", reason: \"viewmodel (state management)\" },\n { glob: \"**/*Store.*\", category: \"state\", reason: \"store (state management)\" },\n { glob: \"**/*Reducer.*\", category: \"state\", reason: \"reducer (state management)\" },\n // Data layer — interface\n { glob: \"**/*Repository.*\", category: \"data-interface\", reason: \"repository interface (data layer contract)\" },\n { glob: \"**/*Dao.*\", category: \"data-interface\", reason: \"DAO (data access)\" },\n { glob: \"**/*DataSource.*\", category: \"data-interface\", reason: \"data source\" },\n // Data layer — implementation (where actual patterns live: mappers, retry, IO dispatchers)\n { glob: \"**/*RepositoryImpl.*\", category: \"data-impl\", reason: \"repository implementation (data layer patterns)\" },\n { glob: \"**/*ServiceImpl.*\", category: \"data-impl\", reason: \"service implementation\" },\n { glob: \"**/*Impl.*\", category: \"data-impl\", reason: \"implementation (concrete patterns)\" },\n // Data transformation\n { glob: \"**/*Mapper.*\", category: \"transform\", reason: \"mapper (data transformation)\" },\n { glob: \"**/*Converter.*\", category: \"transform\", reason: \"converter (data transformation)\" },\n { glob: \"**/*Adapter.*\", category: \"transform\", reason: \"adapter (interface adaptation)\" },\n // Dependency injection / wiring\n { glob: \"**/*Module.*\", category: \"di\", reason: \"module (DI/wiring)\" },\n { glob: \"**/*Provider.*\", category: \"di\", reason: \"provider (DI/wiring)\" },\n { glob: \"**/*Container.*\", category: \"di\", reason: \"container (DI/wiring)\" },\n { glob: \"**/*Factory.*\", category: \"di\", reason: \"factory (object creation)\" },\n // API / network\n { glob: \"**/*Service.*\", category: \"api\", reason: \"service (business/API layer)\" },\n { glob: \"**/*Client.*\", category: \"api\", reason: \"client (API/network layer)\" },\n { glob: \"**/*Api.*\", category: \"api\", reason: \"API interface definition\" },\n // Interface contracts / protocols\n { glob: \"**/*Interface.*\", category: \"contract\", reason: \"interface definition\" },\n { glob: \"**/*Protocol.*\", category: \"contract\", reason: \"protocol definition\" },\n { glob: \"**/*Trait.*\", category: \"contract\", reason: \"trait definition\" },\n // Routing / navigation\n { glob: \"**/*Router.*\", category: \"routing\", reason: \"router (navigation/routing)\" },\n { glob: \"**/*Route.*\", category: \"routing\", reason: \"route definition\" },\n { glob: \"**/*NavHost.*\", category: \"routing\", reason: \"navigation host\" },\n { glob: \"**/*Controller.*\", category: \"routing\", reason: \"controller (request handling)\" },\n { glob: \"**/*Handler.*\", category: \"routing\", reason: \"handler (request handling)\" },\n // Middleware / interceptors\n { glob: \"**/*Middleware.*\", category: \"middleware\", reason: \"middleware (request pipeline)\" },\n { glob: \"**/*Interceptor.*\", category: \"middleware\", reason: \"interceptor (cross-cutting)\" },\n { glob: \"**/*Plugin.*\", category: \"middleware\", reason: \"plugin (extensibility)\" },\n // Models / types\n { glob: \"**/*Model.*\", category: \"model\", reason: \"model (domain types)\" },\n { glob: \"**/*Entity.*\", category: \"model\", reason: \"entity (persistence types)\" },\n { glob: \"**/*Dto.*\", category: \"model\", reason: \"DTO (data transfer types)\" },\n { glob: \"**/*Schema.*\", category: \"model\", reason: \"schema (data validation)\" },\n // Use cases / commands\n { glob: \"**/*UseCase.*\", category: \"usecase\", reason: \"use case (business logic)\" },\n { glob: \"**/*Interactor.*\", category: \"usecase\", reason: \"interactor (business logic)\" },\n { glob: \"**/*Command.*\", category: \"usecase\", reason: \"command (CQRS pattern)\" },\n];\n\nconst IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/.gradle/**\",\n \"**/target/**\",\n \"**/.git/**\",\n \"**/vendor/**\",\n \"**/__pycache__/**\",\n \"**/venv/**\",\n \"**/.venv/**\",\n \"**/*.min.*\",\n \"**/*.map\",\n \"**/package-lock.json\",\n \"**/yarn.lock\",\n \"**/pnpm-lock.yaml\",\n \"**/*.lock\",\n \"**/*.generated.*\",\n \"**/generated/**\",\n \"**/R.java\",\n \"**/BuildConfig.java\",\n];\n\nconst PREVIEW_LINES = 60;\n\nexport interface ProjectConfig {\n patterns?: string[];\n alwaysInclude?: string[];\n ignore?: string[];\n}\n\nexport interface SampledFile {\n path: string;\n preview: string;\n totalLines: number;\n sizeBytes: number;\n reason: string;\n}\n\nasync function loadProjectConfig(\n rootDir: string\n): Promise<ProjectConfig> {\n try {\n const raw = await fs.readFile(\n path.join(rootDir, \".mason\", \"config.json\"),\n \"utf-8\"\n );\n return JSON.parse(raw);\n } catch {\n return {};\n }\n}\n\nasync function getTrackedFiles(rootDir: string): Promise<Set<string> | null> {\n try {\n const { stdout } = await exec(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\n cwd: rootDir,\n maxBuffer: 10_000_000,\n });\n return new Set(stdout.trim().split(\"\\n\").filter(Boolean));\n } catch {\n return null; // Not a git repo — skip filtering\n }\n}\n\nexport async function sampleFiles(\n rootDir: string,\n maxFiles: number = 25\n): Promise<SampledFile[]> {\n const selected = new Map<string, string>(); // path -> reason\n const projectConfig = await loadProjectConfig(rootDir);\n const ignorePatterns = [...IGNORE_PATTERNS, ...(projectConfig.ignore ?? [])];\n const trackedFiles = await getTrackedFiles(rootDir);\n\n // 0. Always-include files from project config (highest priority)\n for (const filePath of projectConfig.alwaysInclude ?? []) {\n if (selected.size >= maxFiles) break;\n // Validate path stays within project root\n const resolvedPath = path.resolve(rootDir, filePath);\n if (!resolvedPath.startsWith(path.resolve(rootDir))) continue;\n selected.set(filePath, \"always-include (project config)\");\n }\n\n // 1. Config files (cap at 5)\n let configCount = 0;\n for (const pattern of CONFIG_FILES) {\n if (configCount >= 5) break;\n const matches = await fg(pattern, {\n cwd: rootDir,\n ignore: ignorePatterns,\n deep: 3,\n });\n for (const match of matches) {\n if (configCount >= 5 || selected.size >= maxFiles) break;\n selected.set(match, \"config file\");\n configCount++;\n }\n }\n\n // 2. Module build/config files — build files from subdirectories reveal dependency graph\n const moduleBuildPatterns = [\n // Gradle\n \"**/build.gradle.kts\",\n \"**/build.gradle\",\n // Cargo workspace members\n \"**/Cargo.toml\",\n // Node workspaces\n \"**/package.json\",\n // Go sub-modules\n \"**/go.mod\",\n ];\n let moduleBuildCount = 0;\n for (const pattern of moduleBuildPatterns) {\n const matches = await fg(pattern, {\n cwd: rootDir,\n ignore: ignorePatterns,\n deep: 4,\n });\n // Skip root-level files (already captured as config)\n const subMatches = matches.filter((m) => m.includes(\"/\"));\n for (const match of subMatches) {\n if (moduleBuildCount >= 4 || selected.size >= maxFiles) break;\n if (!selected.has(match)) {\n selected.set(match, \"module build file (reveals dependency graph)\");\n moduleBuildCount++;\n }\n }\n if (moduleBuildCount >= 4) break;\n }\n\n // 3. Entry points (cap at 2)\n let entryCount = 0;\n for (const pattern of ENTRY_POINT_PATTERNS) {\n if (entryCount >= 2) break;\n const matches = await fg(pattern, {\n cwd: rootDir,\n ignore: ignorePatterns,\n deep: 5,\n });\n for (const match of matches) {\n if (entryCount >= 2 || selected.size >= maxFiles) break;\n if (!selected.has(match)) {\n selected.set(match, \"entry point\");\n entryCount++;\n }\n }\n }\n\n // 4. Hot files from git (up to 5)\n try {\n const { stdout } = await exec(\n \"git\",\n [\"log\", \"--since=3 months ago\", \"--format=\", \"--name-only\"],\n { cwd: rootDir, maxBuffer: 5_000_000 }\n );\n\n const fileCounts = new Map<string, number>();\n for (const line of stdout.split(\"\\n\")) {\n if (!line) continue;\n if (\n line.includes(\"node_modules\") ||\n line.includes(\"/build/\") ||\n line.includes(\".gradle\") ||\n line.includes(\"/generated/\")\n )\n continue;\n const ext = path.extname(line).slice(1);\n if (!SOURCE_EXTENSIONS.includes(ext)) continue;\n fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);\n }\n\n const hotFiles = [...fileCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n for (const [file, count] of hotFiles) {\n if (selected.size >= maxFiles) break;\n if (!selected.has(file)) {\n selected.set(file, `frequently changed (${count} commits in 3 months)`);\n }\n }\n } catch {\n // No git\n }\n\n // 5. Architectural pattern files — one per category (cap at 8)\n const seenCategories = new Set<string>();\n let patternCount = 0;\n for (const pattern of ARCHITECTURAL_PATTERNS) {\n if (patternCount >= 8 || selected.size >= maxFiles) break;\n if (seenCategories.has(pattern.category)) continue;\n\n const matches = await fg(pattern.glob, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n\n if (matches.length > 0) {\n for (const match of matches) {\n if (!selected.has(match)) {\n selected.set(match, pattern.reason);\n seenCategories.add(pattern.category);\n patternCount++;\n break;\n }\n }\n }\n }\n\n // 5b. Custom patterns from project config\n for (const customGlob of projectConfig.patterns ?? []) {\n if (selected.size >= maxFiles) break;\n const matches = await fg(customGlob, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n for (const match of matches) {\n if (selected.size >= maxFiles) break;\n if (!selected.has(match)) {\n selected.set(match, \"custom pattern (project config)\");\n break; // one per pattern\n }\n }\n }\n\n // 6. Test examples — diverse across file types (cap at 3)\n const testPatternGroups = [\n // JS/TS tests\n { patterns: [\"**/*.test.*\", \"**/*.spec.*\"], label: \"JS/TS test\" },\n // JVM tests\n { patterns: [\"**/*Test.kt\", \"**/*Test.java\"], label: \"JVM test\" },\n // Python tests\n { patterns: [\"**/test_*.py\", \"**/*_test.py\"], label: \"Python test\" },\n // Go tests\n { patterns: [\"**/*_test.go\"], label: \"Go test\" },\n // Swift tests\n { patterns: [\"**/*Tests.swift\", \"**/*Test.swift\"], label: \"Swift test\" },\n // Rust tests\n { patterns: [\"**/*_test.rs\"], label: \"Rust test\" },\n ];\n let testCount = 0;\n for (const group of testPatternGroups) {\n if (testCount >= 3 || selected.size >= maxFiles) break;\n const testFiles = await fg(group.patterns, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n if (testFiles.length > 0) {\n for (const file of testFiles) {\n if (!selected.has(file)) {\n selected.set(file, `test example (${group.label})`);\n testCount++;\n break;\n }\n }\n }\n }\n\n // 7. Directory breadth — fill remaining slots with one file per top-level dir\n const sourceGlobs = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);\n const allSourceFiles = await fg(sourceGlobs, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n\n const dirRepresentatives = new Map<string, string>();\n const boringFiles = /\\.(gradle|gradle\\.kts|json|toml|yaml|yml|xml|properties)$/;\n for (const file of allSourceFiles) {\n const topDir = file.split(\"/\")[0];\n if (!dirRepresentatives.has(topDir) && !boringFiles.test(file)) {\n dirRepresentatives.set(topDir, file);\n }\n }\n\n for (const [, file] of dirRepresentatives) {\n if (selected.size >= maxFiles) break;\n if (!selected.has(file)) {\n selected.set(file, \"directory representative\");\n }\n }\n\n // Read file previews\n const results: SampledFile[] = [];\n for (const [filePath, reason] of selected) {\n try {\n const fullPath = path.resolve(rootDir, filePath);\n if (!fullPath.startsWith(path.resolve(rootDir))) continue;\n if (isSensitiveFile(filePath)) continue;\n if (trackedFiles && !trackedFiles.has(filePath)) continue; // respect .gitignore\n const stat = await fs.stat(fullPath);\n if (stat.size > 100_000) continue;\n\n const content = await fs.readFile(fullPath, \"utf-8\");\n const lines = content.split(\"\\n\");\n const preview = lines.slice(0, PREVIEW_LINES).join(\"\\n\");\n\n results.push({\n path: filePath,\n preview,\n totalLines: lines.length,\n sizeBytes: stat.size,\n reason,\n });\n } catch {\n // Skip\n }\n }\n\n return results;\n}\n\nconst SENSITIVE_PATTERNS = [\n /^\\.env$/,\n /^\\.env\\./,\n /\\.pem$/,\n /\\.key$/,\n /\\.p12$/,\n /\\.pfx$/,\n /\\.jks$/,\n /id_rsa/,\n /id_ed25519/,\n /credentials\\./,\n /secret/i,\n /\\.keystore$/,\n /local\\.properties$/,\n];\n\nfunction isSensitiveFile(filePath: string): boolean {\n const basename = path.basename(filePath);\n return SENSITIVE_PATTERNS.some((p) => p.test(basename));\n}\n\nexport async function readFullFile(\n rootDir: string,\n filePath: string\n): Promise<{ path: string; content: string; totalLines: number } | null> {\n try {\n const fullPath = path.join(path.resolve(rootDir), filePath);\n if (!fullPath.startsWith(path.resolve(rootDir))) return null;\n if (isSensitiveFile(filePath)) return null;\n\n const content = await fs.readFile(fullPath, \"utf-8\");\n return {\n path: filePath,\n content,\n totalLines: content.split(\"\\n\").length,\n };\n } catch {\n return null;\n }\n}\n","import path from \"node:path\";\nimport fg from \"fast-glob\";\n\nconst IGNORE = [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/.gradle/**\",\n \"**/target/**\",\n \"**/.git/**\",\n \"**/vendor/**\",\n \"**/__pycache__/**\",\n \"**/venv/**\",\n \"**/.venv/**\",\n \"**/*.min.*\",\n \"**/*.map\",\n];\n\nexport interface TestPair {\n test: string;\n source: string;\n confidence: string;\n}\n\nexport interface TestMapResult {\n totalTestFiles: number;\n paired: TestPair[];\n unmatched: string[];\n}\n\nexport async function buildTestMap(dir: string): Promise<TestMapResult> {\n const rootDir = path.resolve(dir);\n\n // Find all test files\n const testPatterns = [\n \"**/*.test.*\", \"**/*.spec.*\",\n \"**/*Test.kt\", \"**/*Test.java\", \"**/*Tests.kt\", \"**/*Tests.java\",\n \"**/test_*.py\", \"**/*_test.py\",\n \"**/*_test.go\",\n \"**/*Tests.swift\", \"**/*Test.swift\",\n \"**/*_test.rs\",\n ];\n const testFiles = await fg(testPatterns, { cwd: rootDir, ignore: IGNORE });\n\n // Find all source files\n const sourceFiles = await fg(\n \"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}\",\n { cwd: rootDir, ignore: IGNORE }\n );\n\n // Build source file index by base name (without extension)\n const sourceByBaseName = new Map<string, string[]>();\n for (const file of sourceFiles) {\n if (testFiles.includes(file)) continue; // Skip test files\n const baseName = path.basename(file).replace(/\\.[^.]+$/, \"\");\n const existing = sourceByBaseName.get(baseName) ?? [];\n existing.push(file);\n sourceByBaseName.set(baseName, existing);\n }\n\n // Match test files to source files by name\n const paired: TestPair[] = [];\n const unmatched: string[] = [];\n\n for (const testFile of testFiles) {\n const testBaseName = path.basename(testFile).replace(/\\.[^.]+$/, \"\");\n\n // Strip test suffixes/prefixes to get the source name\n const sourceName = testBaseName\n .replace(/Test$|Tests$|Spec$|\\.test$|\\.spec$/, \"\")\n .replace(/^test_|_test$/, \"\");\n\n if (!sourceName) {\n unmatched.push(testFile);\n continue;\n }\n\n const candidates = sourceByBaseName.get(sourceName);\n if (candidates && candidates.length > 0) {\n // If multiple candidates, prefer one in a similar directory path\n const testDir = path.dirname(testFile);\n const bestMatch = candidates.reduce((best, candidate) => {\n const candidateDir = path.dirname(candidate);\n const bestDir = path.dirname(best);\n const candidateOverlap = commonSegments(testDir, candidateDir);\n const bestOverlap = commonSegments(testDir, bestDir);\n return candidateOverlap > bestOverlap ? candidate : best;\n });\n\n paired.push({\n test: testFile,\n source: bestMatch,\n confidence: candidates.length === 1 ? \"exact\" : \"best-guess\",\n });\n } else {\n unmatched.push(testFile);\n }\n }\n\n return { totalTestFiles: testFiles.length, paired, unmatched };\n}\n\nfunction commonSegments(pathA: string, pathB: string): number {\n const segsA = pathA.split(\"/\");\n const segsB = pathB.split(\"/\");\n let count = 0;\n for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {\n if (segsA[i] === segsB[i]) count++;\n else break;\n }\n return count;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { extractClaims } from \"./claims.js\";\nimport { lastCommitOf } from \"./git.js\";\nimport type { CommitRef, DocClaims } from \"./types.js\";\n\nconst exec = promisify(execFile);\n\n/**\n * All context files audited in v1, in the precedence order the setup\n * playbook uses. Every candidate that exists is audited — a repo can\n * legitimately carry both AGENTS.md and CLAUDE.md, and drift can live in\n * either.\n */\nexport const DOC_CANDIDATES = [\n \"AGENTS.md\",\n \"CLAUDE.md\",\n \".claude/CLAUDE.md\",\n] as const;\n\nexport interface AuditDoc {\n /** Repo-relative posix path. */\n path: string;\n content: string;\n lineCount: number;\n /** Null when the doc is untracked. */\n lastCommit: CommitRef | null;\n /** Uncommitted edits present. */\n dirty: boolean;\n claims: DocClaims;\n}\n\nasync function isDirty(resolvedRoot: string, relPath: string): Promise<boolean> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"status\", \"--porcelain\", \"--\", relPath],\n { cwd: resolvedRoot }\n );\n return stdout.trim().length > 0;\n } catch {\n return false;\n }\n}\n\nexport async function discoverDocs(resolvedRoot: string): Promise<AuditDoc[]> {\n const docs: AuditDoc[] = [];\n for (const candidate of DOC_CANDIDATES) {\n let content: string;\n try {\n content = await fs.readFile(path.join(resolvedRoot, candidate), \"utf-8\");\n } catch {\n continue;\n }\n docs.push({\n path: candidate,\n content,\n lineCount: content.split(\"\\n\").length,\n lastCommit: await lastCommitOf(resolvedRoot, candidate),\n dirty: await isDirty(resolvedRoot, candidate),\n claims: extractClaims(content),\n });\n }\n return docs;\n}\n","import type { PathClaim } from \"./types.js\";\n\n/**\n * A fenced block is treated as a directory tree only when it clearly is one —\n * below this many branch-glyph lines it's more likely an ASCII sketch.\n */\nconst MIN_GLYPH_LINES = 3;\n\nconst GLYPHS = [\"├──\", \"└──\"] as const;\n\nfunction glyphIndex(line: string): number {\n for (const glyph of GLYPHS) {\n const idx = line.indexOf(glyph);\n if (idx !== -1) return idx;\n }\n return -1;\n}\n\n/** Tree furniture: blank, or only vertical bars and whitespace. */\nfunction isSpacerLine(line: string): boolean {\n return /^[\\s│|]*$/.test(line);\n}\n\n/**\n * Strip an inline comment/annotation from a tree entry. Entries commonly\n * carry `# comment` or column-aligned notes after two or more spaces.\n */\nfunction entryName(afterGlyph: string): string | null {\n let name = afterGlyph.replace(/^\\s+/, \"\");\n const hash = name.search(/\\s+#/);\n if (hash !== -1) name = name.slice(0, hash);\n const columns = name.search(/\\s{2,}/);\n if (columns !== -1) name = name.slice(0, columns);\n name = name.trim();\n // A name with remaining internal whitespace is not a path — treat the\n // line as malformed rather than guessing.\n if (!name || /\\s/.test(name)) return null;\n return name;\n}\n\n/**\n * Reconstruct full paths from an ASCII directory tree inside a fenced block.\n *\n * The failure mode must always be a missed claim, never an invented path: a\n * line that doesn't parse cleanly aborts reconstruction below it, and every\n * emitted path still passes the deleted-reference provability gate later.\n *\n * `blockLines` are the fence's content lines; `blockStartLine` is the 1-based\n * doc line number of the first content line.\n */\nexport function extractTreeClaims(\n blockLines: string[],\n blockStartLine: number\n): PathClaim[] {\n const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;\n if (glyphLines < MIN_GLYPH_LINES) return [];\n\n const claims: PathClaim[] = [];\n // Directories on the path from the root to the current entry, keyed by the\n // column their branch glyph appeared at.\n const stack: Array<{ col: number; name: string }> = [];\n let rootPrefix = \"\";\n let started = false;\n\n for (let i = 0; i < blockLines.length; i++) {\n const line = blockLines[i];\n const col = glyphIndex(line);\n\n if (col === -1) {\n if (isSpacerLine(line)) continue;\n if (!started) {\n // A bare `src/` line above the first glyph names the tree's root.\n const candidate = line.trim();\n if (candidate.endsWith(\"/\") && !/\\s/.test(candidate)) {\n rootPrefix = candidate.replace(/\\/+$/, \"\");\n claims.push({\n path: rootPrefix,\n line: blockStartLine + i,\n excerpt: candidate,\n });\n }\n continue;\n }\n // Unparseable line after the tree began: stop here, keep what we have.\n return claims;\n }\n\n started = true;\n const name = entryName(line.slice(col + GLYPHS[0].length));\n if (name === null) return claims;\n\n while (stack.length > 0 && stack[stack.length - 1].col >= col) {\n stack.pop();\n }\n\n const isDir = name.endsWith(\"/\");\n const cleanName = name.replace(/\\/+$/, \"\");\n const segments = [\n ...(rootPrefix ? [rootPrefix] : []),\n ...stack.map((s) => s.name),\n cleanName,\n ];\n claims.push({\n path: segments.join(\"/\"),\n line: blockStartLine + i,\n excerpt: name,\n });\n\n if (isDir) stack.push({ col, name: cleanName });\n }\n\n return claims;\n}\n","import type {\n CommandClaim,\n CountClaim,\n DocClaims,\n PathClaim,\n} from \"./types.js\";\nimport { extractTreeClaims } from \"./tree.js\";\n\n/**\n * Single-segment names that count as path claims without containing a \"/\".\n * Anything else without a slash is prose (\"name your file `config.ts`\"), not\n * a claim about this repo.\n */\nconst ROOT_FILE_NAMES = new Set([\n \"package.json\",\n \"package-lock.json\",\n \"pnpm-workspace.yaml\",\n \"tsconfig.json\",\n \"tsup.config.ts\",\n \"vitest.config.ts\",\n \"Makefile\",\n \"Dockerfile\",\n \"docker-compose.yml\",\n \"Cargo.toml\",\n \"go.mod\",\n \"go.sum\",\n \"pyproject.toml\",\n \"requirements.txt\",\n \"Gemfile\",\n \"composer.json\",\n \"settings.gradle.kts\",\n \"settings.gradle\",\n \"build.gradle.kts\",\n \"build.gradle\",\n \"manifest.json\",\n \"server.json\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\",\n \"CLAUDE.md\",\n \"AGENTS.md\",\n \".gitignore\",\n \".env.example\",\n]);\n\nconst SHELL_FENCE_INFOS = new Set([\"\", \"bash\", \"sh\", \"shell\", \"console\", \"zsh\"]);\n\nconst COMMAND_RE = /\\b(npm|pnpm|yarn)\\s+run\\s+([A-Za-z0-9:_.-]+)/g;\nconst COUNT_RE = /(\\d+)\\s+(modules?|packages?|workspaces?|crates?)\\b/gi;\n/** \"3 package managers\" is not a package count. */\nconst COUNT_DENYLIST_RE = /^\\s*(manager|registr|lock|json)/i;\n\nconst IGNORE_LINE = \"<!-- mason:ignore -->\";\nconst IGNORE_START = \"<!-- mason:ignore-start -->\";\nconst IGNORE_END = \"<!-- mason:ignore-end -->\";\n\n/**\n * Normalize a candidate token into a repo-relative path claim, or return\n * null when the token is not a claim about this repo (URL, glob,\n * placeholder, relative import example, bare word).\n */\nexport function normalizePathToken(token: string): string | null {\n let t = token.trim();\n if (!t) return null;\n if (/\\s/.test(t)) return null;\n if (t.includes(\"://\") || t.includes(\"\\\\\")) return null;\n if (/[*?[\\]{}<>$`]/.test(t)) return null;\n if (t.startsWith(\"/\") || t.startsWith(\"~\") || t.startsWith(\"./\") || t.startsWith(\"../\")) {\n return null;\n }\n // `src/mcp/tools.ts:189` claims the file, not the line.\n t = t.replace(/:\\d+(?:-\\d+)?$/, \"\");\n if (t.includes(\":\")) return null;\n const normalized = t.replace(/\\/+$/, \"\");\n if (!normalized) return null;\n if (normalized.includes(\"/\")) return normalized;\n return ROOT_FILE_NAMES.has(normalized) ? normalized : null;\n}\n\n/** A fence line that is exactly one path-shaped token is a file-list claim. */\nfunction exactTokenPath(line: string): string | null {\n const trimmed = line.trim();\n if (!trimmed || /\\s/.test(trimmed) || !trimmed.includes(\"/\")) return null;\n return normalizePathToken(trimmed);\n}\n\nfunction computeIgnoredLines(lines: string[]): boolean[] {\n const ignored = new Array<boolean>(lines.length).fill(false);\n let inRegion = false;\n let ignoreNext = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line.includes(IGNORE_START)) {\n inRegion = true;\n ignored[i] = true;\n continue;\n }\n if (line.includes(IGNORE_END)) {\n inRegion = false;\n ignored[i] = true;\n continue;\n }\n if (inRegion) {\n ignored[i] = true;\n continue;\n }\n if (ignoreNext) {\n if (line.trim().length === 0) continue; // skip blanks to the next real line\n ignored[i] = true;\n ignoreNext = false;\n continue;\n }\n if (line.includes(IGNORE_LINE)) {\n ignored[i] = true;\n const rest = line.replace(IGNORE_LINE, \"\").trim();\n if (rest.length === 0) ignoreNext = true;\n }\n }\n return ignored;\n}\n\n/**\n * Extract every checkable claim from a context-file's markdown. Deterministic\n * and purely lexical — precision comes from the checks' provability gates,\n * not from clever parsing here.\n */\nexport function extractClaims(content: string): DocClaims {\n const lines = content.split(\"\\n\");\n const ignored = computeIgnoredLines(lines);\n\n const paths = new Map<string, PathClaim>();\n const counts: CountClaim[] = [];\n const commands = new Map<string, CommandClaim>();\n\n const addPath = (claim: PathClaim): void => {\n if (!paths.has(claim.path)) paths.set(claim.path, claim);\n };\n const addCommand = (claim: CommandClaim): void => {\n if (!commands.has(claim.scriptName)) commands.set(claim.scriptName, claim);\n };\n\n let inFence = false;\n let fenceInfo = \"\";\n let fenceMarker = \"\";\n let blockLines: string[] = [];\n let blockStartLine = 0;\n\n const processBlock = (): void => {\n for (const claim of extractTreeClaims(blockLines, blockStartLine)) {\n addPath(claim);\n }\n for (let i = 0; i < blockLines.length; i++) {\n const exact = exactTokenPath(blockLines[i]);\n if (exact) {\n addPath({\n path: exact,\n line: blockStartLine + i,\n excerpt: blockLines[i].trim(),\n });\n }\n }\n };\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineNo = i + 1;\n const fenceMatch = line.match(/^\\s*(```+|~~~+)(.*)$/);\n\n if (fenceMatch) {\n if (!inFence) {\n inFence = true;\n fenceMarker = fenceMatch[1][0];\n fenceInfo = fenceMatch[2].trim().toLowerCase();\n blockLines = [];\n blockStartLine = lineNo + 1;\n } else if (fenceMatch[1][0] === fenceMarker) {\n inFence = false;\n processBlock();\n }\n continue;\n }\n\n if (inFence) {\n // Ignored lines become spacers so the rest of a tree still parses.\n blockLines.push(ignored[i] ? \"\" : line);\n if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {\n for (const m of line.matchAll(COMMAND_RE)) {\n addCommand({\n scriptName: m[2],\n invocation: m[0],\n line: lineNo,\n excerpt: m[0],\n });\n }\n }\n continue;\n }\n\n if (ignored[i]) continue;\n\n for (const m of line.matchAll(/`([^`]+)`/g)) {\n const normalized = normalizePathToken(m[1]);\n if (normalized) {\n addPath({ path: normalized, line: lineNo, excerpt: m[1] });\n }\n }\n for (const m of line.matchAll(/\"([A-Za-z][\\w.@-]*(?:\\/[\\w.@-]+)+\\/?)\"/g)) {\n const normalized = normalizePathToken(m[1]);\n if (normalized) {\n addPath({ path: normalized, line: lineNo, excerpt: m[1] });\n }\n }\n for (const m of line.matchAll(COUNT_RE)) {\n const rest = line.slice((m.index ?? 0) + m[0].length);\n if (COUNT_DENYLIST_RE.test(rest)) continue;\n counts.push({\n count: Number.parseInt(m[1], 10),\n unit: m[2].toLowerCase(),\n line: lineNo,\n excerpt: m[0],\n });\n }\n for (const m of line.matchAll(COMMAND_RE)) {\n addCommand({\n scriptName: m[2],\n invocation: m[0],\n line: lineNo,\n excerpt: m[0],\n });\n }\n }\n\n // An unclosed fence still gets its block processed — trees at the end of a\n // truncated doc are claims too.\n if (inFence) processBlock();\n\n return {\n paths: [...paths.values()],\n counts,\n commands: [...commands.values()],\n };\n}\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { CommitRef } from \"./types.js\";\n\nconst exec = promisify(execFile);\n\nconst COMMIT_FORMAT = \"%H%x09%cI%x09%s\";\n\nfunction parseCommitLine(line: string): CommitRef | null {\n const parts = line.split(\"\\t\");\n if (parts.length < 3 || !parts[0]) return null;\n return { hash: parts[0], date: parts[1], subject: parts.slice(2).join(\"\\t\") };\n}\n\n/** Most recent commit touching a path, or null if the path was never tracked. */\nexport async function lastCommitOf(\n resolvedRoot: string,\n relPath: string\n): Promise<CommitRef | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"log\", \"-1\", `--format=${COMMIT_FORMAT}`, \"--\", relPath],\n { cwd: resolvedRoot }\n );\n const line = stdout.trim().split(\"\\n\")[0];\n return line ? parseCommitLine(line) : null;\n } catch {\n return null;\n }\n}\n\n/** The commit that deleted a path, or null if none did. */\nexport async function deletingCommitOf(\n resolvedRoot: string,\n relPath: string\n): Promise<CommitRef | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\n \"log\",\n \"-1\",\n \"--diff-filter=D\",\n `--format=${COMMIT_FORMAT}`,\n \"--\",\n relPath,\n ],\n { cwd: resolvedRoot }\n );\n const line = stdout.trim().split(\"\\n\")[0];\n return line ? parseCommitLine(line) : null;\n } catch {\n return null;\n }\n}\n\n/** Oldest commit touching a path (used to date a directory's appearance). */\nexport async function firstCommitOf(\n resolvedRoot: string,\n relPath: string\n): Promise<CommitRef | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"log\", \"--reverse\", `--format=${COMMIT_FORMAT}`, \"--\", relPath],\n { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }\n );\n const line = stdout.trim().split(\"\\n\")[0];\n return line ? parseCommitLine(line) : null;\n } catch {\n return null;\n }\n}\n\nexport interface RangeCommits {\n commits: Array<CommitRef & { files: string[] }>;\n total: number;\n}\n\n/**\n * Commits in fromHash..HEAD touching any of the pathspecs, newest first, with\n * the touched files per commit. Rev-range, not --since: immune to rebase date\n * skew and CI checkout mtimes. Returns null when the range is uncomputable\n * (unreachable base commit, shallow clone, no git).\n */\nexport async function commitsTouchingSince(\n resolvedRoot: string,\n fromHash: string,\n pathspecs: string[]\n): Promise<RangeCommits | null> {\n if (!fromHash || fromHash === \"unknown\") return null;\n try {\n const { stdout } = await exec(\n \"git\",\n [\n \"log\",\n `${fromHash}..HEAD`,\n `--format=%x01${COMMIT_FORMAT}`,\n \"--name-only\",\n \"--\",\n ...pathspecs,\n ],\n { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }\n );\n\n const commits: Array<CommitRef & { files: string[] }> = [];\n // \\x01 marks each commit header so name-only file lists can't be\n // mistaken for headers.\n for (const block of stdout.split(\"\\x01\")) {\n if (!block.trim()) continue;\n const lines = block.split(\"\\n\").filter((l) => l.trim().length > 0);\n const ref = parseCommitLine(lines[0]);\n if (!ref) continue;\n commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });\n }\n return { commits, total: commits.length };\n } catch {\n return null;\n }\n}\n","export type IssueType =\n | \"deleted-reference\"\n | \"new-module\"\n | \"stale-count\"\n | \"dead-command\";\n\nexport type AdvisoryType = \"deps-changed\" | \"decision-anchor-drift\";\n\nexport type CheckName = IssueType | AdvisoryType;\n\nexport const ALL_CHECKS: CheckName[] = [\n \"deleted-reference\",\n \"new-module\",\n \"stale-count\",\n \"dead-command\",\n \"deps-changed\",\n \"decision-anchor-drift\",\n];\n\n/**\n * \"certain\" — the claim is provably false (a tracked path is gone, a computed\n * count differs, a script exists in no manifest). \"likely\" — evidence-backed\n * but heuristic (an unmentioned directory; a never-tracked path whose parent\n * exists). Certain-class issues are safe to auto-fix; likely-class issues\n * deserve a look.\n */\nexport type Confidence = \"certain\" | \"likely\";\n\nexport interface CommitRef {\n hash: string;\n date: string;\n subject: string;\n}\n\nexport interface DocAnchor {\n /** Repo-relative doc path, e.g. \"CLAUDE.md\" or \".claude/CLAUDE.md\". */\n doc: string;\n /** 1-based line of the claim; null for doc-level issues (new-module). */\n line: number | null;\n /** The claim exactly as written, e.g. \"src/utils/logger.ts\". */\n excerpt: string | null;\n}\n\nexport type Evidence =\n | {\n kind: \"missing-path\";\n claimed: string;\n renamedTo: string | null;\n deletedInCommit: CommitRef | null;\n everTracked: boolean;\n parentDirExists: boolean;\n }\n | {\n kind: \"unmentioned-dir\";\n dir: string;\n sourceFileCount: number;\n firstCommit: CommitRef | null;\n checkedDocs: string[];\n }\n | {\n kind: \"count-mismatch\";\n claimed: number;\n actual: number;\n unit: string;\n /** Where the actual count came from, e.g. \"package.json workspaces\". */\n countedFrom: string;\n members: string[];\n }\n | {\n kind: \"missing-script\";\n scriptName: string;\n invocation: string;\n manifestsChecked: string[];\n availableScripts: string[];\n }\n | {\n kind: \"doc-behind-manifests\";\n docLastCommit: CommitRef;\n manifestCommits: Array<CommitRef & { files: string[] }>;\n totalCommits: number;\n }\n | {\n kind: \"decision-anchor\";\n decisionId: string;\n title: string;\n changedFiles: string[];\n refreshedHash: string;\n };\n\nexport interface AuditIssue {\n type: IssueType;\n message: string;\n anchor: DocAnchor;\n confidence: Confidence;\n evidence: Evidence;\n}\n\n/**\n * Advisories are facts the fixing agent cannot close by editing the doc (a\n * manifest commit after the doc's commit stays true forever; decision records\n * must be re-verified by humans). They NEVER affect the exit code — same\n * precedent as decision staleness in mason-drift.\n */\nexport interface AuditAdvisory {\n type: AdvisoryType;\n message: string;\n anchor: DocAnchor;\n evidence: Evidence;\n}\n\nexport interface AuditDocInfo {\n path: string;\n lastCommit: CommitRef | null;\n /** Uncommitted edits present — deps-changed is suppressed for dirty docs. */\n dirty: boolean;\n lineCount: number;\n}\n\nexport interface AuditReport {\n /** Additive-only schema — this output is a CI contract. */\n version: 1;\n root: string;\n gitAvailable: boolean;\n docs: AuditDocInfo[];\n /** Whether .mason/decisions/ existed and was checked. */\n decisionsChecked: boolean;\n /** Drive exit code 1. */\n issues: AuditIssue[];\n /** Never drive the exit code. */\n advisories: AuditAdvisory[];\n skippedChecks: Array<{ check: string; reason: string }>;\n clean: boolean;\n}\n\nexport interface PathClaim {\n path: string;\n line: number;\n excerpt: string;\n}\n\nexport interface CountClaim {\n count: number;\n unit: string;\n line: number;\n excerpt: string;\n}\n\nexport interface CommandClaim {\n scriptName: string;\n invocation: string;\n line: number;\n excerpt: string;\n}\n\nexport interface DocClaims {\n paths: PathClaim[];\n counts: CountClaim[];\n commands: CommandClaim[];\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { deletingCommitOf, lastCommitOf } from \"../git.js\";\nimport type { AuditIssue } from \"../types.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nasync function exists(absPath: string): Promise<boolean> {\n try {\n await fs.access(absPath);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * A claimed path that is missing on disk is only flagged when the repo can\n * prove it was ever real: a rename since the doc's last commit, or git\n * history for the path, or at least an existing parent directory. Paths with\n * none of those are illustrative examples and are dropped silently.\n */\nexport async function checkDeletedReferences(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n for (const doc of ctx.docs) {\n const changes = ctx.changesSinceDoc.get(doc.path);\n const renames = new Map<string, string>();\n for (const change of changes ?? []) {\n if (change.status === \"renamed\" && change.previousPath) {\n renames.set(change.previousPath, change.path);\n }\n }\n\n for (const claim of doc.claims.paths) {\n // Mason's own metadata is optional state, not repo structure — docs\n // legitimately describe .mason/ files that a given repo doesn't have.\n if (claim.path === \".mason\" || claim.path.startsWith(\".mason/\")) {\n continue;\n }\n if (await exists(path.join(ctx.root, claim.path))) continue;\n\n const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };\n const renamedTo = renames.get(claim.path) ?? null;\n\n if (renamedTo) {\n result.issues.push({\n type: \"deleted-reference\",\n message: `\\`${claim.path}\\` was renamed to \\`${renamedTo}\\``,\n anchor,\n confidence: \"certain\",\n evidence: {\n kind: \"missing-path\",\n claimed: claim.path,\n renamedTo,\n deletedInCommit: null,\n everTracked: true,\n parentDirExists: true,\n },\n });\n continue;\n }\n\n const tracked = await lastCommitOf(ctx.root, claim.path);\n if (tracked) {\n const deleted = await deletingCommitOf(ctx.root, claim.path);\n const detail = deleted\n ? ` – deleted in ${deleted.hash.slice(0, 7)} \"${deleted.subject}\" (${deleted.date.slice(0, 10)})`\n : \"\";\n result.issues.push({\n type: \"deleted-reference\",\n message: `\\`${claim.path}\\` no longer exists${detail}`,\n anchor,\n confidence: \"certain\",\n evidence: {\n kind: \"missing-path\",\n claimed: claim.path,\n renamedTo: null,\n deletedInCommit: deleted,\n everTracked: true,\n parentDirExists: await exists(\n path.join(ctx.root, path.dirname(claim.path))\n ),\n },\n });\n continue;\n }\n\n const parentDirExists = await exists(\n path.join(ctx.root, path.dirname(claim.path))\n );\n if (!parentDirExists) continue;\n\n const issue: AuditIssue = {\n type: \"deleted-reference\",\n message: `\\`${claim.path}\\` does not exist (never tracked in git – possible typo or invented path)`,\n anchor,\n confidence: \"likely\",\n evidence: {\n kind: \"missing-path\",\n claimed: claim.path,\n renamedTo: null,\n deletedInCommit: null,\n everTracked: false,\n parentDirExists: true,\n },\n };\n result.issues.push(issue);\n }\n }\n\n return result;\n}\n","import fg from \"fast-glob\";\nimport path from \"node:path\";\nimport { SOURCE_GLOB, SOURCE_IGNORE } from \"../../snapshot/snapshot.js\";\nimport { firstCommitOf } from \"../git.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\n/** Directories that are never \"modules\" worth documenting. */\nconst DIR_DENYLIST = new Set([\n \"node_modules\",\n \"dist\",\n \"build\",\n \"out\",\n \"coverage\",\n \"target\",\n \"vendor\",\n \"__pycache__\",\n \"venv\",\n \".venv\",\n \".git\",\n \".gradle\",\n \".mason\",\n \".claude\",\n \".github\",\n \".vscode\",\n \".idea\",\n]);\n\n/** Second-level dirs need a bit more substance before they count. */\nconst SECOND_LEVEL_MIN_SOURCE_FILES = 2;\n/**\n * Descend into a top-level dir only when the docs evidently enumerate its\n * children — at least this many of its subdirs already mentioned.\n */\nconst ENUMERATION_THRESHOLD = 2;\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Word-boundary mention check across the union of all docs — `app` must not\n * match \"application\", and a dir mentioned only in AGENTS.md must not be\n * flagged against CLAUDE.md.\n */\nfunction isMentioned(combinedDocs: string, name: string): boolean {\n const re = new RegExp(\n `(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,\n \"im\"\n );\n return re.test(combinedDocs);\n}\n\nasync function listSubdirs(absDir: string): Promise<string[]> {\n const dirs = await fg(\"*\", {\n cwd: absDir,\n onlyDirectories: true,\n suppressErrors: true,\n });\n return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();\n}\n\nasync function countSourceFiles(absDir: string): Promise<number> {\n const files = await fg(SOURCE_GLOB, {\n cwd: absDir,\n ignore: SOURCE_IGNORE,\n suppressErrors: true,\n });\n return files.length;\n}\n\nexport async function checkNewModules(ctx: CheckContext): Promise<CheckResult> {\n const result = emptyResult();\n if (ctx.docs.length === 0) return result;\n\n const combinedDocs = ctx.docs.map((d) => d.content).join(\"\\n\");\n const primaryDoc = ctx.docs[0].path;\n const checkedDocs = ctx.docs.map((d) => d.path);\n\n const flag = async (dir: string, sourceFileCount: number): Promise<void> => {\n result.issues.push({\n type: \"new-module\",\n message: `directory \\`${dir}/\\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? \"\" : \"s\"} but is not mentioned in any context file`,\n anchor: { doc: primaryDoc, line: null, excerpt: dir },\n confidence: \"likely\",\n evidence: {\n kind: \"unmentioned-dir\",\n dir,\n sourceFileCount,\n firstCommit: await firstCommitOf(ctx.root, dir),\n checkedDocs,\n },\n });\n };\n\n for (const topDir of await listSubdirs(ctx.root)) {\n const absTop = path.join(ctx.root, topDir);\n const topMentioned = isMentioned(combinedDocs, topDir);\n\n if (!topMentioned) {\n const count = await countSourceFiles(absTop);\n if (count >= 1) await flag(topDir, count);\n continue;\n }\n\n // The docs know this dir. If they enumerate its children (several\n // subdirs already mentioned), an unmentioned sibling is drift — this is\n // how a freshly added module under src/ gets caught.\n const subdirs = await listSubdirs(absTop);\n const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));\n if (mentioned.length < ENUMERATION_THRESHOLD) continue;\n\n for (const sub of subdirs) {\n if (isMentioned(combinedDocs, sub)) continue;\n const count = await countSourceFiles(path.join(absTop, sub));\n if (count >= SECOND_LEVEL_MIN_SOURCE_FILES) {\n await flag(`${topDir}/${sub}`, count);\n }\n }\n }\n\n return result;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { CountClaim } from \"../types.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nconst MEMBERS_CAP = 50;\n\ninterface CountSource {\n actual: number;\n countedFrom: string;\n members: string[];\n}\n\nasync function readIfExists(absPath: string): Promise<string | null> {\n try {\n return await fs.readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nasync function countGradleModules(root: string): Promise<CountSource | null> {\n for (const name of [\"settings.gradle.kts\", \"settings.gradle\"]) {\n const content = await readIfExists(path.join(root, name));\n if (content === null) continue;\n // include(\":a\", \":b\") — count quoted project strings, not include() calls.\n const members: string[] = [];\n for (const call of content.matchAll(/include\\s*\\(([^)]*)\\)/g)) {\n for (const proj of call[1].matchAll(/[\"']([^\"']+)[\"']/g)) {\n members.push(proj[1]);\n }\n }\n if (members.length === 0) return null;\n return { actual: members.length, countedFrom: name, members };\n }\n return null;\n}\n\nasync function countNpmWorkspaces(root: string): Promise<CountSource | null> {\n const pkgRaw = await readIfExists(path.join(root, \"package.json\"));\n if (pkgRaw !== null) {\n try {\n const pkg = JSON.parse(pkgRaw);\n const globs: string[] = Array.isArray(pkg.workspaces)\n ? pkg.workspaces\n : Array.isArray(pkg.workspaces?.packages)\n ? pkg.workspaces.packages\n : [];\n if (globs.length > 0) {\n const matched = await fg(\n globs.map((g) => `${g.replace(/\\/+$/, \"\")}/package.json`),\n { cwd: root, ignore: [\"**/node_modules/**\"] }\n );\n return {\n actual: matched.length,\n countedFrom: \"package.json workspaces\",\n members: matched.map((m) => path.dirname(m)).sort(),\n };\n }\n } catch {\n // Malformed package.json — nothing provable here.\n }\n }\n\n const pnpmRaw = await readIfExists(path.join(root, \"pnpm-workspace.yaml\"));\n if (pnpmRaw !== null) {\n const globs: string[] = [];\n let inPackages = false;\n for (const line of pnpmRaw.split(\"\\n\")) {\n if (/^packages\\s*:/.test(line)) {\n inPackages = true;\n continue;\n }\n if (inPackages) {\n const entry = line.match(/^\\s*-\\s*[\"']?([^\"'#\\s]+)/);\n if (entry) {\n if (!entry[1].startsWith(\"!\")) globs.push(entry[1]);\n } else if (line.trim().length > 0 && !line.startsWith(\" \")) {\n inPackages = false;\n }\n }\n }\n if (globs.length > 0) {\n const matched = await fg(\n globs.map((g) => `${g.replace(/\\/+$/, \"\")}/package.json`),\n { cwd: root, ignore: [\"**/node_modules/**\"] }\n );\n return {\n actual: matched.length,\n countedFrom: \"pnpm-workspace.yaml\",\n members: matched.map((m) => path.dirname(m)).sort(),\n };\n }\n }\n return null;\n}\n\nasync function countCargoCrates(root: string): Promise<CountSource | null> {\n const content = await readIfExists(path.join(root, \"Cargo.toml\"));\n if (content === null) return null;\n const membersBlock = content.match(/members\\s*=\\s*\\[([\\s\\S]*?)\\]/);\n if (!membersBlock) return null;\n const entries = [...membersBlock[1].matchAll(/[\"']([^\"']+)[\"']/g)].map(\n (m) => m[1]\n );\n if (entries.length === 0) return null;\n\n // Workspace members may be globs (\"crates/*\") — resolve them against\n // directories that actually contain a Cargo.toml.\n const members = new Set<string>();\n for (const entry of entries) {\n if (/[*?[\\]{}]/.test(entry)) {\n const matched = await fg(`${entry.replace(/\\/+$/, \"\")}/Cargo.toml`, {\n cwd: root,\n ignore: [\"**/target/**\"],\n });\n for (const m of matched) members.add(path.dirname(m));\n } else if (\n (await readIfExists(path.join(root, entry, \"Cargo.toml\"))) !== null\n ) {\n members.add(entry);\n }\n }\n if (members.size === 0) return null;\n return {\n actual: members.size,\n countedFrom: \"Cargo.toml workspace members\",\n members: [...members].sort(),\n };\n}\n\n/**\n * Map a claim's unit to the ecosystem that can prove it. When the mapped\n * ecosystem has no workspace manifest in this repo, the claim is skipped —\n * \"12 packages\" in a Gradle repo proves nothing either way.\n */\nasync function resolveCountSource(\n root: string,\n claim: CountClaim\n): Promise<CountSource | null> {\n const unit = claim.unit.replace(/s$/, \"\");\n if (unit === \"module\") return countGradleModules(root);\n if (unit === \"workspace\") return countNpmWorkspaces(root);\n if (unit === \"crate\") return countCargoCrates(root);\n // \"packages\" is ecosystem-ambiguous — first manifest that resolves wins.\n return (\n (await countNpmWorkspaces(root)) ??\n (await countCargoCrates(root)) ??\n (await countGradleModules(root))\n );\n}\n\nexport async function checkStaleCounts(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n for (const doc of ctx.docs) {\n for (const claim of doc.claims.counts) {\n const source = await resolveCountSource(ctx.root, claim);\n if (source === null || source.actual === claim.count) continue;\n result.issues.push({\n type: \"stale-count\",\n message: `says \"${claim.excerpt}\" but ${source.countedFrom} resolves to ${source.actual}`,\n anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },\n confidence: \"certain\",\n evidence: {\n kind: \"count-mismatch\",\n claimed: claim.count,\n actual: source.actual,\n unit: claim.unit,\n countedFrom: source.countedFrom,\n members: source.members.slice(0, MEMBERS_CAP),\n },\n });\n }\n }\n\n return result;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nconst AVAILABLE_SCRIPTS_CAP = 30;\n\nasync function scriptsOf(absManifest: string): Promise<string[] | null> {\n try {\n const pkg = JSON.parse(await fs.readFile(absManifest, \"utf-8\"));\n return pkg && typeof pkg.scripts === \"object\" && pkg.scripts !== null\n ? Object.keys(pkg.scripts)\n : [];\n } catch {\n return null;\n }\n}\n\n/**\n * `npm run <script>` claims checked against package.json scripts — the one\n * ecosystem where task discovery is a single JSON parse. A script missing\n * from the root manifest is searched in every workspace manifest before\n * being flagged; docs legitimately say \"in packages/foo run `npm run build`\".\n */\nexport async function checkDeadCommands(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n const commandClaims = ctx.docs.flatMap((doc) =>\n doc.claims.commands.map((claim) => ({ doc, claim }))\n );\n if (commandClaims.length === 0) return result;\n\n const rootScripts = await scriptsOf(path.join(ctx.root, \"package.json\"));\n if (rootScripts === null) {\n result.skipped.push({\n check: \"dead-command\",\n reason: \"no package.json at the repo root\",\n });\n return result;\n }\n const rootSet = new Set(rootScripts);\n\n let workspaceScripts: Set<string> | null = null;\n let manifestsChecked: string[] = [\"package.json\"];\n const loadWorkspaceScripts = async (): Promise<Set<string>> => {\n if (workspaceScripts !== null) return workspaceScripts;\n workspaceScripts = new Set<string>();\n const manifests = await fg(\"**/package.json\", {\n cwd: ctx.root,\n ignore: [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"package.json\",\n ],\n });\n manifestsChecked = [\"package.json\", ...manifests.sort()];\n for (const manifest of manifests) {\n const scripts = await scriptsOf(path.join(ctx.root, manifest));\n for (const name of scripts ?? []) workspaceScripts.add(name);\n }\n return workspaceScripts;\n };\n\n for (const { doc, claim } of commandClaims) {\n if (rootSet.has(claim.scriptName)) continue;\n const elsewhere = await loadWorkspaceScripts();\n if (elsewhere.has(claim.scriptName)) continue;\n\n result.issues.push({\n type: \"dead-command\",\n message: `\\`${claim.invocation}\\` refers to script \"${claim.scriptName}\", which exists in no package.json`,\n anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },\n confidence: \"certain\",\n evidence: {\n kind: \"missing-script\",\n scriptName: claim.scriptName,\n invocation: claim.invocation,\n manifestsChecked,\n availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP),\n },\n });\n }\n\n return result;\n}\n","import { commitsTouchingSince } from \"../git.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nconst MANIFEST_COMMITS_CAP = 10;\n\n/**\n * Tracked manifest files at any depth. Lockfiles are pure churn and are\n * deliberately not matched.\n */\nconst MANIFEST_PATHSPECS = [\n \":(glob)**/package.json\",\n \":(glob)**/build.gradle.kts\",\n \":(glob)**/build.gradle\",\n \"settings.gradle.kts\",\n \"settings.gradle\",\n \"gradle/libs.versions.toml\",\n \":(glob)**/Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"requirements.txt\",\n \"Gemfile\",\n \"composer.json\",\n];\n\n/**\n * Advisory, never an issue: a manifest commit after the doc's last commit\n * proves recency ordering, not that any specific claim is false — and it can\n * never be closed by editing the doc within the same run.\n */\nexport async function checkDepsChanged(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n for (const doc of ctx.docs) {\n if (!doc.lastCommit) {\n result.skipped.push({\n check: \"deps-changed\",\n reason: `${doc.path} has no commit history`,\n });\n continue;\n }\n if (doc.dirty) {\n result.skipped.push({\n check: \"deps-changed\",\n reason: `${doc.path} has uncommitted edits – suppressed while in flight`,\n });\n continue;\n }\n\n const range = await commitsTouchingSince(\n ctx.root,\n doc.lastCommit.hash,\n MANIFEST_PATHSPECS\n );\n if (range === null) {\n result.skipped.push({\n check: \"deps-changed\",\n reason: `${doc.path}: commit range unreachable (shallow clone?)`,\n });\n continue;\n }\n if (range.total === 0) continue;\n\n const latest = range.commits[0];\n result.advisories.push({\n type: \"deps-changed\",\n message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? \"\" : \"s\"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} \"${latest.subject}\")`,\n anchor: { doc: doc.path, line: null, excerpt: null },\n evidence: {\n kind: \"doc-behind-manifests\",\n docLastCommit: doc.lastCommit,\n manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),\n totalCommits: range.total,\n },\n });\n }\n\n return result;\n}\n","import path from \"node:path\";\nimport { getChangesWithStatus } from \"../drift/drift.js\";\nimport { getCurrentGitHash } from \"../snapshot/snapshot.js\";\nimport { loadDecisions } from \"./decisions.js\";\nimport type { DecisionRecord } from \"./decisions.js\";\n\n/**\n * Deliberately separate from DriftReport: mason-drift's exit codes and\n * --json shape are a CI contract, and `stale` there means MAP staleness.\n * Decision staleness is additive on top.\n */\nexport interface DecisionDriftReport {\n historyAvailable: boolean;\n totalDecisions: number;\n /** Decision id → anchor files changed since the record's refreshedHash. */\n staleDecisions: Record<string, string[]>;\n}\n\n/**\n * Flag active decisions whose anchor files changed since the record was\n * last verified. Anchorless decisions are pure prose and never go stale.\n * Deterministic — git only, no LLM.\n */\nexport async function computeDecisionDrift(\n rootDir: string,\n decisions?: DecisionRecord[]\n): Promise<DecisionDriftReport> {\n const resolvedRoot = path.resolve(rootDir);\n const records = decisions ?? (await loadDecisions(resolvedRoot));\n const report: DecisionDriftReport = {\n historyAvailable: true,\n totalDecisions: records.length,\n staleDecisions: {},\n };\n\n const head = await getCurrentGitHash(resolvedRoot);\n const changesByHash = new Map<string, Set<string> | null>();\n\n for (const record of records) {\n if (record.status !== \"active\" || record.files.length === 0) continue;\n if (record.refreshedHash === head) continue;\n\n let touched = changesByHash.get(record.refreshedHash);\n if (touched === undefined) {\n const changes = await getChangesWithStatus(\n resolvedRoot,\n record.refreshedHash\n );\n if (changes === null) {\n touched = null;\n } else {\n touched = new Set<string>();\n for (const change of changes) {\n touched.add(change.path);\n if (change.previousPath) touched.add(change.previousPath);\n }\n }\n changesByHash.set(record.refreshedHash, touched);\n }\n\n if (touched === null) {\n // Unreachable base commit — we know nothing per-file; surface that\n // rather than silently reporting the record fresh.\n report.historyAvailable = false;\n continue;\n }\n\n const hits = record.files.filter((f) => touched.has(f));\n if (hits.length > 0) {\n report.staleDecisions[record.id] = hits;\n }\n }\n\n return report;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { getCurrentGitHash } from \"../snapshot/snapshot.js\";\nimport { jaccard, tokenSet } from \"../context/lexical.js\";\n\nexport type DecisionCategory =\n | \"decision\"\n | \"gotcha\"\n | \"deprecation\"\n | \"convention\";\nexport type DecisionStatus = \"active\" | \"superseded\";\n\n/**\n * One unit of team knowledge the code alone can't express: a failed\n * approach, a deprecation, a workaround's reason, a review-settled\n * convention. Stored one file per record under .mason/decisions/ so\n * concurrent additions on different branches merge without conflict,\n * while concurrent edits to the SAME record conflict — contested\n * knowledge should reach a human.\n */\nexport interface DecisionRecord {\n version: 1;\n id: string;\n title: string;\n body: string;\n category: DecisionCategory;\n /** Repo-relative anchor files. Empty means pure prose — never goes stale. */\n files: string[];\n createdAt: string;\n updatedAt: string;\n /** Commit this record was last verified against. */\n refreshedHash: string;\n status: DecisionStatus;\n supersededBy?: string;\n}\n\nexport const TITLE_MAX_CHARS = 80;\nexport const BODY_MAX_CHARS = 1500;\nexport const MAX_ACTIVE_DECISIONS = 150;\n\nconst DUPLICATE_JACCARD = 0.5;\nconst DUPLICATE_JACCARD_WITH_SHARED_FILE = 0.35;\n\nfunction decisionsDir(rootDir: string): string {\n return path.join(rootDir, \".mason\", \"decisions\");\n}\n\nexport async function loadDecisions(\n rootDir: string\n): Promise<DecisionRecord[]> {\n let entries: string[];\n try {\n entries = await fs.readdir(decisionsDir(rootDir));\n } catch {\n return [];\n }\n const records: DecisionRecord[] = [];\n for (const entry of entries) {\n if (!entry.endsWith(\".json\")) continue;\n try {\n const raw = await fs.readFile(\n path.join(decisionsDir(rootDir), entry),\n \"utf-8\"\n );\n const parsed = JSON.parse(raw);\n // Skip unknown versions and malformed records individually — one bad\n // merge artifact must not take down the store.\n if (parsed.version !== 1 || !parsed.id || !parsed.title || !parsed.body) {\n continue;\n }\n records.push(parsed);\n } catch {\n continue;\n }\n }\n return records.sort((a, b) => a.id.localeCompare(b.id));\n}\n\nexport async function saveDecisionRecord(\n rootDir: string,\n record: DecisionRecord\n): Promise<void> {\n await fs.mkdir(decisionsDir(rootDir), { recursive: true });\n await fs.writeFile(\n path.join(decisionsDir(rootDir), `${record.id}.json`),\n JSON.stringify(record, null, 2) + \"\\n\",\n \"utf-8\"\n );\n}\n\n/**\n * Deterministic, human-readable id: kebab slug of the title, ≤60 chars.\n * A slug collision with a DIFFERENT record appends a 6-hex content suffix.\n */\nexport function decisionIdFor(\n title: string,\n body: string,\n existingIds: Set<string>\n): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60)\n .replace(/-+$/, \"\");\n if (!existingIds.has(slug)) return slug || \"decision\";\n const suffix = createHash(\"sha1\")\n .update(title + body)\n .digest(\"hex\")\n .slice(0, 6);\n return `${slug}-${suffix}`;\n}\n\nexport function findNearDuplicate(\n candidate: { title: string; body: string; files: string[] },\n existing: DecisionRecord[]\n): { record: DecisionRecord; similarity: number } | null {\n const candidateTokens = tokenSet(`${candidate.title} ${candidate.body}`);\n const candidateFiles = new Set(candidate.files);\n let best: { record: DecisionRecord; similarity: number } | null = null;\n\n for (const record of existing) {\n if (record.status !== \"active\") continue;\n const similarity = jaccard(\n candidateTokens,\n tokenSet(`${record.title} ${record.body}`)\n );\n const sharesFile = record.files.some((f) => candidateFiles.has(f));\n const threshold = sharesFile\n ? DUPLICATE_JACCARD_WITH_SHARED_FILE\n : DUPLICATE_JACCARD;\n if (similarity >= threshold && (!best || similarity > best.similarity)) {\n best = { record, similarity };\n }\n }\n return best;\n}\n\nfunction sanitizeAnchorFiles(rootDir: string, files: string[]): string[] {\n const resolvedRoot = path.resolve(rootDir);\n return files.filter((f) => {\n const resolved = path.resolve(resolvedRoot, f);\n return (\n resolved.startsWith(resolvedRoot) &&\n !f.startsWith(\"/\") &&\n !f.includes(\"..\")\n );\n });\n}\n\nexport interface UpsertDecisionInput {\n title: string;\n body: string;\n category: DecisionCategory;\n files?: string[];\n /** Existing id to update. Same id + unchanged content = re-verify (re-pin to HEAD). */\n id?: string;\n /** Id of a decision this one replaces; the old record is kept, marked superseded. */\n supersedes?: string;\n /** Save even when a near-duplicate was detected. */\n force?: boolean;\n}\n\nexport type UpsertDecisionResult =\n | {\n status: \"created\" | \"updated\" | \"reverified\" | \"superseded_and_created\";\n id: string;\n totalActive: number;\n warnings: string[];\n pruneCandidates?: string[];\n }\n | { status: \"duplicate_suspected\"; existing: DecisionRecord; hint: string }\n | { status: \"error\"; error: string };\n\nexport async function upsertDecision(\n rootDir: string,\n input: UpsertDecisionInput\n): Promise<UpsertDecisionResult> {\n const title = input.title.trim();\n const body = input.body.trim();\n if (title.length === 0 || body.length === 0) {\n return { status: \"error\", error: \"title and body must be non-empty\" };\n }\n if (title.length > TITLE_MAX_CHARS) {\n return {\n status: \"error\",\n error: `title exceeds ${TITLE_MAX_CHARS} chars — tighten it to a specific headline`,\n };\n }\n if (body.length > BODY_MAX_CHARS) {\n return {\n status: \"error\",\n error: `body exceeds ${BODY_MAX_CHARS} chars — record the decision, not the transcript`,\n };\n }\n\n const existing = await loadDecisions(rootDir);\n const byId = new Map(existing.map((r) => [r.id, r]));\n const now = new Date().toISOString();\n const head = await getCurrentGitHash(rootDir);\n const warnings: string[] = [];\n\n const files = sanitizeAnchorFiles(rootDir, input.files ?? []);\n if (input.files && files.length < input.files.length) {\n warnings.push(\"some anchor paths were outside the repo and were dropped\");\n }\n // Nonexistent anchors warn but save — a deprecation note may outlive its file.\n for (const f of files) {\n try {\n await fs.access(path.join(rootDir, f));\n } catch {\n warnings.push(`anchor file does not exist on disk: ${f}`);\n }\n }\n\n // Update / re-verify path\n if (input.id) {\n const record = byId.get(input.id);\n if (!record) {\n return { status: \"error\", error: `no decision with id \"${input.id}\"` };\n }\n const unchanged =\n record.title === title &&\n record.body === body &&\n record.category === input.category &&\n JSON.stringify(record.files) === JSON.stringify(files.length > 0 ? files : record.files);\n const updated: DecisionRecord = {\n ...record,\n title,\n body,\n category: input.category,\n files: input.files !== undefined ? files : record.files,\n updatedAt: now,\n refreshedHash: head,\n };\n await saveDecisionRecord(rootDir, updated);\n return {\n status: unchanged ? \"reverified\" : \"updated\",\n id: record.id,\n totalActive: existing.filter((r) => r.status === \"active\").length,\n warnings,\n };\n }\n\n // Create path — dedupe first\n if (!input.force) {\n const duplicate = findNearDuplicate({ title, body, files }, existing);\n if (duplicate) {\n return {\n status: \"duplicate_suspected\",\n existing: duplicate.record,\n hint: `A similar decision exists (\"${duplicate.record.title}\"). Call save_decision with id=\"${duplicate.record.id}\" to update/merge into it, or force:true if genuinely distinct.`,\n };\n }\n }\n\n // Supersede\n if (input.supersedes) {\n const old = byId.get(input.supersedes);\n if (!old) {\n return {\n status: \"error\",\n error: `no decision with id \"${input.supersedes}\" to supersede`,\n };\n }\n const id = decisionIdFor(title, body, new Set(byId.keys()));\n await saveDecisionRecord(rootDir, {\n ...old,\n status: \"superseded\",\n supersededBy: id,\n updatedAt: now,\n });\n const record: DecisionRecord = {\n version: 1,\n id,\n title,\n body,\n category: input.category,\n files,\n createdAt: now,\n updatedAt: now,\n refreshedHash: head,\n status: \"active\",\n };\n await saveDecisionRecord(rootDir, record);\n return {\n status: \"superseded_and_created\",\n id,\n totalActive: existing.filter((r) => r.status === \"active\").length,\n warnings,\n };\n }\n\n const id = decisionIdFor(title, body, new Set(byId.keys()));\n const record: DecisionRecord = {\n version: 1,\n id,\n title,\n body,\n category: input.category,\n files,\n createdAt: now,\n updatedAt: now,\n refreshedHash: head,\n status: \"active\",\n };\n await saveDecisionRecord(rootDir, record);\n\n const totalActive =\n existing.filter((r) => r.status === \"active\").length + 1;\n const result: UpsertDecisionResult = {\n status: \"created\",\n id,\n totalActive,\n warnings,\n };\n if (totalActive > MAX_ACTIVE_DECISIONS) {\n // Never auto-evict git-committed team knowledge — surface candidates\n // for a human cleanup PR instead.\n result.pruneCandidates = existing\n .filter((r) => r.status === \"superseded\")\n .map((r) => r.id)\n .slice(0, 10);\n warnings.push(\n `${totalActive} active decisions exceeds the soft cap of ${MAX_ACTIVE_DECISIONS} — consider a cleanup PR (superseded records first)`\n );\n }\n return result;\n}\n","import path from \"node:path\";\n\n// Question/filler words that carry no signal about which entry a task\n// touches. Domain words (\"auth\", \"drift\") are never in this list.\nconst STOPWORDS = new Set([\n \"the\", \"a\", \"an\", \"and\", \"or\", \"of\", \"to\", \"in\", \"on\", \"for\", \"with\",\n \"how\", \"does\", \"do\", \"is\", \"are\", \"was\", \"what\", \"where\", \"which\", \"why\",\n \"when\", \"who\", \"i\", \"we\", \"my\", \"our\", \"you\", \"your\", \"it\", \"its\", \"this\",\n \"that\", \"these\", \"those\", \"can\", \"could\", \"should\", \"would\", \"will\",\n \"want\", \"need\", \"please\", \"about\", \"into\", \"from\", \"when\", \"there\", \"any\",\n \"all\", \"some\", \"not\", \"but\", \"also\", \"just\", \"like\", \"get\", \"make\", \"use\",\n \"new\", \"work\", \"works\", \"working\", \"implement\", \"implemented\", \"change\",\n \"changed\", \"file\", \"files\", \"code\",\n]);\n\n/** Split camelCase/PascalCase/kebab/snake/path into lowercase word tokens. */\nexport function tokenize(text: string): string[] {\n return text\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 2 && !STOPWORDS.has(t));\n}\n\n/** Crude singular/plural folding so \"flows\" matches \"flow\" etc. */\nexport function stem(token: string): string {\n return token.length > 3 && token.endsWith(\"s\") ? token.slice(0, -1) : token;\n}\n\nexport function tokenSet(text: string): Set<string> {\n return new Set(tokenize(text).map(stem));\n}\n\nexport interface Scorable {\n name: string;\n description: string;\n files: string[];\n}\n\n/**\n * Lexical relevance of one entry to the task. Name hits are the strongest\n * signal, then description, then file-path words. Each distinct task token\n * counts once at its best weight, so a token appearing everywhere doesn't\n * triple-count.\n */\nexport function scoreEntry(taskTokens: Set<string>, entry: Scorable): number {\n const nameTokens = tokenSet(entry.name);\n const descTokens = tokenSet(entry.description);\n const fileTokens = tokenSet(entry.files.map((f) => path.basename(f)).join(\" \"));\n\n let score = 0;\n for (const token of taskTokens) {\n if (nameTokens.has(token)) score += 3;\n else if (descTokens.has(token)) score += 1;\n else if (fileTokens.has(token)) score += 1;\n }\n return score;\n}\n\n/** Jaccard similarity of two token sets: |∩| / |∪|, 0 when both empty. */\nexport function jaccard(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const token of a) if (b.has(token)) intersection++;\n return intersection / (a.size + b.size - intersection);\n}\n","import { computeDecisionDrift } from \"../../decisions/drift.js\";\nimport { loadDecisions } from \"../../decisions/decisions.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\n/**\n * Advisory only, and only when .mason/decisions/ exists (the zero-setup\n * path stays dark on bare repos). Decision records encode human knowledge —\n * they are surfaced for re-verification, never rewritten by the fix agent.\n */\nexport async function checkDecisionAnchors(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n if (!ctx.decisionsPresent) return result;\n\n const records = await loadDecisions(ctx.root);\n const drift = await computeDecisionDrift(ctx.root, records);\n if (!drift.historyAvailable) {\n result.skipped.push({\n check: \"decision-anchor-drift\",\n reason: \"some decision base commits are unreachable (shallow clone?)\",\n });\n }\n\n const byId = new Map(records.map((r) => [r.id, r]));\n for (const [id, changedFiles] of Object.entries(drift.staleDecisions)) {\n const record = byId.get(id);\n if (!record) continue;\n result.advisories.push({\n type: \"decision-anchor-drift\",\n message: `decision \"${record.title}\" has anchor files that changed since it was verified – needs human re-verification`,\n anchor: {\n doc: `.mason/decisions/${id}.json`,\n line: null,\n excerpt: record.title,\n },\n evidence: {\n kind: \"decision-anchor\",\n decisionId: id,\n title: record.title,\n changedFiles,\n refreshedHash: record.refreshedHash,\n },\n });\n }\n\n return result;\n}\n","import type { FileChange } from \"../../drift/drift.js\";\nimport type { AuditAdvisory, AuditIssue, CheckName } from \"../types.js\";\nimport type { AuditDoc } from \"../docs.js\";\nimport { checkDeletedReferences } from \"./deleted-reference.js\";\nimport { checkNewModules } from \"./new-module.js\";\nimport { checkStaleCounts } from \"./stale-count.js\";\nimport { checkDeadCommands } from \"./dead-command.js\";\nimport { checkDepsChanged } from \"./deps-changed.js\";\nimport { checkDecisionAnchors } from \"./decision-anchor.js\";\n\nexport interface CheckContext {\n root: string;\n docs: AuditDoc[];\n headHash: string;\n /** Doc path → changes since the doc's last commit; null when uncomputable. */\n changesSinceDoc: Map<string, FileChange[] | null>;\n /** Whether .mason/decisions/ exists. */\n decisionsPresent: boolean;\n}\n\nexport interface CheckResult {\n issues: AuditIssue[];\n advisories: AuditAdvisory[];\n skipped: Array<{ check: string; reason: string }>;\n}\n\nexport type CheckFn = (ctx: CheckContext) => Promise<CheckResult>;\n\nexport const CHECKS: Record<CheckName, CheckFn> = {\n \"deleted-reference\": checkDeletedReferences,\n \"new-module\": checkNewModules,\n \"stale-count\": checkStaleCounts,\n \"dead-command\": checkDeadCommands,\n \"deps-changed\": checkDepsChanged,\n \"decision-anchor-drift\": checkDecisionAnchors,\n};\n\nexport function emptyResult(): CheckResult {\n return { issues: [], advisories: [], skipped: [] };\n}\n","import { runAuditCli } from \"../src/audit/cli.js\";\n\nrunAuditCli(process.argv.slice(2)).then(\n (code) => process.exit(code),\n (err) => {\n process.stderr.write(`mason-audit error: ${err}\\n`);\n process.exit(2);\n }\n);\n"],"mappings":"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","names":["path","fs","path","fs","path","execFile","promisify","fs","path","execFile","promisify","fg","path","fg","exec","promisify","execFile","exec","exec","promisify","execFile","exec","fs","path","execFile","promisify","execFile","promisify","exec","exec","promisify","execFile","fs","path","fs","path","fs","path","fg","path","fg","path","fs","path","fg","fs","path","fg","fs","path","fg","fs","path","fg","path","fs","path","path","path","fs","path","path","fs","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/audit/cli.ts","../src/audit/audit.ts","../src/drift/drift.ts","../src/snapshot/snapshot.ts","../src/mcp/sampler.ts","../src/test-map.ts","../src/audit/docs.ts","../src/audit/tree.ts","../src/audit/claims.ts","../src/audit/git.ts","../src/audit/types.ts","../src/audit/checks/deleted-reference.ts","../src/audit/checks/new-module.ts","../src/audit/checks/stale-count.ts","../src/audit/checks/dead-command.ts","../src/audit/checks/deps-changed.ts","../src/decisions/drift.ts","../src/decisions/decisions.ts","../src/context/lexical.ts","../src/audit/checks/decision-anchor.ts","../src/audit/checks/index.ts","../bin/mason-audit.ts"],"sourcesContent":["import path from \"node:path\";\nimport { computeAudit } from \"./audit.js\";\nimport { ALL_CHECKS } from \"./types.js\";\nimport type { AuditIssue, AuditReport, CheckName } from \"./types.js\";\n\nexport const USAGE = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]\n\nAudits the repo's AI context files (CLAUDE.md, .claude/CLAUDE.md, AGENTS.md)\nagainst repo reality: referenced paths that no longer exist, undocumented\nmodules, stale counts, dead npm scripts, and manifests newer than the doc.\nDeterministic: no LLM call, no network – safe for CI. Works on any repo with\na context file; no Mason setup required.\n\nOptions:\n --dir <path> Project root to audit (default: current directory)\n --json Print the full audit report as JSON (additive-only schema)\n --fix-prompt When issues exist, print a work order for ANY coding agent\n (Claude, Codex, Gemini, ...) – pipe it to your agent CLI to\n close the loop. Prints the clean summary when there are none.\n --checks <list> Comma-separated subset of checks to run (default: all):\n ${ALL_CHECKS.join(\", \")}\n --help Show this help\n\nExit codes:\n 0 no issues (advisories may still be present)\n 1 provable issues found\n 2 error (no context file, not a git repository, bad arguments)`;\n\nexport interface AuditCliIo {\n out: (line: string) => void;\n err: (line: string) => void;\n}\n\ninterface ParsedArgs {\n dir: string;\n json: boolean;\n fixPrompt: boolean;\n help: boolean;\n checks: CheckName[] | undefined;\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const parsed: ParsedArgs = {\n dir: process.cwd(),\n json: false,\n fixPrompt: false,\n help: false,\n checks: undefined,\n };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === \"--json\") {\n parsed.json = true;\n } else if (arg === \"--fix-prompt\") {\n parsed.fixPrompt = true;\n } else if (arg === \"--help\" || arg === \"-h\") {\n parsed.help = true;\n } else if (arg === \"--dir\") {\n const value = argv[++i];\n if (!value) throw new Error(\"--dir requires a path argument\");\n parsed.dir = value;\n } else if (arg === \"--checks\") {\n const value = argv[++i];\n if (!value) throw new Error(\"--checks requires a comma-separated list\");\n const names = value.split(\",\").map((n) => n.trim()).filter(Boolean);\n for (const name of names) {\n if (!ALL_CHECKS.includes(name as CheckName)) {\n throw new Error(\n `Unknown check: ${name} (valid: ${ALL_CHECKS.join(\", \")})`\n );\n }\n }\n parsed.checks = names as CheckName[];\n } else if (!arg.startsWith(\"-\") && parsed.dir === process.cwd()) {\n parsed.dir = arg;\n } else {\n throw new Error(`Unknown argument: ${arg}`);\n }\n }\n return parsed;\n}\n\nfunction issueLine(issue: AuditIssue): string {\n const where =\n issue.anchor.line !== null ? `line ${issue.anchor.line}` : \"doc-level\";\n const likely = issue.confidence === \"likely\" ? \" (likely)\" : \"\";\n return ` [${issue.type}]${likely} ${where}: ${issue.message}`;\n}\n\nexport function formatAuditSummary(report: AuditReport): string {\n const lines: string[] = [];\n\n for (const doc of report.docs) {\n const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);\n const committed = doc.lastCommit\n ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}`\n : \"untracked\";\n if (docIssues.length === 0) {\n lines.push(`${doc.path} – clean (${committed})`);\n continue;\n }\n lines.push(\n `${doc.path} – ${docIssues.length} issue${docIssues.length === 1 ? \"\" : \"s\"} (${committed})`\n );\n for (const issue of docIssues) lines.push(issueLine(issue));\n }\n\n // Issues anchored outside the docs list (defensive; new-module anchors to\n // the primary doc, so this should stay empty).\n const docPaths = new Set(report.docs.map((d) => d.path));\n for (const issue of report.issues) {\n if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));\n }\n\n if (report.advisories.length > 0) {\n lines.push(\"Advisories (do not affect the exit code):\");\n for (const advisory of report.advisories) {\n lines.push(` [${advisory.type}] ${advisory.anchor.doc}: ${advisory.message}`);\n }\n }\n if (report.skippedChecks.length > 0) {\n for (const skip of report.skippedChecks) {\n lines.push(` [skipped] ${skip.check}: ${skip.reason}`);\n }\n }\n\n lines.push(\n report.clean\n ? `Context files are clean (${report.docs.length} doc${report.docs.length === 1 ? \"\" : \"s\"} audited).`\n : `${report.issues.length} issue${report.issues.length === 1 ? \"\" : \"s\"} across ${report.docs.length} doc${report.docs.length === 1 ? \"\" : \"s\"}.`\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * Provider-neutral work order: any coding agent can execute it. The evidence\n * is deterministic; the agent's job is judgment scoped to exactly these\n * claims – never a free-form doc rewrite.\n */\nexport function formatFixPrompt(report: AuditReport): string {\n const flaggedDocs = [...new Set(report.issues.map((i) => i.anchor.doc))];\n const lines: string[] = [];\n lines.push(\n \"The AI context files in this repository contain claims that are provably out of date. Fix ONLY the flagged claims. Work autonomously; do not ask questions.\"\n );\n lines.push(\"\");\n lines.push(\"RULES:\");\n lines.push(\n `- Edit ONLY these files: ${flaggedDocs.join(\", \")}. Never modify source code, configs, or anything else – the docs must be brought to match the code, not the other way around.`\n );\n lines.push(\n \"- Keep diffs minimal: change the smallest span that makes each claim true.\"\n );\n lines.push(\n \"- Never invent content. Every replacement must be grounded in the evidence below or in files you read from this repository.\"\n );\n lines.push(\n \"- deleted-reference: if evidence shows renamedTo, update the path; otherwise remove the reference, or rephrase to past tense if the sentence is about history. Deleted paths inside directory trees: delete the tree line.\"\n );\n lines.push(\n \"- stale-count: replace the number with the actual count from the evidence.\"\n );\n lines.push(\n \"- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention.\"\n );\n lines.push(\n \"- new-module: add a one-line factual mention of the directory where sibling modules are described; read the directory's files first and describe only what you verified.\"\n );\n lines.push(\n \"- Do NOT touch anything listed under ADVISORIES – list them in your summary for human review instead.\"\n );\n lines.push(\"\");\n lines.push(\"AUDIT REPORT (deterministic, computed against git HEAD):\");\n lines.push(\n JSON.stringify(\n { issues: report.issues, advisories: report.advisories },\n null,\n 2\n )\n );\n lines.push(\"\");\n lines.push(\n \"Finish by summarizing each edit and citing the evidence item it resolves.\"\n );\n return lines.join(\"\\n\");\n}\n\nexport async function runAuditCli(\n argv: string[],\n io: AuditCliIo = {\n out: (line) => process.stdout.write(`${line}\\n`),\n err: (line) => process.stderr.write(`${line}\\n`),\n }\n): Promise<number> {\n let args: ParsedArgs;\n try {\n args = parseArgs(argv);\n if (args.json && args.fixPrompt) {\n throw new Error(\"--json and --fix-prompt are mutually exclusive\");\n }\n } catch (error) {\n io.err(error instanceof Error ? error.message : String(error));\n io.err(USAGE);\n return 2;\n }\n\n if (args.help) {\n io.out(USAGE);\n return 0;\n }\n\n const rootDir = path.resolve(args.dir);\n const report = await computeAudit(rootDir, { checks: args.checks });\n\n if (!report) {\n io.err(\n `No CLAUDE.md, .claude/CLAUDE.md, or AGENTS.md found in ${rootDir}.`\n );\n return 2;\n }\n\n if (!report.gitAvailable) {\n io.err(\n `Could not determine git HEAD in ${rootDir} – not a git repository, or git is unavailable.`\n );\n return 2;\n }\n\n if (args.fixPrompt) {\n io.out(\n report.clean ? formatAuditSummary(report) : formatFixPrompt(report)\n );\n return report.clean ? 0 : 1;\n }\n\n if (args.json) {\n io.out(JSON.stringify(report, null, 2));\n } else {\n io.out(formatAuditSummary(report));\n }\n return report.clean ? 0 : 1;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getChangesWithStatus } from \"../drift/drift.js\";\nimport type { FileChange } from \"../drift/drift.js\";\nimport { getCurrentGitHash } from \"../snapshot/snapshot.js\";\nimport { discoverDocs } from \"./docs.js\";\nimport { ALL_CHECKS } from \"./types.js\";\nimport type { AuditReport, CheckName } from \"./types.js\";\nimport { CHECKS } from \"./checks/index.js\";\nimport type { CheckContext } from \"./checks/index.js\";\n\nexport interface AuditOptions {\n /** Subset of checks to run; defaults to all. */\n checks?: CheckName[];\n}\n\n/**\n * Audit the repo's context files (CLAUDE.md, .claude/CLAUDE.md, AGENTS.md)\n * against repo reality. Fully deterministic — git, filesystem, and lexical\n * extraction only; no LLM, no network. Returns null when no context file\n * exists.\n */\nexport async function computeAudit(\n rootDir: string,\n options: AuditOptions = {}\n): Promise<AuditReport | null> {\n const resolvedRoot = path.resolve(rootDir);\n const docs = await discoverDocs(resolvedRoot);\n if (docs.length === 0) return null;\n\n const headHash = await getCurrentGitHash(resolvedRoot);\n const report: AuditReport = {\n version: 1,\n root: resolvedRoot,\n gitAvailable: headHash !== \"unknown\",\n docs: docs.map((d) => ({\n path: d.path,\n lastCommit: d.lastCommit,\n dirty: d.dirty,\n lineCount: d.lineCount,\n })),\n decisionsChecked: false,\n issues: [],\n advisories: [],\n skippedChecks: [],\n clean: true,\n };\n\n // Without git the provability gates cannot run — the caller treats this\n // as an error rather than silently degrading precision.\n if (!report.gitAvailable) return report;\n\n const changesSinceDoc = new Map<string, FileChange[] | null>();\n for (const doc of docs) {\n changesSinceDoc.set(\n doc.path,\n doc.lastCommit\n ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash)\n : null\n );\n }\n\n let decisionsPresent = false;\n try {\n await fs.access(path.join(resolvedRoot, \".mason\", \"decisions\"));\n decisionsPresent = true;\n } catch {\n // No decision store — the check stays dark (zero-setup path).\n }\n report.decisionsChecked = decisionsPresent;\n\n const ctx: CheckContext = {\n root: resolvedRoot,\n docs,\n headHash,\n changesSinceDoc,\n decisionsPresent,\n };\n\n const selected = options.checks ?? ALL_CHECKS;\n for (const name of ALL_CHECKS) {\n if (!selected.includes(name)) continue;\n const { issues, advisories, skipped } = await CHECKS[name](ctx);\n report.issues.push(...issues);\n report.advisories.push(...advisories);\n report.skippedChecks.push(...skipped);\n }\n\n report.clean = report.issues.length === 0;\n return report;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport {\n loadSnapshot,\n getCurrentGitHash,\n listSourceFiles,\n} from \"../snapshot/snapshot.js\";\nimport type { Snapshot } from \"../snapshot/snapshot.js\";\n\nconst exec = promisify(execFile);\n\n// Incremental refresh stops paying off once a large share of the map is\n// touched — but small absolute counts are always cheap to refresh in place,\n// so both thresholds must be exceeded before recommending a full rebuild.\nconst FULL_REBUILD_FRACTION = 0.4;\nconst FULL_REBUILD_MIN_CHANGED_MAPPED_FILES = 10;\n\nexport type ChangeStatus = \"added\" | \"modified\" | \"deleted\" | \"renamed\";\n\nexport interface FileChange {\n status: ChangeStatus;\n /** Current path (the new path for renames). */\n path: string;\n /** Pre-rename path, only present for renames. */\n previousPath?: string;\n}\n\nexport type DriftRecommendation = \"up-to-date\" | \"incremental\" | \"full-rebuild\";\n\nexport interface DriftReport {\n stale: boolean;\n snapshotHash: string;\n headHash: string;\n /** Commits between the snapshot and HEAD; null when history is unavailable. */\n commitsBehind: number | null;\n /**\n * False when the snapshot commit is unreachable (shallow clone, rewritten\n * history) — staleFeatures/unmappedFiles/renames cannot be computed then.\n */\n historyAvailable: boolean;\n /** Current paths of every file changed since the snapshot. */\n changedFiles: string[];\n /** Stale feature name → the mapped files that changed under it. */\n staleFeatures: Record<string, string[]>;\n /** Stale flow name → the chain files that changed under it. */\n staleFlows: Record<string, string[]>;\n totalFeatures: number;\n totalFlows: number;\n /** New source files not referenced by any feature or flow. */\n unmappedFiles: string[];\n /** Files referenced by the map that no longer exist on disk. */\n ghostFiles: string[];\n renames: Array<{ from: string; to: string }>;\n recommendation: DriftRecommendation;\n}\n\nexport async function getChangesWithStatus(\n resolvedRoot: string,\n fromHash: string\n): Promise<FileChange[] | null> {\n if (!fromHash || fromHash === \"unknown\") return null;\n try {\n const { stdout } = await exec(\n \"git\",\n [\"diff\", \"--name-status\", \"-M\", fromHash, \"HEAD\"],\n { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }\n );\n\n const changes: FileChange[] = [];\n for (const line of stdout.split(\"\\n\")) {\n if (!line.trim()) continue;\n const parts = line.split(\"\\t\");\n // Mason's own metadata changes on every save — never count it as drift.\n if (parts.some((p) => p.startsWith(\".mason/\"))) continue;\n const code = parts[0];\n if (code.startsWith(\"R\") && parts.length >= 3) {\n changes.push({\n status: \"renamed\",\n path: parts[2],\n previousPath: parts[1],\n });\n } else if (code.startsWith(\"C\") && parts.length >= 3) {\n // A copy leaves the original in place — only the new path is a change.\n changes.push({ status: \"added\", path: parts[2] });\n } else if (code === \"A\" && parts.length >= 2) {\n changes.push({ status: \"added\", path: parts[1] });\n } else if (code === \"D\" && parts.length >= 2) {\n changes.push({ status: \"deleted\", path: parts[1] });\n } else if (parts.length >= 2) {\n // M, T (typechange), and anything unrecognized count as modified.\n changes.push({ status: \"modified\", path: parts[1] });\n }\n }\n return changes;\n } catch {\n return null;\n }\n}\n\nasync function countCommitsBehind(\n resolvedRoot: string,\n fromHash: string\n): Promise<number | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"rev-list\", \"--count\", `${fromHash}..HEAD`],\n { cwd: resolvedRoot }\n );\n const count = Number.parseInt(stdout.trim(), 10);\n return Number.isNaN(count) ? null : count;\n } catch {\n return null;\n }\n}\n\nfunction collectMappedFiles(snapshot: Snapshot): Set<string> {\n const mappedFiles = new Set<string>();\n for (const feature of Object.values(snapshot.features)) {\n for (const f of feature.files) mappedFiles.add(f);\n for (const t of feature.tests ?? []) mappedFiles.add(t);\n }\n for (const flow of Object.values(snapshot.flows)) {\n for (const f of flow.chain) mappedFiles.add(f);\n }\n return mappedFiles;\n}\n\nasync function findGhostFiles(\n resolvedRoot: string,\n mappedFiles: Set<string>\n): Promise<string[]> {\n const ghosts: string[] = [];\n for (const file of mappedFiles) {\n try {\n await fs.access(path.join(resolvedRoot, file));\n } catch {\n ghosts.push(file);\n }\n }\n return ghosts.sort();\n}\n\n/**\n * Compare the concept map against HEAD and report feature-level drift.\n * Fully deterministic — git + filesystem only, no LLM involved.\n * Returns null when no snapshot exists.\n */\nexport async function computeDrift(\n rootDir: string\n): Promise<DriftReport | null> {\n const resolvedRoot = path.resolve(rootDir);\n const snapshot = await loadSnapshot(resolvedRoot);\n if (!snapshot) return null;\n\n const headHash = await getCurrentGitHash(resolvedRoot);\n const totalFeatures = Object.keys(snapshot.features).length;\n const totalFlows = Object.keys(snapshot.flows).length;\n\n // Each entry is only verified as of its refreshedHash (falling back to the\n // top-level gitHash), so drift is evaluated per distinct hash — a partially\n // refreshed map can be fresh at the top level and still hold stale entries.\n const hashFor = (entry: { refreshedHash?: string }): string =>\n entry.refreshedHash ?? snapshot.gitHash;\n\n const distinctHashes = new Set<string>([snapshot.gitHash]);\n for (const feature of Object.values(snapshot.features)) {\n distinctHashes.add(hashFor(feature));\n }\n for (const flow of Object.values(snapshot.flows)) {\n distinctHashes.add(hashFor(flow));\n }\n distinctHashes.delete(\"unknown\");\n\n const staleHashes =\n headHash === \"unknown\"\n ? []\n : [...distinctHashes].filter((h) => h !== headHash);\n const stale = staleHashes.length > 0;\n\n const report: DriftReport = {\n stale,\n snapshotHash: snapshot.gitHash,\n headHash,\n commitsBehind: stale ? null : 0,\n historyAvailable: true,\n changedFiles: [],\n staleFeatures: {},\n staleFlows: {},\n totalFeatures,\n totalFlows,\n unmappedFiles: [],\n ghostFiles: [],\n renames: [],\n recommendation: \"up-to-date\",\n };\n\n if (!stale) return report;\n\n const mappedFiles = collectMappedFiles(snapshot);\n report.ghostFiles = await findGhostFiles(resolvedRoot, mappedFiles);\n\n const changesByHash = new Map<string, FileChange[]>();\n const touchedByHash = new Map<string, Set<string>>();\n for (const hash of staleHashes) {\n const changes = await getChangesWithStatus(resolvedRoot, hash);\n if (changes === null) {\n // One unreachable base commit is enough to make per-entry drift\n // uncomputable — we know the map is stale but not how.\n report.historyAvailable = false;\n report.recommendation = \"full-rebuild\";\n return report;\n }\n changesByHash.set(hash, changes);\n // Every path a change touches, old and new — an entry referencing either\n // side of a rename is stale.\n const touched = new Set<string>();\n for (const change of changes) {\n touched.add(change.path);\n if (change.previousPath) touched.add(change.previousPath);\n }\n touchedByHash.set(hash, touched);\n }\n\n // The oldest verification state in the map is the honest answer to \"how\n // far behind is this snapshot\".\n const commitCounts = await Promise.all(\n staleHashes.map((hash) => countCommitsBehind(resolvedRoot, hash))\n );\n const validCounts = commitCounts.filter((c): c is number => c !== null);\n report.commitsBehind =\n validCounts.length > 0 ? Math.max(...validCounts) : null;\n\n const emptySet = new Set<string>();\n const touchedFor = (entry: { refreshedHash?: string }): Set<string> =>\n touchedByHash.get(hashFor(entry)) ?? emptySet;\n\n for (const [name, feature] of Object.entries(snapshot.features)) {\n const touched = touchedFor(feature);\n const hits = [...feature.files, ...(feature.tests ?? [])].filter((f) =>\n touched.has(f)\n );\n if (hits.length > 0) report.staleFeatures[name] = [...new Set(hits)];\n }\n for (const [name, flow] of Object.entries(snapshot.flows)) {\n const touched = touchedFor(flow);\n const hits = flow.chain.filter((f) => touched.has(f));\n if (hits.length > 0) report.staleFlows[name] = [...new Set(hits)];\n }\n\n const allChanges = [...changesByHash.values()].flat();\n report.changedFiles = [...new Set(allChanges.map((c) => c.path))].sort();\n\n // New source files (added, or the new side of a rename) missing from the map.\n const sourceFileSet = new Set(await listSourceFiles(resolvedRoot));\n const newPaths = allChanges\n .filter((c) => c.status === \"added\" || c.status === \"renamed\")\n .map((c) => c.path);\n report.unmappedFiles = [...new Set(newPaths)]\n .filter((p) => sourceFileSet.has(p) && !mappedFiles.has(p))\n .sort();\n\n const renameKeys = new Set<string>();\n for (const change of allChanges) {\n if (change.status !== \"renamed\" || !change.previousPath) continue;\n const key = `${change.previousPath}\u0000${change.path}`;\n if (renameKeys.has(key)) continue;\n renameKeys.add(key);\n report.renames.push({ from: change.previousPath, to: change.path });\n }\n\n const changedMapped = new Set<string>([\n ...Object.values(report.staleFeatures).flat(),\n ...Object.values(report.staleFlows).flat(),\n ]);\n const changedFraction =\n mappedFiles.size > 0 ? changedMapped.size / mappedFiles.size : 0;\n report.recommendation =\n changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES &&\n changedFraction > FULL_REBUILD_FRACTION\n ? \"full-rebuild\"\n : \"incremental\";\n\n return report;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport fg from \"fast-glob\";\nimport { readFullFile } from \"../mcp/sampler.js\";\nimport { buildTestMap } from \"../test-map.js\";\n\nconst exec = promisify(execFile);\n\nexport interface FeatureEntry {\n description: string;\n files: string[];\n tests?: string[];\n /**\n * Commit this entry was last verified against. Entries updated by an\n * incremental save carry HEAD here; untouched entries keep the hash the\n * map had before the save, so drift stays visible per entry. Absent means\n * \"as of the snapshot's top-level gitHash\".\n */\n refreshedHash?: string;\n /**\n * Whether this is a user-facing capability or internal infrastructure\n * (DI wiring, config loading, logging, provider/transport plumbing).\n * Capabilities are published to product-facing docs (Confluence);\n * infrastructure stays in the AI concept map only. Defaults to \"capability\"\n * when absent (older snapshots) or unrecognized — see normalizeFeatureType.\n */\n type?: \"capability\" | \"infrastructure\";\n /**\n * When an assistant last confirmed this entry's files actually implement\n * the claimed feature (verify_snapshot flow). Absent on older snapshots\n * and never-verified entries. Drift checks freshness against git; this\n * checks the map was CORRECT in the first place.\n */\n verifiedAt?: string;\n /** Set when verification judged the entry wrong — re-map it. */\n verificationFailed?: boolean;\n verificationNote?: string;\n}\n\nexport type FeatureType = \"capability\" | \"infrastructure\";\n\n/**\n * Coerce an arbitrary type value to a known classification. Anything that\n * isn't explicitly \"infrastructure\" defaults to \"capability\" — so older\n * snapshots and unclassified entries are treated as user-facing (published),\n * never silently hidden.\n */\nexport function normalizeFeatureType(value: unknown): FeatureType {\n return value === \"infrastructure\" ? \"infrastructure\" : \"capability\";\n}\n\nexport interface FlowEntry {\n description: string;\n chain: string[];\n /** See FeatureEntry.refreshedHash. */\n refreshedHash?: string;\n /** See FeatureEntry.verifiedAt / verificationFailed. */\n verifiedAt?: string;\n verificationFailed?: boolean;\n verificationNote?: string;\n}\n\nexport interface Snapshot {\n version: 2;\n createdAt: string;\n updatedAt: string;\n gitHash: string;\n features: Record<string, FeatureEntry>;\n flows: Record<string, FlowEntry>;\n}\n\nfunction snapshotDir(rootDir: string): string {\n return path.join(rootDir, \".mason\");\n}\n\nfunction snapshotPath(rootDir: string): string {\n return path.join(snapshotDir(rootDir), \"snapshot.json\");\n}\n\nexport async function loadSnapshot(rootDir: string): Promise<Snapshot | null> {\n try {\n const raw = await fs.readFile(snapshotPath(rootDir), \"utf-8\");\n const parsed = JSON.parse(raw);\n // Skip v1 snapshots — they're the old per-file format\n if (parsed.version !== 2) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nexport async function saveSnapshot(\n rootDir: string,\n snapshot: Snapshot\n): Promise<void> {\n await fs.mkdir(snapshotDir(rootDir), { recursive: true });\n await fs.writeFile(\n snapshotPath(rootDir),\n JSON.stringify(snapshot, null, 2),\n \"utf-8\"\n );\n}\n\nexport async function getCurrentGitHash(rootDir: string): Promise<string> {\n try {\n const { stdout } = await exec(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd: rootDir,\n });\n return stdout.trim();\n } catch {\n return \"unknown\";\n }\n}\n\nexport const SOURCE_GLOB =\n \"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}\";\nexport const SOURCE_IGNORE = [\n \"**/node_modules/**\", \"**/dist/**\", \"**/build/**\", \"**/.gradle/**\",\n \"**/target/**\", \"**/.git/**\", \"**/vendor/**\", \"**/__pycache__/**\",\n \"**/venv/**\", \"**/.venv/**\", \"**/*.min.*\", \"**/*.map\",\n \"**/generated/**\", \"**/R.java\", \"**/BuildConfig.java\",\n];\n\nexport const DEFAULT_BATCH_SIZE = 50;\nconst SKELETON_CHARS = 500;\nconst DEEP_SAMPLE_CHARS = 1500;\nconst DEEP_SAMPLES_PER_BATCH = 3;\n\nexport interface SnapshotBatch {\n offset: number;\n batchSize: number;\n nextOffset: number | null;\n totalFiles: number;\n skeletons: Array<{ path: string; content: string }>;\n samples: Array<{ path: string; content: string }>;\n testPairs: Array<{ test: string; source: string; confidence: string }>;\n}\n\nexport async function listSourceFiles(resolvedRoot: string): Promise<string[]> {\n const all = await fg(SOURCE_GLOB, {\n cwd: resolvedRoot,\n ignore: SOURCE_IGNORE,\n });\n // Deterministic order so the same offset always returns the same batch.\n return [...all].sort();\n}\n\nexport async function prepareSnapshotBatch(\n rootDir: string,\n offset: number,\n batchSize: number = DEFAULT_BATCH_SIZE,\n scopeFiles?: string[]\n): Promise<SnapshotBatch> {\n const resolvedRoot = path.resolve(rootDir);\n let allFiles = await listSourceFiles(resolvedRoot);\n if (scopeFiles) {\n // Intersect with the real source list: keeps ignore rules and path safety,\n // and silently drops scope entries that no longer exist on disk. An empty\n // scope stays empty — it must not fall back to walking the whole project.\n const scopeSet = new Set(scopeFiles);\n allFiles = allFiles.filter((f) => scopeSet.has(f));\n }\n const totalFiles = allFiles.length;\n const safeOffset = Math.max(0, Math.min(offset, totalFiles));\n const batchPaths = allFiles.slice(safeOffset, safeOffset + batchSize);\n\n const skeletons: Array<{ path: string; content: string }> = [];\n for (const filePath of batchPaths) {\n const full = await readFullFile(resolvedRoot, filePath);\n if (full) {\n skeletons.push({\n path: full.path,\n content: full.content.slice(0, SKELETON_CHARS),\n });\n }\n }\n\n // Pick a few files from this batch to read deeply for grounding. Spread\n // evenly across the batch so the deep samples represent the batch's range.\n const samples: Array<{ path: string; content: string }> = [];\n if (skeletons.length > 0) {\n const step = Math.max(1, Math.floor(skeletons.length / DEEP_SAMPLES_PER_BATCH));\n for (let i = 0; i < skeletons.length && samples.length < DEEP_SAMPLES_PER_BATCH; i += step) {\n const full = await readFullFile(resolvedRoot, skeletons[i].path);\n if (full) {\n samples.push({\n path: full.path,\n content: full.content.slice(0, DEEP_SAMPLE_CHARS),\n });\n }\n }\n }\n\n // Only include test pairs that involve files in this batch — keeps the\n // appendix relevant and small.\n const batchPathSet = new Set(batchPaths);\n const allTestPairs = (await buildTestMap(resolvedRoot)).paired;\n const testPairs = allTestPairs.filter(\n (p) => batchPathSet.has(p.test) || batchPathSet.has(p.source)\n );\n\n const nextOffset =\n safeOffset + batchSize >= totalFiles ? null : safeOffset + batchSize;\n\n return {\n offset: safeOffset,\n batchSize,\n nextOffset,\n totalFiles,\n skeletons,\n samples,\n testPairs,\n };\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport fg from \"fast-glob\";\n\nconst exec = promisify(execFile);\n\nconst SOURCE_EXTENSIONS = [\n \"ts\", \"tsx\", \"js\", \"jsx\", \"mts\", \"mjs\",\n \"kt\", \"kts\", \"java\",\n \"py\",\n \"go\",\n \"rs\",\n \"swift\",\n \"rb\",\n \"cs\", \"cpp\", \"c\", \"h\",\n \"dart\",\n];\n\nconst CONFIG_FILES = [\n // Build & project config\n \"package.json\",\n \"tsconfig.json\",\n \"build.gradle.kts\",\n \"build.gradle\",\n \"settings.gradle.kts\",\n \"settings.gradle\",\n \"Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"Gemfile\",\n \"*.csproj\",\n // Version catalogs & dependency locks\n \"gradle/libs.versions.toml\",\n // Code quality & formatting\n \".editorconfig\",\n \".eslintrc.*\",\n \"eslint.config.*\",\n \".prettierrc\",\n \"rustfmt.toml\",\n \".swiftlint.yml\",\n // CI/CD\n \".github/workflows/*.yml\",\n \".gitlab-ci.yml\",\n \"Jenkinsfile\",\n // Containerization\n \"Dockerfile\",\n \"docker-compose.yml\",\n \"docker-compose.yaml\",\n];\n\nconst ENTRY_POINT_PATTERNS = [\n \"src/main.*\",\n \"src/index.*\",\n \"src/app.*\",\n \"main.*\",\n \"index.*\",\n \"app.*\",\n \"App.*\",\n \"**/Main.kt\",\n \"**/Application.kt\",\n \"**/main.py\",\n \"**/main.go\",\n \"**/main.rs\",\n \"**/lib.rs\",\n \"**/Program.cs\",\n];\n\n// Filename patterns that reveal architectural patterns and conventions.\n// These are language-agnostic — the suffixes appear across ecosystems.\n// Ordered by architectural importance — most distinctive patterns first.\nconst ARCHITECTURAL_PATTERNS = [\n // State/data flow\n { glob: \"**/*ViewModel.*\", category: \"state\", reason: \"viewmodel (state management)\" },\n { glob: \"**/*Store.*\", category: \"state\", reason: \"store (state management)\" },\n { glob: \"**/*Reducer.*\", category: \"state\", reason: \"reducer (state management)\" },\n // Data layer — interface\n { glob: \"**/*Repository.*\", category: \"data-interface\", reason: \"repository interface (data layer contract)\" },\n { glob: \"**/*Dao.*\", category: \"data-interface\", reason: \"DAO (data access)\" },\n { glob: \"**/*DataSource.*\", category: \"data-interface\", reason: \"data source\" },\n // Data layer — implementation (where actual patterns live: mappers, retry, IO dispatchers)\n { glob: \"**/*RepositoryImpl.*\", category: \"data-impl\", reason: \"repository implementation (data layer patterns)\" },\n { glob: \"**/*ServiceImpl.*\", category: \"data-impl\", reason: \"service implementation\" },\n { glob: \"**/*Impl.*\", category: \"data-impl\", reason: \"implementation (concrete patterns)\" },\n // Data transformation\n { glob: \"**/*Mapper.*\", category: \"transform\", reason: \"mapper (data transformation)\" },\n { glob: \"**/*Converter.*\", category: \"transform\", reason: \"converter (data transformation)\" },\n { glob: \"**/*Adapter.*\", category: \"transform\", reason: \"adapter (interface adaptation)\" },\n // Dependency injection / wiring\n { glob: \"**/*Module.*\", category: \"di\", reason: \"module (DI/wiring)\" },\n { glob: \"**/*Provider.*\", category: \"di\", reason: \"provider (DI/wiring)\" },\n { glob: \"**/*Container.*\", category: \"di\", reason: \"container (DI/wiring)\" },\n { glob: \"**/*Factory.*\", category: \"di\", reason: \"factory (object creation)\" },\n // API / network\n { glob: \"**/*Service.*\", category: \"api\", reason: \"service (business/API layer)\" },\n { glob: \"**/*Client.*\", category: \"api\", reason: \"client (API/network layer)\" },\n { glob: \"**/*Api.*\", category: \"api\", reason: \"API interface definition\" },\n // Interface contracts / protocols\n { glob: \"**/*Interface.*\", category: \"contract\", reason: \"interface definition\" },\n { glob: \"**/*Protocol.*\", category: \"contract\", reason: \"protocol definition\" },\n { glob: \"**/*Trait.*\", category: \"contract\", reason: \"trait definition\" },\n // Routing / navigation\n { glob: \"**/*Router.*\", category: \"routing\", reason: \"router (navigation/routing)\" },\n { glob: \"**/*Route.*\", category: \"routing\", reason: \"route definition\" },\n { glob: \"**/*NavHost.*\", category: \"routing\", reason: \"navigation host\" },\n { glob: \"**/*Controller.*\", category: \"routing\", reason: \"controller (request handling)\" },\n { glob: \"**/*Handler.*\", category: \"routing\", reason: \"handler (request handling)\" },\n // Middleware / interceptors\n { glob: \"**/*Middleware.*\", category: \"middleware\", reason: \"middleware (request pipeline)\" },\n { glob: \"**/*Interceptor.*\", category: \"middleware\", reason: \"interceptor (cross-cutting)\" },\n { glob: \"**/*Plugin.*\", category: \"middleware\", reason: \"plugin (extensibility)\" },\n // Models / types\n { glob: \"**/*Model.*\", category: \"model\", reason: \"model (domain types)\" },\n { glob: \"**/*Entity.*\", category: \"model\", reason: \"entity (persistence types)\" },\n { glob: \"**/*Dto.*\", category: \"model\", reason: \"DTO (data transfer types)\" },\n { glob: \"**/*Schema.*\", category: \"model\", reason: \"schema (data validation)\" },\n // Use cases / commands\n { glob: \"**/*UseCase.*\", category: \"usecase\", reason: \"use case (business logic)\" },\n { glob: \"**/*Interactor.*\", category: \"usecase\", reason: \"interactor (business logic)\" },\n { glob: \"**/*Command.*\", category: \"usecase\", reason: \"command (CQRS pattern)\" },\n];\n\nconst IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/.gradle/**\",\n \"**/target/**\",\n \"**/.git/**\",\n \"**/vendor/**\",\n \"**/__pycache__/**\",\n \"**/venv/**\",\n \"**/.venv/**\",\n \"**/*.min.*\",\n \"**/*.map\",\n \"**/package-lock.json\",\n \"**/yarn.lock\",\n \"**/pnpm-lock.yaml\",\n \"**/*.lock\",\n \"**/*.generated.*\",\n \"**/generated/**\",\n \"**/R.java\",\n \"**/BuildConfig.java\",\n];\n\nconst PREVIEW_LINES = 60;\n\nexport interface ProjectConfig {\n patterns?: string[];\n alwaysInclude?: string[];\n ignore?: string[];\n}\n\nexport interface SampledFile {\n path: string;\n preview: string;\n totalLines: number;\n sizeBytes: number;\n reason: string;\n}\n\nasync function loadProjectConfig(\n rootDir: string\n): Promise<ProjectConfig> {\n try {\n const raw = await fs.readFile(\n path.join(rootDir, \".mason\", \"config.json\"),\n \"utf-8\"\n );\n return JSON.parse(raw);\n } catch {\n return {};\n }\n}\n\nasync function getTrackedFiles(rootDir: string): Promise<Set<string> | null> {\n try {\n const { stdout } = await exec(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\n cwd: rootDir,\n maxBuffer: 10_000_000,\n });\n return new Set(stdout.trim().split(\"\\n\").filter(Boolean));\n } catch {\n return null; // Not a git repo — skip filtering\n }\n}\n\nexport async function sampleFiles(\n rootDir: string,\n maxFiles: number = 25\n): Promise<SampledFile[]> {\n const selected = new Map<string, string>(); // path -> reason\n const projectConfig = await loadProjectConfig(rootDir);\n const ignorePatterns = [...IGNORE_PATTERNS, ...(projectConfig.ignore ?? [])];\n const trackedFiles = await getTrackedFiles(rootDir);\n\n // 0. Always-include files from project config (highest priority)\n for (const filePath of projectConfig.alwaysInclude ?? []) {\n if (selected.size >= maxFiles) break;\n // Validate path stays within project root\n const resolvedPath = path.resolve(rootDir, filePath);\n if (!resolvedPath.startsWith(path.resolve(rootDir))) continue;\n selected.set(filePath, \"always-include (project config)\");\n }\n\n // 1. Config files (cap at 5)\n let configCount = 0;\n for (const pattern of CONFIG_FILES) {\n if (configCount >= 5) break;\n const matches = await fg(pattern, {\n cwd: rootDir,\n ignore: ignorePatterns,\n deep: 3,\n });\n for (const match of matches) {\n if (configCount >= 5 || selected.size >= maxFiles) break;\n selected.set(match, \"config file\");\n configCount++;\n }\n }\n\n // 2. Module build/config files — build files from subdirectories reveal dependency graph\n const moduleBuildPatterns = [\n // Gradle\n \"**/build.gradle.kts\",\n \"**/build.gradle\",\n // Cargo workspace members\n \"**/Cargo.toml\",\n // Node workspaces\n \"**/package.json\",\n // Go sub-modules\n \"**/go.mod\",\n ];\n let moduleBuildCount = 0;\n for (const pattern of moduleBuildPatterns) {\n const matches = await fg(pattern, {\n cwd: rootDir,\n ignore: ignorePatterns,\n deep: 4,\n });\n // Skip root-level files (already captured as config)\n const subMatches = matches.filter((m) => m.includes(\"/\"));\n for (const match of subMatches) {\n if (moduleBuildCount >= 4 || selected.size >= maxFiles) break;\n if (!selected.has(match)) {\n selected.set(match, \"module build file (reveals dependency graph)\");\n moduleBuildCount++;\n }\n }\n if (moduleBuildCount >= 4) break;\n }\n\n // 3. Entry points (cap at 2)\n let entryCount = 0;\n for (const pattern of ENTRY_POINT_PATTERNS) {\n if (entryCount >= 2) break;\n const matches = await fg(pattern, {\n cwd: rootDir,\n ignore: ignorePatterns,\n deep: 5,\n });\n for (const match of matches) {\n if (entryCount >= 2 || selected.size >= maxFiles) break;\n if (!selected.has(match)) {\n selected.set(match, \"entry point\");\n entryCount++;\n }\n }\n }\n\n // 4. Hot files from git (up to 5)\n try {\n const { stdout } = await exec(\n \"git\",\n [\"log\", \"--since=3 months ago\", \"--format=\", \"--name-only\"],\n { cwd: rootDir, maxBuffer: 5_000_000 }\n );\n\n const fileCounts = new Map<string, number>();\n for (const line of stdout.split(\"\\n\")) {\n if (!line) continue;\n if (\n line.includes(\"node_modules\") ||\n line.includes(\"/build/\") ||\n line.includes(\".gradle\") ||\n line.includes(\"/generated/\")\n )\n continue;\n const ext = path.extname(line).slice(1);\n if (!SOURCE_EXTENSIONS.includes(ext)) continue;\n fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);\n }\n\n const hotFiles = [...fileCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n for (const [file, count] of hotFiles) {\n if (selected.size >= maxFiles) break;\n if (!selected.has(file)) {\n selected.set(file, `frequently changed (${count} commits in 3 months)`);\n }\n }\n } catch {\n // No git\n }\n\n // 5. Architectural pattern files — one per category (cap at 8)\n const seenCategories = new Set<string>();\n let patternCount = 0;\n for (const pattern of ARCHITECTURAL_PATTERNS) {\n if (patternCount >= 8 || selected.size >= maxFiles) break;\n if (seenCategories.has(pattern.category)) continue;\n\n const matches = await fg(pattern.glob, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n\n if (matches.length > 0) {\n for (const match of matches) {\n if (!selected.has(match)) {\n selected.set(match, pattern.reason);\n seenCategories.add(pattern.category);\n patternCount++;\n break;\n }\n }\n }\n }\n\n // 5b. Custom patterns from project config\n for (const customGlob of projectConfig.patterns ?? []) {\n if (selected.size >= maxFiles) break;\n const matches = await fg(customGlob, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n for (const match of matches) {\n if (selected.size >= maxFiles) break;\n if (!selected.has(match)) {\n selected.set(match, \"custom pattern (project config)\");\n break; // one per pattern\n }\n }\n }\n\n // 6. Test examples — diverse across file types (cap at 3)\n const testPatternGroups = [\n // JS/TS tests\n { patterns: [\"**/*.test.*\", \"**/*.spec.*\"], label: \"JS/TS test\" },\n // JVM tests\n { patterns: [\"**/*Test.kt\", \"**/*Test.java\"], label: \"JVM test\" },\n // Python tests\n { patterns: [\"**/test_*.py\", \"**/*_test.py\"], label: \"Python test\" },\n // Go tests\n { patterns: [\"**/*_test.go\"], label: \"Go test\" },\n // Swift tests\n { patterns: [\"**/*Tests.swift\", \"**/*Test.swift\"], label: \"Swift test\" },\n // Rust tests\n { patterns: [\"**/*_test.rs\"], label: \"Rust test\" },\n ];\n let testCount = 0;\n for (const group of testPatternGroups) {\n if (testCount >= 3 || selected.size >= maxFiles) break;\n const testFiles = await fg(group.patterns, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n if (testFiles.length > 0) {\n for (const file of testFiles) {\n if (!selected.has(file)) {\n selected.set(file, `test example (${group.label})`);\n testCount++;\n break;\n }\n }\n }\n }\n\n // 7. Directory breadth — fill remaining slots with one file per top-level dir\n const sourceGlobs = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);\n const allSourceFiles = await fg(sourceGlobs, {\n cwd: rootDir,\n ignore: ignorePatterns,\n });\n\n const dirRepresentatives = new Map<string, string>();\n const boringFiles = /\\.(gradle|gradle\\.kts|json|toml|yaml|yml|xml|properties)$/;\n for (const file of allSourceFiles) {\n const topDir = file.split(\"/\")[0];\n if (!dirRepresentatives.has(topDir) && !boringFiles.test(file)) {\n dirRepresentatives.set(topDir, file);\n }\n }\n\n for (const [, file] of dirRepresentatives) {\n if (selected.size >= maxFiles) break;\n if (!selected.has(file)) {\n selected.set(file, \"directory representative\");\n }\n }\n\n // Read file previews\n const results: SampledFile[] = [];\n for (const [filePath, reason] of selected) {\n try {\n const fullPath = path.resolve(rootDir, filePath);\n if (!fullPath.startsWith(path.resolve(rootDir))) continue;\n if (isSensitiveFile(filePath)) continue;\n if (trackedFiles && !trackedFiles.has(filePath)) continue; // respect .gitignore\n const stat = await fs.stat(fullPath);\n if (stat.size > 100_000) continue;\n\n const content = await fs.readFile(fullPath, \"utf-8\");\n const lines = content.split(\"\\n\");\n const preview = lines.slice(0, PREVIEW_LINES).join(\"\\n\");\n\n results.push({\n path: filePath,\n preview,\n totalLines: lines.length,\n sizeBytes: stat.size,\n reason,\n });\n } catch {\n // Skip\n }\n }\n\n return results;\n}\n\nconst SENSITIVE_PATTERNS = [\n /^\\.env$/,\n /^\\.env\\./,\n /\\.pem$/,\n /\\.key$/,\n /\\.p12$/,\n /\\.pfx$/,\n /\\.jks$/,\n /id_rsa/,\n /id_ed25519/,\n /credentials\\./,\n /secret/i,\n /\\.keystore$/,\n /local\\.properties$/,\n];\n\nfunction isSensitiveFile(filePath: string): boolean {\n const basename = path.basename(filePath);\n return SENSITIVE_PATTERNS.some((p) => p.test(basename));\n}\n\nexport async function readFullFile(\n rootDir: string,\n filePath: string\n): Promise<{ path: string; content: string; totalLines: number } | null> {\n try {\n const fullPath = path.join(path.resolve(rootDir), filePath);\n if (!fullPath.startsWith(path.resolve(rootDir))) return null;\n if (isSensitiveFile(filePath)) return null;\n\n const content = await fs.readFile(fullPath, \"utf-8\");\n return {\n path: filePath,\n content,\n totalLines: content.split(\"\\n\").length,\n };\n } catch {\n return null;\n }\n}\n","import path from \"node:path\";\nimport fg from \"fast-glob\";\n\nconst IGNORE = [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/.gradle/**\",\n \"**/target/**\",\n \"**/.git/**\",\n \"**/vendor/**\",\n \"**/__pycache__/**\",\n \"**/venv/**\",\n \"**/.venv/**\",\n \"**/*.min.*\",\n \"**/*.map\",\n];\n\nexport interface TestPair {\n test: string;\n source: string;\n confidence: string;\n}\n\nexport interface TestMapResult {\n totalTestFiles: number;\n paired: TestPair[];\n unmatched: string[];\n}\n\nexport async function buildTestMap(dir: string): Promise<TestMapResult> {\n const rootDir = path.resolve(dir);\n\n // Find all test files\n const testPatterns = [\n \"**/*.test.*\", \"**/*.spec.*\",\n \"**/*Test.kt\", \"**/*Test.java\", \"**/*Tests.kt\", \"**/*Tests.java\",\n \"**/test_*.py\", \"**/*_test.py\",\n \"**/*_test.go\",\n \"**/*Tests.swift\", \"**/*Test.swift\",\n \"**/*_test.rs\",\n ];\n const testFiles = await fg(testPatterns, { cwd: rootDir, ignore: IGNORE });\n\n // Find all source files\n const sourceFiles = await fg(\n \"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}\",\n { cwd: rootDir, ignore: IGNORE }\n );\n\n // Build source file index by base name (without extension)\n const sourceByBaseName = new Map<string, string[]>();\n for (const file of sourceFiles) {\n if (testFiles.includes(file)) continue; // Skip test files\n const baseName = path.basename(file).replace(/\\.[^.]+$/, \"\");\n const existing = sourceByBaseName.get(baseName) ?? [];\n existing.push(file);\n sourceByBaseName.set(baseName, existing);\n }\n\n // Match test files to source files by name\n const paired: TestPair[] = [];\n const unmatched: string[] = [];\n\n for (const testFile of testFiles) {\n const testBaseName = path.basename(testFile).replace(/\\.[^.]+$/, \"\");\n\n // Strip test suffixes/prefixes to get the source name\n const sourceName = testBaseName\n .replace(/Test$|Tests$|Spec$|\\.test$|\\.spec$/, \"\")\n .replace(/^test_|_test$/, \"\");\n\n if (!sourceName) {\n unmatched.push(testFile);\n continue;\n }\n\n const candidates = sourceByBaseName.get(sourceName);\n if (candidates && candidates.length > 0) {\n // If multiple candidates, prefer one in a similar directory path\n const testDir = path.dirname(testFile);\n const bestMatch = candidates.reduce((best, candidate) => {\n const candidateDir = path.dirname(candidate);\n const bestDir = path.dirname(best);\n const candidateOverlap = commonSegments(testDir, candidateDir);\n const bestOverlap = commonSegments(testDir, bestDir);\n return candidateOverlap > bestOverlap ? candidate : best;\n });\n\n paired.push({\n test: testFile,\n source: bestMatch,\n confidence: candidates.length === 1 ? \"exact\" : \"best-guess\",\n });\n } else {\n unmatched.push(testFile);\n }\n }\n\n return { totalTestFiles: testFiles.length, paired, unmatched };\n}\n\nfunction commonSegments(pathA: string, pathB: string): number {\n const segsA = pathA.split(\"/\");\n const segsB = pathB.split(\"/\");\n let count = 0;\n for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {\n if (segsA[i] === segsB[i]) count++;\n else break;\n }\n return count;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { extractClaims } from \"./claims.js\";\nimport { lastCommitOf } from \"./git.js\";\nimport type { CommitRef, DocClaims } from \"./types.js\";\n\nconst exec = promisify(execFile);\n\n/**\n * All context files audited in v1, in the precedence order the setup\n * playbook uses. Every candidate that exists is audited — a repo can\n * legitimately carry both AGENTS.md and CLAUDE.md, and drift can live in\n * either.\n */\nexport const DOC_CANDIDATES = [\n \"AGENTS.md\",\n \"CLAUDE.md\",\n \".claude/CLAUDE.md\",\n] as const;\n\nexport interface AuditDoc {\n /** Repo-relative posix path. */\n path: string;\n content: string;\n lineCount: number;\n /** Null when the doc is untracked. */\n lastCommit: CommitRef | null;\n /** Uncommitted edits present. */\n dirty: boolean;\n claims: DocClaims;\n}\n\nasync function isDirty(resolvedRoot: string, relPath: string): Promise<boolean> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"status\", \"--porcelain\", \"--\", relPath],\n { cwd: resolvedRoot }\n );\n return stdout.trim().length > 0;\n } catch {\n return false;\n }\n}\n\nexport async function discoverDocs(resolvedRoot: string): Promise<AuditDoc[]> {\n const docs: AuditDoc[] = [];\n for (const candidate of DOC_CANDIDATES) {\n let content: string;\n try {\n content = await fs.readFile(path.join(resolvedRoot, candidate), \"utf-8\");\n } catch {\n continue;\n }\n docs.push({\n path: candidate,\n content,\n lineCount: content.split(\"\\n\").length,\n lastCommit: await lastCommitOf(resolvedRoot, candidate),\n dirty: await isDirty(resolvedRoot, candidate),\n claims: extractClaims(content),\n });\n }\n return docs;\n}\n","import type { PathClaim } from \"./types.js\";\n\n/**\n * A fenced block is treated as a directory tree only when it clearly is one —\n * below this many branch-glyph lines it's more likely an ASCII sketch.\n */\nconst MIN_GLYPH_LINES = 3;\n\nconst GLYPHS = [\"├──\", \"└──\"] as const;\n\nfunction glyphIndex(line: string): number {\n for (const glyph of GLYPHS) {\n const idx = line.indexOf(glyph);\n if (idx !== -1) return idx;\n }\n return -1;\n}\n\n/** Tree furniture: blank, or only vertical bars and whitespace. */\nfunction isSpacerLine(line: string): boolean {\n return /^[\\s│|]*$/.test(line);\n}\n\n/**\n * Strip an inline comment/annotation from a tree entry. Entries commonly\n * carry `# comment` or column-aligned notes after two or more spaces.\n */\nfunction entryName(afterGlyph: string): string | null {\n let name = afterGlyph.replace(/^\\s+/, \"\");\n const hash = name.search(/\\s+#/);\n if (hash !== -1) name = name.slice(0, hash);\n const columns = name.search(/\\s{2,}/);\n if (columns !== -1) name = name.slice(0, columns);\n name = name.trim();\n // A name with remaining internal whitespace is not a path — treat the\n // line as malformed rather than guessing.\n if (!name || /\\s/.test(name)) return null;\n return name;\n}\n\n/**\n * Reconstruct full paths from an ASCII directory tree inside a fenced block.\n *\n * The failure mode must always be a missed claim, never an invented path: a\n * line that doesn't parse cleanly aborts reconstruction below it, and every\n * emitted path still passes the deleted-reference provability gate later.\n *\n * `blockLines` are the fence's content lines; `blockStartLine` is the 1-based\n * doc line number of the first content line.\n */\nexport function extractTreeClaims(\n blockLines: string[],\n blockStartLine: number\n): PathClaim[] {\n const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;\n if (glyphLines < MIN_GLYPH_LINES) return [];\n\n const claims: PathClaim[] = [];\n // Directories on the path from the root to the current entry, keyed by the\n // column their branch glyph appeared at.\n const stack: Array<{ col: number; name: string }> = [];\n let rootPrefix = \"\";\n let started = false;\n\n for (let i = 0; i < blockLines.length; i++) {\n const line = blockLines[i];\n const col = glyphIndex(line);\n\n if (col === -1) {\n if (isSpacerLine(line)) continue;\n if (!started) {\n // A bare `src/` line above the first glyph names the tree's root.\n const candidate = line.trim();\n if (candidate.endsWith(\"/\") && !/\\s/.test(candidate)) {\n rootPrefix = candidate.replace(/\\/+$/, \"\");\n claims.push({\n path: rootPrefix,\n line: blockStartLine + i,\n excerpt: candidate,\n });\n }\n continue;\n }\n // Unparseable line after the tree began: stop here, keep what we have.\n return claims;\n }\n\n started = true;\n const name = entryName(line.slice(col + GLYPHS[0].length));\n if (name === null) return claims;\n\n while (stack.length > 0 && stack[stack.length - 1].col >= col) {\n stack.pop();\n }\n\n const isDir = name.endsWith(\"/\");\n const cleanName = name.replace(/\\/+$/, \"\");\n const segments = [\n ...(rootPrefix ? [rootPrefix] : []),\n ...stack.map((s) => s.name),\n cleanName,\n ];\n claims.push({\n path: segments.join(\"/\"),\n line: blockStartLine + i,\n excerpt: name,\n });\n\n if (isDir) stack.push({ col, name: cleanName });\n }\n\n return claims;\n}\n","import type {\n CommandClaim,\n CountClaim,\n DocClaims,\n PathClaim,\n} from \"./types.js\";\nimport { extractTreeClaims } from \"./tree.js\";\n\n/**\n * Single-segment names that count as path claims without containing a \"/\".\n * Anything else without a slash is prose (\"name your file `config.ts`\"), not\n * a claim about this repo.\n */\nconst ROOT_FILE_NAMES = new Set([\n \"package.json\",\n \"package-lock.json\",\n \"pnpm-workspace.yaml\",\n \"tsconfig.json\",\n \"tsup.config.ts\",\n \"vitest.config.ts\",\n \"Makefile\",\n \"Dockerfile\",\n \"docker-compose.yml\",\n \"Cargo.toml\",\n \"go.mod\",\n \"go.sum\",\n \"pyproject.toml\",\n \"requirements.txt\",\n \"Gemfile\",\n \"composer.json\",\n \"settings.gradle.kts\",\n \"settings.gradle\",\n \"build.gradle.kts\",\n \"build.gradle\",\n \"manifest.json\",\n \"server.json\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\",\n \"CLAUDE.md\",\n \"AGENTS.md\",\n \".gitignore\",\n \".env.example\",\n]);\n\nconst SHELL_FENCE_INFOS = new Set([\"\", \"bash\", \"sh\", \"shell\", \"console\", \"zsh\"]);\n\nconst COMMAND_RE = /\\b(npm|pnpm|yarn)\\s+run\\s+([A-Za-z0-9:_.-]+)/g;\nconst COUNT_RE = /(\\d+)\\s+(modules?|packages?|workspaces?|crates?)\\b/gi;\n/** \"3 package managers\" is not a package count. */\nconst COUNT_DENYLIST_RE = /^\\s*(manager|registr|lock|json)/i;\n\nconst IGNORE_LINE = \"<!-- mason:ignore -->\";\nconst IGNORE_START = \"<!-- mason:ignore-start -->\";\nconst IGNORE_END = \"<!-- mason:ignore-end -->\";\n\n/**\n * Normalize a candidate token into a repo-relative path claim, or return\n * null when the token is not a claim about this repo (URL, glob,\n * placeholder, relative import example, bare word).\n */\nexport function normalizePathToken(token: string): string | null {\n let t = token.trim();\n if (!t) return null;\n if (/\\s/.test(t)) return null;\n if (t.includes(\"://\") || t.includes(\"\\\\\")) return null;\n if (/[*?[\\]{}<>$`]/.test(t)) return null;\n if (t.startsWith(\"/\") || t.startsWith(\"~\") || t.startsWith(\"./\") || t.startsWith(\"../\")) {\n return null;\n }\n // `src/mcp/tools.ts:189` claims the file, not the line.\n t = t.replace(/:\\d+(?:-\\d+)?$/, \"\");\n if (t.includes(\":\")) return null;\n const normalized = t.replace(/\\/+$/, \"\");\n if (!normalized) return null;\n // `server/src/test/kotlin/...` — a dots-only segment is an \"and so on\"\n // placeholder, not a claim.\n if (normalized.split(\"/\").some((seg) => /^\\.+$/.test(seg))) return null;\n if (normalized.includes(\"/\")) return normalized;\n return ROOT_FILE_NAMES.has(normalized) ? normalized : null;\n}\n\n/** A fence line that is exactly one path-shaped token is a file-list claim. */\nfunction exactTokenPath(line: string): string | null {\n const trimmed = line.trim();\n if (!trimmed || /\\s/.test(trimmed) || !trimmed.includes(\"/\")) return null;\n return normalizePathToken(trimmed);\n}\n\nfunction computeIgnoredLines(lines: string[]): boolean[] {\n const ignored = new Array<boolean>(lines.length).fill(false);\n let inRegion = false;\n let ignoreNext = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line.includes(IGNORE_START)) {\n inRegion = true;\n ignored[i] = true;\n continue;\n }\n if (line.includes(IGNORE_END)) {\n inRegion = false;\n ignored[i] = true;\n continue;\n }\n if (inRegion) {\n ignored[i] = true;\n continue;\n }\n if (ignoreNext) {\n if (line.trim().length === 0) continue; // skip blanks to the next real line\n ignored[i] = true;\n ignoreNext = false;\n continue;\n }\n if (line.includes(IGNORE_LINE)) {\n ignored[i] = true;\n const rest = line.replace(IGNORE_LINE, \"\").trim();\n if (rest.length === 0) ignoreNext = true;\n }\n }\n return ignored;\n}\n\n/**\n * Extract every checkable claim from a context-file's markdown. Deterministic\n * and purely lexical — precision comes from the checks' provability gates,\n * not from clever parsing here.\n */\nexport function extractClaims(content: string): DocClaims {\n const lines = content.split(\"\\n\");\n const ignored = computeIgnoredLines(lines);\n\n const paths = new Map<string, PathClaim>();\n const counts: CountClaim[] = [];\n const commands = new Map<string, CommandClaim>();\n\n const addPath = (claim: PathClaim): void => {\n if (!paths.has(claim.path)) paths.set(claim.path, claim);\n };\n const addCommand = (claim: CommandClaim): void => {\n if (!commands.has(claim.scriptName)) commands.set(claim.scriptName, claim);\n };\n\n let inFence = false;\n let fenceInfo = \"\";\n let fenceMarker = \"\";\n let blockLines: string[] = [];\n let blockStartLine = 0;\n\n const processBlock = (): void => {\n for (const claim of extractTreeClaims(blockLines, blockStartLine)) {\n addPath(claim);\n }\n for (let i = 0; i < blockLines.length; i++) {\n const exact = exactTokenPath(blockLines[i]);\n if (exact) {\n addPath({\n path: exact,\n line: blockStartLine + i,\n excerpt: blockLines[i].trim(),\n });\n }\n }\n };\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineNo = i + 1;\n const fenceMatch = line.match(/^\\s*(```+|~~~+)(.*)$/);\n\n if (fenceMatch) {\n if (!inFence) {\n inFence = true;\n fenceMarker = fenceMatch[1][0];\n fenceInfo = fenceMatch[2].trim().toLowerCase();\n blockLines = [];\n blockStartLine = lineNo + 1;\n } else if (fenceMatch[1][0] === fenceMarker) {\n inFence = false;\n processBlock();\n }\n continue;\n }\n\n if (inFence) {\n // Ignored lines become spacers so the rest of a tree still parses.\n blockLines.push(ignored[i] ? \"\" : line);\n if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {\n for (const m of line.matchAll(COMMAND_RE)) {\n addCommand({\n scriptName: m[2],\n invocation: m[0],\n line: lineNo,\n excerpt: m[0],\n });\n }\n }\n continue;\n }\n\n if (ignored[i]) continue;\n\n for (const m of line.matchAll(/`([^`]+)`/g)) {\n const normalized = normalizePathToken(m[1]);\n if (normalized) {\n addPath({ path: normalized, line: lineNo, excerpt: m[1] });\n }\n }\n for (const m of line.matchAll(/\"([A-Za-z][\\w.@-]*(?:\\/[\\w.@-]+)+\\/?)\"/g)) {\n const normalized = normalizePathToken(m[1]);\n if (normalized) {\n addPath({ path: normalized, line: lineNo, excerpt: m[1] });\n }\n }\n for (const m of line.matchAll(COUNT_RE)) {\n const rest = line.slice((m.index ?? 0) + m[0].length);\n if (COUNT_DENYLIST_RE.test(rest)) continue;\n counts.push({\n count: Number.parseInt(m[1], 10),\n unit: m[2].toLowerCase(),\n line: lineNo,\n excerpt: m[0],\n });\n }\n for (const m of line.matchAll(COMMAND_RE)) {\n addCommand({\n scriptName: m[2],\n invocation: m[0],\n line: lineNo,\n excerpt: m[0],\n });\n }\n }\n\n // An unclosed fence still gets its block processed — trees at the end of a\n // truncated doc are claims too.\n if (inFence) processBlock();\n\n return {\n paths: [...paths.values()],\n counts,\n commands: [...commands.values()],\n };\n}\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { CommitRef } from \"./types.js\";\n\nconst exec = promisify(execFile);\n\nconst COMMIT_FORMAT = \"%H%x09%cI%x09%s\";\n\nfunction parseCommitLine(line: string): CommitRef | null {\n const parts = line.split(\"\\t\");\n if (parts.length < 3 || !parts[0]) return null;\n return { hash: parts[0], date: parts[1], subject: parts.slice(2).join(\"\\t\") };\n}\n\n/** Most recent commit touching a path, or null if the path was never tracked. */\nexport async function lastCommitOf(\n resolvedRoot: string,\n relPath: string\n): Promise<CommitRef | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"log\", \"-1\", `--format=${COMMIT_FORMAT}`, \"--\", relPath],\n { cwd: resolvedRoot }\n );\n const line = stdout.trim().split(\"\\n\")[0];\n return line ? parseCommitLine(line) : null;\n } catch {\n return null;\n }\n}\n\n/** The commit that deleted a path, or null if none did. */\nexport async function deletingCommitOf(\n resolvedRoot: string,\n relPath: string\n): Promise<CommitRef | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\n \"log\",\n \"-1\",\n \"--diff-filter=D\",\n `--format=${COMMIT_FORMAT}`,\n \"--\",\n relPath,\n ],\n { cwd: resolvedRoot }\n );\n const line = stdout.trim().split(\"\\n\")[0];\n return line ? parseCommitLine(line) : null;\n } catch {\n return null;\n }\n}\n\n/** Oldest commit touching a path (used to date a directory's appearance). */\nexport async function firstCommitOf(\n resolvedRoot: string,\n relPath: string\n): Promise<CommitRef | null> {\n try {\n const { stdout } = await exec(\n \"git\",\n [\"log\", \"--reverse\", `--format=${COMMIT_FORMAT}`, \"--\", relPath],\n { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }\n );\n const line = stdout.trim().split(\"\\n\")[0];\n return line ? parseCommitLine(line) : null;\n } catch {\n return null;\n }\n}\n\nexport interface RangeCommits {\n commits: Array<CommitRef & { files: string[] }>;\n total: number;\n}\n\n/**\n * Commits in fromHash..HEAD touching any of the pathspecs, newest first, with\n * the touched files per commit. Rev-range, not --since: immune to rebase date\n * skew and CI checkout mtimes. Returns null when the range is uncomputable\n * (unreachable base commit, shallow clone, no git).\n */\nexport async function commitsTouchingSince(\n resolvedRoot: string,\n fromHash: string,\n pathspecs: string[]\n): Promise<RangeCommits | null> {\n if (!fromHash || fromHash === \"unknown\") return null;\n try {\n const { stdout } = await exec(\n \"git\",\n [\n \"log\",\n `${fromHash}..HEAD`,\n `--format=%x01${COMMIT_FORMAT}`,\n \"--name-only\",\n \"--\",\n ...pathspecs,\n ],\n { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }\n );\n\n const commits: Array<CommitRef & { files: string[] }> = [];\n // \\x01 marks each commit header so name-only file lists can't be\n // mistaken for headers.\n for (const block of stdout.split(\"\\x01\")) {\n if (!block.trim()) continue;\n const lines = block.split(\"\\n\").filter((l) => l.trim().length > 0);\n const ref = parseCommitLine(lines[0]);\n if (!ref) continue;\n commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });\n }\n return { commits, total: commits.length };\n } catch {\n return null;\n }\n}\n","export type IssueType =\n | \"deleted-reference\"\n | \"new-module\"\n | \"stale-count\"\n | \"dead-command\";\n\nexport type AdvisoryType = \"deps-changed\" | \"decision-anchor-drift\";\n\nexport type CheckName = IssueType | AdvisoryType;\n\nexport const ALL_CHECKS: CheckName[] = [\n \"deleted-reference\",\n \"new-module\",\n \"stale-count\",\n \"dead-command\",\n \"deps-changed\",\n \"decision-anchor-drift\",\n];\n\n/**\n * \"certain\" — the claim is provably false (a tracked path is gone, a computed\n * count differs, a script exists in no manifest). \"likely\" — evidence-backed\n * but heuristic (an unmentioned directory; a never-tracked path whose parent\n * exists). Certain-class issues are safe to auto-fix; likely-class issues\n * deserve a look.\n */\nexport type Confidence = \"certain\" | \"likely\";\n\nexport interface CommitRef {\n hash: string;\n date: string;\n subject: string;\n}\n\nexport interface DocAnchor {\n /** Repo-relative doc path, e.g. \"CLAUDE.md\" or \".claude/CLAUDE.md\". */\n doc: string;\n /** 1-based line of the claim; null for doc-level issues (new-module). */\n line: number | null;\n /** The claim exactly as written, e.g. \"src/utils/logger.ts\". */\n excerpt: string | null;\n}\n\nexport type Evidence =\n | {\n kind: \"missing-path\";\n claimed: string;\n renamedTo: string | null;\n deletedInCommit: CommitRef | null;\n everTracked: boolean;\n parentDirExists: boolean;\n }\n | {\n kind: \"unmentioned-dir\";\n dir: string;\n sourceFileCount: number;\n firstCommit: CommitRef | null;\n checkedDocs: string[];\n }\n | {\n kind: \"count-mismatch\";\n claimed: number;\n actual: number;\n unit: string;\n /** Where the actual count came from, e.g. \"package.json workspaces\". */\n countedFrom: string;\n members: string[];\n }\n | {\n kind: \"missing-script\";\n scriptName: string;\n invocation: string;\n manifestsChecked: string[];\n availableScripts: string[];\n }\n | {\n kind: \"doc-behind-manifests\";\n docLastCommit: CommitRef;\n manifestCommits: Array<CommitRef & { files: string[] }>;\n totalCommits: number;\n }\n | {\n kind: \"decision-anchor\";\n decisionId: string;\n title: string;\n changedFiles: string[];\n refreshedHash: string;\n };\n\nexport interface AuditIssue {\n type: IssueType;\n message: string;\n anchor: DocAnchor;\n confidence: Confidence;\n evidence: Evidence;\n}\n\n/**\n * Advisories are facts the fixing agent cannot close by editing the doc (a\n * manifest commit after the doc's commit stays true forever; decision records\n * must be re-verified by humans). They NEVER affect the exit code — same\n * precedent as decision staleness in mason-drift.\n */\nexport interface AuditAdvisory {\n type: AdvisoryType;\n message: string;\n anchor: DocAnchor;\n evidence: Evidence;\n}\n\nexport interface AuditDocInfo {\n path: string;\n lastCommit: CommitRef | null;\n /** Uncommitted edits present — deps-changed is suppressed for dirty docs. */\n dirty: boolean;\n lineCount: number;\n}\n\nexport interface AuditReport {\n /** Additive-only schema — this output is a CI contract. */\n version: 1;\n root: string;\n gitAvailable: boolean;\n docs: AuditDocInfo[];\n /** Whether .mason/decisions/ existed and was checked. */\n decisionsChecked: boolean;\n /** Drive exit code 1. */\n issues: AuditIssue[];\n /** Never drive the exit code. */\n advisories: AuditAdvisory[];\n skippedChecks: Array<{ check: string; reason: string }>;\n clean: boolean;\n}\n\nexport interface PathClaim {\n path: string;\n line: number;\n excerpt: string;\n}\n\nexport interface CountClaim {\n count: number;\n unit: string;\n line: number;\n excerpt: string;\n}\n\nexport interface CommandClaim {\n scriptName: string;\n invocation: string;\n line: number;\n excerpt: string;\n}\n\nexport interface DocClaims {\n paths: PathClaim[];\n counts: CountClaim[];\n commands: CommandClaim[];\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { deletingCommitOf, lastCommitOf } from \"../git.js\";\nimport type { AuditIssue } from \"../types.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nasync function exists(absPath: string): Promise<boolean> {\n try {\n await fs.access(absPath);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * A claimed path that is missing on disk is only flagged when the repo can\n * prove it was ever real: a rename since the doc's last commit, or git\n * history for the path, or at least an existing parent directory. Paths with\n * none of those are illustrative examples and are dropped silently.\n */\nexport async function checkDeletedReferences(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n for (const doc of ctx.docs) {\n const changes = ctx.changesSinceDoc.get(doc.path);\n const renames = new Map<string, string>();\n for (const change of changes ?? []) {\n if (change.status === \"renamed\" && change.previousPath) {\n renames.set(change.previousPath, change.path);\n }\n }\n\n for (const claim of doc.claims.paths) {\n // Mason's own metadata is optional state, not repo structure — docs\n // legitimately describe .mason/ files that a given repo doesn't have.\n if (claim.path === \".mason\" || claim.path.startsWith(\".mason/\")) {\n continue;\n }\n if (await exists(path.join(ctx.root, claim.path))) continue;\n\n const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };\n const renamedTo = renames.get(claim.path) ?? null;\n\n if (renamedTo) {\n result.issues.push({\n type: \"deleted-reference\",\n message: `\\`${claim.path}\\` was renamed to \\`${renamedTo}\\``,\n anchor,\n confidence: \"certain\",\n evidence: {\n kind: \"missing-path\",\n claimed: claim.path,\n renamedTo,\n deletedInCommit: null,\n everTracked: true,\n parentDirExists: true,\n },\n });\n continue;\n }\n\n const tracked = await lastCommitOf(ctx.root, claim.path);\n if (tracked) {\n const deleted = await deletingCommitOf(ctx.root, claim.path);\n const detail = deleted\n ? ` – deleted in ${deleted.hash.slice(0, 7)} \"${deleted.subject}\" (${deleted.date.slice(0, 10)})`\n : \"\";\n result.issues.push({\n type: \"deleted-reference\",\n message: `\\`${claim.path}\\` no longer exists${detail}`,\n anchor,\n confidence: \"certain\",\n evidence: {\n kind: \"missing-path\",\n claimed: claim.path,\n renamedTo: null,\n deletedInCommit: deleted,\n everTracked: true,\n parentDirExists: await exists(\n path.join(ctx.root, path.dirname(claim.path))\n ),\n },\n });\n continue;\n }\n\n const parentDirExists = await exists(\n path.join(ctx.root, path.dirname(claim.path))\n );\n if (!parentDirExists) continue;\n\n const issue: AuditIssue = {\n type: \"deleted-reference\",\n message: `\\`${claim.path}\\` does not exist (never tracked in git – possible typo or invented path)`,\n anchor,\n confidence: \"likely\",\n evidence: {\n kind: \"missing-path\",\n claimed: claim.path,\n renamedTo: null,\n deletedInCommit: null,\n everTracked: false,\n parentDirExists: true,\n },\n };\n result.issues.push(issue);\n }\n }\n\n return result;\n}\n","import fg from \"fast-glob\";\nimport path from \"node:path\";\nimport { SOURCE_GLOB, SOURCE_IGNORE } from \"../../snapshot/snapshot.js\";\nimport { firstCommitOf } from \"../git.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\n/** Directories that are never \"modules\" worth documenting. */\nconst DIR_DENYLIST = new Set([\n \"node_modules\",\n \"dist\",\n \"build\",\n \"out\",\n \"coverage\",\n \"target\",\n \"vendor\",\n \"__pycache__\",\n \"venv\",\n \".venv\",\n \".git\",\n \".gradle\",\n \".mason\",\n \".claude\",\n \".github\",\n \".vscode\",\n \".idea\",\n]);\n\n/** Second-level dirs need a bit more substance before they count. */\nconst SECOND_LEVEL_MIN_SOURCE_FILES = 2;\n/**\n * Descend into a top-level dir only when the docs evidently enumerate its\n * children — at least this many of its subdirs already mentioned.\n */\nconst ENUMERATION_THRESHOLD = 2;\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Word-boundary mention check across the union of all docs — `app` must not\n * match \"application\", and a dir mentioned only in AGENTS.md must not be\n * flagged against CLAUDE.md.\n */\nfunction isMentioned(combinedDocs: string, name: string): boolean {\n const re = new RegExp(\n `(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,\n \"im\"\n );\n return re.test(combinedDocs);\n}\n\nasync function listSubdirs(absDir: string): Promise<string[]> {\n const dirs = await fg(\"*\", {\n cwd: absDir,\n onlyDirectories: true,\n suppressErrors: true,\n });\n return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();\n}\n\nasync function countSourceFiles(absDir: string): Promise<number> {\n const files = await fg(SOURCE_GLOB, {\n cwd: absDir,\n ignore: SOURCE_IGNORE,\n suppressErrors: true,\n });\n return files.length;\n}\n\nexport async function checkNewModules(ctx: CheckContext): Promise<CheckResult> {\n const result = emptyResult();\n if (ctx.docs.length === 0) return result;\n\n const combinedDocs = ctx.docs.map((d) => d.content).join(\"\\n\");\n const primaryDoc = ctx.docs[0].path;\n const checkedDocs = ctx.docs.map((d) => d.path);\n\n const flag = async (dir: string, sourceFileCount: number): Promise<void> => {\n result.issues.push({\n type: \"new-module\",\n message: `directory \\`${dir}/\\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? \"\" : \"s\"} but is not mentioned in any context file`,\n anchor: { doc: primaryDoc, line: null, excerpt: dir },\n confidence: \"likely\",\n evidence: {\n kind: \"unmentioned-dir\",\n dir,\n sourceFileCount,\n firstCommit: await firstCommitOf(ctx.root, dir),\n checkedDocs,\n },\n });\n };\n\n for (const topDir of await listSubdirs(ctx.root)) {\n const absTop = path.join(ctx.root, topDir);\n const topMentioned = isMentioned(combinedDocs, topDir);\n\n if (!topMentioned) {\n const count = await countSourceFiles(absTop);\n if (count >= 1) await flag(topDir, count);\n continue;\n }\n\n // The docs know this dir. If they enumerate its children (several\n // subdirs already mentioned), an unmentioned sibling is drift — this is\n // how a freshly added module under src/ gets caught.\n const subdirs = await listSubdirs(absTop);\n const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));\n if (mentioned.length < ENUMERATION_THRESHOLD) continue;\n\n for (const sub of subdirs) {\n if (isMentioned(combinedDocs, sub)) continue;\n const count = await countSourceFiles(path.join(absTop, sub));\n if (count >= SECOND_LEVEL_MIN_SOURCE_FILES) {\n await flag(`${topDir}/${sub}`, count);\n }\n }\n }\n\n return result;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { CountClaim } from \"../types.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nconst MEMBERS_CAP = 50;\n\ninterface CountSource {\n actual: number;\n countedFrom: string;\n members: string[];\n}\n\nasync function readIfExists(absPath: string): Promise<string | null> {\n try {\n return await fs.readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nasync function countGradleModules(root: string): Promise<CountSource | null> {\n for (const name of [\"settings.gradle.kts\", \"settings.gradle\"]) {\n const content = await readIfExists(path.join(root, name));\n if (content === null) continue;\n // include(\":a\", \":b\") — count quoted project strings, not include() calls.\n const members: string[] = [];\n for (const call of content.matchAll(/include\\s*\\(([^)]*)\\)/g)) {\n for (const proj of call[1].matchAll(/[\"']([^\"']+)[\"']/g)) {\n members.push(proj[1]);\n }\n }\n if (members.length === 0) return null;\n return { actual: members.length, countedFrom: name, members };\n }\n return null;\n}\n\nasync function countNpmWorkspaces(root: string): Promise<CountSource | null> {\n const pkgRaw = await readIfExists(path.join(root, \"package.json\"));\n if (pkgRaw !== null) {\n try {\n const pkg = JSON.parse(pkgRaw);\n const globs: string[] = Array.isArray(pkg.workspaces)\n ? pkg.workspaces\n : Array.isArray(pkg.workspaces?.packages)\n ? pkg.workspaces.packages\n : [];\n if (globs.length > 0) {\n const matched = await fg(\n globs.map((g) => `${g.replace(/\\/+$/, \"\")}/package.json`),\n { cwd: root, ignore: [\"**/node_modules/**\"] }\n );\n return {\n actual: matched.length,\n countedFrom: \"package.json workspaces\",\n members: matched.map((m) => path.dirname(m)).sort(),\n };\n }\n } catch {\n // Malformed package.json — nothing provable here.\n }\n }\n\n const pnpmRaw = await readIfExists(path.join(root, \"pnpm-workspace.yaml\"));\n if (pnpmRaw !== null) {\n const globs: string[] = [];\n let inPackages = false;\n for (const line of pnpmRaw.split(\"\\n\")) {\n if (/^packages\\s*:/.test(line)) {\n inPackages = true;\n continue;\n }\n if (inPackages) {\n const entry = line.match(/^\\s*-\\s*[\"']?([^\"'#\\s]+)/);\n if (entry) {\n if (!entry[1].startsWith(\"!\")) globs.push(entry[1]);\n } else if (line.trim().length > 0 && !line.startsWith(\" \")) {\n inPackages = false;\n }\n }\n }\n if (globs.length > 0) {\n const matched = await fg(\n globs.map((g) => `${g.replace(/\\/+$/, \"\")}/package.json`),\n { cwd: root, ignore: [\"**/node_modules/**\"] }\n );\n return {\n actual: matched.length,\n countedFrom: \"pnpm-workspace.yaml\",\n members: matched.map((m) => path.dirname(m)).sort(),\n };\n }\n }\n return null;\n}\n\nasync function countCargoCrates(root: string): Promise<CountSource | null> {\n const content = await readIfExists(path.join(root, \"Cargo.toml\"));\n if (content === null) return null;\n const membersBlock = content.match(/members\\s*=\\s*\\[([\\s\\S]*?)\\]/);\n if (!membersBlock) return null;\n const entries = [...membersBlock[1].matchAll(/[\"']([^\"']+)[\"']/g)].map(\n (m) => m[1]\n );\n if (entries.length === 0) return null;\n\n // Workspace members may be globs (\"crates/*\") — resolve them against\n // directories that actually contain a Cargo.toml.\n const members = new Set<string>();\n for (const entry of entries) {\n if (/[*?[\\]{}]/.test(entry)) {\n const matched = await fg(`${entry.replace(/\\/+$/, \"\")}/Cargo.toml`, {\n cwd: root,\n ignore: [\"**/target/**\"],\n });\n for (const m of matched) members.add(path.dirname(m));\n } else if (\n (await readIfExists(path.join(root, entry, \"Cargo.toml\"))) !== null\n ) {\n members.add(entry);\n }\n }\n if (members.size === 0) return null;\n return {\n actual: members.size,\n countedFrom: \"Cargo.toml workspace members\",\n members: [...members].sort(),\n };\n}\n\n/**\n * Map a claim's unit to the ecosystem that can prove it. When the mapped\n * ecosystem has no workspace manifest in this repo, the claim is skipped —\n * \"12 packages\" in a Gradle repo proves nothing either way.\n */\nasync function resolveCountSource(\n root: string,\n claim: CountClaim\n): Promise<CountSource | null> {\n const unit = claim.unit.replace(/s$/, \"\");\n if (unit === \"module\") return countGradleModules(root);\n if (unit === \"workspace\") return countNpmWorkspaces(root);\n if (unit === \"crate\") return countCargoCrates(root);\n // \"packages\" is ecosystem-ambiguous — first manifest that resolves wins.\n return (\n (await countNpmWorkspaces(root)) ??\n (await countCargoCrates(root)) ??\n (await countGradleModules(root))\n );\n}\n\nexport async function checkStaleCounts(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n for (const doc of ctx.docs) {\n for (const claim of doc.claims.counts) {\n const source = await resolveCountSource(ctx.root, claim);\n if (source === null || source.actual === claim.count) continue;\n result.issues.push({\n type: \"stale-count\",\n message: `says \"${claim.excerpt}\" but ${source.countedFrom} resolves to ${source.actual}`,\n anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },\n confidence: \"certain\",\n evidence: {\n kind: \"count-mismatch\",\n claimed: claim.count,\n actual: source.actual,\n unit: claim.unit,\n countedFrom: source.countedFrom,\n members: source.members.slice(0, MEMBERS_CAP),\n },\n });\n }\n }\n\n return result;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nconst AVAILABLE_SCRIPTS_CAP = 30;\n\nasync function scriptsOf(absManifest: string): Promise<string[] | null> {\n try {\n const pkg = JSON.parse(await fs.readFile(absManifest, \"utf-8\"));\n return pkg && typeof pkg.scripts === \"object\" && pkg.scripts !== null\n ? Object.keys(pkg.scripts)\n : [];\n } catch {\n return null;\n }\n}\n\n/**\n * `npm run <script>` claims checked against package.json scripts — the one\n * ecosystem where task discovery is a single JSON parse. A script missing\n * from the root manifest is searched in every workspace manifest before\n * being flagged; docs legitimately say \"in packages/foo run `npm run build`\".\n */\nexport async function checkDeadCommands(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n const commandClaims = ctx.docs.flatMap((doc) =>\n doc.claims.commands.map((claim) => ({ doc, claim }))\n );\n if (commandClaims.length === 0) return result;\n\n const rootScripts = await scriptsOf(path.join(ctx.root, \"package.json\"));\n if (rootScripts === null) {\n result.skipped.push({\n check: \"dead-command\",\n reason: \"no package.json at the repo root\",\n });\n return result;\n }\n const rootSet = new Set(rootScripts);\n\n let workspaceScripts: Set<string> | null = null;\n let manifestsChecked: string[] = [\"package.json\"];\n const loadWorkspaceScripts = async (): Promise<Set<string>> => {\n if (workspaceScripts !== null) return workspaceScripts;\n workspaceScripts = new Set<string>();\n const manifests = await fg(\"**/package.json\", {\n cwd: ctx.root,\n ignore: [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"package.json\",\n ],\n });\n manifestsChecked = [\"package.json\", ...manifests.sort()];\n for (const manifest of manifests) {\n const scripts = await scriptsOf(path.join(ctx.root, manifest));\n for (const name of scripts ?? []) workspaceScripts.add(name);\n }\n return workspaceScripts;\n };\n\n for (const { doc, claim } of commandClaims) {\n if (rootSet.has(claim.scriptName)) continue;\n const elsewhere = await loadWorkspaceScripts();\n if (elsewhere.has(claim.scriptName)) continue;\n\n result.issues.push({\n type: \"dead-command\",\n message: `\\`${claim.invocation}\\` refers to script \"${claim.scriptName}\", which exists in no package.json`,\n anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },\n confidence: \"certain\",\n evidence: {\n kind: \"missing-script\",\n scriptName: claim.scriptName,\n invocation: claim.invocation,\n manifestsChecked,\n availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP),\n },\n });\n }\n\n return result;\n}\n","import { commitsTouchingSince } from \"../git.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\nconst MANIFEST_COMMITS_CAP = 10;\n\n/**\n * Tracked manifest files at any depth. Lockfiles are pure churn and are\n * deliberately not matched.\n */\nconst MANIFEST_PATHSPECS = [\n \":(glob)**/package.json\",\n \":(glob)**/build.gradle.kts\",\n \":(glob)**/build.gradle\",\n \"settings.gradle.kts\",\n \"settings.gradle\",\n \"gradle/libs.versions.toml\",\n \":(glob)**/Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"requirements.txt\",\n \"Gemfile\",\n \"composer.json\",\n];\n\n/**\n * Advisory, never an issue: a manifest commit after the doc's last commit\n * proves recency ordering, not that any specific claim is false — and it can\n * never be closed by editing the doc within the same run.\n */\nexport async function checkDepsChanged(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n\n for (const doc of ctx.docs) {\n if (!doc.lastCommit) {\n result.skipped.push({\n check: \"deps-changed\",\n reason: `${doc.path} has no commit history`,\n });\n continue;\n }\n if (doc.dirty) {\n result.skipped.push({\n check: \"deps-changed\",\n reason: `${doc.path} has uncommitted edits – suppressed while in flight`,\n });\n continue;\n }\n\n const range = await commitsTouchingSince(\n ctx.root,\n doc.lastCommit.hash,\n MANIFEST_PATHSPECS\n );\n if (range === null) {\n result.skipped.push({\n check: \"deps-changed\",\n reason: `${doc.path}: commit range unreachable (shallow clone?)`,\n });\n continue;\n }\n if (range.total === 0) continue;\n\n const latest = range.commits[0];\n result.advisories.push({\n type: \"deps-changed\",\n message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? \"\" : \"s\"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} \"${latest.subject}\")`,\n anchor: { doc: doc.path, line: null, excerpt: null },\n evidence: {\n kind: \"doc-behind-manifests\",\n docLastCommit: doc.lastCommit,\n manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),\n totalCommits: range.total,\n },\n });\n }\n\n return result;\n}\n","import path from \"node:path\";\nimport { getChangesWithStatus } from \"../drift/drift.js\";\nimport { getCurrentGitHash } from \"../snapshot/snapshot.js\";\nimport { loadDecisions } from \"./decisions.js\";\nimport type { DecisionRecord } from \"./decisions.js\";\n\n/**\n * Deliberately separate from DriftReport: mason-drift's exit codes and\n * --json shape are a CI contract, and `stale` there means MAP staleness.\n * Decision staleness is additive on top.\n */\nexport interface DecisionDriftReport {\n historyAvailable: boolean;\n totalDecisions: number;\n /** Decision id → anchor files changed since the record's refreshedHash. */\n staleDecisions: Record<string, string[]>;\n}\n\n/**\n * Flag active decisions whose anchor files changed since the record was\n * last verified. Anchorless decisions are pure prose and never go stale.\n * Deterministic — git only, no LLM.\n */\nexport async function computeDecisionDrift(\n rootDir: string,\n decisions?: DecisionRecord[]\n): Promise<DecisionDriftReport> {\n const resolvedRoot = path.resolve(rootDir);\n const records = decisions ?? (await loadDecisions(resolvedRoot));\n const report: DecisionDriftReport = {\n historyAvailable: true,\n totalDecisions: records.length,\n staleDecisions: {},\n };\n\n const head = await getCurrentGitHash(resolvedRoot);\n const changesByHash = new Map<string, Set<string> | null>();\n\n for (const record of records) {\n if (record.status !== \"active\" || record.files.length === 0) continue;\n if (record.refreshedHash === head) continue;\n\n let touched = changesByHash.get(record.refreshedHash);\n if (touched === undefined) {\n const changes = await getChangesWithStatus(\n resolvedRoot,\n record.refreshedHash\n );\n if (changes === null) {\n touched = null;\n } else {\n touched = new Set<string>();\n for (const change of changes) {\n touched.add(change.path);\n if (change.previousPath) touched.add(change.previousPath);\n }\n }\n changesByHash.set(record.refreshedHash, touched);\n }\n\n if (touched === null) {\n // Unreachable base commit — we know nothing per-file; surface that\n // rather than silently reporting the record fresh.\n report.historyAvailable = false;\n continue;\n }\n\n const hits = record.files.filter((f) => touched.has(f));\n if (hits.length > 0) {\n report.staleDecisions[record.id] = hits;\n }\n }\n\n return report;\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { getCurrentGitHash } from \"../snapshot/snapshot.js\";\nimport { jaccard, tokenSet } from \"../context/lexical.js\";\n\nexport type DecisionCategory =\n | \"decision\"\n | \"gotcha\"\n | \"deprecation\"\n | \"convention\";\nexport type DecisionStatus = \"active\" | \"superseded\";\n\n/**\n * One unit of team knowledge the code alone can't express: a failed\n * approach, a deprecation, a workaround's reason, a review-settled\n * convention. Stored one file per record under .mason/decisions/ so\n * concurrent additions on different branches merge without conflict,\n * while concurrent edits to the SAME record conflict — contested\n * knowledge should reach a human.\n */\nexport interface DecisionRecord {\n version: 1;\n id: string;\n title: string;\n body: string;\n category: DecisionCategory;\n /** Repo-relative anchor files. Empty means pure prose — never goes stale. */\n files: string[];\n createdAt: string;\n updatedAt: string;\n /** Commit this record was last verified against. */\n refreshedHash: string;\n status: DecisionStatus;\n supersededBy?: string;\n}\n\nexport const TITLE_MAX_CHARS = 80;\nexport const BODY_MAX_CHARS = 1500;\nexport const MAX_ACTIVE_DECISIONS = 150;\n\nconst DUPLICATE_JACCARD = 0.5;\nconst DUPLICATE_JACCARD_WITH_SHARED_FILE = 0.35;\n\nfunction decisionsDir(rootDir: string): string {\n return path.join(rootDir, \".mason\", \"decisions\");\n}\n\nexport async function loadDecisions(\n rootDir: string\n): Promise<DecisionRecord[]> {\n let entries: string[];\n try {\n entries = await fs.readdir(decisionsDir(rootDir));\n } catch {\n return [];\n }\n const records: DecisionRecord[] = [];\n for (const entry of entries) {\n if (!entry.endsWith(\".json\")) continue;\n try {\n const raw = await fs.readFile(\n path.join(decisionsDir(rootDir), entry),\n \"utf-8\"\n );\n const parsed = JSON.parse(raw);\n // Skip unknown versions and malformed records individually — one bad\n // merge artifact must not take down the store.\n if (parsed.version !== 1 || !parsed.id || !parsed.title || !parsed.body) {\n continue;\n }\n records.push(parsed);\n } catch {\n continue;\n }\n }\n return records.sort((a, b) => a.id.localeCompare(b.id));\n}\n\nexport async function saveDecisionRecord(\n rootDir: string,\n record: DecisionRecord\n): Promise<void> {\n await fs.mkdir(decisionsDir(rootDir), { recursive: true });\n await fs.writeFile(\n path.join(decisionsDir(rootDir), `${record.id}.json`),\n JSON.stringify(record, null, 2) + \"\\n\",\n \"utf-8\"\n );\n}\n\n/**\n * Deterministic, human-readable id: kebab slug of the title, ≤60 chars.\n * A slug collision with a DIFFERENT record appends a 6-hex content suffix.\n */\nexport function decisionIdFor(\n title: string,\n body: string,\n existingIds: Set<string>\n): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60)\n .replace(/-+$/, \"\");\n if (!existingIds.has(slug)) return slug || \"decision\";\n const suffix = createHash(\"sha1\")\n .update(title + body)\n .digest(\"hex\")\n .slice(0, 6);\n return `${slug}-${suffix}`;\n}\n\nexport function findNearDuplicate(\n candidate: { title: string; body: string; files: string[] },\n existing: DecisionRecord[]\n): { record: DecisionRecord; similarity: number } | null {\n const candidateTokens = tokenSet(`${candidate.title} ${candidate.body}`);\n const candidateFiles = new Set(candidate.files);\n let best: { record: DecisionRecord; similarity: number } | null = null;\n\n for (const record of existing) {\n if (record.status !== \"active\") continue;\n const similarity = jaccard(\n candidateTokens,\n tokenSet(`${record.title} ${record.body}`)\n );\n const sharesFile = record.files.some((f) => candidateFiles.has(f));\n const threshold = sharesFile\n ? DUPLICATE_JACCARD_WITH_SHARED_FILE\n : DUPLICATE_JACCARD;\n if (similarity >= threshold && (!best || similarity > best.similarity)) {\n best = { record, similarity };\n }\n }\n return best;\n}\n\nfunction sanitizeAnchorFiles(rootDir: string, files: string[]): string[] {\n const resolvedRoot = path.resolve(rootDir);\n return files.filter((f) => {\n const resolved = path.resolve(resolvedRoot, f);\n return (\n resolved.startsWith(resolvedRoot) &&\n !f.startsWith(\"/\") &&\n !f.includes(\"..\")\n );\n });\n}\n\nexport interface UpsertDecisionInput {\n title: string;\n body: string;\n category: DecisionCategory;\n files?: string[];\n /** Existing id to update. Same id + unchanged content = re-verify (re-pin to HEAD). */\n id?: string;\n /** Id of a decision this one replaces; the old record is kept, marked superseded. */\n supersedes?: string;\n /** Save even when a near-duplicate was detected. */\n force?: boolean;\n}\n\nexport type UpsertDecisionResult =\n | {\n status: \"created\" | \"updated\" | \"reverified\" | \"superseded_and_created\";\n id: string;\n totalActive: number;\n warnings: string[];\n pruneCandidates?: string[];\n }\n | { status: \"duplicate_suspected\"; existing: DecisionRecord; hint: string }\n | { status: \"error\"; error: string };\n\nexport async function upsertDecision(\n rootDir: string,\n input: UpsertDecisionInput\n): Promise<UpsertDecisionResult> {\n const title = input.title.trim();\n const body = input.body.trim();\n if (title.length === 0 || body.length === 0) {\n return { status: \"error\", error: \"title and body must be non-empty\" };\n }\n if (title.length > TITLE_MAX_CHARS) {\n return {\n status: \"error\",\n error: `title exceeds ${TITLE_MAX_CHARS} chars — tighten it to a specific headline`,\n };\n }\n if (body.length > BODY_MAX_CHARS) {\n return {\n status: \"error\",\n error: `body exceeds ${BODY_MAX_CHARS} chars — record the decision, not the transcript`,\n };\n }\n\n const existing = await loadDecisions(rootDir);\n const byId = new Map(existing.map((r) => [r.id, r]));\n const now = new Date().toISOString();\n const head = await getCurrentGitHash(rootDir);\n const warnings: string[] = [];\n\n const files = sanitizeAnchorFiles(rootDir, input.files ?? []);\n if (input.files && files.length < input.files.length) {\n warnings.push(\"some anchor paths were outside the repo and were dropped\");\n }\n // Nonexistent anchors warn but save — a deprecation note may outlive its file.\n for (const f of files) {\n try {\n await fs.access(path.join(rootDir, f));\n } catch {\n warnings.push(`anchor file does not exist on disk: ${f}`);\n }\n }\n\n // Update / re-verify path\n if (input.id) {\n const record = byId.get(input.id);\n if (!record) {\n return { status: \"error\", error: `no decision with id \"${input.id}\"` };\n }\n const unchanged =\n record.title === title &&\n record.body === body &&\n record.category === input.category &&\n JSON.stringify(record.files) === JSON.stringify(files.length > 0 ? files : record.files);\n const updated: DecisionRecord = {\n ...record,\n title,\n body,\n category: input.category,\n files: input.files !== undefined ? files : record.files,\n updatedAt: now,\n refreshedHash: head,\n };\n await saveDecisionRecord(rootDir, updated);\n return {\n status: unchanged ? \"reverified\" : \"updated\",\n id: record.id,\n totalActive: existing.filter((r) => r.status === \"active\").length,\n warnings,\n };\n }\n\n // Create path — dedupe first\n if (!input.force) {\n const duplicate = findNearDuplicate({ title, body, files }, existing);\n if (duplicate) {\n return {\n status: \"duplicate_suspected\",\n existing: duplicate.record,\n hint: `A similar decision exists (\"${duplicate.record.title}\"). Call save_decision with id=\"${duplicate.record.id}\" to update/merge into it, or force:true if genuinely distinct.`,\n };\n }\n }\n\n // Supersede\n if (input.supersedes) {\n const old = byId.get(input.supersedes);\n if (!old) {\n return {\n status: \"error\",\n error: `no decision with id \"${input.supersedes}\" to supersede`,\n };\n }\n const id = decisionIdFor(title, body, new Set(byId.keys()));\n await saveDecisionRecord(rootDir, {\n ...old,\n status: \"superseded\",\n supersededBy: id,\n updatedAt: now,\n });\n const record: DecisionRecord = {\n version: 1,\n id,\n title,\n body,\n category: input.category,\n files,\n createdAt: now,\n updatedAt: now,\n refreshedHash: head,\n status: \"active\",\n };\n await saveDecisionRecord(rootDir, record);\n return {\n status: \"superseded_and_created\",\n id,\n totalActive: existing.filter((r) => r.status === \"active\").length,\n warnings,\n };\n }\n\n const id = decisionIdFor(title, body, new Set(byId.keys()));\n const record: DecisionRecord = {\n version: 1,\n id,\n title,\n body,\n category: input.category,\n files,\n createdAt: now,\n updatedAt: now,\n refreshedHash: head,\n status: \"active\",\n };\n await saveDecisionRecord(rootDir, record);\n\n const totalActive =\n existing.filter((r) => r.status === \"active\").length + 1;\n const result: UpsertDecisionResult = {\n status: \"created\",\n id,\n totalActive,\n warnings,\n };\n if (totalActive > MAX_ACTIVE_DECISIONS) {\n // Never auto-evict git-committed team knowledge — surface candidates\n // for a human cleanup PR instead.\n result.pruneCandidates = existing\n .filter((r) => r.status === \"superseded\")\n .map((r) => r.id)\n .slice(0, 10);\n warnings.push(\n `${totalActive} active decisions exceeds the soft cap of ${MAX_ACTIVE_DECISIONS} — consider a cleanup PR (superseded records first)`\n );\n }\n return result;\n}\n","import path from \"node:path\";\n\n// Question/filler words that carry no signal about which entry a task\n// touches. Domain words (\"auth\", \"drift\") are never in this list.\nconst STOPWORDS = new Set([\n \"the\", \"a\", \"an\", \"and\", \"or\", \"of\", \"to\", \"in\", \"on\", \"for\", \"with\",\n \"how\", \"does\", \"do\", \"is\", \"are\", \"was\", \"what\", \"where\", \"which\", \"why\",\n \"when\", \"who\", \"i\", \"we\", \"my\", \"our\", \"you\", \"your\", \"it\", \"its\", \"this\",\n \"that\", \"these\", \"those\", \"can\", \"could\", \"should\", \"would\", \"will\",\n \"want\", \"need\", \"please\", \"about\", \"into\", \"from\", \"when\", \"there\", \"any\",\n \"all\", \"some\", \"not\", \"but\", \"also\", \"just\", \"like\", \"get\", \"make\", \"use\",\n \"new\", \"work\", \"works\", \"working\", \"implement\", \"implemented\", \"change\",\n \"changed\", \"file\", \"files\", \"code\",\n]);\n\n/** Split camelCase/PascalCase/kebab/snake/path into lowercase word tokens. */\nexport function tokenize(text: string): string[] {\n return text\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 2 && !STOPWORDS.has(t));\n}\n\n/** Crude singular/plural folding so \"flows\" matches \"flow\" etc. */\nexport function stem(token: string): string {\n return token.length > 3 && token.endsWith(\"s\") ? token.slice(0, -1) : token;\n}\n\nexport function tokenSet(text: string): Set<string> {\n return new Set(tokenize(text).map(stem));\n}\n\nexport interface Scorable {\n name: string;\n description: string;\n files: string[];\n}\n\n/**\n * Lexical relevance of one entry to the task. Name hits are the strongest\n * signal, then description, then file-path words. Each distinct task token\n * counts once at its best weight, so a token appearing everywhere doesn't\n * triple-count.\n */\nexport function scoreEntry(taskTokens: Set<string>, entry: Scorable): number {\n const nameTokens = tokenSet(entry.name);\n const descTokens = tokenSet(entry.description);\n const fileTokens = tokenSet(entry.files.map((f) => path.basename(f)).join(\" \"));\n\n let score = 0;\n for (const token of taskTokens) {\n if (nameTokens.has(token)) score += 3;\n else if (descTokens.has(token)) score += 1;\n else if (fileTokens.has(token)) score += 1;\n }\n return score;\n}\n\n/** Jaccard similarity of two token sets: |∩| / |∪|, 0 when both empty. */\nexport function jaccard(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const token of a) if (b.has(token)) intersection++;\n return intersection / (a.size + b.size - intersection);\n}\n","import { computeDecisionDrift } from \"../../decisions/drift.js\";\nimport { loadDecisions } from \"../../decisions/decisions.js\";\nimport type { CheckContext, CheckResult } from \"./index.js\";\nimport { emptyResult } from \"./index.js\";\n\n/**\n * Advisory only, and only when .mason/decisions/ exists (the zero-setup\n * path stays dark on bare repos). Decision records encode human knowledge —\n * they are surfaced for re-verification, never rewritten by the fix agent.\n */\nexport async function checkDecisionAnchors(\n ctx: CheckContext\n): Promise<CheckResult> {\n const result = emptyResult();\n if (!ctx.decisionsPresent) return result;\n\n const records = await loadDecisions(ctx.root);\n const drift = await computeDecisionDrift(ctx.root, records);\n if (!drift.historyAvailable) {\n result.skipped.push({\n check: \"decision-anchor-drift\",\n reason: \"some decision base commits are unreachable (shallow clone?)\",\n });\n }\n\n const byId = new Map(records.map((r) => [r.id, r]));\n for (const [id, changedFiles] of Object.entries(drift.staleDecisions)) {\n const record = byId.get(id);\n if (!record) continue;\n result.advisories.push({\n type: \"decision-anchor-drift\",\n message: `decision \"${record.title}\" has anchor files that changed since it was verified – needs human re-verification`,\n anchor: {\n doc: `.mason/decisions/${id}.json`,\n line: null,\n excerpt: record.title,\n },\n evidence: {\n kind: \"decision-anchor\",\n decisionId: id,\n title: record.title,\n changedFiles,\n refreshedHash: record.refreshedHash,\n },\n });\n }\n\n return result;\n}\n","import type { FileChange } from \"../../drift/drift.js\";\nimport type { AuditAdvisory, AuditIssue, CheckName } from \"../types.js\";\nimport type { AuditDoc } from \"../docs.js\";\nimport { checkDeletedReferences } from \"./deleted-reference.js\";\nimport { checkNewModules } from \"./new-module.js\";\nimport { checkStaleCounts } from \"./stale-count.js\";\nimport { checkDeadCommands } from \"./dead-command.js\";\nimport { checkDepsChanged } from \"./deps-changed.js\";\nimport { checkDecisionAnchors } from \"./decision-anchor.js\";\n\nexport interface CheckContext {\n root: string;\n docs: AuditDoc[];\n headHash: string;\n /** Doc path → changes since the doc's last commit; null when uncomputable. */\n changesSinceDoc: Map<string, FileChange[] | null>;\n /** Whether .mason/decisions/ exists. */\n decisionsPresent: boolean;\n}\n\nexport interface CheckResult {\n issues: AuditIssue[];\n advisories: AuditAdvisory[];\n skipped: Array<{ check: string; reason: string }>;\n}\n\nexport type CheckFn = (ctx: CheckContext) => Promise<CheckResult>;\n\nexport const CHECKS: Record<CheckName, CheckFn> = {\n \"deleted-reference\": checkDeletedReferences,\n \"new-module\": checkNewModules,\n \"stale-count\": checkStaleCounts,\n \"dead-command\": checkDeadCommands,\n \"deps-changed\": checkDepsChanged,\n \"decision-anchor-drift\": checkDecisionAnchors,\n};\n\nexport function emptyResult(): CheckResult {\n return { issues: [], advisories: [], skipped: [] };\n}\n","import { runAuditCli } from \"../src/audit/cli.js\";\n\nrunAuditCli(process.argv.slice(2)).then(\n (code) => process.exit(code),\n (err) => {\n process.stderr.write(`mason-audit error: ${err}\\n`);\n process.exit(2);\n }\n);\n"],"mappings":"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","names":["path","fs","path","fs","path","execFile","promisify","fs","path","execFile","promisify","fg","path","fg","exec","promisify","execFile","exec","exec","promisify","execFile","exec","fs","path","execFile","promisify","execFile","promisify","exec","exec","promisify","execFile","fs","path","fs","path","fs","path","fg","path","fg","path","fs","path","fg","fs","path","fg","fs","path","fg","fs","path","fg","path","fs","path","path","path","fs","path","path","fs","path"]}
|
package/dist/mason-mcp.js
CHANGED
|
@@ -3947,7 +3947,7 @@ function createMcpServer() {
|
|
|
3947
3947
|
const server = new McpServer(
|
|
3948
3948
|
{
|
|
3949
3949
|
name: "mason",
|
|
3950
|
-
version: "0.8.
|
|
3950
|
+
version: "0.8.1"
|
|
3951
3951
|
},
|
|
3952
3952
|
{
|
|
3953
3953
|
instructions: "Mason maintains a persistent feature-to-file concept map of this codebase so you can skip manual exploration. RULE: when given a task, bug, or change request, call `get_context` with the task text first \u2014 one call returns the relevant features, files, tests, blast radius, and freshness. Before answering ANY question about features, architecture, data flows, or where something lives \u2014 and before any grep/glob/file-read exploration for such a question \u2014 call `get_snapshot` first. One call returns the whole map and replaces 5-10 search round-trips; if it has drifted it says so and self-corrects. Likewise call `get_impact` BEFORE editing or refactoring a file (git co-change history + references + related tests \u2014 signals you cannot get from reading the file itself), and `mason_check_drift` to verify the map is fresh in long sessions. When you learn something the code alone can't tell you \u2014 a failed approach, a deprecation, a workaround's reason, a review-settled convention \u2014 record it with `save_decision` so the whole team's assistants inherit it; `get_context` returns matching decisions as constraints. If `get_snapshot` reports no snapshot exists, offer to set Mason up: `mason_init` returns a setup playbook (a Map-Reduce loop of `generate_snapshot_batch` + `save_partial_snapshot`, then `reduce_snapshot` + `save_snapshot`, optionally `mason_set_confluence`, then `mason_complete_init`). `full_analysis`, `analyze_project`, and `get_code_samples` are read-only diagnostics for unmapped projects and never need init. Mason has no CLI; everything happens through these tools."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mason-context",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "MCP server for codebase context engineering — feature-to-file concept maps, change impact, and Confluence wiki sync for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"mcpName": "com.adrianczuczka/mason",
|