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,1000 @@
1
+ /** wwa casestudy — extract a structured case study from project methodology files. */
2
+
3
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
4
+ import { resolve, join, basename, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ findEntryPoint,
8
+ findSessionLog,
9
+ safeWriteFile,
10
+ readMethodVersion,
11
+ packageRoot,
12
+ } from "./helpers.js";
13
+ import { resolveTokensPath, resolveDocsMapPath } from "../boundaries.js";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ export function register(program) {
19
+ program
20
+ .command("casestudy [directory]")
21
+ .description(
22
+ "Extract a structured case study from SESSION-LOG.md and project files"
23
+ )
24
+ .option("-o, --output <path>", "Output file path")
25
+ .option("--name <name>", "Project name override")
26
+ .option("--json", "Output as JSON")
27
+ .option(
28
+ "--yaml-only",
29
+ "Write a .yaml case study (no markdown file; all aspects in YAML)"
30
+ )
31
+ .option("--internal-registry", "Include docs/internal/doc-registry.yaml data in output")
32
+ .action(async (directory, opts) => {
33
+ directory = directory || ".";
34
+ const d = resolve(directory);
35
+
36
+ // Gather all project data
37
+ const data = await gatherProjectData(d, opts.name);
38
+
39
+ // Load internal registry if requested
40
+ if (opts.internalRegistry) {
41
+ const { loadInternalRegistry } = await import("./doc-review.js");
42
+ const registry = await loadInternalRegistry(d);
43
+ if (registry) {
44
+ data.internalRegistry = registry;
45
+ } else {
46
+ data.warnings.push(
47
+ "No docs/internal/doc-registry.yaml found — run `wwa doc-review` to generate it"
48
+ );
49
+ }
50
+ }
51
+
52
+ if (data.errors.length > 0 && data.sessions.length === 0) {
53
+ console.error("Cannot generate case study:");
54
+ for (const e of data.errors) console.error(` - ${e}`);
55
+ process.exit(1);
56
+ }
57
+
58
+ if (opts.json) {
59
+ const output = opts.output;
60
+ const result = JSON.stringify(data, null, 2);
61
+ if (output) {
62
+ safeWriteFile(output, result);
63
+ console.log(`Case study data written to ${output}`);
64
+ } else {
65
+ console.log(result);
66
+ }
67
+ return;
68
+ }
69
+
70
+ // YAML-only mode: write a single .yaml artifact with all structured data.
71
+ if (opts.yamlOnly) {
72
+ const baseDir = join(d, "case-studies");
73
+ const baseSlug = slugify(data.projectName);
74
+ let yamlOutputPath = opts.output || join(baseDir, `${baseSlug}.yaml`);
75
+
76
+ // Avoid overwriting existing case studies when using the default path.
77
+ if (!opts.output) {
78
+ let counter = 1;
79
+ while (existsSync(yamlOutputPath)) {
80
+ yamlOutputPath = join(baseDir, `${baseSlug}-${counter}.yaml`);
81
+ counter += 1;
82
+ }
83
+ }
84
+
85
+ const yamlDir = resolve(yamlOutputPath, "..");
86
+ if (!existsSync(yamlDir)) {
87
+ mkdirSync(yamlDir, { recursive: true });
88
+ }
89
+
90
+ const ok = await writeCaseStudyYaml(yamlOutputPath, data);
91
+ if (ok) {
92
+ console.log(
93
+ `Case study YAML written to ${yamlOutputPath} (${data.sessions.length} sessions)`
94
+ );
95
+ } else {
96
+ console.error("Failed to write YAML case study.");
97
+ process.exit(1);
98
+ }
99
+ } else {
100
+ // Default: write markdown case study plus companion YAML.
101
+ const baseDir = join(d, "case-studies");
102
+ const baseSlug = slugify(data.projectName);
103
+ let markdownPath = opts.output || join(baseDir, `${baseSlug}.md`);
104
+
105
+ // Avoid overwriting existing case studies when using the default path.
106
+ if (!opts.output) {
107
+ let counter = 1;
108
+ while (existsSync(markdownPath)) {
109
+ markdownPath = join(baseDir, `${baseSlug}-${counter}.md`);
110
+ counter += 1;
111
+ }
112
+ }
113
+
114
+ const outputDir = resolve(markdownPath, "..");
115
+ if (!existsSync(outputDir)) {
116
+ mkdirSync(outputDir, { recursive: true });
117
+ }
118
+
119
+ const caseStudy = generateCaseStudy(data);
120
+ safeWriteFile(markdownPath, caseStudy);
121
+ console.log(
122
+ `Case study written to ${markdownPath} (${data.sessions.length} sessions)`
123
+ );
124
+
125
+ const yamlPath = markdownPath.replace(/\.md$/, ".yaml");
126
+ const yamlResult = await writeCaseStudyYaml(yamlPath, data);
127
+ if (yamlResult) {
128
+ console.log(`Case study YAML written to ${yamlPath}`);
129
+ }
130
+ }
131
+
132
+ if (data.warnings.length > 0) {
133
+ console.log("\nWarnings:");
134
+ for (const w of data.warnings) console.log(` - ${w}`);
135
+ }
136
+ });
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Data gathering
141
+ // ---------------------------------------------------------------------------
142
+
143
+ async function gatherProjectData(dir, nameOverride) {
144
+ const data = {
145
+ projectName: nameOverride || "Unknown Project",
146
+ projectType: "unknown",
147
+ lifecycleStart: "unknown",
148
+ lifecycleEnd: "unknown",
149
+ templateTier: "unknown",
150
+ extensions: "none",
151
+ entryPointFile: null,
152
+ integrationProfile: "unknown",
153
+ modelTier: "unknown",
154
+ methodVersion: null,
155
+ projectDescription: "",
156
+ studyGoals: "",
157
+ sessions: [],
158
+ decisions: [],
159
+ workflows: new Set(),
160
+ features: new Set(),
161
+ frictionPoints: [],
162
+ findings: [],
163
+ agentWorkflows: null,
164
+ tokenRegistry: null,
165
+ nameMappings: null,
166
+ docsMap: null,
167
+ errors: [],
168
+ warnings: [],
169
+ };
170
+
171
+ // --- Entry point ---
172
+ const entryPoint = findEntryPoint(dir);
173
+ if (entryPoint) {
174
+ data.entryPointFile = basename(entryPoint);
175
+ data.methodVersion = readMethodVersion(entryPoint);
176
+ const epContent = readFileSync(entryPoint, "utf-8");
177
+ const tierMatch = epContent.match(/tier:\s*(\S+)/);
178
+ if (tierMatch) data.templateTier = tierMatch[1];
179
+ const modeMatch = epContent.match(/mode:\s*(\S+)/);
180
+ if (modeMatch) data.integrationProfile = modeMatch[1];
181
+ } else {
182
+ data.warnings.push("No entry point found (CLAUDE.md / .cursorrules / AGENT.md)");
183
+ }
184
+
185
+ // --- PROJECT.md ---
186
+ const projectPath = join(dir, "PROJECT.md");
187
+ if (existsSync(projectPath)) {
188
+ const content = readFileSync(projectPath, "utf-8");
189
+ const titleMatch = content.match(/^#\s+(.+)/m);
190
+ if (titleMatch && !nameOverride) {
191
+ data.projectName = titleMatch[1].trim();
192
+ }
193
+ // Extract description (first paragraph after the title)
194
+ const descMatch = content.match(/^#[^\n]+\n+(.+?)(?:\n\n|\n##)/s);
195
+ if (descMatch) {
196
+ data.projectDescription = descMatch[1].trim();
197
+ }
198
+ } else {
199
+ data.warnings.push("No PROJECT.md found — project name and description may be incomplete");
200
+ }
201
+
202
+ // --- PROJECT-PROFILE.md ---
203
+ const profilePath = join(dir, "PROJECT-PROFILE.md");
204
+ if (existsSync(profilePath)) {
205
+ const content = readFileSync(profilePath, "utf-8");
206
+ const typeMatch = content.match(/project.?type[:\s|]+(\S+)/i);
207
+ if (typeMatch) data.projectType = typeMatch[1].toLowerCase();
208
+ const stageMatch = content.match(/lifecycle.?stage[:\s|]+(\S+)/i);
209
+ if (stageMatch) data.lifecycleStart = stageMatch[1].toLowerCase();
210
+ const extMatch = content.match(/extension[s]?[:\s|]+([^\n|]+)/i);
211
+ if (extMatch) data.extensions = extMatch[1].trim();
212
+ }
213
+
214
+ // --- STATE.md (decisions) ---
215
+ const statePath = join(dir, "STATE.md");
216
+ if (existsSync(statePath)) {
217
+ const content = readFileSync(statePath, "utf-8");
218
+ // Extract decisions from table rows
219
+ const decisionSection = content.match(
220
+ /##\s*Decisions?\s*\n([\s\S]*?)(?=\n##|\n$|$)/i
221
+ );
222
+ if (decisionSection) {
223
+ const rows = decisionSection[1].match(/\|[^|\n]+\|[^|\n]+\|/g) || [];
224
+ for (const row of rows) {
225
+ const cols = row
226
+ .split("|")
227
+ .map((c) => c.trim())
228
+ .filter((c) => c && !c.match(/^[-:]+$/));
229
+ if (cols.length >= 2 && !cols[0].toLowerCase().includes("date")) {
230
+ data.decisions.push({ date: cols[0], decision: cols.slice(1).join(" — ") });
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ // --- SESSION-LOG.md ---
237
+ const sessionLog = findSessionLog(dir);
238
+ if (sessionLog) {
239
+ const content = readFileSync(sessionLog, "utf-8");
240
+ data.sessions = parseSessionEntries(content);
241
+
242
+ // Extract project context header
243
+ const ctxMatch = content.match(
244
+ /## Project context\s*\n\s*\|[^\n]+\n\s*\|[-| ]+\n((?:\|[^\n]+\n)*)/
245
+ );
246
+ if (ctxMatch) {
247
+ for (const row of ctxMatch[1].trim().split("\n")) {
248
+ const cols = row
249
+ .split("|")
250
+ .map((c) => c.trim())
251
+ .filter((c) => c);
252
+ if (cols.length >= 2) {
253
+ const key = cols[0].toLowerCase();
254
+ if (key.includes("project name") && !nameOverride) {
255
+ data.projectName = cols[1];
256
+ }
257
+ if (key.includes("project type")) data.projectType = cols[1].toLowerCase();
258
+ if (key.includes("profile")) data.integrationProfile = cols[1];
259
+ if (key.includes("extension")) data.extensions = cols[1];
260
+ if (key.includes("model")) data.modelTier = cols[1];
261
+ }
262
+ }
263
+ }
264
+
265
+ // Aggregate workflows and features
266
+ for (const s of data.sessions) {
267
+ if (s.workflow) data.workflows.add(s.workflow);
268
+ if (s.features) {
269
+ for (const f of s.features.split(",").map((x) => x.trim())) {
270
+ if (f) data.features.add(f);
271
+ }
272
+ }
273
+ if (s.friction && s.friction.toLowerCase() !== "none") {
274
+ data.frictionPoints.push({ session: s.session, date: s.date, text: s.friction });
275
+ }
276
+ if (s.finding && s.finding.toLowerCase() !== "none") {
277
+ data.findings.push({ session: s.session, date: s.date, text: s.finding });
278
+ }
279
+ }
280
+ } else {
281
+ data.errors.push("No SESSION-LOG.md found — session data is required for case study extraction");
282
+ }
283
+
284
+ // --- agentWorkflows/INDEX.md ---
285
+ const wfPath = join(dir, "agentWorkflows", "INDEX.md");
286
+ if (existsSync(wfPath)) {
287
+ data.agentWorkflows = readFileSync(wfPath, "utf-8");
288
+ }
289
+
290
+ // --- SUMMARY.md ---
291
+ const summaryPath = join(dir, "SUMMARY.md");
292
+ if (existsSync(summaryPath)) {
293
+ const content = readFileSync(summaryPath, "utf-8");
294
+ // Count summary entries
295
+ const entryCount = (content.match(/^##\s+/gm) || []).length;
296
+ if (entryCount > 0) {
297
+ data.warnings.push(
298
+ `SUMMARY.md has ${entryCount} entries — cross-reference with session log for completeness`
299
+ );
300
+ }
301
+ }
302
+
303
+ // --- Token registry ---
304
+ // Priority: project tokens (configured path) > package docs/internal/doc-tokens.yaml
305
+ const projectTokensPath = await resolveTokensPath(dir);
306
+ const packageTokensPath = join(packageRoot, "docs", "internal", "doc-tokens.yaml");
307
+ try {
308
+ const yaml = (await import("js-yaml")).default;
309
+ // Load project-specific tokens (populated by wwa close)
310
+ if (existsSync(projectTokensPath)) {
311
+ const raw = readFileSync(projectTokensPath, "utf-8");
312
+ const parsed = yaml.load(raw);
313
+ if (parsed && parsed.tokens) data.tokenRegistry = parsed.tokens;
314
+ }
315
+ // Load methodology name mappings from package (always available)
316
+ if (existsSync(packageTokensPath)) {
317
+ const raw = readFileSync(packageTokensPath, "utf-8");
318
+ const parsed = yaml.load(raw);
319
+ if (parsed && parsed.names) data.nameMappings = parsed.names;
320
+ // If no project tokens, fall back to package tokens
321
+ if (!data.tokenRegistry && parsed && parsed.tokens) data.tokenRegistry = parsed.tokens;
322
+ }
323
+ } catch (_) {
324
+ data.warnings.push("Could not parse doc-tokens.yaml — token registry omitted");
325
+ }
326
+
327
+ // --- DOCS-MAP.md (from project) ---
328
+ const docsMapPath = await resolveDocsMapPath(dir);
329
+ if (existsSync(docsMapPath)) {
330
+ const content = readFileSync(docsMapPath, "utf-8");
331
+ data.docsMap = parseDocsMapForCaseStudy(content);
332
+ }
333
+
334
+ // Convert sets to arrays for serialization
335
+ data.workflows = [...data.workflows];
336
+ data.features = [...data.features];
337
+ data.lifecycleEnd = data.lifecycleStart; // default; override if detectable
338
+
339
+ return data;
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Session log parsing (aligned with SESSION-LOG.md Entry format — Phase 7r §5.6)
344
+ // ---------------------------------------------------------------------------
345
+
346
+ /** Map SESSION-LOG.md field labels to canonical keys used by case study output. */
347
+ const SESSION_LOG_KEY_MAP = {
348
+ "llm tokens": "tokens",
349
+ "wall time": "time",
350
+ "tool calls": "tool_calls",
351
+ "delta notes": "delta_notes",
352
+ };
353
+
354
+ /** Known session entry keys (Entry format); ignore parsed key:value from checklist/bullets. */
355
+ const SESSION_ENTRY_KEYS = new Set([
356
+ "model", "profile", "workflow", "effort", "ambiguity", "context", "tokens", "tool_calls", "time",
357
+ "queries", "features", "cascades", "decisions", "response", "revisions", "magnitude", "delta",
358
+ "survival", "delta_notes", "friction", "finding",
359
+ ]);
360
+
361
+ function parseSessionEntries(content) {
362
+ const entries = [];
363
+ // Normalize line endings and allow em dash (—) or " - " between session number, date, and title
364
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
365
+ // Body: from the line after the title until the next "### S" session header or end of string.
366
+ // Use (?![\s\S]) for end-of-string so we don't stop at newlines (.(?!.) matches before \n).
367
+ const entryPattern =
368
+ /###\s+S(\d+)\s*(?:\u2014|-\s*-?)\s*(\S+)\s*(?:\u2014|-\s*-?)\s*(.+?)\n([\s\S]*?)(?=###\s+S\d+\s|(?![\s\S]))/gm;
369
+ let m;
370
+ while ((m = entryPattern.exec(normalized)) !== null) {
371
+ const [, num, date, title, body] = m;
372
+ const entry = {
373
+ session: parseInt(num, 10),
374
+ date,
375
+ title: title.trim(),
376
+ };
377
+
378
+ for (const line of body.split("\n")) {
379
+ const trimmed = line.trim();
380
+ if (!trimmed || trimmed.startsWith("#")) continue;
381
+ // Parse pipe-delimited key: value pairs (SESSION-LOG Entry format)
382
+ const segments = trimmed.split("|").map((s) => s.trim());
383
+ for (const seg of segments) {
384
+ if (seg.includes(":")) {
385
+ const idx = seg.indexOf(":");
386
+ const rawKey = seg.slice(0, idx).trim().toLowerCase();
387
+ const val = seg.slice(idx + 1).trim();
388
+ if (!rawKey || !val) continue;
389
+ const key = SESSION_LOG_KEY_MAP[rawKey] ?? rawKey;
390
+ if (SESSION_ENTRY_KEYS.has(key)) entry[key] = val;
391
+ }
392
+ }
393
+ }
394
+ entries.push(entry);
395
+ }
396
+ return entries;
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Case study generation
401
+ // ---------------------------------------------------------------------------
402
+
403
+ function generateCaseStudy(data) {
404
+ const lines = [];
405
+
406
+ // Section 1: Overview
407
+ lines.push(
408
+ `# Case Study: ${data.projectName}`,
409
+ "",
410
+ "## Overview",
411
+ "",
412
+ "| Field | Value |",
413
+ "|-------|-------|",
414
+ `| Project name | ${data.projectName} |`,
415
+ `| Project type | ${data.projectType} |`,
416
+ `| Lifecycle stage at start | ${data.lifecycleStart} |`,
417
+ `| Lifecycle stage at end | ${data.lifecycleEnd} |`,
418
+ `| Template tier | ${data.templateTier} |`,
419
+ `| Extensions applied | ${data.extensions} |`,
420
+ `| Entry point | ${data.entryPointFile || "unknown"} |`,
421
+ `| Integration profile | ${data.integrationProfile} |`,
422
+ `| Model tier | ${data.modelTier} |`,
423
+ `| Sessions documented | ${data.sessions.length} |`,
424
+ `| Method version | ${data.methodVersion || "unknown"} |`,
425
+ ""
426
+ );
427
+
428
+ if (data.projectDescription) {
429
+ lines.push("## Project description", "", data.projectDescription, "");
430
+ }
431
+
432
+ lines.push(
433
+ "## Study goals",
434
+ "",
435
+ "<!-- Fill in: what this case study specifically aims to validate -->",
436
+ ""
437
+ );
438
+
439
+ // Section 2: Setup
440
+ lines.push(
441
+ "---",
442
+ "",
443
+ "## Setup",
444
+ "",
445
+ "### Bootstrap experience",
446
+ "",
447
+ "| Step | Time | Friction | Notes |",
448
+ "|------|------|----------|-------|"
449
+ );
450
+
451
+ if (data.sessions.length > 0) {
452
+ const first = data.sessions[0];
453
+ lines.push(
454
+ `| First session (S${first.session}) | ${first.time || "unknown"} | ${first.friction || "none"} | ${first.title} |`
455
+ );
456
+ }
457
+
458
+ lines.push("");
459
+
460
+ // Section 3: Feature activation
461
+ lines.push(
462
+ "---",
463
+ "",
464
+ "## Feature activation",
465
+ "",
466
+ "### Features activated",
467
+ "",
468
+ "| Feature | Sessions | Evidence |",
469
+ "|---------|:--------:|----------|"
470
+ );
471
+
472
+ if (data.features.length > 0) {
473
+ for (const f of data.features.sort()) {
474
+ // Find which sessions activated this feature
475
+ const activeSessions = data.sessions
476
+ .filter((s) => s.features && s.features.includes(f))
477
+ .map((s) => `S${s.session}`)
478
+ .join(", ");
479
+ lines.push(`| ${f} | ${activeSessions} | Observed in session log |`);
480
+ }
481
+ } else {
482
+ lines.push("| (no features recorded) | -- | -- |");
483
+ }
484
+
485
+ lines.push("");
486
+
487
+ lines.push("### Workflow usage", "", "| Workflow | Sessions | Completed? |", "|---------|:--------:|:----------:|");
488
+ if (data.workflows.length > 0) {
489
+ for (const wf of data.workflows.sort()) {
490
+ const wfSessions = data.sessions
491
+ .filter((s) => s.workflow === wf)
492
+ .map((s) => `S${s.session}`)
493
+ .join(", ");
494
+ lines.push(`| ${wf} | ${wfSessions} | yes |`);
495
+ }
496
+ } else {
497
+ lines.push("| (no workflows recorded) | -- | -- |");
498
+ }
499
+ lines.push("");
500
+
501
+ // Section 4: Session log
502
+ lines.push("---", "", "## Session log", "");
503
+
504
+ for (const s of data.sessions) {
505
+ lines.push(
506
+ `### Session ${s.session}: ${s.title}`,
507
+ "",
508
+ "| Field | Value |",
509
+ "|-------|-------|",
510
+ `| Date | ${s.date} |`,
511
+ `| Model | ${s.model || "unknown"} |`,
512
+ `| Profile | ${s.profile || "unknown"} |`,
513
+ `| Effort | ${s.effort || "unknown"} |`,
514
+ `| Ambiguity | ${s.ambiguity || "unknown"} |`,
515
+ `| Context | ${s.context || "unknown"} |`,
516
+ `| Tokens | ${s.tokens || "unknown"} |`,
517
+ `| Time | ${s.time || "unknown"} |`,
518
+ `| Tool calls | ${s.tool_calls ?? "—"} |`,
519
+ `| Workflow | ${s.workflow || "unknown"} |`,
520
+ `| Features | ${s.features || "none"} |`,
521
+ `| Cascades | ${s.cascades || "unknown"} |`,
522
+ `| Decisions | ${s.decisions || "0"} |`,
523
+ `| Response | ${s.response || "unknown"} |`,
524
+ `| Revisions | ${s.revisions ?? "—"} |`,
525
+ `| Magnitude | ${s.magnitude ?? "—"} |`,
526
+ `| Delta | ${s.delta ?? "n/a"} |`,
527
+ `| Survival | ${s.survival ?? "—"} |`,
528
+ `| Delta notes | ${s.delta_notes ?? "n/a"} |`,
529
+ `| Friction | ${s.friction || "none"} |`,
530
+ `| Finding | ${s.finding || "none"} |`
531
+ );
532
+
533
+ lines.push("");
534
+ }
535
+
536
+ // Section 5: Scoring rubric
537
+ lines.push(
538
+ "---",
539
+ "",
540
+ "## Methodology effectiveness scoring",
541
+ "",
542
+ "Rate each dimension 1-5 with specific evidence.",
543
+ "",
544
+ "| Dimension | Score | Evidence | Notes |",
545
+ "|-----------|:-----:|----------|-------|"
546
+ );
547
+
548
+ const dimensions = [
549
+ "Context quality",
550
+ "Decision preservation",
551
+ "Scope discipline",
552
+ "Cascade coverage",
553
+ "Audit completeness",
554
+ "Bootstrap speed",
555
+ "Lifecycle fit",
556
+ "Model adequacy",
557
+ ];
558
+
559
+ for (const dim of dimensions) {
560
+ lines.push(`| ${dim} | -- | | |`);
561
+ }
562
+ lines.push("| **Overall** | -- | | |", "");
563
+
564
+ // Section 6: Findings
565
+ lines.push("---", "", "## Findings", "", "### What worked", "");
566
+
567
+ if (data.findings.length > 0) {
568
+ lines.push("| # | Finding | Session | Evidence |", "|---|---------|---------|----------|");
569
+ let wn = 1;
570
+ for (const f of data.findings) {
571
+ lines.push(`| W${wn} | ${f.text} | S${f.session} (${f.date}) | Session log |`);
572
+ wn++;
573
+ }
574
+ } else {
575
+ lines.push("<!-- Fill in findings from session review -->");
576
+ }
577
+
578
+ lines.push("", "### What needs improvement", "");
579
+
580
+ if (data.frictionPoints.length > 0) {
581
+ lines.push(
582
+ "| # | Finding | Session | Severity | Suggested change |",
583
+ "|---|---------|---------|----------|-----------------|"
584
+ );
585
+ let fn = 1;
586
+ for (const f of data.frictionPoints) {
587
+ lines.push(
588
+ `| I${fn} | ${f.text} | S${f.session} (${f.date}) | -- | <!-- suggest fix --> |`
589
+ );
590
+ fn++;
591
+ }
592
+ } else {
593
+ lines.push("<!-- Fill in improvement findings from session review -->");
594
+ }
595
+
596
+ lines.push("", "### What's missing", "", "<!-- Fill in gaps discovered during the study -->", "");
597
+
598
+ // Section 7: Refinement recommendations
599
+ lines.push(
600
+ "---",
601
+ "",
602
+ "## Refinement recommendations",
603
+ "",
604
+ "| # | Finding ref | Target | Change type | Specific change | Validation |",
605
+ "|---|------------|--------|-------------|-----------------|------------|"
606
+ );
607
+
608
+ if (data.frictionPoints.length > 0) {
609
+ let rn = 1;
610
+ for (let i = 0; i < data.frictionPoints.length; i++) {
611
+ lines.push(
612
+ `| R${rn} | I${i + 1} | <!-- target --> | <!-- type --> | <!-- change --> | <!-- validation --> |`
613
+ );
614
+ rn++;
615
+ }
616
+ } else {
617
+ lines.push("| R1 | -- | -- | -- | -- | -- |");
618
+ }
619
+
620
+ lines.push("");
621
+
622
+ // Agent workflow annotations
623
+ if (data.agentWorkflows) {
624
+ lines.push(
625
+ "---",
626
+ "",
627
+ "## Agent workflow annotations",
628
+ "",
629
+ "Extracted from agentWorkflows/INDEX.md:",
630
+ "",
631
+ data.agentWorkflows,
632
+ ""
633
+ );
634
+ }
635
+
636
+ // Statistics summary
637
+ lines.push(
638
+ "---",
639
+ "",
640
+ "## Statistics",
641
+ "",
642
+ `- **Total sessions**: ${data.sessions.length}`,
643
+ `- **Workflows used**: ${data.workflows.join(", ") || "none recorded"}`,
644
+ `- **Features observed**: ${data.features.length}`,
645
+ `- **Decisions recorded**: ${data.decisions.length}`,
646
+ `- **Friction points**: ${data.frictionPoints.length}`,
647
+ `- **Findings**: ${data.findings.length}`,
648
+ ""
649
+ );
650
+
651
+ // Methodology reference data
652
+ if (data.tokenRegistry || data.nameMappings || data.docsMap) {
653
+ lines.push("---", "", "## Methodology reference data", "");
654
+ }
655
+
656
+ if (data.tokenRegistry) {
657
+ lines.push("### Token registry snapshot", "");
658
+ lines.push("| Token | Value |", "|-------|-------|");
659
+ const keySubset = [
660
+ "registry_version", "feature_count", "domain_count", "directive_count",
661
+ "workflow_count", "query_pattern_count", "starter_file_count", "full_file_count",
662
+ "max_file_lines", "cli_command", "npm_package", "pip_package",
663
+ "developer_cmd_count", "pipeline_cmd_count", "total_cmd_count",
664
+ ];
665
+ for (const key of keySubset) {
666
+ if (data.tokenRegistry[key] !== undefined) {
667
+ lines.push(`| ${key} | ${data.tokenRegistry[key]} |`);
668
+ }
669
+ }
670
+ lines.push("");
671
+ }
672
+
673
+ if (data.nameMappings) {
674
+ lines.push("### Name mappings", "");
675
+
676
+ if (data.nameMappings.project_types) {
677
+ lines.push("**Project types**", "", "| Canonical | CLI alias |", "|-----------|-----------|");
678
+ for (const t of data.nameMappings.project_types) {
679
+ lines.push(`| ${t.canonical} | ${t.cli_alias || "—"} |`);
680
+ }
681
+ lines.push("");
682
+ }
683
+
684
+ if (data.nameMappings.guided_workflows) {
685
+ lines.push("**Workflows**", "", "| ID | Name | Stages |", "|----|------|--------|");
686
+ for (const wf of data.nameMappings.guided_workflows) {
687
+ lines.push(`| ${wf.id} | ${wf.display} | ${wf.context || ""} |`);
688
+ }
689
+ lines.push("");
690
+ }
691
+
692
+ if (data.nameMappings.feature_domains) {
693
+ lines.push("**Feature domains**", "", "| ID | Domain | Range |", "|----|--------|-------|");
694
+ for (const d of data.nameMappings.feature_domains) {
695
+ lines.push(`| ${d.id} | ${d.canonical} | ${d.feature_range} |`);
696
+ }
697
+ lines.push("");
698
+ }
699
+
700
+ if (data.nameMappings.protocol_directives) {
701
+ lines.push("**Protocol directives**", "", "| ID | Name | Rule |", "|----|------|------|");
702
+ for (const p of data.nameMappings.protocol_directives) {
703
+ lines.push(`| ${p.id} | ${p.canonical} | ${p.short} |`);
704
+ }
705
+ lines.push("");
706
+ }
707
+ }
708
+
709
+ if (data.docsMap) {
710
+ lines.push("### Docs dependency map", "");
711
+ if (data.docsMap.inventory.length > 0) {
712
+ lines.push("**Docs inventory**", "", "| Path | Purpose | Status |", "|------|---------|--------|");
713
+ for (const item of data.docsMap.inventory) {
714
+ lines.push(`| ${item.path} | ${item.purpose} | ${item.status} |`);
715
+ }
716
+ lines.push("");
717
+ }
718
+ if (data.docsMap.mappings.length > 0) {
719
+ lines.push("**Component mappings**", "", "| Component | Documented in | Trigger |", "|-----------|--------------|---------|");
720
+ for (const m of data.docsMap.mappings) {
721
+ lines.push(`| ${m.component} | ${m.documentedIn} | ${m.trigger} |`);
722
+ }
723
+ lines.push("");
724
+ }
725
+ lines.push(`**Scaffolding rules**: ${data.docsMap.scaffolding.length} active`, "");
726
+ }
727
+
728
+ // Internal document registry (when --internal-registry is used)
729
+ if (data.internalRegistry) {
730
+ lines.push("---", "", "## Internal document registry", "");
731
+ const reg = data.internalRegistry;
732
+
733
+ if (reg.health) {
734
+ lines.push(
735
+ "### Registry health",
736
+ "",
737
+ "| Metric | Value |",
738
+ "|--------|-------|",
739
+ `| Total nodes | ${reg.health.totalDocs ?? reg.health.total_docs ?? "—"} |`,
740
+ `| Nodes on disk | ${reg.health.docsOnDisk ?? reg.health.docs_on_disk ?? "—"} |`,
741
+ `| Orphan nodes | ${reg.health.orphanNodes ?? reg.health.orphan_nodes ?? "—"} |`,
742
+ `| Dependency coverage | ${reg.health.dependencyCoverage ?? reg.health.dependency_coverage ?? "—"} |`,
743
+ ""
744
+ );
745
+ }
746
+
747
+ if (reg.dependency_graph?.read_order?.length > 0) {
748
+ lines.push("### Dependency read order", "");
749
+ for (let i = 0; i < reg.dependency_graph.read_order.length; i++) {
750
+ lines.push(`${i + 1}. \`${reg.dependency_graph.read_order[i]}\``);
751
+ }
752
+ lines.push("");
753
+ }
754
+
755
+ if (reg.terminology) {
756
+ const t = reg.terminology;
757
+ const termCount =
758
+ (t.modules?.length || 0) +
759
+ (t.apis?.length || 0) +
760
+ (t.entities?.length || 0) +
761
+ (t.conventions?.length || 0);
762
+ lines.push(`### Terminology snapshot`, "", `${termCount} term(s) captured across modules, APIs, entities, and conventions.`, "");
763
+ }
764
+ }
765
+
766
+ // Synthesized analysis — agent-level understanding from session metrics
767
+ lines.push("---", "", "## Synthesized analysis", "");
768
+ lines.push(generateSynthesizedAnalysis(data), "");
769
+
770
+ lines.push("---", "", "*Generated by `wwa casestudy` from project methodology files.*", "");
771
+
772
+ return lines.join("\n");
773
+ }
774
+
775
+ // ---------------------------------------------------------------------------
776
+ // Utilities
777
+ // ---------------------------------------------------------------------------
778
+
779
+ function generateSynthesizedAnalysis(data) {
780
+ const lines = [];
781
+
782
+ // Effort distribution
783
+ const effortDist = {};
784
+ const ambiguityDist = {};
785
+ const responseDist = {};
786
+ let totalCascades = 0;
787
+ let highEffortCount = 0;
788
+
789
+ for (const s of data.sessions) {
790
+ if (s.effort) effortDist[s.effort] = (effortDist[s.effort] || 0) + 1;
791
+ if (s.ambiguity) ambiguityDist[s.ambiguity] = (ambiguityDist[s.ambiguity] || 0) + 1;
792
+ if (s.response) responseDist[s.response] = (responseDist[s.response] || 0) + 1;
793
+ if (s.cascades) totalCascades += parseInt(s.cascades, 10) || 0;
794
+ if (s.effort === "high") highEffortCount++;
795
+ }
796
+
797
+ lines.push("### Session profile");
798
+ lines.push("");
799
+ lines.push(`- **${data.sessions.length} sessions** across ${data.workflows.length} workflow type(s)`);
800
+
801
+ const effortSummary = Object.entries(effortDist).map(([k, v]) => `${k}: ${v}`).join(", ");
802
+ if (effortSummary) lines.push(`- Effort distribution: ${effortSummary}`);
803
+
804
+ const ambiguitySummary = Object.entries(ambiguityDist).map(([k, v]) => `${k}: ${v}`).join(", ");
805
+ if (ambiguitySummary) lines.push(`- Ambiguity distribution: ${ambiguitySummary}`);
806
+
807
+ const responseSummary = Object.entries(responseDist).map(([k, v]) => `${k}: ${v}`).join(", ");
808
+ if (responseSummary) lines.push(`- Response outcomes: ${responseSummary}`);
809
+
810
+ lines.push(`- Total cascades triggered: ${totalCascades}`);
811
+ lines.push(`- High-effort sessions: ${highEffortCount}/${data.sessions.length}`);
812
+ lines.push("");
813
+
814
+ // Feature coverage
815
+ lines.push("### Feature coverage");
816
+ lines.push("");
817
+ if (data.features.length > 0) {
818
+ lines.push(`${data.features.length} features activated: ${data.features.join(", ")}`);
819
+ } else {
820
+ lines.push("No features explicitly tracked in session log.");
821
+ }
822
+ lines.push("");
823
+
824
+ // Methodology health
825
+ lines.push("### Methodology health signals");
826
+ lines.push("");
827
+
828
+ const acceptedCount = responseDist["accepted"] || 0;
829
+ const totalResponses = Object.values(responseDist).reduce((a, b) => a + b, 0);
830
+ if (totalResponses > 0) {
831
+ const acceptRate = Math.round((acceptedCount / totalResponses) * 100);
832
+ lines.push(`- Acceptance rate: ${acceptRate}% (${acceptedCount}/${totalResponses} sessions)`);
833
+ }
834
+
835
+ if (data.frictionPoints.length > 0) {
836
+ lines.push(`- Friction points: ${data.frictionPoints.length} recorded`);
837
+ const frictionSessions = data.frictionPoints.map(f => `S${f.session}`).join(", ");
838
+ lines.push(` - Sessions with friction: ${frictionSessions}`);
839
+ } else {
840
+ lines.push("- No friction points recorded — either smooth workflow or under-reporting");
841
+ }
842
+
843
+ if (data.findings.length > 0) {
844
+ lines.push(`- Findings: ${data.findings.length} methodology-relevant observations`);
845
+ }
846
+
847
+ lines.push(`- Decisions logged: ${data.decisions.length}`);
848
+ lines.push("");
849
+
850
+ // Lifecycle assessment
851
+ lines.push("### Lifecycle assessment");
852
+ lines.push("");
853
+ lines.push(`Project type \`${data.projectType}\` at lifecycle stage \`${data.lifecycleStart}\` using \`${data.templateTier}\` tier.`);
854
+
855
+ if (data.templateTier === "standard" && highEffortCount > 5) {
856
+ lines.push("Consider upgrading to **full** tier — high-effort session count suggests complex project needing REQUIREMENTS.md and REGISTRY.md.");
857
+ }
858
+ if (data.templateTier === "full" && data.sessions.length > 0 && highEffortCount === 0) {
859
+ lines.push("Full tier may be over-provisioned — no high-effort sessions recorded. Standard tier may suffice.");
860
+ }
861
+ lines.push("");
862
+
863
+ return lines.join("\n");
864
+ }
865
+
866
+ function parseDocsMapForCaseStudy(content) {
867
+ const result = { inventory: [], mappings: [], scaffolding: [] };
868
+
869
+ const parseTable = (sectionName) => {
870
+ const re = new RegExp(
871
+ `## ${sectionName}[ \\t]*\\n\\n?\\|[^\\n]+\\n\\|[-| :]+\\n((?:\\|[^\\n]+\\n)*)`,
872
+ );
873
+ const m = content.match(re);
874
+ if (!m) return [];
875
+ return m[1].trim().split("\n").map((row) => {
876
+ const cols = row.split("|").map((c) => c.trim()).filter((c) => c && !c.startsWith("<!--"));
877
+ return cols;
878
+ }).filter((cols) => cols.length >= 2);
879
+ };
880
+
881
+ for (const cols of parseTable("Docs inventory")) {
882
+ result.inventory.push({ path: cols[0], purpose: cols[1], sources: cols[2] || "", status: cols[3] || "" });
883
+ }
884
+ for (const cols of parseTable("Component-to-docs mapping")) {
885
+ result.mappings.push({ component: cols[0], documentedIn: cols[1], trigger: cols[2] || "" });
886
+ }
887
+ for (const cols of parseTable("Scaffolding rules")) {
888
+ result.scaffolding.push({ condition: cols[0], proposedDoc: cols[1], seedFrom: cols[2] || "" });
889
+ }
890
+
891
+ return result;
892
+ }
893
+
894
+ async function writeCaseStudyYaml(yamlPath, data) {
895
+ try {
896
+ const yaml = (await import("js-yaml")).default;
897
+ const today = new Date().toISOString().slice(0, 10);
898
+
899
+ // Compute session metrics summary
900
+ const effortDist = {};
901
+ const ambiguityDist = {};
902
+ const responseDist = {};
903
+ let totalCascades = 0;
904
+ let totalDecisionsSessions = 0;
905
+
906
+ for (const s of data.sessions) {
907
+ if (s.effort) effortDist[s.effort] = (effortDist[s.effort] || 0) + 1;
908
+ if (s.ambiguity) ambiguityDist[s.ambiguity] = (ambiguityDist[s.ambiguity] || 0) + 1;
909
+ if (s.response) responseDist[s.response] = (responseDist[s.response] || 0) + 1;
910
+ if (s.cascades) totalCascades += parseInt(s.cascades, 10) || 0;
911
+ if (s.decisions) totalDecisionsSessions += parseInt(s.decisions, 10) || 0;
912
+ }
913
+
914
+ const structured = {
915
+ generated: today,
916
+ generator: "wwa casestudy",
917
+ overview: {
918
+ project_name: data.projectName,
919
+ project_type: data.projectType,
920
+ lifecycle_start: data.lifecycleStart,
921
+ lifecycle_end: data.lifecycleEnd,
922
+ template_tier: data.templateTier,
923
+ extensions: data.extensions,
924
+ entry_point: data.entryPointFile,
925
+ integration_profile: data.integrationProfile,
926
+ model_tier: data.modelTier,
927
+ method_version: data.methodVersion,
928
+ },
929
+ session_metrics: {
930
+ total_sessions: data.sessions.length,
931
+ effort_distribution: effortDist,
932
+ ambiguity_distribution: ambiguityDist,
933
+ response_distribution: responseDist,
934
+ total_cascades: totalCascades,
935
+ total_decisions_in_sessions: totalDecisionsSessions,
936
+ workflows_used: data.workflows,
937
+ features_activated: data.features,
938
+ },
939
+ sessions: data.sessions.map(s => ({
940
+ session: s.session,
941
+ date: s.date,
942
+ title: s.title,
943
+ model: s.model || null,
944
+ profile: s.profile || null,
945
+ effort: s.effort || null,
946
+ ambiguity: s.ambiguity || null,
947
+ context: s.context || null,
948
+ tokens: s.tokens || null,
949
+ tool_calls: s.tool_calls || null,
950
+ time: s.time || null,
951
+ workflow: s.workflow || null,
952
+ features: s.features || null,
953
+ cascades: s.cascades || null,
954
+ decisions: s.decisions || null,
955
+ response: s.response || null,
956
+ revisions: s.revisions || null,
957
+ magnitude: s.magnitude || null,
958
+ delta: s.delta || null,
959
+ survival: s.survival || null,
960
+ delta_notes: s.delta_notes || null,
961
+ friction: s.friction || null,
962
+ finding: s.finding || null,
963
+ })),
964
+ decisions: data.decisions,
965
+ friction_points: data.frictionPoints,
966
+ findings: data.findings,
967
+ token_registry: data.tokenRegistry,
968
+ name_mappings: data.nameMappings ? {
969
+ project_types: data.nameMappings.project_types || null,
970
+ guided_workflows: data.nameMappings.guided_workflows || null,
971
+ feature_domains: data.nameMappings.feature_domains || null,
972
+ protocol_directives: data.nameMappings.protocol_directives || null,
973
+ } : null,
974
+ docs_map: data.docsMap,
975
+ internal_registry: data.internalRegistry || null,
976
+ };
977
+
978
+ const yamlOutput = yaml.dump(structured, { lineWidth: 120, noRefs: true, quotingType: '"' });
979
+ const header = [
980
+ "# Case Study — structured session metrics and methodology data",
981
+ `# Project: ${data.projectName}`,
982
+ `# Generated: ${today} by wwa casestudy`,
983
+ "# Companion to: " + yamlPath.replace(/\.yaml$/, ".md"),
984
+ "",
985
+ ].join("\n");
986
+
987
+ safeWriteFile(yamlPath, header + yamlOutput, "utf-8");
988
+ return true;
989
+ } catch (_) {
990
+ return false;
991
+ }
992
+ }
993
+
994
+ function slugify(name) {
995
+ return name
996
+ .toLowerCase()
997
+ .replace(/[^a-z0-9]+/g, "-")
998
+ .replace(/^-|-$/g, "")
999
+ || "project";
1000
+ }