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.
- package/README.md +343 -0
- package/bin/wwa.js +115 -0
- package/docs/internal/cli-commands.yaml +259 -0
- package/docs/internal/doc-tokens.yaml +1103 -0
- package/docs/internal/feature-registry.yaml +1643 -0
- package/lib/boundaries.js +247 -0
- package/lib/cli/add.js +170 -0
- package/lib/cli/casestudy.js +1000 -0
- package/lib/cli/check.js +323 -0
- package/lib/cli/close.js +838 -0
- package/lib/cli/completion.js +735 -0
- package/lib/cli/deps.js +234 -0
- package/lib/cli/digest.js +73 -0
- package/lib/cli/doc-review.js +486 -0
- package/lib/cli/docs.js +315 -0
- package/lib/cli/helpers.js +198 -0
- package/lib/cli/implement.js +169 -0
- package/lib/cli/init.js +280 -0
- package/lib/cli/pipeline.js +206 -0
- package/lib/cli/plan.js +140 -0
- package/lib/cli/record.js +98 -0
- package/lib/cli/refine.js +202 -0
- package/lib/cli/report-helpers.js +113 -0
- package/lib/cli/review.js +76 -0
- package/lib/cli/routable.js +109 -0
- package/lib/cli/route.js +101 -0
- package/lib/cli/scan.js +133 -0
- package/lib/cli/serve.js +23 -0
- package/lib/cli/status.js +65 -0
- package/lib/cli/update-docs.js +574 -0
- package/lib/cli/upgrade.js +222 -0
- package/lib/cli/watch.js +32 -0
- package/lib/dependencies.js +196 -0
- package/lib/init.js +692 -0
- package/lib/mcp-server.js +612 -0
- package/lib/pipeline.js +907 -0
- package/lib/registry.js +132 -0
- package/lib/watcher.js +165 -0
- package/package.json +54 -0
- package/templates/README.md +363 -0
- package/templates/entry-points/.cursorrules +90 -0
- package/templates/entry-points/AGENT.md +90 -0
- package/templates/entry-points/CLAUDE.md +88 -0
- package/templates/extensions/MANIFEST.md +110 -0
- package/templates/extensions/analytical-system.md +96 -0
- package/templates/extensions/code-project.md +77 -0
- package/templates/extensions/data-exploration.md +117 -0
- package/templates/full/.context/BASE.md +101 -0
- package/templates/full/.context/COMPOSITION.md +47 -0
- package/templates/full/.context/INDEX.yaml +56 -0
- package/templates/full/.context/METHODOLOGY.md +246 -0
- package/templates/full/.context/PROTOCOL.yaml +169 -0
- package/templates/full/.context/REGISTRY.md +75 -0
- package/templates/full/.cursorrules +90 -0
- package/templates/full/AGENT.md +90 -0
- package/templates/full/CLAUDE.md +90 -0
- package/templates/full/Management/DIGEST.md +23 -0
- package/templates/full/Management/STATUS.md +46 -0
- package/templates/full/PLAN.md +67 -0
- package/templates/full/PROJECT-PROFILE.md +61 -0
- package/templates/full/PROJECT.md +80 -0
- package/templates/full/REQUIREMENTS.md +30 -0
- package/templates/full/ROADMAP.md +39 -0
- package/templates/full/Reviews/INDEX.md +41 -0
- package/templates/full/Reviews/backlog.md +52 -0
- package/templates/full/Reviews/plan.md +43 -0
- package/templates/full/Reviews/project.md +41 -0
- package/templates/full/Reviews/requirements.md +42 -0
- package/templates/full/Reviews/roadmap.md +41 -0
- package/templates/full/Reviews/state.md +56 -0
- package/templates/full/SESSION-LOG.md +102 -0
- package/templates/full/STATE.md +42 -0
- package/templates/full/SUMMARY.md +27 -0
- package/templates/full/agentWorkflows/INDEX.md +42 -0
- package/templates/full/agentWorkflows/observations.md +65 -0
- package/templates/full/agentWorkflows/patterns.md +68 -0
- package/templates/full/agentWorkflows/sessions.md +92 -0
- package/templates/full/intro/README.md +39 -0
- package/templates/full/registry/feature-registry.yaml +25 -0
- package/templates/full/registry/features/catalog.yaml +743 -0
- package/templates/full/registry/features/protocol.yaml +121 -0
- package/templates/full/registry/features/routing.yaml +358 -0
- package/templates/full/registry/features/workflows.yaml +404 -0
- package/templates/full/todos/backlog.md +19 -0
- package/templates/starter/.context/BASE.md +66 -0
- package/templates/starter/.context/INDEX.yaml +51 -0
- package/templates/starter/.context/METHODOLOGY.md +228 -0
- package/templates/starter/.context/PROTOCOL.yaml +165 -0
- package/templates/starter/.cursorrules +90 -0
- package/templates/starter/AGENT.md +90 -0
- package/templates/starter/CLAUDE.md +90 -0
- package/templates/starter/Management/DIGEST.md +23 -0
- package/templates/starter/Management/STATUS.md +46 -0
- package/templates/starter/PLAN.md +67 -0
- package/templates/starter/PROJECT-PROFILE.md +44 -0
- package/templates/starter/PROJECT.md +80 -0
- package/templates/starter/ROADMAP.md +39 -0
- package/templates/starter/Reviews/INDEX.md +75 -0
- package/templates/starter/SESSION-LOG.md +102 -0
- package/templates/starter/STATE.md +42 -0
- package/templates/starter/SUMMARY.md +27 -0
- package/templates/starter/agentWorkflows/INDEX.md +61 -0
- package/templates/starter/intro/README.md +37 -0
- package/templates/starter/registry/feature-registry.yaml +25 -0
- package/templates/starter/registry/features/catalog.yaml +743 -0
- package/templates/starter/registry/features/protocol.yaml +121 -0
- package/templates/starter/registry/features/routing.yaml +358 -0
- 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
|
+
}
|