agent-method 1.5.12

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.
Files changed (108) hide show
  1. package/README.md +343 -0
  2. package/bin/wwa.js +115 -0
  3. package/docs/internal/cli-commands.yaml +259 -0
  4. package/docs/internal/doc-tokens.yaml +1103 -0
  5. package/docs/internal/feature-registry.yaml +1643 -0
  6. package/lib/boundaries.js +247 -0
  7. package/lib/cli/add.js +170 -0
  8. package/lib/cli/casestudy.js +1000 -0
  9. package/lib/cli/check.js +323 -0
  10. package/lib/cli/close.js +838 -0
  11. package/lib/cli/completion.js +735 -0
  12. package/lib/cli/deps.js +234 -0
  13. package/lib/cli/digest.js +73 -0
  14. package/lib/cli/doc-review.js +486 -0
  15. package/lib/cli/docs.js +315 -0
  16. package/lib/cli/helpers.js +198 -0
  17. package/lib/cli/implement.js +169 -0
  18. package/lib/cli/init.js +280 -0
  19. package/lib/cli/pipeline.js +206 -0
  20. package/lib/cli/plan.js +140 -0
  21. package/lib/cli/record.js +98 -0
  22. package/lib/cli/refine.js +202 -0
  23. package/lib/cli/report-helpers.js +113 -0
  24. package/lib/cli/review.js +76 -0
  25. package/lib/cli/routable.js +109 -0
  26. package/lib/cli/route.js +101 -0
  27. package/lib/cli/scan.js +133 -0
  28. package/lib/cli/serve.js +23 -0
  29. package/lib/cli/status.js +65 -0
  30. package/lib/cli/update-docs.js +574 -0
  31. package/lib/cli/upgrade.js +222 -0
  32. package/lib/cli/watch.js +32 -0
  33. package/lib/dependencies.js +196 -0
  34. package/lib/init.js +692 -0
  35. package/lib/mcp-server.js +612 -0
  36. package/lib/pipeline.js +907 -0
  37. package/lib/registry.js +132 -0
  38. package/lib/watcher.js +165 -0
  39. package/package.json +54 -0
  40. package/templates/README.md +363 -0
  41. package/templates/entry-points/.cursorrules +90 -0
  42. package/templates/entry-points/AGENT.md +90 -0
  43. package/templates/entry-points/CLAUDE.md +88 -0
  44. package/templates/extensions/MANIFEST.md +110 -0
  45. package/templates/extensions/analytical-system.md +96 -0
  46. package/templates/extensions/code-project.md +77 -0
  47. package/templates/extensions/data-exploration.md +117 -0
  48. package/templates/full/.context/BASE.md +101 -0
  49. package/templates/full/.context/COMPOSITION.md +47 -0
  50. package/templates/full/.context/INDEX.yaml +56 -0
  51. package/templates/full/.context/METHODOLOGY.md +246 -0
  52. package/templates/full/.context/PROTOCOL.yaml +169 -0
  53. package/templates/full/.context/REGISTRY.md +75 -0
  54. package/templates/full/.cursorrules +90 -0
  55. package/templates/full/AGENT.md +90 -0
  56. package/templates/full/CLAUDE.md +90 -0
  57. package/templates/full/Management/DIGEST.md +23 -0
  58. package/templates/full/Management/STATUS.md +46 -0
  59. package/templates/full/PLAN.md +67 -0
  60. package/templates/full/PROJECT-PROFILE.md +61 -0
  61. package/templates/full/PROJECT.md +80 -0
  62. package/templates/full/REQUIREMENTS.md +30 -0
  63. package/templates/full/ROADMAP.md +39 -0
  64. package/templates/full/Reviews/INDEX.md +41 -0
  65. package/templates/full/Reviews/backlog.md +52 -0
  66. package/templates/full/Reviews/plan.md +43 -0
  67. package/templates/full/Reviews/project.md +41 -0
  68. package/templates/full/Reviews/requirements.md +42 -0
  69. package/templates/full/Reviews/roadmap.md +41 -0
  70. package/templates/full/Reviews/state.md +56 -0
  71. package/templates/full/SESSION-LOG.md +102 -0
  72. package/templates/full/STATE.md +42 -0
  73. package/templates/full/SUMMARY.md +27 -0
  74. package/templates/full/agentWorkflows/INDEX.md +42 -0
  75. package/templates/full/agentWorkflows/observations.md +65 -0
  76. package/templates/full/agentWorkflows/patterns.md +68 -0
  77. package/templates/full/agentWorkflows/sessions.md +92 -0
  78. package/templates/full/intro/README.md +39 -0
  79. package/templates/full/registry/feature-registry.yaml +25 -0
  80. package/templates/full/registry/features/catalog.yaml +743 -0
  81. package/templates/full/registry/features/protocol.yaml +121 -0
  82. package/templates/full/registry/features/routing.yaml +358 -0
  83. package/templates/full/registry/features/workflows.yaml +404 -0
  84. package/templates/full/todos/backlog.md +19 -0
  85. package/templates/starter/.context/BASE.md +66 -0
  86. package/templates/starter/.context/INDEX.yaml +51 -0
  87. package/templates/starter/.context/METHODOLOGY.md +228 -0
  88. package/templates/starter/.context/PROTOCOL.yaml +165 -0
  89. package/templates/starter/.cursorrules +90 -0
  90. package/templates/starter/AGENT.md +90 -0
  91. package/templates/starter/CLAUDE.md +90 -0
  92. package/templates/starter/Management/DIGEST.md +23 -0
  93. package/templates/starter/Management/STATUS.md +46 -0
  94. package/templates/starter/PLAN.md +67 -0
  95. package/templates/starter/PROJECT-PROFILE.md +44 -0
  96. package/templates/starter/PROJECT.md +80 -0
  97. package/templates/starter/ROADMAP.md +39 -0
  98. package/templates/starter/Reviews/INDEX.md +75 -0
  99. package/templates/starter/SESSION-LOG.md +102 -0
  100. package/templates/starter/STATE.md +42 -0
  101. package/templates/starter/SUMMARY.md +27 -0
  102. package/templates/starter/agentWorkflows/INDEX.md +61 -0
  103. package/templates/starter/intro/README.md +37 -0
  104. package/templates/starter/registry/feature-registry.yaml +25 -0
  105. package/templates/starter/registry/features/catalog.yaml +743 -0
  106. package/templates/starter/registry/features/protocol.yaml +121 -0
  107. package/templates/starter/registry/features/routing.yaml +358 -0
  108. package/templates/starter/registry/features/workflows.yaml +404 -0
