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
package/lib/cli/close.js
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/** wwa close — session close checklist, cascade reminders, and project snapshot. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, statSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { resolve, join, dirname, relative } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { findEntryPoint, findSessionLog, readMethodVersion, packageRoot, safeWriteFile } from "./helpers.js";
|
|
7
|
+
import { findOrphans, detectNewFiles, inferCategory } from "../dependencies.js";
|
|
8
|
+
import { resolveTokensPath, resolveDocsMapPath, resolveIndexPath, resolveFeatureRegistryPath, loadBoundaries } from "../boundaries.js";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
export function register(program) {
|
|
14
|
+
program
|
|
15
|
+
.command("close [directory]")
|
|
16
|
+
.description("Show session close checklist, cascade reminders, and project snapshot")
|
|
17
|
+
.option("--json", "Output as JSON")
|
|
18
|
+
.option("--internal-registry", "Include docs/internal/doc-registry.yaml health in management output")
|
|
19
|
+
.action(async (directory, opts) => {
|
|
20
|
+
directory = directory || ".";
|
|
21
|
+
const d = resolve(directory);
|
|
22
|
+
|
|
23
|
+
const checklist = [];
|
|
24
|
+
const warnings = [];
|
|
25
|
+
|
|
26
|
+
// 1. Check SESSION-LOG.md exists
|
|
27
|
+
const sessionLog = findSessionLog(directory);
|
|
28
|
+
if (sessionLog) {
|
|
29
|
+
checklist.push({ item: "SESSION-LOG.md exists", ok: true });
|
|
30
|
+
} else {
|
|
31
|
+
checklist.push({ item: "SESSION-LOG.md exists", ok: false });
|
|
32
|
+
warnings.push("No SESSION-LOG.md found — create one to track session metrics");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Check STATE.md exists and has current position
|
|
36
|
+
const statePath = join(d, "STATE.md");
|
|
37
|
+
if (existsSync(statePath)) {
|
|
38
|
+
checklist.push({ item: "STATE.md exists", ok: true });
|
|
39
|
+
} else {
|
|
40
|
+
checklist.push({ item: "STATE.md exists", ok: false });
|
|
41
|
+
warnings.push("No STATE.md found — decisions may be lost between sessions");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Check PLAN.md for incomplete steps
|
|
45
|
+
const planPath = join(d, "PLAN.md");
|
|
46
|
+
if (existsSync(planPath)) {
|
|
47
|
+
const planContent = readFileSync(planPath, "utf-8");
|
|
48
|
+
const todoSteps = (planContent.match(/\|\s*todo\s*\|/gi) || []).length;
|
|
49
|
+
if (todoSteps > 0) {
|
|
50
|
+
checklist.push({
|
|
51
|
+
item: `PLAN.md has ${todoSteps} incomplete step(s)`,
|
|
52
|
+
ok: false,
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
checklist.push({ item: "PLAN.md — all steps complete", ok: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Check Management files
|
|
60
|
+
const digestPath = join(d, "Management", "DIGEST.md");
|
|
61
|
+
if (existsSync(digestPath)) {
|
|
62
|
+
checklist.push({ item: "Management/DIGEST.md exists", ok: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 5. Check SUMMARY.md
|
|
66
|
+
const summaryPath = join(d, "SUMMARY.md");
|
|
67
|
+
if (existsSync(summaryPath)) {
|
|
68
|
+
checklist.push({ item: "SUMMARY.md exists for audit entry", ok: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build cascade reminders from PROTOCOL.yaml or fallback to defaults.
|
|
72
|
+
// If PROTOCOL.yaml has an empty close_check list, fall back to defaults so
|
|
73
|
+
// the user always sees helpful reminders.
|
|
74
|
+
let cascadeReminders;
|
|
75
|
+
const protocolPath = join(d, ".context", "PROTOCOL.yaml");
|
|
76
|
+
if (existsSync(protocolPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const yaml = (await import("js-yaml")).default;
|
|
79
|
+
let raw = readFileSync(protocolPath, "utf-8");
|
|
80
|
+
const parsed = yaml.load(raw);
|
|
81
|
+
cascadeReminders = parsed?.close_check || getDefaultReminders();
|
|
82
|
+
// Update last_verified timestamp (comment-preserving)
|
|
83
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
84
|
+
const lvPattern = /^(last_verified:) .+$/m;
|
|
85
|
+
if (lvPattern.test(raw)) {
|
|
86
|
+
raw = raw.replace(lvPattern, `$1 "${today}"`);
|
|
87
|
+
} else {
|
|
88
|
+
const vLine = /^(version: .+)$/m;
|
|
89
|
+
raw = raw.replace(vLine, `$1\nlast_verified: "${today}"`);
|
|
90
|
+
}
|
|
91
|
+
safeWriteFile(protocolPath, raw, "utf-8");
|
|
92
|
+
} catch (_) {
|
|
93
|
+
cascadeReminders = getDefaultReminders();
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
cascadeReminders = getDefaultReminders();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If we still have no reminders (empty array), use defaults.
|
|
100
|
+
if (!cascadeReminders || cascadeReminders.length === 0) {
|
|
101
|
+
cascadeReminders = getDefaultReminders();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Compute project snapshot — methodology reference data
|
|
105
|
+
const snapshot = await computeProjectSnapshot(d);
|
|
106
|
+
|
|
107
|
+
// Update project doc-tokens.yaml regardless of output mode
|
|
108
|
+
const tokenUpdateResult = await updateProjectTokens(d, snapshot);
|
|
109
|
+
|
|
110
|
+
// Update INDEX.yaml metrics
|
|
111
|
+
const indexUpdateResult = await updateIndexMetrics(d, snapshot);
|
|
112
|
+
|
|
113
|
+
// Load internal registry if requested
|
|
114
|
+
let internalRegistry = null;
|
|
115
|
+
if (opts.internalRegistry) {
|
|
116
|
+
try {
|
|
117
|
+
const { loadInternalRegistry } = await import("./doc-review.js");
|
|
118
|
+
internalRegistry = await loadInternalRegistry(d);
|
|
119
|
+
if (!internalRegistry) {
|
|
120
|
+
console.log(" Note: No docs/internal/doc-registry.yaml found — run `wwa doc-review` to generate it.");
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// doc-review module not available — skip silently
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Always write management files (close-snapshot.yaml + STATUS.md) at close.
|
|
128
|
+
const writeResults = await writeManagementFiles(
|
|
129
|
+
d,
|
|
130
|
+
snapshot,
|
|
131
|
+
checklist,
|
|
132
|
+
cascadeReminders,
|
|
133
|
+
internalRegistry
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (opts.json) {
|
|
137
|
+
console.log(
|
|
138
|
+
JSON.stringify({ checklist, warnings, cascadeReminders, snapshot, tokensUpdated: !!tokenUpdateResult, indexUpdated: !!indexUpdateResult, writeResults }, null, 2)
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log("\n Session Close Checklist\n");
|
|
144
|
+
|
|
145
|
+
for (const item of checklist) {
|
|
146
|
+
const icon = item.ok ? "[x]" : "[ ]";
|
|
147
|
+
console.log(` ${icon} ${item.item}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (warnings.length > 0) {
|
|
151
|
+
console.log("\n Warnings:");
|
|
152
|
+
for (const w of warnings) {
|
|
153
|
+
console.log(` ! ${w}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log("\n Cascade reminders:");
|
|
158
|
+
for (const r of cascadeReminders) {
|
|
159
|
+
console.log(` - ${r}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Print project snapshot
|
|
163
|
+
printSnapshot(snapshot);
|
|
164
|
+
|
|
165
|
+
if (tokenUpdateResult) {
|
|
166
|
+
console.log("\n Updated doc-tokens.yaml with current project metrics.");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (indexUpdateResult) {
|
|
170
|
+
console.log(" Updated INDEX.yaml with current project metrics.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (writeResults) {
|
|
174
|
+
console.log("\n Management files written:");
|
|
175
|
+
if (writeResults.snapshotYaml) console.log(` - ${writeResults.snapshotYaml}`);
|
|
176
|
+
if (writeResults.statusMd) console.log(` - ${writeResults.statusMd}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("");
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Project snapshot — computed at close, chunked for large projects
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
async function computeProjectSnapshot(dir) {
|
|
188
|
+
const snapshot = {
|
|
189
|
+
project: {},
|
|
190
|
+
methodologyFiles: { present: [], missing: [] },
|
|
191
|
+
decisions: { count: 0, recent: [] },
|
|
192
|
+
sessions: { count: 0, latestDate: null },
|
|
193
|
+
features: [],
|
|
194
|
+
registry: null,
|
|
195
|
+
docsMap: null,
|
|
196
|
+
fileStats: {},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// --- Chunk 1: Project identity ---
|
|
200
|
+
const entryPoint = findEntryPoint(dir);
|
|
201
|
+
if (entryPoint) {
|
|
202
|
+
snapshot.project.entryPoint = relative(dir, entryPoint) || entryPoint;
|
|
203
|
+
snapshot.project.methodVersion = readMethodVersion(entryPoint);
|
|
204
|
+
const epContent = readFileSync(entryPoint, "utf-8");
|
|
205
|
+
const tierMatch = epContent.match(/tier:\s*(\S+)/);
|
|
206
|
+
if (tierMatch) snapshot.project.tier = tierMatch[1];
|
|
207
|
+
const modeMatch = epContent.match(/mode:\s*(\S+)/);
|
|
208
|
+
if (modeMatch) snapshot.project.interactionLevel = modeMatch[1];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const projectPath = join(dir, "PROJECT.md");
|
|
212
|
+
if (existsSync(projectPath)) {
|
|
213
|
+
const content = readFileSync(projectPath, "utf-8");
|
|
214
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
215
|
+
if (titleMatch) snapshot.project.name = titleMatch[1].trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const profilePath = join(dir, "PROJECT-PROFILE.md");
|
|
219
|
+
if (existsSync(profilePath)) {
|
|
220
|
+
const content = readFileSync(profilePath, "utf-8");
|
|
221
|
+
const typeMatch = content.match(/project.?type[:\s|]+(\S+)/i);
|
|
222
|
+
if (typeMatch) snapshot.project.type = typeMatch[1].toLowerCase();
|
|
223
|
+
const stageMatch = content.match(/lifecycle.?stage[:\s|]+(\S+)/i);
|
|
224
|
+
if (stageMatch) snapshot.project.lifecycleStage = stageMatch[1].toLowerCase();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Chunk 2: Methodology file inventory ---
|
|
228
|
+
const expectedFiles = [
|
|
229
|
+
"STATE.md", "PLAN.md", "ROADMAP.md", "SUMMARY.md",
|
|
230
|
+
"PROJECT.md", "PROJECT-PROFILE.md", "SESSION-LOG.md",
|
|
231
|
+
".context/BASE.md", ".context/METHODOLOGY.md",
|
|
232
|
+
".context/PROTOCOL.yaml", ".context/INDEX.yaml",
|
|
233
|
+
];
|
|
234
|
+
for (const f of expectedFiles) {
|
|
235
|
+
const p = join(dir, f);
|
|
236
|
+
if (existsSync(p)) {
|
|
237
|
+
const stat = statSync(p);
|
|
238
|
+
const lines = readFileSync(p, "utf-8").split("\n").length;
|
|
239
|
+
snapshot.methodologyFiles.present.push({ file: f, lines, bytes: stat.size });
|
|
240
|
+
// Track files approaching 300-line threshold
|
|
241
|
+
if (lines > 250) {
|
|
242
|
+
snapshot.fileStats[f] = { lines, nearThreshold: lines >= 280 };
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
snapshot.methodologyFiles.missing.push(f);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Chunk 3: Decisions from STATE.md ---
|
|
250
|
+
const statePath = join(dir, "STATE.md");
|
|
251
|
+
if (existsSync(statePath)) {
|
|
252
|
+
const content = readFileSync(statePath, "utf-8");
|
|
253
|
+
const decisionSection = content.match(
|
|
254
|
+
/##\s*Decisions?\s*\n([\s\S]*?)(?=\n##|\n$|$)/i
|
|
255
|
+
);
|
|
256
|
+
if (decisionSection) {
|
|
257
|
+
const rows = decisionSection[1].match(/\|[^|\n]+\|[^|\n]+\|/g) || [];
|
|
258
|
+
const decisions = [];
|
|
259
|
+
for (const row of rows) {
|
|
260
|
+
const cols = row.split("|").map((c) => c.trim()).filter((c) => c && !c.match(/^[-:]+$/));
|
|
261
|
+
if (cols.length >= 2 && !cols[0].toLowerCase().includes("date")) {
|
|
262
|
+
decisions.push({ date: cols[0], decision: cols.slice(1).join(" — ") });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
snapshot.decisions.count = decisions.length;
|
|
266
|
+
snapshot.decisions.recent = decisions.slice(-5); // last 5
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- Chunk 4: Session count from SESSION-LOG.md ---
|
|
271
|
+
const sessionLog = findSessionLog(dir);
|
|
272
|
+
if (sessionLog) {
|
|
273
|
+
const content = readFileSync(sessionLog, "utf-8");
|
|
274
|
+
const entries = content.match(/###\s+S\d+/g) || [];
|
|
275
|
+
snapshot.sessions.count = entries.length;
|
|
276
|
+
// Find the latest date
|
|
277
|
+
const dates = content.match(/###\s+S\d+\s*(?:—|--)\s*(\S+)/g) || [];
|
|
278
|
+
if (dates.length > 0) {
|
|
279
|
+
const lastDate = dates[dates.length - 1].match(/(\d{4}-\d{2}-\d{2})/);
|
|
280
|
+
if (lastDate) snapshot.sessions.latestDate = lastDate[1];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- Chunk 5: Feature registry (from project registry/ or installed package) ---
|
|
285
|
+
const resolvedRegistry = await resolveFeatureRegistryPath(dir);
|
|
286
|
+
const pkgRegistry = join(packageRoot, "docs", "internal", "feature-registry.yaml");
|
|
287
|
+
const registryPath = existsSync(resolvedRegistry) ? resolvedRegistry : (existsSync(pkgRegistry) ? pkgRegistry : null);
|
|
288
|
+
|
|
289
|
+
if (registryPath) {
|
|
290
|
+
try {
|
|
291
|
+
const yaml = (await import("js-yaml")).default;
|
|
292
|
+
const raw = readFileSync(registryPath, "utf-8");
|
|
293
|
+
const parsed = yaml.load(raw);
|
|
294
|
+
if (parsed) {
|
|
295
|
+
snapshot.registry = {
|
|
296
|
+
source: existsSync(localRegistry) ? "project" : "package",
|
|
297
|
+
version: parsed.version || null,
|
|
298
|
+
featureCount: parsed.features ? Object.keys(parsed.features).length : 0,
|
|
299
|
+
workflowCount: parsed.guided_workflows ? parsed.guided_workflows.length : 0,
|
|
300
|
+
};
|
|
301
|
+
// Collect active features from project entry point scoping
|
|
302
|
+
if (entryPoint) {
|
|
303
|
+
const epContent = readFileSync(entryPoint, "utf-8");
|
|
304
|
+
const featureRefs = epContent.match(/[A-Z]{2,5}-\d{2}/g) || [];
|
|
305
|
+
snapshot.features = [...new Set(featureRefs)].sort();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (_) {
|
|
309
|
+
// Registry parse failed — skip silently
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- Chunk 6: Docs map status ---
|
|
314
|
+
const docsMapPath = await resolveDocsMapPath(dir);
|
|
315
|
+
if (existsSync(docsMapPath)) {
|
|
316
|
+
const content = readFileSync(docsMapPath, "utf-8");
|
|
317
|
+
const inventoryRows = (content.match(/^\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|$/gm) || [])
|
|
318
|
+
.filter((r) => !r.includes("---") && !r.includes("Path") && !r.includes("Purpose"));
|
|
319
|
+
const mappingRows = (content.match(/^\|[^|]+\|[^|]+\|[^|]+\|$/gm) || [])
|
|
320
|
+
.filter((r) => !r.includes("---") && !r.includes("Project component") && !r.includes("When this"));
|
|
321
|
+
|
|
322
|
+
// Check which docs actually exist on disk
|
|
323
|
+
let docsExist = 0;
|
|
324
|
+
let docsTotal = 0;
|
|
325
|
+
for (const row of inventoryRows) {
|
|
326
|
+
const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
|
|
327
|
+
if (cols.length >= 1) {
|
|
328
|
+
docsTotal++;
|
|
329
|
+
if (existsSync(join(dir, cols[0]))) docsExist++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
snapshot.docsMap = {
|
|
334
|
+
inventoryCount: docsTotal,
|
|
335
|
+
docsOnDisk: docsExist,
|
|
336
|
+
docsMissing: docsTotal - docsExist,
|
|
337
|
+
componentMappings: mappingRows.length,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- Chunk 7: Dependency graph health ---
|
|
342
|
+
const docTokensPath = await resolveTokensPath(dir);
|
|
343
|
+
if (existsSync(docTokensPath)) {
|
|
344
|
+
try {
|
|
345
|
+
const yaml = (await import("js-yaml")).default;
|
|
346
|
+
const raw = readFileSync(docTokensPath, "utf-8");
|
|
347
|
+
const parsed = yaml.load(raw);
|
|
348
|
+
const docGraph = parsed?.doc_graph;
|
|
349
|
+
if (docGraph) {
|
|
350
|
+
const nodes = docGraph.nodes || [];
|
|
351
|
+
const edges = docGraph.edges || [];
|
|
352
|
+
|
|
353
|
+
// Scan for methodology files on disk that aren't in the graph
|
|
354
|
+
const currentFiles = snapshot.methodologyFiles.present.map((f) => f.file);
|
|
355
|
+
const newFiles = detectNewFiles(nodes, currentFiles);
|
|
356
|
+
const orphans = findOrphans(docGraph);
|
|
357
|
+
|
|
358
|
+
snapshot.dependencyHealth = {
|
|
359
|
+
nodeCount: nodes.length,
|
|
360
|
+
edgeCount: edges.length,
|
|
361
|
+
orphanCount: orphans.length,
|
|
362
|
+
newFiles,
|
|
363
|
+
lastScan: docGraph.last_scan || null,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
// doc-tokens.yaml parse error — skip dependency health
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return snapshot;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Display
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
function printSnapshot(snapshot) {
|
|
379
|
+
console.log("\n Project Snapshot\n");
|
|
380
|
+
|
|
381
|
+
// Project identity
|
|
382
|
+
if (snapshot.project.name) {
|
|
383
|
+
console.log(` Project: ${snapshot.project.name}`);
|
|
384
|
+
}
|
|
385
|
+
if (snapshot.project.type) {
|
|
386
|
+
console.log(` Type: ${snapshot.project.type} | Stage: ${snapshot.project.lifecycleStage || "unknown"}`);
|
|
387
|
+
}
|
|
388
|
+
if (snapshot.project.methodVersion) {
|
|
389
|
+
console.log(` Method: v${snapshot.project.methodVersion} | Tier: ${snapshot.project.tier || "unknown"}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// File inventory
|
|
393
|
+
const present = snapshot.methodologyFiles.present.length;
|
|
394
|
+
const missing = snapshot.methodologyFiles.missing.length;
|
|
395
|
+
console.log(`\n Methodology files: ${present} present, ${missing} missing`);
|
|
396
|
+
if (missing > 0) {
|
|
397
|
+
console.log(` Missing: ${snapshot.methodologyFiles.missing.join(", ")}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Threshold warnings
|
|
401
|
+
const nearThreshold = Object.entries(snapshot.fileStats)
|
|
402
|
+
.filter(([, s]) => s.nearThreshold);
|
|
403
|
+
if (nearThreshold.length > 0) {
|
|
404
|
+
console.log(` Scale warnings:`);
|
|
405
|
+
for (const [file, stats] of nearThreshold) {
|
|
406
|
+
console.log(` ! ${file}: ${stats.lines} lines (threshold: 300)`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Decisions
|
|
411
|
+
if (snapshot.decisions.count > 0) {
|
|
412
|
+
console.log(`\n Decisions: ${snapshot.decisions.count} total`);
|
|
413
|
+
if (snapshot.decisions.recent.length > 0) {
|
|
414
|
+
console.log(` Recent:`);
|
|
415
|
+
for (const d of snapshot.decisions.recent) {
|
|
416
|
+
const text = d.decision.length > 60 ? d.decision.slice(0, 57) + "..." : d.decision;
|
|
417
|
+
console.log(` ${d.date}: ${text}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Sessions
|
|
423
|
+
if (snapshot.sessions.count > 0) {
|
|
424
|
+
console.log(`\n Sessions: ${snapshot.sessions.count} logged (latest: ${snapshot.sessions.latestDate || "unknown"})`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Registry
|
|
428
|
+
if (snapshot.registry) {
|
|
429
|
+
console.log(`\n Registry: ${snapshot.registry.source} (${snapshot.registry.featureCount} features, ${snapshot.registry.workflowCount} workflows)`);
|
|
430
|
+
}
|
|
431
|
+
if (snapshot.features.length > 0) {
|
|
432
|
+
console.log(` Features referenced: ${snapshot.features.length} (${snapshot.features.slice(0, 8).join(", ")}${snapshot.features.length > 8 ? "..." : ""})`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Docs
|
|
436
|
+
if (snapshot.docsMap) {
|
|
437
|
+
const dm = snapshot.docsMap;
|
|
438
|
+
console.log(`\n Docs: ${dm.docsOnDisk}/${dm.inventoryCount} exist | ${dm.componentMappings} component mappings`);
|
|
439
|
+
if (dm.docsMissing > 0) {
|
|
440
|
+
console.log(` Missing docs: ${dm.docsMissing} (run \`wwa docs\` for details)`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Dependencies
|
|
445
|
+
if (snapshot.dependencyHealth) {
|
|
446
|
+
const dh = snapshot.dependencyHealth;
|
|
447
|
+
console.log(`\n Dependencies: ${dh.nodeCount} nodes, ${dh.edgeCount} edges, ${dh.orphanCount} orphans`);
|
|
448
|
+
if (dh.newFiles.length > 0) {
|
|
449
|
+
console.log(` New files detected: ${dh.newFiles.join(", ")}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Comment-preserving YAML update helpers
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Update scalar values in a named YAML section, preserving comments and structure.
|
|
460
|
+
* Existing keys are updated in-place; missing keys are appended at section end.
|
|
461
|
+
*/
|
|
462
|
+
function patchYamlSection(raw, sectionName, updates) {
|
|
463
|
+
let result = raw;
|
|
464
|
+
const newKeys = [];
|
|
465
|
+
|
|
466
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
467
|
+
const formatted = typeof value === "number" ? String(value) : `"${value}"`;
|
|
468
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
469
|
+
const regex = new RegExp(`^( ${escaped}:) .+$`, "m");
|
|
470
|
+
if (regex.test(result)) {
|
|
471
|
+
result = result.replace(regex, `$1 ${formatted}`);
|
|
472
|
+
} else {
|
|
473
|
+
newKeys.push(` ${key}: ${formatted}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (newKeys.length > 0) {
|
|
478
|
+
const sectionIdx = result.search(new RegExp(`^${sectionName}:`, "m"));
|
|
479
|
+
if (sectionIdx !== -1) {
|
|
480
|
+
const afterSection = result.slice(sectionIdx);
|
|
481
|
+
const lines = afterSection.split("\n");
|
|
482
|
+
let offset = sectionIdx;
|
|
483
|
+
let insertAt = -1;
|
|
484
|
+
for (let i = 1; i < lines.length; i++) {
|
|
485
|
+
offset += lines[i - 1].length + 1;
|
|
486
|
+
if (lines[i].length > 0 && !lines[i].startsWith(" ") && !lines[i].startsWith("#")) {
|
|
487
|
+
insertAt = offset;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (insertAt === -1) insertAt = result.length;
|
|
492
|
+
result = result.slice(0, insertAt) + newKeys.join("\n") + "\n" + result.slice(insertAt);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Append a node entry to doc_graph.nodes array, preserving existing content.
|
|
501
|
+
* Inserts before the " edges:" line.
|
|
502
|
+
*/
|
|
503
|
+
function appendDocGraphNode(raw, node) {
|
|
504
|
+
const edgesIdx = raw.search(/^ edges:/m);
|
|
505
|
+
if (edgesIdx === -1) return raw;
|
|
506
|
+
const entry = ` - path: "${node.path}"\n category: "${node.category}"\n role: "${node.role}"\n`;
|
|
507
|
+
return raw.slice(0, edgesIdx) + entry + raw.slice(edgesIdx);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// Update project doc-tokens.yaml
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
async function updateProjectTokens(dir, snapshot) {
|
|
515
|
+
const tokensPath = await resolveTokensPath(dir);
|
|
516
|
+
if (!existsSync(tokensPath)) return null;
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const yaml = (await import("js-yaml")).default;
|
|
520
|
+
let raw = readFileSync(tokensPath, "utf-8");
|
|
521
|
+
const parsed = yaml.load(raw) || {};
|
|
522
|
+
const tokens = parsed.tokens || {};
|
|
523
|
+
|
|
524
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
525
|
+
tokens.last_close = today;
|
|
526
|
+
|
|
527
|
+
// File counts — count methodology files (.md/.yaml/.yml) in project
|
|
528
|
+
let methodologyCount = 0;
|
|
529
|
+
let docsCount = 0;
|
|
530
|
+
let contextCount = 0;
|
|
531
|
+
const countFiles = (base) => {
|
|
532
|
+
if (!existsSync(base)) return;
|
|
533
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
534
|
+
const p = join(base, entry.name);
|
|
535
|
+
if (entry.isDirectory()) {
|
|
536
|
+
// Track docs/ and .context/ separately
|
|
537
|
+
if (entry.name === "docs" && base === dir) {
|
|
538
|
+
docsCount += countDir(p);
|
|
539
|
+
} else if (entry.name === ".context" && base === dir) {
|
|
540
|
+
contextCount += countDir(p);
|
|
541
|
+
} else {
|
|
542
|
+
countFiles(p);
|
|
543
|
+
}
|
|
544
|
+
} else if (/\.(md|yaml|yml)$/i.test(entry.name)) {
|
|
545
|
+
methodologyCount++;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const countDir = (d) => {
|
|
550
|
+
let c = 0;
|
|
551
|
+
if (!existsSync(d)) return c;
|
|
552
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
553
|
+
if (entry.isDirectory()) c += countDir(join(d, entry.name));
|
|
554
|
+
else if (/\.(md|yaml|yml)$/i.test(entry.name)) c++;
|
|
555
|
+
}
|
|
556
|
+
return c;
|
|
557
|
+
};
|
|
558
|
+
countFiles(dir);
|
|
559
|
+
tokens.methodology_file_count = methodologyCount;
|
|
560
|
+
tokens.docs_file_count = docsCount;
|
|
561
|
+
tokens.context_file_count = contextCount;
|
|
562
|
+
|
|
563
|
+
// Session metrics
|
|
564
|
+
tokens.session_count = snapshot.sessions.count;
|
|
565
|
+
tokens.decision_count = snapshot.decisions.count;
|
|
566
|
+
tokens.total_features_referenced = snapshot.features.length;
|
|
567
|
+
|
|
568
|
+
// Scale indicators
|
|
569
|
+
let nearThreshold = 0;
|
|
570
|
+
let overThreshold = 0;
|
|
571
|
+
for (const f of snapshot.methodologyFiles.present) {
|
|
572
|
+
if (f.lines >= 300) overThreshold++;
|
|
573
|
+
else if (f.lines >= 250) nearThreshold++;
|
|
574
|
+
}
|
|
575
|
+
tokens.files_near_threshold = nearThreshold;
|
|
576
|
+
tokens.files_over_threshold = overThreshold;
|
|
577
|
+
|
|
578
|
+
// Docs coverage
|
|
579
|
+
if (snapshot.docsMap) {
|
|
580
|
+
tokens.docs_inventory_count = snapshot.docsMap.inventoryCount;
|
|
581
|
+
tokens.docs_on_disk = snapshot.docsMap.docsOnDisk;
|
|
582
|
+
tokens.docs_missing = snapshot.docsMap.docsMissing;
|
|
583
|
+
tokens.component_mappings = snapshot.docsMap.componentMappings;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Update doc_graph.last_scan and add new file nodes (comment-preserving)
|
|
587
|
+
if (parsed.doc_graph && snapshot.dependencyHealth) {
|
|
588
|
+
const lsPattern = /^( last_scan:) .+$/m;
|
|
589
|
+
if (lsPattern.test(raw)) {
|
|
590
|
+
raw = raw.replace(lsPattern, `$1 "${today}"`);
|
|
591
|
+
}
|
|
592
|
+
const newFiles = snapshot.dependencyHealth.newFiles || [];
|
|
593
|
+
// Filter against scan.include boundaries to avoid adding non-docs files (I3/R3)
|
|
594
|
+
const bounds = await loadBoundaries(dir);
|
|
595
|
+
const scanInclude = bounds.scan?.include || [];
|
|
596
|
+
const filteredNew = newFiles.filter((f) =>
|
|
597
|
+
scanInclude.some((prefix) => f.replace(/\\/g, "/").startsWith(prefix))
|
|
598
|
+
);
|
|
599
|
+
for (const f of filteredNew) {
|
|
600
|
+
raw = appendDocGraphNode(raw, {
|
|
601
|
+
path: f,
|
|
602
|
+
category: inferCategory(f),
|
|
603
|
+
role: "detected at close — review during next session",
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Apply token updates to raw YAML (comment-preserving — fixes I2/R2)
|
|
609
|
+
raw = patchYamlSection(raw, "tokens", tokens);
|
|
610
|
+
|
|
611
|
+
safeWriteFile(tokensPath, raw, "utf-8");
|
|
612
|
+
return tokens;
|
|
613
|
+
} catch (_) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Write management files — YAML snapshot + STATUS.md update
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
async function writeManagementFiles(dir, snapshot, checklist, cascadeReminders, internalRegistry) {
|
|
623
|
+
const results = {};
|
|
624
|
+
const mgmtDir = join(dir, "Management");
|
|
625
|
+
if (!existsSync(mgmtDir)) mkdirSync(mgmtDir, { recursive: true });
|
|
626
|
+
|
|
627
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
628
|
+
|
|
629
|
+
// --- 1. Write close-snapshot.yaml ---
|
|
630
|
+
try {
|
|
631
|
+
const yaml = (await import("js-yaml")).default;
|
|
632
|
+
const snapshotData = {
|
|
633
|
+
generated: today,
|
|
634
|
+
generator: "wwa close --write",
|
|
635
|
+
project: snapshot.project,
|
|
636
|
+
methodology_files: {
|
|
637
|
+
present: snapshot.methodologyFiles.present.map(f => f.file),
|
|
638
|
+
missing: snapshot.methodologyFiles.missing,
|
|
639
|
+
count: snapshot.methodologyFiles.present.length,
|
|
640
|
+
},
|
|
641
|
+
decisions: {
|
|
642
|
+
total: snapshot.decisions.count,
|
|
643
|
+
recent: snapshot.decisions.recent,
|
|
644
|
+
},
|
|
645
|
+
sessions: snapshot.sessions,
|
|
646
|
+
features: snapshot.features,
|
|
647
|
+
registry: snapshot.registry,
|
|
648
|
+
docs: snapshot.docsMap,
|
|
649
|
+
scale_warnings: Object.entries(snapshot.fileStats)
|
|
650
|
+
.filter(([, s]) => s.nearThreshold)
|
|
651
|
+
.map(([file, s]) => ({ file, lines: s.lines })),
|
|
652
|
+
checklist: checklist.map(c => ({ item: c.item, ok: c.ok })),
|
|
653
|
+
cascade_reminders: cascadeReminders,
|
|
654
|
+
dependency_health: snapshot.dependencyHealth || null,
|
|
655
|
+
internal_registry: internalRegistry ? {
|
|
656
|
+
generated: internalRegistry.generated || null,
|
|
657
|
+
health: internalRegistry.health || null,
|
|
658
|
+
node_count: internalRegistry.dependency_graph?.nodes?.length || 0,
|
|
659
|
+
edge_count: internalRegistry.dependency_graph?.edges?.length || 0,
|
|
660
|
+
} : null,
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const yamlOutput = yaml.dump(snapshotData, { lineWidth: 120, noRefs: true, quotingType: '"' });
|
|
664
|
+
const header = [
|
|
665
|
+
"# Close Snapshot — structured session data for management and case studies",
|
|
666
|
+
`# Generated: ${today} by wwa close --write`,
|
|
667
|
+
"# Consumed by: wwa casestudy, agent close-check, management review",
|
|
668
|
+
"",
|
|
669
|
+
].join("\n");
|
|
670
|
+
|
|
671
|
+
const snapshotPath = join(mgmtDir, "close-snapshot.yaml");
|
|
672
|
+
safeWriteFile(snapshotPath, header + yamlOutput, "utf-8");
|
|
673
|
+
results.snapshotYaml = "Management/close-snapshot.yaml";
|
|
674
|
+
} catch (_) {
|
|
675
|
+
// YAML write failed — continue with .md
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// --- 2. Update STATUS.md with synthesized insights ---
|
|
679
|
+
try {
|
|
680
|
+
const statusPath = join(mgmtDir, "STATUS.md");
|
|
681
|
+
const status = generateStatusMd(snapshot, today, internalRegistry);
|
|
682
|
+
safeWriteFile(statusPath, status, "utf-8");
|
|
683
|
+
results.statusMd = "Management/STATUS.md";
|
|
684
|
+
} catch (_) {
|
|
685
|
+
// STATUS.md write failed
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return results;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function generateStatusMd(snapshot, today, internalRegistry) {
|
|
692
|
+
const lines = [];
|
|
693
|
+
const p = snapshot.project;
|
|
694
|
+
|
|
695
|
+
lines.push(
|
|
696
|
+
"# Project Status",
|
|
697
|
+
"",
|
|
698
|
+
`<!-- Updated by wwa close --write on ${today} -->`,
|
|
699
|
+
"",
|
|
700
|
+
"## Current state",
|
|
701
|
+
"",
|
|
702
|
+
"| Field | Value |",
|
|
703
|
+
"|-------|-------|",
|
|
704
|
+
`| Phase | ${p.lifecycleStage || "unknown"} |`,
|
|
705
|
+
`| Status | active |`,
|
|
706
|
+
`| Active work | ${p.name || "unknown"} — ongoing |`,
|
|
707
|
+
`| Last milestone | Session ${snapshot.sessions.count} (${snapshot.sessions.latestDate || today}) |`,
|
|
708
|
+
`| Next milestone | — |`,
|
|
709
|
+
""
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
// Recent decisions
|
|
713
|
+
lines.push("## Recent decisions", "");
|
|
714
|
+
if (snapshot.decisions.recent.length > 0) {
|
|
715
|
+
lines.push("| Date | Decision | Impact |", "|------|----------|--------|");
|
|
716
|
+
for (const d of snapshot.decisions.recent) {
|
|
717
|
+
const text = d.decision.length > 80 ? d.decision.slice(0, 77) + "..." : d.decision;
|
|
718
|
+
lines.push(`| ${d.date} | ${text} | — |`);
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
lines.push("No decisions recorded yet.");
|
|
722
|
+
}
|
|
723
|
+
lines.push("");
|
|
724
|
+
|
|
725
|
+
// Blockers & risks
|
|
726
|
+
lines.push(
|
|
727
|
+
"## Blockers & risks",
|
|
728
|
+
"",
|
|
729
|
+
"| Type | Description | Since | Severity |",
|
|
730
|
+
"|------|-------------|-------|----------|"
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const nearThreshold = Object.entries(snapshot.fileStats).filter(([, s]) => s.nearThreshold);
|
|
734
|
+
if (nearThreshold.length > 0) {
|
|
735
|
+
for (const [file, stats] of nearThreshold) {
|
|
736
|
+
lines.push(`| risk | ${file} at ${stats.lines}/300 lines — approaching split threshold | ${today} | medium |`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (snapshot.methodologyFiles.missing.length > 0) {
|
|
740
|
+
lines.push(`| risk | Missing methodology files: ${snapshot.methodologyFiles.missing.join(", ")} | ${today} | low |`);
|
|
741
|
+
}
|
|
742
|
+
lines.push("");
|
|
743
|
+
|
|
744
|
+
// Health indicators
|
|
745
|
+
const present = snapshot.methodologyFiles.present.length;
|
|
746
|
+
const total = present + snapshot.methodologyFiles.missing.length;
|
|
747
|
+
const coverage = total > 0 ? Math.round((present / total) * 100) : 0;
|
|
748
|
+
const docsCoverage = snapshot.docsMap
|
|
749
|
+
? `${snapshot.docsMap.docsOnDisk}/${snapshot.docsMap.inventoryCount} docs exist`
|
|
750
|
+
: "no docs map";
|
|
751
|
+
|
|
752
|
+
lines.push(
|
|
753
|
+
"## Health indicators",
|
|
754
|
+
"",
|
|
755
|
+
"| Indicator | Status | Notes |",
|
|
756
|
+
"|-----------|--------|-------|",
|
|
757
|
+
`| Methodology coverage | ${coverage}% | ${present}/${total} files present |`,
|
|
758
|
+
`| Sessions logged | ${snapshot.sessions.count} | Latest: ${snapshot.sessions.latestDate || "none"} |`,
|
|
759
|
+
`| Decisions tracked | ${snapshot.decisions.count} | Last 5 shown above |`,
|
|
760
|
+
`| Features active | ${snapshot.features.length} | ${snapshot.features.slice(0, 5).join(", ") || "none"} |`,
|
|
761
|
+
`| Docs coverage | ${docsCoverage} | ${snapshot.docsMap ? snapshot.docsMap.componentMappings + " component mappings" : "—"} |`,
|
|
762
|
+
`| Scale health | ${nearThreshold.length === 0 ? "good" : nearThreshold.length + " file(s) near threshold"} | Threshold: 300 lines |`,
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
if (internalRegistry?.health) {
|
|
766
|
+
const rh = internalRegistry.health;
|
|
767
|
+
const regAge = internalRegistry.generated
|
|
768
|
+
? `generated ${internalRegistry.generated}`
|
|
769
|
+
: "unknown age";
|
|
770
|
+
lines.push(
|
|
771
|
+
`| Doc registry | ${rh.dependency_coverage || rh.dependencyCoverage || "—"} coverage | ${regAge} |`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
lines.push("");
|
|
776
|
+
|
|
777
|
+
lines.push(`---`, "", `*Updated by \`wwa close --write\` on ${today}.*`, "");
|
|
778
|
+
return lines.join("\n");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
// Default cascade reminders (fallback when PROTOCOL.yaml not present)
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
|
|
785
|
+
function getDefaultReminders() {
|
|
786
|
+
return [
|
|
787
|
+
"Decision made? → STATE.md decisions table",
|
|
788
|
+
"File changed? → check cascade table",
|
|
789
|
+
"Phase complete? → SUMMARY.md + STATE.md + ROADMAP.md",
|
|
790
|
+
"High effort? → SESSION-LOG.md + Management/DIGEST.md",
|
|
791
|
+
"Structure changed? → .context/BASE.md",
|
|
792
|
+
"Milestone? → Management/STATUS.md",
|
|
793
|
+
"Docs changed? → .context/DOCS-MAP.md + docs/index.md",
|
|
794
|
+
];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ---------------------------------------------------------------------------
|
|
798
|
+
// Update INDEX.yaml metrics
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
|
|
801
|
+
async function updateIndexMetrics(dir, snapshot) {
|
|
802
|
+
const indexPath = await resolveIndexPath(dir);
|
|
803
|
+
if (!existsSync(indexPath)) return null;
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
const yaml = (await import("js-yaml")).default;
|
|
807
|
+
const raw = readFileSync(indexPath, "utf-8");
|
|
808
|
+
const parsed = yaml.load(raw) || {};
|
|
809
|
+
const metrics = parsed.metrics || {};
|
|
810
|
+
|
|
811
|
+
metrics.last_close = new Date().toISOString().slice(0, 10);
|
|
812
|
+
metrics.sessions = snapshot.sessions.count;
|
|
813
|
+
metrics.decisions = snapshot.decisions.count;
|
|
814
|
+
|
|
815
|
+
let nearThreshold = 0;
|
|
816
|
+
let overThreshold = 0;
|
|
817
|
+
for (const f of snapshot.methodologyFiles.present) {
|
|
818
|
+
if (f.lines >= 300) overThreshold++;
|
|
819
|
+
else if (f.lines >= 250) nearThreshold++;
|
|
820
|
+
}
|
|
821
|
+
metrics.files_near_threshold = nearThreshold;
|
|
822
|
+
metrics.files_over_threshold = overThreshold;
|
|
823
|
+
|
|
824
|
+
// Comment-preserving update of metrics section
|
|
825
|
+
if (/^metrics:/m.test(raw)) {
|
|
826
|
+
safeWriteFile(indexPath, patchYamlSection(raw, "metrics", metrics), "utf-8");
|
|
827
|
+
} else {
|
|
828
|
+
// First time — append metrics section
|
|
829
|
+
const metricsYaml = "\nmetrics:\n" + Object.entries(metrics)
|
|
830
|
+
.map(([k, v]) => ` ${k}: ${typeof v === "number" ? v : `"${v}"`}`)
|
|
831
|
+
.join("\n") + "\n";
|
|
832
|
+
safeWriteFile(indexPath, raw.trimEnd() + metricsYaml, "utf-8");
|
|
833
|
+
}
|
|
834
|
+
return metrics;
|
|
835
|
+
} catch (_) {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|