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,574 @@
1
+ /** wwa update-docs — term-based documentation index and update tool. */
2
+
3
+ import { readFileSync, existsSync, readdirSync, statSync, mkdirSync } from "node:fs";
4
+ import { resolve, join, relative } from "node:path";
5
+ import { safeWriteFile } from "./helpers.js";
6
+ import {
7
+ resolveTokensPath,
8
+ resolveOutputDir,
9
+ loadBoundaries,
10
+ } from "../boundaries.js";
11
+
12
+ export function register(program) {
13
+ program
14
+ .command("update-docs [directory]")
15
+ .description(
16
+ "Build or query a term-to-file index for documentation updates"
17
+ )
18
+ .option("--scan", "Scan docs/ and build doc-terms-index.yaml")
19
+ .option("--query <term>", "Look up which files reference a term")
20
+ .option("--category <cat>", "Filter to a category (cli_commands, feature_ids, workflow_ids, directive_ids, validation_ids, versions, concepts, counts)")
21
+ .option("--stale", "Show terms where doc-tokens.yaml value differs from docs content")
22
+ .option("--json", "Output as JSON")
23
+ .action(async (directory, opts) => {
24
+ directory = directory || ".";
25
+ const d = resolve(directory);
26
+
27
+ if (opts.scan) {
28
+ await scanAndBuildIndex(d, opts);
29
+ return;
30
+ }
31
+
32
+ // Load existing index
33
+ const outputDir = await resolveOutputDir(d);
34
+ const indexPath = join(outputDir, "doc-terms-index.yaml");
35
+ if (!existsSync(indexPath)) {
36
+ console.log("\n No doc-terms-index.yaml found.");
37
+ console.log(" Run `wwa update-docs --scan` to build the term index.\n");
38
+ return;
39
+ }
40
+
41
+ const yaml = (await import("js-yaml")).default;
42
+ const index = yaml.load(readFileSync(indexPath, "utf-8"));
43
+
44
+ if (opts.query) {
45
+ await queryIndex(index, opts.query, opts);
46
+ return;
47
+ }
48
+
49
+ if (opts.stale) {
50
+ await showStale(d, index, opts);
51
+ return;
52
+ }
53
+
54
+ // Default: show summary
55
+ printIndexSummary(index, opts);
56
+ });
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Scan all docs/ and build the term-to-file reverse index
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const MAX_CONTEXT_LENGTH = 120;
64
+
65
+ function snippet(line) {
66
+ const trimmed = line.trim();
67
+ if (trimmed.length <= MAX_CONTEXT_LENGTH) return trimmed;
68
+ return trimmed.slice(0, MAX_CONTEXT_LENGTH) + "...";
69
+ }
70
+
71
+ const TERM_PATTERNS = {
72
+ cli_commands: {
73
+ description: "CLI command references (wwa <cmd>)",
74
+ pattern: /\bwwa\s+[a-z][\w-]*/g,
75
+ },
76
+ feature_ids: {
77
+ description: "Feature identifiers (CTX-01, QRY-02, HAI-05, SCAN-03, etc.)",
78
+ pattern: /\b(?:CTX|QRY|STT|TSK|HAI|TPL|EXP|SCAN)-\d{2}\b/g,
79
+ },
80
+ workflow_ids: {
81
+ description: "Workflow identifiers (WF-01 through WF-08)",
82
+ pattern: /\bWF-\d{2}\b/g,
83
+ },
84
+ directive_ids: {
85
+ description: "Protocol directive references (P-01 through P-10)",
86
+ pattern: /\bP-\d{2}\b/g,
87
+ },
88
+ validation_ids: {
89
+ description: "Validation check identifiers (V-01 through V-12)",
90
+ pattern: /\bV-\d{2}\b/g,
91
+ },
92
+ versions: {
93
+ description: "Methodology version references",
94
+ pattern: /\bv1\.\d+(?:\.\d+)?\b/g,
95
+ },
96
+ concepts: {
97
+ description: "Core methodology concepts",
98
+ patterns: [
99
+ "context pairing",
100
+ "dependency cascade",
101
+ "scoping rules",
102
+ "intelligence layer",
103
+ "interaction level",
104
+ "entry point",
105
+ "cold start",
106
+ "specialist context",
107
+ "scale management",
108
+ "session protocol",
109
+ "cascade table",
110
+ "integration profile",
111
+ ],
112
+ },
113
+ counts: {
114
+ description: "Numeric values tied to methodology metrics",
115
+ matchers: [
116
+ { pattern: /\b(\d+)\s+features?\b/g, token: "feature_count" },
117
+ { pattern: /\b(\d+)\s+domains?\b/g, token: "domain_count" },
118
+ { pattern: /\b(\d+)\s+directives?\b/g, token: "directive_count" },
119
+ { pattern: /\b(\d+)\s+workflows?\b/g, token: "workflow_count" },
120
+ { pattern: /\b(\d+)\s+query.?patterns?\b/g, token: "query_pattern_count" },
121
+ { pattern: /\b(\d+)\s+(?:total\s+)?commands?\b/g, token: "total_cmd_count" },
122
+ ],
123
+ },
124
+ };
125
+
126
+ async function scanAndBuildIndex(dir, opts) {
127
+ const bounds = await loadBoundaries(dir);
128
+ const scanDirs = bounds.scan?.include || ["docs/", ".context/", "templates/"];
129
+
130
+ // Collect all .md files in scan directories
131
+ const files = [];
132
+ for (const scanDir of scanDirs) {
133
+ const absDir = join(dir, scanDir);
134
+ if (existsSync(absDir)) {
135
+ collectMdFiles(absDir, dir, files);
136
+ }
137
+ }
138
+
139
+ console.log(`\n Scanning ${files.length} documentation files...`);
140
+
141
+ // Load doc-tokens.yaml for cross-referencing token values and names
142
+ let tokens = {};
143
+ let names = {};
144
+ try {
145
+ const tokensPath = await resolveTokensPath(dir);
146
+ if (existsSync(tokensPath)) {
147
+ const yaml = (await import("js-yaml")).default;
148
+ const parsed = yaml.load(readFileSync(tokensPath, "utf-8"));
149
+ tokens = parsed?.tokens || {};
150
+ names = parsed?.names || {};
151
+ }
152
+ } catch { /* skip */ }
153
+
154
+ const index = {
155
+ generated: new Date().toISOString().slice(0, 10),
156
+ source_directory: scanDirs.join(", "),
157
+ files_scanned: files.length,
158
+ categories: {},
159
+ };
160
+
161
+ // Scan each file for each term category
162
+ for (const [catKey, catDef] of Object.entries(TERM_PATTERNS)) {
163
+ if (catKey === "counts") {
164
+ // Counts use a different structure — metrics grouped by token key
165
+ index.categories.counts = buildCountsCategory(catDef, files, dir, tokens);
166
+ continue;
167
+ }
168
+
169
+ const terms = {};
170
+
171
+ for (const filePath of files) {
172
+ const content = readFileSync(join(dir, filePath), "utf-8");
173
+ const lines = content.split("\n");
174
+
175
+ if (catKey === "concepts") {
176
+ // String-based matching for concept terms
177
+ for (const concept of catDef.patterns) {
178
+ const lower = concept.toLowerCase();
179
+ for (let i = 0; i < lines.length; i++) {
180
+ if (lines[i].toLowerCase().includes(lower)) {
181
+ addUsage(terms, concept, filePath, i + 1, snippet(lines[i]));
182
+ }
183
+ }
184
+ }
185
+ } else {
186
+ // Regex-based matching
187
+ for (let i = 0; i < lines.length; i++) {
188
+ const re = new RegExp(catDef.pattern.source, catDef.pattern.flags);
189
+ let m;
190
+ while ((m = re.exec(lines[i])) !== null) {
191
+ addUsage(terms, m[0], filePath, i + 1, snippet(lines[i]));
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // Compute total_references and sort
198
+ const sorted = {};
199
+ for (const key of Object.keys(terms).sort()) {
200
+ const t = terms[key];
201
+ t.total_references = t.referenced_in.reduce((s, f) => s + f.occurrences, 0);
202
+ t.referenced_in.sort((a, b) => b.occurrences - a.occurrences);
203
+ sorted[key] = t;
204
+ }
205
+
206
+ // Enrich with canonical names from doc-tokens.yaml
207
+ if (catKey === "workflow_ids" && names.guided_workflows) {
208
+ enrichFromNames(sorted, names.guided_workflows);
209
+ } else if (catKey === "feature_ids" && names.feature_domains) {
210
+ enrichFeatureIds(sorted, names.feature_domains);
211
+ } else if (catKey === "directive_ids" && names.protocol_directives) {
212
+ enrichFromNames(sorted, names.protocol_directives);
213
+ }
214
+
215
+ if (Object.keys(sorted).length > 0) {
216
+ index.categories[catKey] = {
217
+ description: catDef.description,
218
+ term_count: Object.keys(sorted).length,
219
+ terms: sorted,
220
+ };
221
+ }
222
+ }
223
+
224
+ // Compute totals
225
+ let totalTerms = 0;
226
+ for (const cat of Object.values(index.categories)) {
227
+ totalTerms += cat.term_count || Object.keys(cat.metrics || {}).length;
228
+ }
229
+ index.total_terms = totalTerms;
230
+
231
+ // Write the index
232
+ const outputDir = await resolveOutputDir(dir);
233
+ if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
234
+ const indexPath = join(outputDir, "doc-terms-index.yaml");
235
+
236
+ const yaml = (await import("js-yaml")).default;
237
+ const header = [
238
+ "# Documentation Term Index — reverse lookup for documentation updates",
239
+ "# Generated by: wwa update-docs --scan",
240
+ "# Used by: wwa update-docs --query <term>",
241
+ "#",
242
+ "# Schema: categories → terms → referenced_in (files) → usages (line + context)",
243
+ "# When a value changes, this index tells you exactly which files need updating.",
244
+ "#",
245
+ `# Last generated: ${index.generated}`,
246
+ "",
247
+ ].join("\n");
248
+
249
+ const output = yaml.dump(index, {
250
+ lineWidth: 120,
251
+ noRefs: true,
252
+ quotingType: '"',
253
+ sortKeys: false,
254
+ });
255
+ safeWriteFile(indexPath, header + output, "utf-8");
256
+
257
+ if (opts.json) {
258
+ console.log(JSON.stringify(index, null, 2));
259
+ } else {
260
+ console.log(` Generated: ${relative(dir, indexPath)}`);
261
+ console.log(` Files scanned: ${files.length}`);
262
+ console.log(` Terms indexed: ${totalTerms}`);
263
+ console.log(` Categories: ${Object.keys(index.categories).length}\n`);
264
+ for (const [key, cat] of Object.entries(index.categories)) {
265
+ if (cat.metrics) {
266
+ console.log(` ${key}: ${Object.keys(cat.metrics).length} metrics — ${cat.description}`);
267
+ } else {
268
+ console.log(` ${key}: ${cat.term_count} terms — ${cat.description}`);
269
+ }
270
+ }
271
+ console.log("");
272
+ }
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Shared helper: add a usage to the terms map
277
+ // ---------------------------------------------------------------------------
278
+
279
+ function addUsage(terms, termKey, filePath, lineNum, context) {
280
+ if (!terms[termKey]) {
281
+ terms[termKey] = { referenced_in: [] };
282
+ }
283
+ const existing = terms[termKey].referenced_in.find((f) => f.file === filePath);
284
+ if (existing) {
285
+ existing.usages.push({ line: lineNum, context });
286
+ existing.occurrences++;
287
+ } else {
288
+ terms[termKey].referenced_in.push({
289
+ file: filePath,
290
+ occurrences: 1,
291
+ usages: [{ line: lineNum, context }],
292
+ });
293
+ }
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Build counts category with metrics-grouped structure
298
+ // ---------------------------------------------------------------------------
299
+
300
+ function buildCountsCategory(catDef, files, dir, tokens) {
301
+ // Collect raw matches grouped by token key
302
+ const metricsRaw = {}; // token_key → { value → [{ file, line, context }] }
303
+
304
+ for (const filePath of files) {
305
+ const content = readFileSync(join(dir, filePath), "utf-8");
306
+ const lines = content.split("\n");
307
+
308
+ for (const matcher of catDef.matchers) {
309
+ for (let i = 0; i < lines.length; i++) {
310
+ const re = new RegExp(matcher.pattern.source, matcher.pattern.flags);
311
+ let m;
312
+ while ((m = re.exec(lines[i])) !== null) {
313
+ const value = parseInt(m[1], 10);
314
+ if (!metricsRaw[matcher.token]) metricsRaw[matcher.token] = {};
315
+ if (!metricsRaw[matcher.token][value]) metricsRaw[matcher.token][value] = [];
316
+ metricsRaw[matcher.token][value].push({
317
+ file: filePath,
318
+ line: i + 1,
319
+ context: snippet(lines[i]),
320
+ });
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ // Build structured metrics
327
+ const metrics = {};
328
+ for (const tokenKey of Object.keys(metricsRaw).sort()) {
329
+ const expected = tokens[tokenKey] ?? null;
330
+ const valuesFound = [];
331
+
332
+ for (const [valueStr, usages] of Object.entries(metricsRaw[tokenKey])) {
333
+ const value = parseInt(valueStr, 10);
334
+ // Group usages by file
335
+ const byFile = {};
336
+ for (const u of usages) {
337
+ if (!byFile[u.file]) byFile[u.file] = { file: u.file, occurrences: 0, usages: [] };
338
+ byFile[u.file].usages.push({ line: u.line, context: u.context });
339
+ byFile[u.file].occurrences++;
340
+ }
341
+
342
+ valuesFound.push({
343
+ value,
344
+ status: expected !== null && value === expected ? "current" : expected !== null ? "stale" : "unverified",
345
+ total_references: usages.length,
346
+ referenced_in: Object.values(byFile).sort((a, b) => b.occurrences - a.occurrences),
347
+ });
348
+ }
349
+
350
+ // Sort: current values first, then by value descending
351
+ valuesFound.sort((a, b) => {
352
+ if (a.status === "current" && b.status !== "current") return -1;
353
+ if (a.status !== "current" && b.status === "current") return 1;
354
+ return b.value - a.value;
355
+ });
356
+
357
+ metrics[tokenKey] = { expected, values_found: valuesFound };
358
+ }
359
+
360
+ return {
361
+ description: catDef.description,
362
+ term_count: Object.keys(metrics).length,
363
+ metrics,
364
+ };
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Enrichment: add canonical_name from doc-tokens.yaml names
369
+ // ---------------------------------------------------------------------------
370
+
371
+ function enrichFromNames(terms, nameEntries) {
372
+ for (const termKey of Object.keys(terms)) {
373
+ const entry = nameEntries.find((n) => n.id === termKey);
374
+ if (entry) {
375
+ terms[termKey].canonical_name = entry.display || entry.canonical;
376
+ }
377
+ }
378
+ }
379
+
380
+ function enrichFeatureIds(terms, featureDomains) {
381
+ for (const termKey of Object.keys(terms)) {
382
+ const prefix = termKey.replace(/-\d{2}$/, "");
383
+ const domain = featureDomains.find((d) => d.id === prefix);
384
+ if (domain) {
385
+ terms[termKey].canonical_name = `${domain.canonical} (${domain.feature_range})`;
386
+ }
387
+ }
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Query the index for a specific term
392
+ // ---------------------------------------------------------------------------
393
+
394
+ async function queryIndex(index, query, opts) {
395
+ const results = [];
396
+ const lowerQuery = query.toLowerCase();
397
+
398
+ for (const [catKey, cat] of Object.entries(index.categories || {})) {
399
+ if (opts.category && catKey !== opts.category) continue;
400
+
401
+ if (cat.metrics) {
402
+ // Counts category — search metric keys
403
+ for (const [metricKey, metricData] of Object.entries(cat.metrics)) {
404
+ if (metricKey.toLowerCase().includes(lowerQuery) || lowerQuery.includes(metricKey.toLowerCase())) {
405
+ results.push({ category: catKey, term: metricKey, metric: true, ...metricData });
406
+ }
407
+ }
408
+ continue;
409
+ }
410
+
411
+ for (const [termKey, termData] of Object.entries(cat.terms || {})) {
412
+ if (
413
+ termKey.toLowerCase().includes(lowerQuery) ||
414
+ lowerQuery.includes(termKey.toLowerCase())
415
+ ) {
416
+ results.push({ category: catKey, term: termKey, ...termData });
417
+ }
418
+ }
419
+ }
420
+
421
+ if (opts.json) {
422
+ console.log(JSON.stringify({ query, results }, null, 2));
423
+ return;
424
+ }
425
+
426
+ if (results.length === 0) {
427
+ console.log(`\n No matches for "${query}" in the term index.`);
428
+ console.log(" Try `wwa update-docs --scan` to rebuild, or broaden your query.\n");
429
+ return;
430
+ }
431
+
432
+ console.log(`\n Results for "${query}" (${results.length} match${results.length > 1 ? "es" : ""}):\n`);
433
+ for (const r of results) {
434
+ if (r.metric) {
435
+ // Counts metric display
436
+ console.log(` [${r.category}] ${r.term} (expected: ${r.expected ?? "unknown"})`);
437
+ for (const v of r.values_found) {
438
+ const statusTag = v.status === "current" ? "current" : v.status === "stale" ? "STALE" : "unverified";
439
+ console.log(` value ${v.value} [${statusTag}] — ${v.total_references} reference${v.total_references > 1 ? "s" : ""}`);
440
+ for (const f of v.referenced_in) {
441
+ console.log(` ${f.file} (${f.occurrences}x)`);
442
+ printUsageSnippets(f.usages, " ");
443
+ }
444
+ }
445
+ } else {
446
+ // Standard term display
447
+ const nameStr = r.canonical_name ? ` — ${r.canonical_name}` : "";
448
+ console.log(` [${r.category}] ${r.term}${nameStr} (${r.total_references} reference${r.total_references > 1 ? "s" : ""})`);
449
+ for (const f of r.referenced_in) {
450
+ console.log(` ${f.file} (${f.occurrences}x)`);
451
+ printUsageSnippets(f.usages, " ");
452
+ }
453
+ }
454
+ }
455
+ console.log("");
456
+ }
457
+
458
+ function printUsageSnippets(usages, indent, max = 3) {
459
+ const show = usages.slice(0, max);
460
+ for (const u of show) {
461
+ console.log(`${indent}L${u.line}: ${u.context}`);
462
+ }
463
+ if (usages.length > max) {
464
+ console.log(`${indent}... (+${usages.length - max} more)`);
465
+ }
466
+ }
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // Show stale terms — where token value doesn't match docs content
470
+ // ---------------------------------------------------------------------------
471
+
472
+ async function showStale(dir, index, opts) {
473
+ const countsCategory = index.categories?.counts;
474
+ if (!countsCategory || !countsCategory.metrics) {
475
+ console.log("\n No counts metrics in index. Run `wwa update-docs --scan` first.\n");
476
+ return;
477
+ }
478
+
479
+ const stale = [];
480
+ for (const [metricKey, metricData] of Object.entries(countsCategory.metrics)) {
481
+ for (const v of metricData.values_found) {
482
+ if (v.status === "stale") {
483
+ stale.push({
484
+ metric: metricKey,
485
+ expected: metricData.expected,
486
+ found: v.value,
487
+ total_references: v.total_references,
488
+ referenced_in: v.referenced_in,
489
+ });
490
+ }
491
+ }
492
+ }
493
+
494
+ if (opts.json) {
495
+ console.log(JSON.stringify({ stale }, null, 2));
496
+ return;
497
+ }
498
+
499
+ if (stale.length === 0) {
500
+ console.log("\n No stale count values detected. All docs match doc-tokens.yaml.\n");
501
+ return;
502
+ }
503
+
504
+ console.log(`\n Stale values (${stale.length}):\n`);
505
+ for (const s of stale) {
506
+ console.log(` ${s.metric}: expected ${s.expected}, found ${s.found} (${s.total_references} reference${s.total_references > 1 ? "s" : ""})`);
507
+ for (const f of s.referenced_in) {
508
+ console.log(` ${f.file} (${f.occurrences}x)`);
509
+ printUsageSnippets(f.usages, " ");
510
+ }
511
+ }
512
+ console.log("");
513
+ }
514
+
515
+ // ---------------------------------------------------------------------------
516
+ // Print index summary
517
+ // ---------------------------------------------------------------------------
518
+
519
+ function printIndexSummary(index, opts) {
520
+ if (opts.json) {
521
+ console.log(JSON.stringify(index, null, 2));
522
+ return;
523
+ }
524
+
525
+ console.log("\n Documentation Term Index\n");
526
+ console.log(` Generated: ${index.generated}`);
527
+ console.log(` Source: ${index.source_directory}`);
528
+ console.log(` Files scanned: ${index.files_scanned}`);
529
+ console.log(` Total terms: ${index.total_terms}\n`);
530
+
531
+ for (const [key, cat] of Object.entries(index.categories || {})) {
532
+ if (cat.metrics) {
533
+ // Counts category
534
+ const topMetrics = Object.entries(cat.metrics)
535
+ .sort((a, b) => {
536
+ const aRefs = a[1].values_found.reduce((s, v) => s + v.total_references, 0);
537
+ const bRefs = b[1].values_found.reduce((s, v) => s + v.total_references, 0);
538
+ return bRefs - aRefs;
539
+ })
540
+ .slice(0, 3)
541
+ .map(([t]) => t);
542
+ console.log(` [${key}] ${Object.keys(cat.metrics).length} metrics — ${cat.description}`);
543
+ console.log(` Top: ${topMetrics.join(", ")}`);
544
+ } else {
545
+ const topTerms = Object.entries(cat.terms || {})
546
+ .sort((a, b) => (b[1].total_references || 0) - (a[1].total_references || 0))
547
+ .slice(0, 3)
548
+ .map(([t, d]) => {
549
+ const name = d.canonical_name ? ` (${d.canonical_name})` : "";
550
+ return `${t}${name}`;
551
+ });
552
+ console.log(` [${key}] ${cat.term_count} terms — ${cat.description}`);
553
+ console.log(` Top: ${topTerms.join(", ")}`);
554
+ }
555
+ }
556
+ console.log("");
557
+ }
558
+
559
+ // ---------------------------------------------------------------------------
560
+ // Helpers
561
+ // ---------------------------------------------------------------------------
562
+
563
+ function collectMdFiles(absDir, rootDir, files) {
564
+ for (const entry of readdirSync(absDir, { withFileTypes: true })) {
565
+ const fullPath = join(absDir, entry.name);
566
+ if (entry.isDirectory()) {
567
+ if (!["node_modules", ".git", "dist", "build"].includes(entry.name)) {
568
+ collectMdFiles(fullPath, rootDir, files);
569
+ }
570
+ } else if (entry.name.endsWith(".md") || entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) {
571
+ files.push(relative(rootDir, fullPath).replace(/\\/g, "/"));
572
+ }
573
+ }
574
+ }