@@ -0,0 +1,222 @@
1
+ /** wwa upgrade — brownfield-safe methodology update. */
2
+
3
+ import {
4
+ readFileSync, existsSync, mkdirSync, copyFileSync, unlinkSync,
5
+ } from "node:fs";
6
+ import { resolve, join, dirname } from "node:path";
7
+ import {
8
+ findEntryPoint, readMethodVersion, basename_of, pkg, packageRoot,
9
+ safeWriteFile, safeCopyFile,
10
+ } from "./helpers.js";
11
+
12
+ export function register(program) {
13
+ program
14
+ .command("upgrade [directory]")
15
+ .description("Update your project to the latest methodology version")
16
+ .option("--dry-run", "Show what would change without modifying files")
17
+ .action(async (directory, opts) => {
18
+ directory = directory || ".";
19
+ const d = resolve(directory);
20
+ const ep = findEntryPoint(directory);
21
+ const actions = [];
22
+
23
+ const templateDirs = {
24
+ starter: join(packageRoot, "templates", "starter"),
25
+ full: join(packageRoot, "templates", "full"),
26
+ };
27
+
28
+ // Determine which template tier this project uses
29
+ let tier = "starter";
30
+ if (existsSync(join(d, ".context", "REGISTRY.md"))) {
31
+ tier = "full";
32
+ } else if (
33
+ existsSync(join(d, "REQUIREMENTS.md")) &&
34
+ existsSync(join(d, "SUMMARY.md"))
35
+ ) {
36
+ tier = "full";
37
+ }
38
+ const srcDir = templateDirs[tier] || templateDirs.starter;
39
+
40
+ if (!existsSync(srcDir)) {
41
+ console.error(
42
+ `Template directory not found: ${srcDir}. ` +
43
+ "Run from the wwa repo or install from source."
44
+ );
45
+ process.exit(1);
46
+ }
47
+
48
+ // 1. Update method_version in entry point
49
+ if (ep) {
50
+ let content = readFileSync(ep, "utf-8");
51
+ const currentVer = readMethodVersion(ep);
52
+ const newVer = pkg.version.split(".").slice(0, 2).join(".");
53
+ if (currentVer && currentVer !== newVer) {
54
+ actions.push(
55
+ `Update ${basename_of(ep)}: method_version ${currentVer} -> ${newVer}`
56
+ );
57
+ if (!opts.dryRun) {
58
+ content = content.replace(/(method_version:\s*)\S+/, `$1${newVer}`);
59
+ safeWriteFile(ep, content, "utf-8");
60
+ }
61
+ } else if (!currentVer) {
62
+ actions.push(`Add method_version: ${newVer} to ${basename_of(ep)}`);
63
+ if (!opts.dryRun) {
64
+ if (content.includes("## Conventions")) {
65
+ const insert =
66
+ `## Method version\n\n` +
67
+ `method_version: ${newVer}\n` +
68
+ `<!-- Tracks which methodology version generated this entry point -->\n` +
69
+ `<!-- Use \`wwa status\` to compare against latest -->\n\n`;
70
+ content = content.replace("## Conventions", insert + "## Conventions");
71
+ safeWriteFile(ep, content, "utf-8");
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ // 2. Add missing methodology files (brownfield-safe: skip existing)
78
+ const methodologyFiles = ["SESSION-LOG.md", "PROJECT-PROFILE.md"];
79
+ if (tier === "full") {
80
+ methodologyFiles.push(join(".context", "METHODOLOGY.md"));
81
+ }
82
+
83
+ for (const fname of methodologyFiles) {
84
+ const src = join(srcDir, fname);
85
+ const dst = join(d, fname);
86
+ if (existsSync(src) && !existsSync(dst)) {
87
+ actions.push(`Create ${fname} (new)`);
88
+ if (!opts.dryRun) {
89
+ mkdirSync(dirname(dst), { recursive: true });
90
+ safeCopyFile(src, dst);
91
+ }
92
+ }
93
+ }
94
+
95
+ // 2b. Ensure CLI command metadata file exists in project (safe to add).
96
+ // Only docs/internal/cli-commands.yaml is copied from the package; all other
97
+ // docs/internal files remain dev-only.
98
+ try {
99
+ const cliSrc = join(packageRoot, "docs", "internal", "cli-commands.yaml");
100
+ const cliDst = join(d, "docs", "internal", "cli-commands.yaml");
101
+ if (existsSync(cliSrc) && !existsSync(cliDst)) {
102
+ actions.push("Create docs/internal/cli-commands.yaml (CLI metadata)");
103
+ if (!opts.dryRun) {
104
+ mkdirSync(dirname(cliDst), { recursive: true });
105
+ safeCopyFile(cliSrc, cliDst);
106
+ }
107
+ }
108
+ } catch {
109
+ // Best-effort; upgrade should not fail if CLI metadata copy fails.
110
+ }
111
+
112
+ // 3. Check for session close cascade (add if missing)
113
+ if (ep) {
114
+ let content = readFileSync(ep, "utf-8");
115
+ if (!content.toLowerCase().includes("session close")) {
116
+ actions.push(`Add session close cascade rule to ${basename_of(ep)}`);
117
+ if (!opts.dryRun) {
118
+ if (content.includes("<!-- INSTRUCTION: Add project-specific cascade")) {
119
+ content = content.replace(
120
+ "<!-- INSTRUCTION: Add project-specific cascade",
121
+ "| Session close or high-effort task completion | SESSION-LOG.md (append metrics entry" +
122
+ " \u2014 effort, ambiguity, context level, tokens, time, user response, refinement delta, workflow, features, cascades, friction, findings) " +
123
+ "|\n\n<!-- INSTRUCTION: Add project-specific cascade"
124
+ );
125
+ safeWriteFile(ep, content, "utf-8");
126
+ }
127
+ }
128
+ }
129
+
130
+ // 4. Check for session observation convention
131
+ content = readFileSync(ep, "utf-8");
132
+ if (!content.toLowerCase().includes("session-log")) {
133
+ actions.push(
134
+ `Add session observation convention to ${basename_of(ep)}`
135
+ );
136
+ if (!opts.dryRun) {
137
+ if (content.includes("## Do not")) {
138
+ content = content.replace(
139
+ "## Do not",
140
+ "- At session close or after any high-effort task, append a metrics entry to " +
141
+ "SESSION-LOG.md \u2014 include effort level, question ambiguity, context level, " +
142
+ "estimated tokens, time, user response (accepted/edited/revised/rejected/redirected), " +
143
+ "and for medium/high effort tasks: revision count, refinement magnitude " +
144
+ "(none/minor/moderate/major/rework), delta categories, and survival rate. Never skip, never read " +
145
+ "previous entries during normal work\n\n" +
146
+ "## Do not"
147
+ );
148
+ safeWriteFile(ep, content, "utf-8");
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // 5. Migrate registry files from .context/ to registry/
155
+ const legacyTokens = join(d, ".context", "doc-tokens.yaml");
156
+ const legacyFeatures = join(d, ".context", "feature-registry.yaml");
157
+ const registryDir = join(d, "registry");
158
+ const hasLegacy = existsSync(legacyTokens) || existsSync(legacyFeatures);
159
+ const hasRegistry = existsSync(registryDir);
160
+
161
+ if (hasLegacy && !hasRegistry) {
162
+ actions.push("Create registry/ directory");
163
+ if (!opts.dryRun) mkdirSync(registryDir, { recursive: true });
164
+
165
+ if (existsSync(legacyTokens)) {
166
+ const newTokens = join(registryDir, "doc-tokens.yaml");
167
+ actions.push("Move .context/doc-tokens.yaml → registry/doc-tokens.yaml");
168
+ if (!opts.dryRun) {
169
+ copyFileSync(legacyTokens, newTokens);
170
+ unlinkSync(legacyTokens);
171
+ }
172
+ }
173
+
174
+ if (existsSync(legacyFeatures)) {
175
+ const newFeatures = join(registryDir, "feature-registry.yaml");
176
+ actions.push("Move .context/feature-registry.yaml → registry/feature-registry.yaml");
177
+ if (!opts.dryRun) {
178
+ copyFileSync(legacyFeatures, newFeatures);
179
+ unlinkSync(legacyFeatures);
180
+ }
181
+ }
182
+
183
+ // Update path references in PROTOCOL.yaml and INDEX.yaml
184
+ const protocolPath = join(d, ".context", "PROTOCOL.yaml");
185
+ const indexPath = join(d, ".context", "INDEX.yaml");
186
+ const replacements = [
187
+ [".context/doc-tokens.yaml", "registry/doc-tokens.yaml"],
188
+ [".context/feature-registry.yaml", "registry/feature-registry.yaml"],
189
+ ];
190
+
191
+ for (const filePath of [protocolPath, indexPath]) {
192
+ if (existsSync(filePath)) {
193
+ let content = readFileSync(filePath, "utf-8");
194
+ let changed = false;
195
+ for (const [oldRef, newRef] of replacements) {
196
+ if (content.includes(oldRef)) {
197
+ content = content.replaceAll(oldRef, newRef);
198
+ changed = true;
199
+ }
200
+ }
201
+ if (changed) {
202
+ actions.push(`Update path references in ${filePath.replace(d, "").replace(/^[\\/]/, "")}`);
203
+ if (!opts.dryRun) safeWriteFile(filePath, content, "utf-8");
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ // Report
210
+ if (actions.length === 0) {
211
+ console.log(`Project at ${directory} is up to date (v${pkg.version}).`);
212
+ } else if (opts.dryRun) {
213
+ console.log(`Dry run \u2014 ${actions.length} changes needed:`);
214
+ for (const a of actions) console.log(` - ${a}`);
215
+ } else {
216
+ console.log(
217
+ `Upgraded ${directory} to methodology v${pkg.version} (${actions.length} changes):`
218
+ );
219
+ for (const a of actions) console.log(` - ${a}`);
220
+ }
221
+ });
222
+ }
@@ -0,0 +1,32 @@
1
+ /** wwa watch — file watcher for proactive validation. */
2
+
3
+ export function register(program) {
4
+ program
5
+ .command("watch [directory]")
6
+ .description("Watch entry points and markdown files for changes, validate on save")
7
+ .option("--registry <path>", "Path to feature-registry.yaml")
8
+ .action(async (directory, opts) => {
9
+ const dir = directory || ".";
10
+
11
+ console.log(`Watching: ${dir}`);
12
+ console.log("Monitoring entry points, registry, and markdown files...");
13
+ console.log("Press Ctrl+C to stop.\n");
14
+
15
+ try {
16
+ const { createWatcher } = await import("../watcher.js");
17
+ const watcher = createWatcher(dir, {
18
+ registryPath: opts.registry || undefined,
19
+ });
20
+
21
+ // Keep process alive
22
+ process.on("SIGINT", () => {
23
+ watcher.close();
24
+ console.log("\nWatcher stopped.");
25
+ process.exit(0);
26
+ });
27
+ } catch (err) {
28
+ console.error(`Error: ${err.message}`);
29
+ process.exit(1);
30
+ }
31
+ });
32
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Document dependency graph operations — build, query, and sort.
3
+ * Used by: wwa init (seeding), wwa close (growth tracking),
4
+ * wwa route (dependency ordering), wwa deps (visualization).
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Project-type default edges (derived from pipeline.js cascade tables)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const UNIVERSAL_EDGES = [
12
+ { from: "STATE.md", to: "ROADMAP.md", type: "cascades_to", trigger: "phase_complete" },
13
+ { from: "STATE.md", to: "PLAN.md", type: "cascades_to", trigger: "requirements_change" },
14
+ { from: "SUMMARY.md", to: "STATE.md", type: "cascades_to", trigger: "phase_complete" },
15
+ { from: ".context/BASE.md", to: ".context/DOCS-MAP.md", type: "cascades_to", trigger: "structure_changed" },
16
+ { from: ".context/PROTOCOL.yaml", to: "STATE.md", type: "validates", trigger: "close_check" },
17
+ { from: "ROADMAP.md", to: "PLAN.md", type: "reads_from" },
18
+ { from: "PLAN.md", to: "STATE.md", type: "reads_from" },
19
+ ];
20
+
21
+ const CODE_EDGES = [
22
+ { from: ".context/DATABASE.md", to: ".context/BASE.md", type: "cascades_to", trigger: "database_schema" },
23
+ { from: ".context/API.md", to: ".context/BASE.md", type: "cascades_to", trigger: "api_route" },
24
+ { from: ".context/BASE.md", to: ".context/API.md", type: "reads_from" },
25
+ { from: ".context/BASE.md", to: ".context/DATABASE.md", type: "reads_from" },
26
+ ];
27
+
28
+ const DATA_EDGES = [
29
+ { from: ".context/SCHEMA.md", to: ".context/BASE.md", type: "cascades_to", trigger: "schema_change" },
30
+ { from: ".context/SCHEMA.md", to: ".context/RELATIONSHIPS.md", type: "cascades_to", trigger: "schema_change" },
31
+ { from: ".context/DOCUMENTS.md", to: ".context/BASE.md", type: "cascades_to", trigger: "new_document" },
32
+ { from: ".context/BASE.md", to: ".context/SCHEMA.md", type: "reads_from" },
33
+ ];
34
+
35
+ const ANALYTICAL_EDGES = [
36
+ { from: ".context/COMPOSITION.md", to: ".context/EVALUATION.md", type: "cascades_to", trigger: "prompt_template" },
37
+ { from: ".context/DOMAIN.md", to: ".context/COMPOSITION.md", type: "cascades_to", trigger: "domain_knowledge" },
38
+ { from: ".context/EXECUTION.md", to: ".context/BASE.md", type: "cascades_to", trigger: "pipeline_stage" },
39
+ { from: ".context/BASE.md", to: ".context/EXECUTION.md", type: "reads_from" },
40
+ ];
41
+
42
+ export const DOC_GRAPH_DEFAULTS = {
43
+ universal: UNIVERSAL_EDGES,
44
+ code: CODE_EDGES,
45
+ data: DATA_EDGES,
46
+ analytical: ANALYTICAL_EDGES,
47
+ };
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Node/edge builders
51
+ // ---------------------------------------------------------------------------
52
+
53
+ const STARTER_FILES = [
54
+ { path: "STATE.md", category: "intelligence", role: "decisions and position" },
55
+ { path: "PLAN.md", category: "intelligence", role: "current task" },
56
+ { path: "ROADMAP.md", category: "intelligence", role: "phase breakdown" },
57
+ { path: "SUMMARY.md", category: "intelligence", role: "audit trail" },
58
+ { path: "PROJECT.md", category: "intelligence", role: "project vision" },
59
+ { path: "PROJECT-PROFILE.md", category: "intelligence", role: "type/stage/extensions" },
60
+ { path: "SESSION-LOG.md", category: "intelligence", role: "session metrics" },
61
+ { path: ".context/BASE.md", category: "context", role: "system model" },
62
+ { path: ".context/PROTOCOL.yaml", category: "context", role: "agent rules" },
63
+ { path: ".context/INDEX.yaml", category: "context", role: "file inventory" },
64
+ { path: ".context/METHODOLOGY.md", category: "context", role: "onboarding protocol" },
65
+ { path: ".context/DOCS-MAP.md", category: "context", role: "docs navigation" },
66
+ { path: "registry/doc-tokens.yaml", category: "registry", role: "token registry" },
67
+ { path: "registry/feature-registry.yaml", category: "registry", role: "feature catalog" },
68
+ { path: "Management/DIGEST.md", category: "management", role: "high-effort task digest" },
69
+ { path: "Management/STATUS.md", category: "management", role: "project health snapshot" },
70
+ ];
71
+
72
+ const FULL_EXTRA_FILES = [
73
+ { path: "REQUIREMENTS.md", category: "intelligence", role: "formal requirements" },
74
+ { path: ".context/REGISTRY.md", category: "context", role: "navigation for split files" },
75
+ { path: ".context/COMPOSITION.md", category: "context", role: "extension composition" },
76
+ { path: "todos/backlog.md", category: "intelligence", role: "future ideas" },
77
+ ];
78
+
79
+ /** Return default node list for a template tier. */
80
+ export function buildDefaultNodes(tier) {
81
+ const nodes = [...STARTER_FILES];
82
+ if (tier === "full") nodes.push(...FULL_EXTRA_FILES);
83
+ return nodes;
84
+ }
85
+
86
+ /** Return default edge list for a project type (universal + type-specific). */
87
+ export function buildDefaultEdges(projectType) {
88
+ const edges = [...UNIVERSAL_EDGES];
89
+ if (projectType === "code" || projectType === "mixed") edges.push(...CODE_EDGES);
90
+ if (projectType === "data" || projectType === "mixed") edges.push(...DATA_EDGES);
91
+ if (projectType === "analytical" || projectType === "mixed") edges.push(...ANALYTICAL_EDGES);
92
+ return edges;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Graph operations
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Topological sort of a file set by reads_from edges.
101
+ * Files that are read by others come first. Falls back to original order for
102
+ * files not in the graph or when no graph is provided.
103
+ */
104
+ export function orderByDependencies(fileSet, docGraph) {
105
+ if (!docGraph || !docGraph.edges || docGraph.edges.length === 0) {
106
+ return [...fileSet].sort();
107
+ }
108
+
109
+ // Build adjacency: for reads_from edges, "from" depends on "to"
110
+ // So "to" should come before "from" in reading order
111
+ const dependsOn = new Map(); // file -> set of files it depends on
112
+ for (const edge of docGraph.edges) {
113
+ if (edge.type === "reads_from") {
114
+ if (!dependsOn.has(edge.from)) dependsOn.set(edge.from, new Set());
115
+ dependsOn.get(edge.from).add(edge.to);
116
+ }
117
+ }
118
+
119
+ const fileSetLower = new Set(fileSet.map(f => f.toLowerCase()));
120
+ const inSet = (f) => fileSetLower.has(f.toLowerCase());
121
+
122
+ // Kahn's algorithm for topological sort
123
+ const inDegree = new Map();
124
+ const adj = new Map();
125
+ const relevant = fileSet.filter(f => dependsOn.has(f) || [...dependsOn.values()].some(s => s.has(f)));
126
+
127
+ for (const f of fileSet) {
128
+ inDegree.set(f, 0);
129
+ adj.set(f, []);
130
+ }
131
+
132
+ for (const [file, deps] of dependsOn) {
133
+ if (!inSet(file)) continue;
134
+ for (const dep of deps) {
135
+ if (!inSet(dep)) continue;
136
+ adj.get(dep)?.push(file);
137
+ inDegree.set(file, (inDegree.get(file) || 0) + 1);
138
+ }
139
+ }
140
+
141
+ const queue = [];
142
+ for (const [f, deg] of inDegree) {
143
+ if (deg === 0) queue.push(f);
144
+ }
145
+ queue.sort(); // stable alphabetical within same level
146
+
147
+ const result = [];
148
+ while (queue.length > 0) {
149
+ const f = queue.shift();
150
+ result.push(f);
151
+ for (const next of (adj.get(f) || [])) {
152
+ const newDeg = (inDegree.get(next) || 1) - 1;
153
+ inDegree.set(next, newDeg);
154
+ if (newDeg === 0) queue.push(next);
155
+ }
156
+ }
157
+
158
+ // Add any files not reached by the sort (disconnected from graph)
159
+ for (const f of fileSet) {
160
+ if (!result.includes(f)) result.push(f);
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ /** Find orphan nodes — nodes with no incoming or outgoing edges. */
167
+ export function findOrphans(docGraph) {
168
+ if (!docGraph || !docGraph.nodes || !docGraph.edges) return [];
169
+
170
+ const connected = new Set();
171
+ for (const edge of docGraph.edges) {
172
+ connected.add(edge.from);
173
+ connected.add(edge.to);
174
+ }
175
+
176
+ return docGraph.nodes
177
+ .map(n => n.path)
178
+ .filter(p => !connected.has(p));
179
+ }
180
+
181
+ /** Detect files on disk that are not yet in the dependency graph nodes. */
182
+ export function detectNewFiles(existingNodes, currentFiles) {
183
+ const nodePaths = new Set((existingNodes || []).map(n => n.path));
184
+ return currentFiles.filter(f => !nodePaths.has(f));
185
+ }
186
+
187
+ /** Infer node category from file path. */
188
+ export function inferCategory(filePath) {
189
+ const p = filePath.replace(/\\/g, "/").toLowerCase();
190
+ if (p.startsWith("docs/")) return "docs";
191
+ if (p.startsWith(".context/")) return "context";
192
+ if (p.startsWith("management/")) return "management";
193
+ if (p.startsWith("reviews/") || p.startsWith("agentworkflows/")) return "reporting";
194
+ if (p.startsWith("intro/")) return "docs";
195
+ return "unknown";
196
+ }