@grainulation/wheat 1.0.3 → 1.0.5

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 (43) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -31
  3. package/bin/wheat.js +63 -40
  4. package/compiler/detect-sprints.js +108 -66
  5. package/compiler/generate-manifest.js +116 -69
  6. package/compiler/wheat-compiler.js +763 -471
  7. package/lib/compiler.js +11 -6
  8. package/lib/connect.js +273 -134
  9. package/lib/defaults.js +32 -0
  10. package/lib/disconnect.js +61 -40
  11. package/lib/guard.js +20 -17
  12. package/lib/index.js +8 -8
  13. package/lib/init.js +260 -142
  14. package/lib/install-prompt.js +26 -26
  15. package/lib/load-claims.js +88 -0
  16. package/lib/quickstart.js +225 -111
  17. package/lib/serve-mcp.js +495 -180
  18. package/lib/server.js +198 -111
  19. package/lib/stats.js +65 -39
  20. package/lib/status.js +65 -34
  21. package/lib/update.js +13 -11
  22. package/package.json +8 -4
  23. package/templates/claude.md +31 -17
  24. package/templates/commands/blind-spot.md +9 -2
  25. package/templates/commands/brief.md +11 -1
  26. package/templates/commands/calibrate.md +3 -1
  27. package/templates/commands/challenge.md +4 -1
  28. package/templates/commands/connect.md +12 -1
  29. package/templates/commands/evaluate.md +4 -0
  30. package/templates/commands/feedback.md +3 -1
  31. package/templates/commands/handoff.md +11 -7
  32. package/templates/commands/init.md +4 -1
  33. package/templates/commands/merge.md +4 -1
  34. package/templates/commands/next.md +1 -0
  35. package/templates/commands/present.md +3 -0
  36. package/templates/commands/prototype.md +2 -0
  37. package/templates/commands/pull.md +103 -0
  38. package/templates/commands/replay.md +8 -0
  39. package/templates/commands/research.md +1 -0
  40. package/templates/commands/resolve.md +4 -1
  41. package/templates/commands/status.md +4 -0
  42. package/templates/commands/sync.md +94 -0
  43. package/templates/commands/witness.md +6 -2
@@ -14,36 +14,43 @@
14
14
  * node wheat-compiler.js --diff A B # diff two compilation.json files
15
15
  */
16
16
 
17
- import fs from 'fs';
18
- import crypto from 'crypto';
19
- import path from 'path';
17
+ import fs from "fs";
18
+ import crypto from "crypto";
19
+ import path from "path";
20
20
 
21
- import { fileURLToPath } from 'url';
21
+ import { fileURLToPath } from "url";
22
22
 
23
23
  // Sprint detection — git-based, no config pointer needed (p013/f001)
24
- import { detectSprints } from './detect-sprints.js';
24
+ import { detectSprints } from "./detect-sprints.js";
25
25
  // Direct manifest generation — avoids subprocess + redundant detectSprints call
26
- import { buildManifest } from './generate-manifest.js';
26
+ import { buildManifest } from "./generate-manifest.js";
27
27
 
28
28
  const __filename = fileURLToPath(import.meta.url);
29
29
  const __dirname = path.dirname(__filename);
30
30
 
31
31
  // ─── --dir: target directory (defaults to script location for backwards compat) ─
32
- const _dirIdx = process.argv.indexOf('--dir');
33
- const TARGET_DIR = _dirIdx !== -1 && process.argv[_dirIdx + 1]
34
- ? path.resolve(process.argv[_dirIdx + 1])
35
- : __dirname;
32
+ const _dirIdx = process.argv.indexOf("--dir");
33
+ const TARGET_DIR =
34
+ _dirIdx !== -1 && process.argv[_dirIdx + 1]
35
+ ? path.resolve(process.argv[_dirIdx + 1])
36
+ : __dirname;
36
37
 
37
38
  // ─── Configuration ──────────────────────────────────────────────────────────
38
39
  /** @returns {{ dirs: Object<string, string>, compiler: Object<string, string> }} Merged config from wheat.config.json with defaults */
39
40
  function loadConfig(dir) {
40
- const configPath = path.join(dir, 'wheat.config.json');
41
+ const configPath = path.join(dir, "wheat.config.json");
41
42
  const defaults = {
42
- dirs: { output: 'output', research: 'research', prototypes: 'prototypes', evidence: 'evidence', templates: 'templates' },
43
- compiler: { claims: 'claims.json', compilation: 'compilation.json' },
43
+ dirs: {
44
+ output: "output",
45
+ research: "research",
46
+ prototypes: "prototypes",
47
+ evidence: "evidence",
48
+ templates: "templates",
49
+ },
50
+ compiler: { claims: "claims.json", compilation: "compilation.json" },
44
51
  };
45
52
  try {
46
- const raw = fs.readFileSync(configPath, 'utf8');
53
+ const raw = fs.readFileSync(configPath, "utf8");
47
54
  const config = JSON.parse(raw);
48
55
  return {
49
56
  dirs: { ...defaults.dirs, ...(config.dirs || {}) },
@@ -59,24 +66,44 @@ const config = loadConfig(TARGET_DIR);
59
66
  // ─── Evidence tier hierarchy (higher = stronger) ─────────────────────────────
60
67
  /** @type {Object<string, number>} Maps evidence tier names to numeric strength (1–5) */
61
68
  const EVIDENCE_TIERS = {
62
- stated: 1,
63
- web: 2,
69
+ stated: 1,
70
+ web: 2,
64
71
  documented: 3,
65
- tested: 4,
72
+ tested: 4,
66
73
  production: 5,
67
74
  };
68
75
 
69
76
  /** @type {string[]} Allowed claim type values */
70
- const VALID_TYPES = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
71
- const VALID_STATUSES = ['active', 'superseded', 'conflicted', 'resolved'];
72
- const VALID_PHASES = ['define', 'research', 'prototype', 'evaluate', 'feedback'];
73
- const PHASE_ORDER = ['init', 'define', 'research', 'prototype', 'evaluate', 'compile'];
77
+ const VALID_TYPES = [
78
+ "constraint",
79
+ "factual",
80
+ "estimate",
81
+ "risk",
82
+ "recommendation",
83
+ "feedback",
84
+ ];
85
+ const VALID_STATUSES = ["active", "superseded", "conflicted", "resolved"];
86
+ const VALID_PHASES = [
87
+ "define",
88
+ "research",
89
+ "prototype",
90
+ "evaluate",
91
+ "feedback",
92
+ ];
93
+ const PHASE_ORDER = [
94
+ "init",
95
+ "define",
96
+ "research",
97
+ "prototype",
98
+ "evaluate",
99
+ "compile",
100
+ ];
74
101
 
75
102
  // Burn-residue ID prefix — synthetic claims from /control-burn must never persist
76
- const BURN_PREFIX = 'burn-';
103
+ const BURN_PREFIX = "burn-";
77
104
 
78
105
  // ─── Schema Migration Framework [r237] ──────────────────────────────────────
79
- const CURRENT_SCHEMA = '1.0';
106
+ const CURRENT_SCHEMA = "1.0";
80
107
 
81
108
  /**
82
109
  * Ordered list of migration functions. Each entry migrates from one version to the next.
@@ -93,8 +120,8 @@ const SCHEMA_MIGRATIONS = [
93
120
  * Returns -1 if a < b, 0 if equal, 1 if a > b.
94
121
  */
95
122
  function compareVersions(a, b) {
96
- const pa = a.split('.').map(Number);
97
- const pb = b.split('.').map(Number);
123
+ const pa = a.split(".").map(Number);
124
+ const pb = b.split(".").map(Number);
98
125
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
99
126
  const na = pa[i] || 0;
100
127
  const nb = pb[i] || 0;
@@ -111,67 +138,94 @@ function compareVersions(a, b) {
111
138
  * - If schema_version < CURRENT_SCHEMA, runs migrations in order.
112
139
  */
113
140
  function checkAndMigrateSchema(claimsData) {
114
- const meta = claimsData.meta || {};
115
- const fileVersion = meta.schema_version || '1.0';
141
+ // schema_version lives at the JSON root (document envelope), not inside meta.
142
+ // init.js writes it at root; we read from root with fallback to meta for
143
+ // backwards compatibility with any files that stored it in meta.
144
+ const fileVersion =
145
+ claimsData.schema_version ||
146
+ (claimsData.meta || {}).schema_version ||
147
+ "1.0";
116
148
 
117
149
  // Future version — this compiler cannot handle it
118
150
  if (compareVersions(fileVersion, CURRENT_SCHEMA) > 0) {
119
151
  return {
120
152
  data: claimsData,
121
- errors: [{
122
- code: 'E_SCHEMA_VERSION',
123
- message: `claims.json uses schema v${fileVersion} but this compiler only supports up to v${CURRENT_SCHEMA}. Run: npx @grainulation/wheat@latest compile`,
124
- }],
153
+ errors: [
154
+ {
155
+ code: "E_SCHEMA_VERSION",
156
+ message: `claims.json uses schema v${fileVersion} but this compiler only supports up to v${CURRENT_SCHEMA}. Run: npx @grainulation/wheat@latest compile`,
157
+ },
158
+ ],
125
159
  };
126
160
  }
127
161
 
128
162
  // Run migrations if file version is behind current
129
163
  let currentVersion = fileVersion;
130
164
  for (const migration of SCHEMA_MIGRATIONS) {
131
- if (compareVersions(currentVersion, migration.from) === 0 &&
132
- compareVersions(currentVersion, CURRENT_SCHEMA) < 0) {
165
+ if (
166
+ compareVersions(currentVersion, migration.from) === 0 &&
167
+ compareVersions(currentVersion, CURRENT_SCHEMA) < 0
168
+ ) {
133
169
  claimsData = migration.migrate(claimsData);
134
170
  currentVersion = migration.to;
135
- if (!claimsData.meta) claimsData.meta = {};
136
- claimsData.meta.schema_version = currentVersion;
171
+ // Write schema_version at root level (document envelope convention)
172
+ claimsData.schema_version = currentVersion;
137
173
  }
138
174
  }
139
175
 
140
176
  return { data: claimsData, errors: [] };
141
177
  }
142
178
 
143
- export { CURRENT_SCHEMA, SCHEMA_MIGRATIONS, checkAndMigrateSchema, compareVersions };
179
+ export { CURRENT_SCHEMA, SCHEMA_MIGRATIONS, checkAndMigrateSchema };
180
+
181
+ // Internal utilities — exported for testing only. Not part of the public API
182
+ // surface and may be removed or changed without notice.
183
+ export const _internals = { compareVersions };
144
184
 
145
185
  // ─── Pass 1: Schema Validation (+ burn-residue safety check) ────────────────
146
186
  function validateSchema(claims) {
147
187
  const errors = [];
148
- const requiredFields = ['id', 'type', 'topic', 'content', 'source', 'evidence', 'status'];
188
+ const requiredFields = [
189
+ "id",
190
+ "type",
191
+ "topic",
192
+ "content",
193
+ "source",
194
+ "evidence",
195
+ "status",
196
+ ];
149
197
 
150
198
  claims.forEach((claim, i) => {
151
199
  // Burn-residue safety check: reject claims with burn- prefix
152
200
  if (claim.id && claim.id.startsWith(BURN_PREFIX)) {
153
201
  errors.push({
154
- code: 'E_BURN_RESIDUE',
202
+ code: "E_BURN_RESIDUE",
155
203
  message: `Claim ${claim.id} has burn- prefix — synthetic claims from /control-burn must not persist in claims.json. Remove it before compiling.`,
156
204
  claims: [claim.id],
157
205
  });
158
206
  }
159
207
 
160
- requiredFields.forEach(field => {
161
- if (claim[field] === undefined || claim[field] === null || claim[field] === '') {
208
+ requiredFields.forEach((field) => {
209
+ if (
210
+ claim[field] === undefined ||
211
+ claim[field] === null ||
212
+ claim[field] === ""
213
+ ) {
162
214
  errors.push({
163
- code: 'E_SCHEMA',
164
- message: `Claim ${claim.id || `[index ${i}]`} missing required field: ${field}`,
215
+ code: "E_SCHEMA",
216
+ message: `Claim ${
217
+ claim.id || `[index ${i}]`
218
+ } missing required field: ${field}`,
165
219
  claims: [claim.id || `index:${i}`],
166
220
  });
167
221
  }
168
222
  });
169
223
 
170
224
  // Check for duplicate IDs
171
- const dupes = claims.filter(c => c.id === claim.id);
225
+ const dupes = claims.filter((c) => c.id === claim.id);
172
226
  if (dupes.length > 1 && claims.indexOf(claim) === i) {
173
227
  errors.push({
174
- code: 'E_DUPLICATE_ID',
228
+ code: "E_DUPLICATE_ID",
175
229
  message: `Duplicate claim ID: ${claim.id}`,
176
230
  claims: [claim.id],
177
231
  });
@@ -185,27 +239,33 @@ function validateSchema(claims) {
185
239
  function validateTypes(claims) {
186
240
  const errors = [];
187
241
 
188
- claims.forEach(claim => {
242
+ claims.forEach((claim) => {
189
243
  if (!VALID_TYPES.includes(claim.type)) {
190
244
  errors.push({
191
- code: 'E_TYPE',
192
- message: `Claim ${claim.id}: invalid type "${claim.type}". Must be one of: ${VALID_TYPES.join(', ')}`,
245
+ code: "E_TYPE",
246
+ message: `Claim ${claim.id}: invalid type "${
247
+ claim.type
248
+ }". Must be one of: ${VALID_TYPES.join(", ")}`,
193
249
  claims: [claim.id],
194
250
  });
195
251
  }
196
252
 
197
253
  if (!Object.keys(EVIDENCE_TIERS).includes(claim.evidence)) {
198
254
  errors.push({
199
- code: 'E_EVIDENCE_TIER',
200
- message: `Claim ${claim.id}: invalid evidence tier "${claim.evidence}". Must be one of: ${Object.keys(EVIDENCE_TIERS).join(', ')}`,
255
+ code: "E_EVIDENCE_TIER",
256
+ message: `Claim ${claim.id}: invalid evidence tier "${
257
+ claim.evidence
258
+ }". Must be one of: ${Object.keys(EVIDENCE_TIERS).join(", ")}`,
201
259
  claims: [claim.id],
202
260
  });
203
261
  }
204
262
 
205
263
  if (!VALID_STATUSES.includes(claim.status)) {
206
264
  errors.push({
207
- code: 'E_STATUS',
208
- message: `Claim ${claim.id}: invalid status "${claim.status}". Must be one of: ${VALID_STATUSES.join(', ')}`,
265
+ code: "E_STATUS",
266
+ message: `Claim ${claim.id}: invalid status "${
267
+ claim.status
268
+ }". Must be one of: ${VALID_STATUSES.join(", ")}`,
209
269
  claims: [claim.id],
210
270
  });
211
271
  }
@@ -217,17 +277,20 @@ function validateTypes(claims) {
217
277
  // ─── Pass 3: Evidence Tier Sorting (deterministic: tier → id) ────────────────
218
278
  function sortByEvidenceTier(claims) {
219
279
  return [...claims].sort((a, b) => {
220
- const tierDiff = (EVIDENCE_TIERS[b.evidence] || 0) - (EVIDENCE_TIERS[a.evidence] || 0);
280
+ const tierDiff =
281
+ (EVIDENCE_TIERS[b.evidence] || 0) - (EVIDENCE_TIERS[a.evidence] || 0);
221
282
  if (tierDiff !== 0) return tierDiff;
222
283
  // Deterministic tiebreak: lexicographic by claim ID (stable across runs)
223
- return (a.id || '').localeCompare(b.id || '');
284
+ return (a.id || "").localeCompare(b.id || "");
224
285
  });
225
286
  }
226
287
 
227
288
  // ─── Pass 4: Conflict Detection ──────────────────────────────────────────────
228
289
  function detectConflicts(claims) {
229
290
  const conflicts = [];
230
- const activeClaims = claims.filter(c => c.status === 'active' || c.status === 'conflicted');
291
+ const activeClaims = claims.filter(
292
+ (c) => c.status === "active" || c.status === "conflicted"
293
+ );
231
294
 
232
295
  for (let i = 0; i < activeClaims.length; i++) {
233
296
  for (let j = i + 1; j < activeClaims.length; j++) {
@@ -251,12 +314,12 @@ function autoResolve(claims, conflicts) {
251
314
  const resolved = [];
252
315
  const unresolved = [];
253
316
 
254
- conflicts.forEach(conflict => {
255
- const claimA = claims.find(c => c.id === conflict.claimA);
256
- const claimB = claims.find(c => c.id === conflict.claimB);
317
+ conflicts.forEach((conflict) => {
318
+ const claimA = claims.find((c) => c.id === conflict.claimA);
319
+ const claimB = claims.find((c) => c.id === conflict.claimB);
257
320
 
258
321
  if (!claimA || !claimB) {
259
- unresolved.push({ ...conflict, reason: 'claim_not_found' });
322
+ unresolved.push({ ...conflict, reason: "claim_not_found" });
260
323
  return;
261
324
  }
262
325
 
@@ -269,7 +332,7 @@ function autoResolve(claims, conflicts) {
269
332
  loser: claimB.id,
270
333
  reason: `evidence_tier: ${claimA.evidence} (${tierA}) > ${claimB.evidence} (${tierB})`,
271
334
  });
272
- claimB.status = 'superseded';
335
+ claimB.status = "superseded";
273
336
  claimB.resolved_by = claimA.id;
274
337
  } else if (tierB > tierA) {
275
338
  resolved.push({
@@ -277,7 +340,7 @@ function autoResolve(claims, conflicts) {
277
340
  loser: claimA.id,
278
341
  reason: `evidence_tier: ${claimB.evidence} (${tierB}) > ${claimA.evidence} (${tierA})`,
279
342
  });
280
- claimA.status = 'superseded';
343
+ claimA.status = "superseded";
281
344
  claimA.resolved_by = claimB.id;
282
345
  } else {
283
346
  // Same evidence tier — cannot auto-resolve
@@ -287,8 +350,8 @@ function autoResolve(claims, conflicts) {
287
350
  topic: conflict.topic,
288
351
  reason: `same_evidence_tier: both ${claimA.evidence}`,
289
352
  });
290
- claimA.status = 'conflicted';
291
- claimB.status = 'conflicted';
353
+ claimA.status = "conflicted";
354
+ claimB.status = "conflicted";
292
355
  }
293
356
  });
294
357
 
@@ -298,15 +361,17 @@ function autoResolve(claims, conflicts) {
298
361
  // ─── Pass 6: Coverage Analysis (enhanced with source/type diversity + corroboration) ─
299
362
  function analyzeCoverage(claims) {
300
363
  const coverage = {};
301
- const activeClaims = claims.filter(c => c.status === 'active' || c.status === 'resolved');
364
+ const activeClaims = claims.filter(
365
+ (c) => c.status === "active" || c.status === "resolved"
366
+ );
302
367
 
303
- activeClaims.forEach(claim => {
368
+ activeClaims.forEach((claim) => {
304
369
  if (!claim.topic) return;
305
370
 
306
371
  if (!coverage[claim.topic]) {
307
372
  coverage[claim.topic] = {
308
373
  claims: 0,
309
- max_evidence: 'stated',
374
+ max_evidence: "stated",
310
375
  max_evidence_rank: 0,
311
376
  types: new Set(),
312
377
  claim_ids: [],
@@ -319,7 +384,7 @@ function analyzeCoverage(claims) {
319
384
  entry.claims++;
320
385
  entry.types.add(claim.type);
321
386
  entry.claim_ids.push(claim.id);
322
- if (claim.type === 'constraint' || claim.type === 'feedback') {
387
+ if (claim.type === "constraint" || claim.type === "feedback") {
323
388
  entry.constraint_count++;
324
389
  }
325
390
 
@@ -337,16 +402,22 @@ function analyzeCoverage(claims) {
337
402
 
338
403
  // Compute corroboration: how many other claims reference/support each claim
339
404
  const corroboration = {};
340
- const allClaims = claims.filter(c => c.status !== 'superseded');
341
- allClaims.forEach(claim => {
405
+ const allClaims = claims.filter((c) => c.status !== "superseded");
406
+ allClaims.forEach((claim) => {
342
407
  corroboration[claim.id] = 0;
343
408
  });
344
409
  // A claim corroborates another if it has source.witnessed_claim or source.challenged_claim
345
410
  // or shares the same topic and type with supporting relationship
346
- allClaims.forEach(claim => {
411
+ allClaims.forEach((claim) => {
347
412
  if (claim.source) {
348
- if (claim.source.witnessed_claim && corroboration[claim.source.witnessed_claim] !== undefined) {
349
- if (claim.source.relationship === 'full_support' || claim.source.relationship === 'partial_support') {
413
+ if (
414
+ claim.source.witnessed_claim &&
415
+ corroboration[claim.source.witnessed_claim] !== undefined
416
+ ) {
417
+ if (
418
+ claim.source.relationship === "full_support" ||
419
+ claim.source.relationship === "partial_support"
420
+ ) {
350
421
  corroboration[claim.source.witnessed_claim]++;
351
422
  }
352
423
  }
@@ -355,33 +426,36 @@ function analyzeCoverage(claims) {
355
426
 
356
427
  // Convert sets to arrays and compute status (deterministic key ordering)
357
428
  const result = {};
358
- Object.entries(coverage).sort(([a], [b]) => a.localeCompare(b)).forEach(([topic, entry]) => {
359
- let status = 'weak';
360
- if (entry.max_evidence_rank >= EVIDENCE_TIERS.tested) status = 'strong';
361
- else if (entry.max_evidence_rank >= EVIDENCE_TIERS.documented) status = 'moderate';
362
-
363
- // Type diversity: how many of the 6 possible types are present
364
- const allTypes = [...entry.types].sort();
365
- const missingTypes = VALID_TYPES.filter(t => !allTypes.includes(t));
366
-
367
- // Source origins (sorted for determinism)
368
- const sourceOrigins = [...entry.source_origins].sort();
369
-
370
- result[topic] = {
371
- claims: entry.claims,
372
- max_evidence: entry.max_evidence,
373
- status,
374
- types: allTypes,
375
- claim_ids: entry.claim_ids,
376
- constraint_count: entry.constraint_count,
377
- // New: source diversity
378
- source_origins: sourceOrigins,
379
- source_count: sourceOrigins.length,
380
- // New: type diversity
381
- type_diversity: allTypes.length,
382
- missing_types: missingTypes,
383
- };
384
- });
429
+ Object.entries(coverage)
430
+ .sort(([a], [b]) => a.localeCompare(b))
431
+ .forEach(([topic, entry]) => {
432
+ let status = "weak";
433
+ if (entry.max_evidence_rank >= EVIDENCE_TIERS.tested) status = "strong";
434
+ else if (entry.max_evidence_rank >= EVIDENCE_TIERS.documented)
435
+ status = "moderate";
436
+
437
+ // Type diversity: how many of the 6 possible types are present
438
+ const allTypes = [...entry.types].sort();
439
+ const missingTypes = VALID_TYPES.filter((t) => !allTypes.includes(t));
440
+
441
+ // Source origins (sorted for determinism)
442
+ const sourceOrigins = [...entry.source_origins].sort();
443
+
444
+ result[topic] = {
445
+ claims: entry.claims,
446
+ max_evidence: entry.max_evidence,
447
+ status,
448
+ types: allTypes,
449
+ claim_ids: entry.claim_ids,
450
+ constraint_count: entry.constraint_count,
451
+ // New: source diversity
452
+ source_origins: sourceOrigins,
453
+ source_count: sourceOrigins.length,
454
+ // New: type diversity
455
+ type_diversity: allTypes.length,
456
+ missing_types: missingTypes,
457
+ };
458
+ });
385
459
 
386
460
  return { coverage: result, corroboration };
387
461
  }
@@ -391,9 +465,9 @@ function checkReadiness(errors, unresolvedConflicts, coverage) {
391
465
  const blockers = [...errors];
392
466
 
393
467
  // Unresolved conflicts are blockers
394
- unresolvedConflicts.forEach(conflict => {
468
+ unresolvedConflicts.forEach((conflict) => {
395
469
  blockers.push({
396
- code: 'E_CONFLICT',
470
+ code: "E_CONFLICT",
397
471
  message: `Unresolved conflict between ${conflict.claimA} and ${conflict.claimB} (topic: ${conflict.topic}) — ${conflict.reason}`,
398
472
  claims: [conflict.claimA, conflict.claimB],
399
473
  });
@@ -401,43 +475,49 @@ function checkReadiness(errors, unresolvedConflicts, coverage) {
401
475
 
402
476
  // Weak coverage is a warning, not a blocker (sorted for determinism)
403
477
  const warnings = [];
404
- Object.entries(coverage).sort(([a], [b]) => a.localeCompare(b)).forEach(([topic, entry]) => {
405
- if (entry.status === 'weak') {
406
- // Constraint-dominated topics (>50% constraint/feedback) get a softer warning
407
- const constraintRatio = (entry.constraint_count || 0) / entry.claims;
408
- if (constraintRatio > 0.5) {
478
+ Object.entries(coverage)
479
+ .sort(([a], [b]) => a.localeCompare(b))
480
+ .forEach(([topic, entry]) => {
481
+ if (entry.status === "weak") {
482
+ // Constraint-dominated topics (>50% constraint/feedback) get a softer warning
483
+ const constraintRatio = (entry.constraint_count || 0) / entry.claims;
484
+ if (constraintRatio > 0.5) {
485
+ warnings.push({
486
+ code: "W_CONSTRAINT_ONLY",
487
+ message: `Topic "${topic}" is constraint-dominated (${entry.constraint_count}/${entry.claims} claims are constraints/feedback) — stated-level evidence is expected`,
488
+ claims: entry.claim_ids,
489
+ });
490
+ } else {
491
+ warnings.push({
492
+ code: "W_WEAK_EVIDENCE",
493
+ message: `Topic "${topic}" has only ${entry.max_evidence}-level evidence (${entry.claims} claims)`,
494
+ claims: entry.claim_ids,
495
+ });
496
+ }
497
+ }
498
+
499
+ // Type monoculture warning
500
+ if (entry.type_diversity < 2 && entry.claims >= 1) {
409
501
  warnings.push({
410
- code: 'W_CONSTRAINT_ONLY',
411
- message: `Topic "${topic}" is constraint-dominated (${entry.constraint_count}/${entry.claims} claims are constraints/feedback) — stated-level evidence is expected`,
502
+ code: "W_TYPE_MONOCULTURE",
503
+ message: `Topic "${topic}" has only ${
504
+ entry.type_diversity
505
+ } claim type(s): ${entry.types.join(
506
+ ", "
507
+ )}. Missing: ${entry.missing_types.join(", ")}`,
412
508
  claims: entry.claim_ids,
413
509
  });
414
- } else {
510
+ }
511
+
512
+ // Echo chamber warning: all claims from single source origin
513
+ if (entry.source_count === 1 && entry.claims >= 3) {
415
514
  warnings.push({
416
- code: 'W_WEAK_EVIDENCE',
417
- message: `Topic "${topic}" has only ${entry.max_evidence}-level evidence (${entry.claims} claims)`,
515
+ code: "W_ECHO_CHAMBER",
516
+ message: `Topic "${topic}" has ${entry.claims} claims but all from a single source origin: ${entry.source_origins[0]}`,
418
517
  claims: entry.claim_ids,
419
518
  });
420
519
  }
421
- }
422
-
423
- // Type monoculture warning
424
- if (entry.type_diversity < 2 && entry.claims >= 1) {
425
- warnings.push({
426
- code: 'W_TYPE_MONOCULTURE',
427
- message: `Topic "${topic}" has only ${entry.type_diversity} claim type(s): ${entry.types.join(', ')}. Missing: ${entry.missing_types.join(', ')}`,
428
- claims: entry.claim_ids,
429
- });
430
- }
431
-
432
- // Echo chamber warning: all claims from single source origin
433
- if (entry.source_count === 1 && entry.claims >= 3) {
434
- warnings.push({
435
- code: 'W_ECHO_CHAMBER',
436
- message: `Topic "${topic}" has ${entry.claims} claims but all from a single source origin: ${entry.source_origins[0]}`,
437
- claims: entry.claim_ids,
438
- });
439
- }
440
- });
520
+ });
441
521
 
442
522
  return { blockers, warnings };
443
523
  }
@@ -445,8 +525,8 @@ function checkReadiness(errors, unresolvedConflicts, coverage) {
445
525
  // ─── Phase Summary ───────────────────────────────────────────────────────────
446
526
  function summarizePhases(claims) {
447
527
  const summary = {};
448
- VALID_PHASES.forEach(phase => {
449
- const phaseClaims = claims.filter(c => c.phase_added === phase);
528
+ VALID_PHASES.forEach((phase) => {
529
+ const phaseClaims = claims.filter((c) => c.phase_added === phase);
450
530
  summary[phase] = {
451
531
  claims: phaseClaims.length,
452
532
  complete: phaseClaims.length > 0,
@@ -457,17 +537,22 @@ function summarizePhases(claims) {
457
537
 
458
538
  // ─── Canonical JSON — key-order-independent serialization ────────────────────
459
539
  function canonicalJSON(obj) {
460
- if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
461
- if (Array.isArray(obj)) return '[' + obj.map(canonicalJSON).join(',') + ']';
540
+ if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
541
+ if (Array.isArray(obj)) return "[" + obj.map(canonicalJSON).join(",") + "]";
462
542
  const keys = Object.keys(obj).sort();
463
- return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
543
+ return (
544
+ "{" +
545
+ keys.map((k) => JSON.stringify(k) + ":" + canonicalJSON(obj[k])).join(",") +
546
+ "}"
547
+ );
464
548
  }
465
549
 
466
550
  // ─── Compilation Certificate ─────────────────────────────────────────────────
467
551
  function generateCertificate(claimsData, compilerVersion) {
468
- const hash = crypto.createHash('sha256')
552
+ const hash = crypto
553
+ .createHash("sha256")
469
554
  .update(canonicalJSON(claimsData))
470
- .digest('hex');
555
+ .digest("hex");
471
556
 
472
557
  return {
473
558
  input_hash: `sha256:${hash}`,
@@ -499,21 +584,25 @@ function diffCompilations(before, after) {
499
584
  };
500
585
 
501
586
  // Claim IDs
502
- const beforeIds = new Set((before.resolved_claims || []).map(c => c.id));
503
- const afterIds = new Set((after.resolved_claims || []).map(c => c.id));
587
+ const beforeIds = new Set((before.resolved_claims || []).map((c) => c.id));
588
+ const afterIds = new Set((after.resolved_claims || []).map((c) => c.id));
504
589
 
505
- afterIds.forEach(id => {
590
+ afterIds.forEach((id) => {
506
591
  if (!beforeIds.has(id)) delta.new_claims.push(id);
507
592
  });
508
- beforeIds.forEach(id => {
593
+ beforeIds.forEach((id) => {
509
594
  if (!afterIds.has(id)) delta.removed_claims.push(id);
510
595
  });
511
596
 
512
597
  // Status changes on claims that exist in both
513
598
  const beforeClaimsMap = {};
514
- (before.resolved_claims || []).forEach(c => { beforeClaimsMap[c.id] = c; });
599
+ (before.resolved_claims || []).forEach((c) => {
600
+ beforeClaimsMap[c.id] = c;
601
+ });
515
602
  const afterClaimsMap = {};
516
- (after.resolved_claims || []).forEach(c => { afterClaimsMap[c.id] = c; });
603
+ (after.resolved_claims || []).forEach((c) => {
604
+ afterClaimsMap[c.id] = c;
605
+ });
517
606
 
518
607
  for (const id of beforeIds) {
519
608
  if (afterIds.has(id)) {
@@ -528,43 +617,77 @@ function diffCompilations(before, after) {
528
617
  // Coverage changes
529
618
  const beforeCov = before.coverage || {};
530
619
  const afterCov = after.coverage || {};
531
- const allTopics = new Set([...Object.keys(beforeCov), ...Object.keys(afterCov)]);
532
- allTopics.forEach(topic => {
620
+ const allTopics = new Set([
621
+ ...Object.keys(beforeCov),
622
+ ...Object.keys(afterCov),
623
+ ]);
624
+ allTopics.forEach((topic) => {
533
625
  const bc = beforeCov[topic];
534
626
  const ac = afterCov[topic];
535
627
  if (!bc && ac) {
536
- delta.coverage_changes.push({ topic, type: 'added', after: ac });
628
+ delta.coverage_changes.push({ topic, type: "added", after: ac });
537
629
  } else if (bc && !ac) {
538
- delta.coverage_changes.push({ topic, type: 'removed', before: bc });
630
+ delta.coverage_changes.push({ topic, type: "removed", before: bc });
539
631
  } else if (bc && ac) {
540
632
  const changes = {};
541
- if (bc.max_evidence !== ac.max_evidence) changes.max_evidence = { from: bc.max_evidence, to: ac.max_evidence };
542
- if (bc.status !== ac.status) changes.status = { from: bc.status, to: ac.status };
543
- if (bc.claims !== ac.claims) changes.claims = { from: bc.claims, to: ac.claims };
633
+ if (bc.max_evidence !== ac.max_evidence)
634
+ changes.max_evidence = { from: bc.max_evidence, to: ac.max_evidence };
635
+ if (bc.status !== ac.status)
636
+ changes.status = { from: bc.status, to: ac.status };
637
+ if (bc.claims !== ac.claims)
638
+ changes.claims = { from: bc.claims, to: ac.claims };
544
639
  if (Object.keys(changes).length > 0) {
545
- delta.coverage_changes.push({ topic, type: 'changed', changes });
640
+ delta.coverage_changes.push({ topic, type: "changed", changes });
546
641
  }
547
642
  }
548
643
  });
549
644
 
550
645
  // Conflict graph changes
551
- const beforeResolved = new Set((before.conflict_graph?.resolved || []).map(r => `${r.winner}>${r.loser}`));
552
- const afterResolved = new Set((after.conflict_graph?.resolved || []).map(r => `${r.winner}>${r.loser}`));
553
- const beforeUnresolved = new Set((before.conflict_graph?.unresolved || []).map(u => `${u.claimA}|${u.claimB}`));
554
- const afterUnresolved = new Set((after.conflict_graph?.unresolved || []).map(u => `${u.claimA}|${u.claimB}`));
555
-
556
- afterResolved.forEach(r => { if (!beforeResolved.has(r)) delta.conflict_changes.new_resolved.push(r); });
557
- beforeResolved.forEach(r => { if (!afterResolved.has(r)) delta.conflict_changes.removed_resolved.push(r); });
558
- afterUnresolved.forEach(u => { if (!beforeUnresolved.has(u)) delta.conflict_changes.new_unresolved.push(u); });
559
- beforeUnresolved.forEach(u => { if (!afterUnresolved.has(u)) delta.conflict_changes.removed_unresolved.push(u); });
646
+ const beforeResolved = new Set(
647
+ (before.conflict_graph?.resolved || []).map((r) => `${r.winner}>${r.loser}`)
648
+ );
649
+ const afterResolved = new Set(
650
+ (after.conflict_graph?.resolved || []).map((r) => `${r.winner}>${r.loser}`)
651
+ );
652
+ const beforeUnresolved = new Set(
653
+ (before.conflict_graph?.unresolved || []).map(
654
+ (u) => `${u.claimA}|${u.claimB}`
655
+ )
656
+ );
657
+ const afterUnresolved = new Set(
658
+ (after.conflict_graph?.unresolved || []).map(
659
+ (u) => `${u.claimA}|${u.claimB}`
660
+ )
661
+ );
662
+
663
+ afterResolved.forEach((r) => {
664
+ if (!beforeResolved.has(r)) delta.conflict_changes.new_resolved.push(r);
665
+ });
666
+ beforeResolved.forEach((r) => {
667
+ if (!afterResolved.has(r)) delta.conflict_changes.removed_resolved.push(r);
668
+ });
669
+ afterUnresolved.forEach((u) => {
670
+ if (!beforeUnresolved.has(u)) delta.conflict_changes.new_unresolved.push(u);
671
+ });
672
+ beforeUnresolved.forEach((u) => {
673
+ if (!afterUnresolved.has(u))
674
+ delta.conflict_changes.removed_unresolved.push(u);
675
+ });
560
676
 
561
677
  // Meta changes
562
- if (before.status !== after.status) delta.meta_changes.status = { from: before.status, to: after.status };
563
- if ((before.sprint_meta?.phase) !== (after.sprint_meta?.phase)) {
564
- delta.meta_changes.phase = { from: before.sprint_meta?.phase, to: after.sprint_meta?.phase };
678
+ if (before.status !== after.status)
679
+ delta.meta_changes.status = { from: before.status, to: after.status };
680
+ if (before.sprint_meta?.phase !== after.sprint_meta?.phase) {
681
+ delta.meta_changes.phase = {
682
+ from: before.sprint_meta?.phase,
683
+ to: after.sprint_meta?.phase,
684
+ };
565
685
  }
566
- if ((before.sprint_meta?.total_claims) !== (after.sprint_meta?.total_claims)) {
567
- delta.meta_changes.total_claims = { from: before.sprint_meta?.total_claims, to: after.sprint_meta?.total_claims };
686
+ if (before.sprint_meta?.total_claims !== after.sprint_meta?.total_claims) {
687
+ delta.meta_changes.total_claims = {
688
+ from: before.sprint_meta?.total_claims,
689
+ to: after.sprint_meta?.total_claims,
690
+ };
568
691
  }
569
692
 
570
693
  return delta;
@@ -580,9 +703,11 @@ function generateManifest(compilation, dir, sprintsInfo) {
580
703
  const baseDir = dir || TARGET_DIR;
581
704
  try {
582
705
  const result = buildManifest(baseDir, { sprintsInfo });
583
- if (result && process.argv.includes('--summary')) {
706
+ if (result && process.argv.includes("--summary")) {
584
707
  console.log(`\nManifest: wheat-manifest.json generated`);
585
- console.log(` Topics: ${result.topicCount} | Files: ${result.fileCount} | Sprints: ${result.sprintCount}`);
708
+ console.log(
709
+ ` Topics: ${result.topicCount} | Files: ${result.fileCount} | Sprints: ${result.sprintCount}`
710
+ );
586
711
  }
587
712
  } catch (err) {
588
713
  // Non-fatal: warn but don't block compilation
@@ -598,23 +723,30 @@ function generateManifest(compilation, dir, sprintsInfo) {
598
723
  * @returns {object} The compiled output object
599
724
  */
600
725
  function compile(inputPath, outputPath, dir, opts = {}) {
601
- const compilerVersion = '0.2.0';
726
+ const compilerVersion = "0.2.0";
602
727
  const baseDir = dir || TARGET_DIR;
603
728
  const claimsPath = inputPath || path.join(baseDir, config.compiler.claims);
604
- const compilationOutputPath = outputPath || path.join(baseDir, config.compiler.compilation);
729
+ const compilationOutputPath =
730
+ outputPath || path.join(baseDir, config.compiler.compilation);
605
731
 
606
732
  // Read claims
607
733
  if (!fs.existsSync(claimsPath)) {
608
- console.error(`Error: ${path.basename(claimsPath)} not found. Run "wheat init" to start a sprint.`);
734
+ console.error(
735
+ `Error: ${path.basename(
736
+ claimsPath
737
+ )} not found. Run "wheat init" to start a sprint.`
738
+ );
609
739
  process.exit(1);
610
740
  }
611
741
 
612
- const raw = fs.readFileSync(claimsPath, 'utf8');
742
+ const raw = fs.readFileSync(claimsPath, "utf8");
613
743
  let claimsData;
614
744
  try {
615
745
  claimsData = JSON.parse(raw);
616
746
  } catch (e) {
617
- console.error(`Error: ${path.basename(claimsPath)} is not valid JSON — ${e.message}`);
747
+ console.error(
748
+ `Error: ${path.basename(claimsPath)} is not valid JSON — ${e.message}`
749
+ );
618
750
  process.exit(1);
619
751
  }
620
752
  // ── Schema version check + migration [r237] ──────────────────────────────
@@ -639,22 +771,30 @@ function compile(inputPath, outputPath, dir, opts = {}) {
639
771
  let conflictGraph = { resolved: [], unresolved: [] };
640
772
  let coverageResult = { coverage: {}, corroboration: {} };
641
773
  let readiness = { blockers: allValidationErrors, warnings: [] };
642
- let resolvedClaims = claims.filter(c => c.status === 'active' || c.status === 'resolved');
774
+ let resolvedClaims = claims.filter(
775
+ (c) => c.status === "active" || c.status === "resolved"
776
+ );
643
777
 
644
778
  if (allValidationErrors.length === 0) {
645
779
  const sortedClaims = sortByEvidenceTier(claims);
646
780
  const conflicts = detectConflicts(sortedClaims);
647
781
  conflictGraph = autoResolve(claims, conflicts);
648
782
  coverageResult = analyzeCoverage(claims);
649
- readiness = checkReadiness([], conflictGraph.unresolved, coverageResult.coverage);
650
- resolvedClaims = claims.filter(c => c.status === 'active' || c.status === 'resolved');
783
+ readiness = checkReadiness(
784
+ [],
785
+ conflictGraph.unresolved,
786
+ coverageResult.coverage
787
+ );
788
+ resolvedClaims = claims.filter(
789
+ (c) => c.status === "active" || c.status === "resolved"
790
+ );
651
791
  }
652
792
 
653
793
  const phaseSummary = summarizePhases(claims);
654
794
  const certificate = generateCertificate(claimsData, compilerVersion);
655
795
 
656
796
  // Determine overall status
657
- const status = readiness.blockers.length > 0 ? 'blocked' : 'ready';
797
+ const status = readiness.blockers.length > 0 ? "blocked" : "ready";
658
798
 
659
799
  // Determine current phase from meta or infer from claims
660
800
  const currentPhase = meta.phase || inferPhase(phaseSummary);
@@ -671,7 +811,7 @@ function compile(inputPath, outputPath, dir, opts = {}) {
671
811
  }
672
812
 
673
813
  // Build sprint summaries: active sprint gets full compilation, others get summary entries
674
- sprintSummaries = sprintsInfo.sprints.map(s => ({
814
+ sprintSummaries = sprintsInfo.sprints.map((s) => ({
675
815
  name: s.name,
676
816
  path: s.path,
677
817
  status: s.status,
@@ -685,16 +825,22 @@ function compile(inputPath, outputPath, dir, opts = {}) {
685
825
  }
686
826
 
687
827
  const compilation = {
688
- compiled_at: new Date().toISOString(), // Non-deterministic metadata (excluded from certificate)
828
+ compiled_at: new Date().toISOString(), // Non-deterministic metadata (excluded from certificate)
689
829
  claims_hash: certificate.input_hash.slice(7, 14),
690
830
  compiler_version: compilerVersion,
691
831
  status,
692
832
  errors: readiness.blockers,
693
833
  warnings: readiness.warnings,
694
- resolved_claims: resolvedClaims.map(c => ({
695
- id: c.id, type: c.type, topic: c.topic,
696
- evidence: c.evidence, status: c.status, phase_added: c.phase_added,
697
- source: c.source, conflicts_with: c.conflicts_with, resolved_by: c.resolved_by,
834
+ resolved_claims: resolvedClaims.map((c) => ({
835
+ id: c.id,
836
+ type: c.type,
837
+ topic: c.topic,
838
+ evidence: c.evidence,
839
+ status: c.status,
840
+ phase_added: c.phase_added,
841
+ source: c.source,
842
+ conflicts_with: c.conflicts_with,
843
+ resolved_by: c.resolved_by,
698
844
  tags: c.tags,
699
845
  })),
700
846
  conflict_graph: conflictGraph,
@@ -703,14 +849,14 @@ function compile(inputPath, outputPath, dir, opts = {}) {
703
849
  phase_summary: phaseSummary,
704
850
  sprints: sprintSummaries,
705
851
  sprint_meta: {
706
- question: meta.question || '',
852
+ question: meta.question || "",
707
853
  audience: meta.audience || [],
708
- initiated: meta.initiated || '',
854
+ initiated: meta.initiated || "",
709
855
  phase: currentPhase,
710
856
  total_claims: claims.length,
711
- active_claims: claims.filter(c => c.status === 'active').length,
712
- conflicted_claims: claims.filter(c => c.status === 'conflicted').length,
713
- superseded_claims: claims.filter(c => c.status === 'superseded').length,
857
+ active_claims: claims.filter((c) => c.status === "active").length,
858
+ conflicted_claims: claims.filter((c) => c.status === "conflicted").length,
859
+ superseded_claims: claims.filter((c) => c.status === "superseded").length,
714
860
  connectors: meta.connectors || [],
715
861
  },
716
862
  compilation_certificate: certificate,
@@ -730,31 +876,34 @@ function compile(inputPath, outputPath, dir, opts = {}) {
730
876
 
731
877
  function inferPhase(phaseSummary) {
732
878
  // Walk backwards through phases to find the latest completed one
733
- const phases = ['evaluate', 'prototype', 'research', 'define'];
879
+ const phases = ["evaluate", "prototype", "research", "define"];
734
880
  for (const phase of phases) {
735
881
  if (phaseSummary[phase] && phaseSummary[phase].complete) {
736
882
  return phase;
737
883
  }
738
884
  }
739
- return 'init';
885
+ return "init";
740
886
  }
741
887
 
742
888
  // ─── Self-Containment Scanner ────────────────────────────────────────────────
743
889
  function scanSelfContainment(dirs) {
744
- const extPattern = /(?:<script[^>]+src=["'](?!data:)|<link[^>]+href=["'](?!#|data:)|@import\s+url\(["']?(?!data:)|<img[^>]+src=["'](?!data:))(https?:\/\/[^"'\s)]+)/gi;
890
+ const extPattern =
891
+ /(?:<script[^>]+src=["'](?!data:)|<link[^>]+href=["'](?!#|data:)|@import\s+url\(["']?(?!data:)|<img[^>]+src=["'](?!data:))(https?:\/\/[^"'\s)]+)/gi;
745
892
  const results = [];
746
893
  for (const dir of dirs) {
747
894
  if (!fs.existsSync(dir)) continue;
748
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.html'));
895
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".html"));
749
896
  for (const file of files) {
750
- const raw = fs.readFileSync(path.join(dir, file), 'utf8');
897
+ const raw = fs.readFileSync(path.join(dir, file), "utf8");
751
898
  // Strip inline script/style bodies so URLs inside JS/CSS data aren't flagged.
752
899
  // Preserve <script src="..."> tags (external scripts we DO want to detect).
753
- const content = raw.replace(/(<script(?:\s[^>]*)?)>([\s\S]*?)<\/script>/gi, (_, open) => {
754
- return open + '></script>';
755
- }).replace(/(<style(?:\s[^>]*)?)>([\s\S]*?)<\/style>/gi, (_, open) => {
756
- return open + '></style>';
757
- });
900
+ const content = raw
901
+ .replace(/(<script(?:\s[^>]*)?)>([\s\S]*?)<\/script>/gi, (_, open) => {
902
+ return open + "></script>";
903
+ })
904
+ .replace(/(<style(?:\s[^>]*)?)>([\s\S]*?)<\/style>/gi, (_, open) => {
905
+ return open + "></style>";
906
+ });
758
907
  const matches = [];
759
908
  let m;
760
909
  while ((m = extPattern.exec(content)) !== null) {
@@ -767,15 +916,16 @@ function scanSelfContainment(dirs) {
767
916
  }
768
917
 
769
918
  // ─── CLI ─────────────────────────────────────────────────────────────────────
770
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
919
+ const isMain =
920
+ process.argv[1] &&
921
+ fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
771
922
 
772
923
  if (isMain) {
924
+ const args = process.argv.slice(2);
773
925
 
774
- const args = process.argv.slice(2);
775
-
776
- // --help / -h
777
- if (args.includes('--help') || args.includes('-h')) {
778
- console.log(`Wheat Compiler v0.2.0 — Bran-based compilation for research claims
926
+ // --help / -h
927
+ if (args.includes("--help") || args.includes("-h")) {
928
+ console.log(`Wheat Compiler v0.2.0 — Bran-based compilation for research claims
779
929
 
780
930
  Usage:
781
931
  node wheat-compiler.js Compile claims.json → compilation.json
@@ -792,269 +942,383 @@ Options:
792
942
  --quiet, -q One-liner output (for scripts and AI agents)
793
943
  --help, -h Show this help message
794
944
  --json Output as JSON (works with --summary, --check, --gate, --scan, --next)`);
795
- process.exit(0);
796
- }
945
+ process.exit(0);
946
+ }
797
947
 
798
- // --scan mode: check HTML artifacts for external dependencies
799
- if (args.includes('--scan')) {
800
- const scanDirs = ['output', 'research', 'evidence', 'prototypes'].map(d => path.join(TARGET_DIR, d));
801
- // Also scan nested dirs one level deep (e.g. prototypes/live-dashboard/)
802
- const allDirs = [...scanDirs];
803
- for (const d of scanDirs) {
804
- if (fs.existsSync(d)) {
805
- fs.readdirSync(d, { withFileTypes: true })
806
- .filter(e => e.isDirectory())
807
- .forEach(e => allDirs.push(path.join(d, e.name)));
948
+ // --scan mode: check HTML artifacts for external dependencies
949
+ if (args.includes("--scan")) {
950
+ const scanDirs = ["output", "research", "evidence", "prototypes"].map((d) =>
951
+ path.join(TARGET_DIR, d)
952
+ );
953
+ // Also scan nested dirs one level deep (e.g. prototypes/live-dashboard/)
954
+ const allDirs = [...scanDirs];
955
+ for (const d of scanDirs) {
956
+ if (fs.existsSync(d)) {
957
+ fs.readdirSync(d, { withFileTypes: true })
958
+ .filter((e) => e.isDirectory())
959
+ .forEach((e) => allDirs.push(path.join(d, e.name)));
960
+ }
808
961
  }
809
- }
810
- const results = scanSelfContainment(allDirs);
811
- const clean = results.filter(r => r.external.length === 0);
812
- const dirty = results.filter(r => r.external.length > 0);
813
-
814
- const scanJsonFlag = args.includes('--json');
815
- if (scanJsonFlag) {
816
- console.log(JSON.stringify({ scanned: results.length, clean: clean.length, dirty: dirty.length, files: dirty }, null, 2));
817
- process.exit(dirty.length > 0 ? 1 : 0);
962
+ const results = scanSelfContainment(allDirs);
963
+ const clean = results.filter((r) => r.external.length === 0);
964
+ const dirty = results.filter((r) => r.external.length > 0);
965
+
966
+ const scanJsonFlag = args.includes("--json");
967
+ if (scanJsonFlag) {
968
+ console.log(
969
+ JSON.stringify(
970
+ {
971
+ scanned: results.length,
972
+ clean: clean.length,
973
+ dirty: dirty.length,
974
+ files: dirty,
975
+ },
976
+ null,
977
+ 2
978
+ )
979
+ );
980
+ process.exit(dirty.length > 0 ? 1 : 0);
981
+ }
982
+
983
+ console.log(`Self-Containment Scan`);
984
+ console.log("=".repeat(50));
985
+ console.log(`Scanned: ${results.length} HTML files`);
986
+ console.log(`Clean: ${clean.length}`);
987
+ console.log(`Dirty: ${dirty.length}`);
988
+ if (dirty.length > 0) {
989
+ console.log("\nExternal dependencies found:");
990
+ dirty.forEach((r) => {
991
+ console.log(` ${r.file}:`);
992
+ r.external.forEach((url) => console.log(` → ${url}`));
993
+ });
994
+ process.exit(1);
995
+ } else {
996
+ console.log("\n✓ All HTML artifacts are self-contained.");
997
+ }
998
+ process.exit(0);
818
999
  }
819
1000
 
820
- console.log(`Self-Containment Scan`);
821
- console.log('='.repeat(50));
822
- console.log(`Scanned: ${results.length} HTML files`);
823
- console.log(`Clean: ${clean.length}`);
824
- console.log(`Dirty: ${dirty.length}`);
825
- if (dirty.length > 0) {
826
- console.log('\nExternal dependencies found:');
827
- dirty.forEach(r => {
828
- console.log(` ${r.file}:`);
829
- r.external.forEach(url => console.log(` → ${url}`));
830
- });
831
- process.exit(1);
832
- } else {
833
- console.log('\n✓ All HTML artifacts are self-contained.');
1001
+ // --diff mode: compare two compilation files
1002
+ if (args.includes("--diff")) {
1003
+ const diffIdx = args.indexOf("--diff");
1004
+ const fileA = args[diffIdx + 1];
1005
+ const fileB = args[diffIdx + 2];
1006
+ if (!fileA || !fileB) {
1007
+ console.error(
1008
+ "Usage: node wheat-compiler.js --diff <before.json> <after.json>"
1009
+ );
1010
+ process.exit(1);
1011
+ }
1012
+ let before, after;
1013
+ try {
1014
+ before = JSON.parse(fs.readFileSync(fileA, "utf8"));
1015
+ } catch (e) {
1016
+ console.error(`Error: ${fileA} is not valid JSON — ${e.message}`);
1017
+ process.exit(1);
1018
+ }
1019
+ try {
1020
+ after = JSON.parse(fs.readFileSync(fileB, "utf8"));
1021
+ } catch (e) {
1022
+ console.error(`Error: ${fileB} is not valid JSON — ${e.message}`);
1023
+ process.exit(1);
1024
+ }
1025
+ const delta = diffCompilations(before, after);
1026
+ console.log(JSON.stringify(delta, null, 2));
1027
+ process.exit(0);
834
1028
  }
835
- process.exit(0);
836
- }
837
1029
 
838
- // --diff mode: compare two compilation files
839
- if (args.includes('--diff')) {
840
- const diffIdx = args.indexOf('--diff');
841
- const fileA = args[diffIdx + 1];
842
- const fileB = args[diffIdx + 2];
843
- if (!fileA || !fileB) {
844
- console.error('Usage: node wheat-compiler.js --diff <before.json> <after.json>');
845
- process.exit(1);
1030
+ // Parse --input and --output flags
1031
+ let inputPath = null;
1032
+ let outputPath = null;
1033
+ const inputIdx = args.indexOf("--input");
1034
+ if (inputIdx !== -1 && args[inputIdx + 1]) {
1035
+ inputPath = path.resolve(args[inputIdx + 1]);
1036
+ }
1037
+ const outputIdx = args.indexOf("--output");
1038
+ if (outputIdx !== -1 && args[outputIdx + 1]) {
1039
+ outputPath = path.resolve(args[outputIdx + 1]);
846
1040
  }
847
- let before, after;
848
- try { before = JSON.parse(fs.readFileSync(fileA, 'utf8')); }
849
- catch (e) { console.error(`Error: ${fileA} is not valid JSON — ${e.message}`); process.exit(1); }
850
- try { after = JSON.parse(fs.readFileSync(fileB, 'utf8')); }
851
- catch (e) { console.error(`Error: ${fileB} is not valid JSON — ${e.message}`); process.exit(1); }
852
- const delta = diffCompilations(before, after);
853
- console.log(JSON.stringify(delta, null, 2));
854
- process.exit(0);
855
- }
856
1041
 
857
- // Parse --input and --output flags
858
- let inputPath = null;
859
- let outputPath = null;
860
- const inputIdx = args.indexOf('--input');
861
- if (inputIdx !== -1 && args[inputIdx + 1]) {
862
- inputPath = path.resolve(args[inputIdx + 1]);
863
- }
864
- const outputIdx = args.indexOf('--output');
865
- if (outputIdx !== -1 && args[outputIdx + 1]) {
866
- outputPath = path.resolve(args[outputIdx + 1]);
867
- }
1042
+ const jsonFlag = args.includes("--json");
1043
+ const quietFlag = args.includes("--quiet") || args.includes("-q");
1044
+ const compilation = compile(inputPath, outputPath, undefined, {
1045
+ skipSprintDetection: quietFlag && !args.includes("--summary"),
1046
+ });
868
1047
 
869
- const jsonFlag = args.includes('--json');
870
- const quietFlag = args.includes('--quiet') || args.includes('-q');
871
- const compilation = compile(inputPath, outputPath, undefined, { skipSprintDetection: quietFlag && !args.includes('--summary') });
872
-
873
- // --quiet / -q: one-liner output for scripts and AI agents (~13 tokens vs ~4,600)
874
- if (quietFlag && !args.includes('--summary')) {
875
- const c = compilation;
876
- const conflicts = c.sprint_meta.conflicted_claims || 0;
877
- const suffix = conflicts > 0 ? ` (${conflicts} conflicts)` : '';
878
- const line = `wheat: compiled ${c.sprint_meta.total_claims} claims, ${Object.keys(c.coverage).length} topics${suffix}`;
879
- if (jsonFlag) {
880
- console.log(JSON.stringify({
881
- status: c.status,
882
- claims: c.sprint_meta.total_claims,
883
- active: c.sprint_meta.active_claims,
884
- conflicts,
885
- topics: Object.keys(c.coverage).length,
886
- errors: c.errors.length,
887
- warnings: c.warnings.length,
888
- }));
889
- } else {
890
- console.log(line);
1048
+ // --quiet / -q: one-liner output for scripts and AI agents (~13 tokens vs ~4,600)
1049
+ if (quietFlag && !args.includes("--summary")) {
1050
+ const c = compilation;
1051
+ const conflicts = c.sprint_meta.conflicted_claims || 0;
1052
+ const suffix = conflicts > 0 ? ` (${conflicts} conflicts)` : "";
1053
+ const line = `wheat: compiled ${c.sprint_meta.total_claims} claims, ${
1054
+ Object.keys(c.coverage).length
1055
+ } topics${suffix}`;
1056
+ if (jsonFlag) {
1057
+ console.log(
1058
+ JSON.stringify({
1059
+ status: c.status,
1060
+ claims: c.sprint_meta.total_claims,
1061
+ active: c.sprint_meta.active_claims,
1062
+ conflicts,
1063
+ topics: Object.keys(c.coverage).length,
1064
+ errors: c.errors.length,
1065
+ warnings: c.warnings.length,
1066
+ })
1067
+ );
1068
+ } else {
1069
+ console.log(line);
1070
+ }
1071
+ process.exit(c.status === "blocked" ? 1 : 0);
891
1072
  }
892
- process.exit(c.status === 'blocked' ? 1 : 0);
893
- }
894
1073
 
895
- if (args.includes('--summary')) {
896
- const c = compilation;
897
- const statusIcon = c.status === 'ready' ? '\u2713' : '\u2717';
898
- console.log(`\nWheat Compiler v${c.compiler_version}`);
899
- console.log(`${'='.repeat(50)}`);
900
- console.log(`Sprint: ${c.sprint_meta.question || '(not initialized)'}`);
901
- console.log(`Phase: ${c.sprint_meta.phase}`);
902
- console.log(`Status: ${statusIcon} ${c.status.toUpperCase()}`);
903
- console.log(`Claims: ${c.sprint_meta.total_claims} total, ${c.sprint_meta.active_claims} active, ${c.sprint_meta.conflicted_claims} conflicted`);
904
-
905
- if (c.sprints && c.sprints.length > 0) {
906
- console.log(`Sprints: ${c.sprints.length} detected`);
907
- c.sprints.forEach(s => {
908
- const icon = s.status === 'active' ? '>>' : ' ';
909
- console.log(` ${icon} [${s.status.toUpperCase().padEnd(8)}] ${s.name} (${s.phase}, ${s.claims_count} claims)`);
910
- });
911
- }
912
- console.log();
913
-
914
- if (Object.keys(c.coverage).length > 0) {
915
- console.log('Coverage:');
916
- Object.entries(c.coverage).forEach(([topic, entry]) => {
917
- const bar = '\u2588'.repeat(Math.min(entry.claims, 10)) + '\u2591'.repeat(Math.max(0, 10 - entry.claims));
918
- const constraintDominated = (entry.constraint_count || 0) / entry.claims > 0.5;
919
- const icon = entry.status === 'strong' ? '\u2713' : entry.status === 'moderate' ? '~' : constraintDominated ? '\u2139' : '\u26A0';
920
- const srcInfo = entry.source_count !== undefined ? ` [${entry.source_count} src]` : '';
921
- const typeInfo = entry.type_diversity !== undefined ? ` [${entry.type_diversity}/${VALID_TYPES.length} types]` : '';
922
- console.log(` ${icon} ${topic.padEnd(20)} ${bar} ${entry.max_evidence} (${entry.claims} claims)${srcInfo}${typeInfo}`);
923
- });
1074
+ if (args.includes("--summary")) {
1075
+ const c = compilation;
1076
+ const statusIcon = c.status === "ready" ? "\u2713" : "\u2717";
1077
+ console.log(`\nWheat Compiler v${c.compiler_version}`);
1078
+ console.log(`${"=".repeat(50)}`);
1079
+ console.log(`Sprint: ${c.sprint_meta.question || "(not initialized)"}`);
1080
+ console.log(`Phase: ${c.sprint_meta.phase}`);
1081
+ console.log(`Status: ${statusIcon} ${c.status.toUpperCase()}`);
1082
+ console.log(
1083
+ `Claims: ${c.sprint_meta.total_claims} total, ${c.sprint_meta.active_claims} active, ${c.sprint_meta.conflicted_claims} conflicted`
1084
+ );
1085
+
1086
+ if (c.sprints && c.sprints.length > 0) {
1087
+ console.log(`Sprints: ${c.sprints.length} detected`);
1088
+ c.sprints.forEach((s) => {
1089
+ const icon = s.status === "active" ? ">>" : " ";
1090
+ console.log(
1091
+ ` ${icon} [${s.status.toUpperCase().padEnd(8)}] ${s.name} (${
1092
+ s.phase
1093
+ }, ${s.claims_count} claims)`
1094
+ );
1095
+ });
1096
+ }
924
1097
  console.log();
925
- }
926
1098
 
927
- if (c.corroboration && Object.keys(c.corroboration).length > 0) {
928
- const corroborated = Object.entries(c.corroboration).filter(([, v]) => v > 0);
929
- if (corroborated.length > 0) {
930
- console.log('Corroborated claims:');
931
- corroborated.forEach(([id, count]) => {
932
- console.log(` ${id}: ${count} supporting witness(es)`);
1099
+ if (Object.keys(c.coverage).length > 0) {
1100
+ console.log("Coverage:");
1101
+ Object.entries(c.coverage).forEach(([topic, entry]) => {
1102
+ const bar =
1103
+ "\u2588".repeat(Math.min(entry.claims, 10)) +
1104
+ "\u2591".repeat(Math.max(0, 10 - entry.claims));
1105
+ const constraintDominated =
1106
+ (entry.constraint_count || 0) / entry.claims > 0.5;
1107
+ const icon =
1108
+ entry.status === "strong"
1109
+ ? "\u2713"
1110
+ : entry.status === "moderate"
1111
+ ? "~"
1112
+ : constraintDominated
1113
+ ? "\u2139"
1114
+ : "\u26A0";
1115
+ const srcInfo =
1116
+ entry.source_count !== undefined
1117
+ ? ` [${entry.source_count} src]`
1118
+ : "";
1119
+ const typeInfo =
1120
+ entry.type_diversity !== undefined
1121
+ ? ` [${entry.type_diversity}/${VALID_TYPES.length} types]`
1122
+ : "";
1123
+ console.log(
1124
+ ` ${icon} ${topic.padEnd(20)} ${bar} ${entry.max_evidence} (${
1125
+ entry.claims
1126
+ } claims)${srcInfo}${typeInfo}`
1127
+ );
933
1128
  });
934
1129
  console.log();
935
1130
  }
936
- }
937
1131
 
938
- if (c.errors.length > 0) {
939
- console.log('Errors:');
940
- c.errors.forEach(e => console.log(` ${e.code}: ${e.message}`));
941
- console.log();
1132
+ if (c.corroboration && Object.keys(c.corroboration).length > 0) {
1133
+ const corroborated = Object.entries(c.corroboration).filter(
1134
+ ([, v]) => v > 0
1135
+ );
1136
+ if (corroborated.length > 0) {
1137
+ console.log("Corroborated claims:");
1138
+ corroborated.forEach(([id, count]) => {
1139
+ console.log(` ${id}: ${count} supporting witness(es)`);
1140
+ });
1141
+ console.log();
1142
+ }
1143
+ }
942
1144
 
943
- // Show expected shape if schema errors exist
944
- const hasSchemaErrors = c.errors.some(e => e.code === 'E_SCHEMA' || e.code === 'E_TYPE' || e.code === 'E_EVIDENCE_TIER');
945
- if (hasSchemaErrors) {
946
- console.log(' Expected claim shape:');
947
- console.log(' {');
948
- console.log(' "id": "r001",');
949
- console.log(' "type": "constraint|factual|estimate|risk|recommendation|feedback",');
950
- console.log(' "topic": "topic-slug",');
951
- console.log(' "content": "The claim text",');
952
- console.log(' "source": { "origin": "research", "artifact": null, "connector": null },');
953
- console.log(' "evidence": "stated|web|documented|tested|production",');
954
- console.log(' "status": "active",');
955
- console.log(' "phase_added": "define|research|prototype|evaluate|feedback|challenge",');
956
- console.log(' "timestamp": "2026-01-01T00:00:00.000Z",');
957
- console.log(' "conflicts_with": [],');
958
- console.log(' "resolved_by": null,');
959
- console.log(' "tags": []');
960
- console.log(' }');
1145
+ if (c.errors.length > 0) {
1146
+ console.log("Errors:");
1147
+ c.errors.forEach((e) => console.log(` ${e.code}: ${e.message}`));
961
1148
  console.log();
962
- console.log(' Hint: Run "wheat init --headless --question ..." to generate a valid claims.json');
963
- }
964
- }
965
1149
 
966
- if (c.warnings.length > 0) {
967
- console.log('Warnings:');
968
- c.warnings.forEach(w => console.log(` ${w.code}: ${w.message}`));
969
- console.log();
970
- }
1150
+ // Show expected shape if schema errors exist
1151
+ const hasSchemaErrors = c.errors.some(
1152
+ (e) =>
1153
+ e.code === "E_SCHEMA" ||
1154
+ e.code === "E_TYPE" ||
1155
+ e.code === "E_EVIDENCE_TIER"
1156
+ );
1157
+ if (hasSchemaErrors) {
1158
+ console.log(" Expected claim shape:");
1159
+ console.log(" {");
1160
+ console.log(' "id": "r001",');
1161
+ console.log(
1162
+ ' "type": "constraint|factual|estimate|risk|recommendation|feedback",'
1163
+ );
1164
+ console.log(' "topic": "topic-slug",');
1165
+ console.log(' "content": "The claim text",');
1166
+ console.log(
1167
+ ' "source": { "origin": "research", "artifact": null, "connector": null },'
1168
+ );
1169
+ console.log(
1170
+ ' "evidence": "stated|web|documented|tested|production",'
1171
+ );
1172
+ console.log(' "status": "active",');
1173
+ console.log(
1174
+ ' "phase_added": "define|research|prototype|evaluate|feedback|challenge",'
1175
+ );
1176
+ console.log(' "timestamp": "2026-01-01T00:00:00.000Z",');
1177
+ console.log(' "conflicts_with": [],');
1178
+ console.log(' "resolved_by": null,');
1179
+ console.log(' "tags": []');
1180
+ console.log(" }");
1181
+ console.log();
1182
+ console.log(
1183
+ ' Hint: Run "wheat init --headless --question ..." to generate a valid claims.json'
1184
+ );
1185
+ }
1186
+ }
971
1187
 
972
- console.log(`Certificate: ${c.compilation_certificate.input_hash.slice(0, 20)}...`);
1188
+ if (c.warnings.length > 0) {
1189
+ console.log("Warnings:");
1190
+ c.warnings.forEach((w) => console.log(` ${w.code}: ${w.message}`));
1191
+ console.log();
1192
+ }
973
1193
 
974
- if (jsonFlag) {
975
- console.log(JSON.stringify(c, null, 2));
976
- }
977
- }
1194
+ console.log(
1195
+ `Certificate: ${c.compilation_certificate.input_hash.slice(0, 20)}...`
1196
+ );
978
1197
 
979
- if (args.includes('--check')) {
980
- if (compilation.status === 'blocked') {
981
1198
  if (jsonFlag) {
982
- console.log(JSON.stringify({ status: 'blocked', errors: compilation.errors }, null, 2));
983
- } else {
984
- console.error(`Compilation blocked: ${compilation.errors.length} error(s)`);
985
- compilation.errors.forEach(e => console.error(` ${e.code}: ${e.message}`));
1199
+ console.log(JSON.stringify(c, null, 2));
986
1200
  }
987
- process.exit(1);
988
- } else {
989
- if (jsonFlag) {
990
- console.log(JSON.stringify({ status: 'ready' }, null, 2));
1201
+ }
1202
+
1203
+ if (args.includes("--check")) {
1204
+ if (compilation.status === "blocked") {
1205
+ if (jsonFlag) {
1206
+ console.log(
1207
+ JSON.stringify(
1208
+ { status: "blocked", errors: compilation.errors },
1209
+ null,
1210
+ 2
1211
+ )
1212
+ );
1213
+ } else {
1214
+ console.error(
1215
+ `Compilation blocked: ${compilation.errors.length} error(s)`
1216
+ );
1217
+ compilation.errors.forEach((e) =>
1218
+ console.error(` ${e.code}: ${e.message}`)
1219
+ );
1220
+ }
1221
+ process.exit(1);
991
1222
  } else {
992
- console.log('Compilation ready.');
1223
+ if (jsonFlag) {
1224
+ console.log(JSON.stringify({ status: "ready" }, null, 2));
1225
+ } else {
1226
+ console.log("Compilation ready.");
1227
+ }
1228
+ process.exit(0);
993
1229
  }
994
- process.exit(0);
995
1230
  }
996
- }
997
1231
 
998
- if (args.includes('--gate')) {
999
- // Staleness check: is compilation.json older than claims.json?
1000
- const compilationPath = path.join(TARGET_DIR, config.compiler.compilation);
1001
- const claimsPath = path.join(TARGET_DIR, config.compiler.claims);
1232
+ if (args.includes("--gate")) {
1233
+ // Staleness check: is compilation.json older than claims.json?
1234
+ const compilationPath = path.join(TARGET_DIR, config.compiler.compilation);
1235
+ const claimsPath = path.join(TARGET_DIR, config.compiler.claims);
1002
1236
 
1003
- if (fs.existsSync(compilationPath) && fs.existsSync(claimsPath)) {
1004
- const compilationMtime = fs.statSync(compilationPath).mtimeMs;
1005
- const claimsMtime = fs.statSync(claimsPath).mtimeMs;
1237
+ if (fs.existsSync(compilationPath) && fs.existsSync(claimsPath)) {
1238
+ const compilationMtime = fs.statSync(compilationPath).mtimeMs;
1239
+ const claimsMtime = fs.statSync(claimsPath).mtimeMs;
1006
1240
 
1007
- if (claimsMtime > compilationMtime) {
1008
- console.error('Gate FAILED: compilation.json is stale. Recompiling now...');
1009
- // The compile() call above already refreshed it, so this is informational
1241
+ if (claimsMtime > compilationMtime) {
1242
+ console.error(
1243
+ "Gate FAILED: compilation.json is stale. Recompiling now..."
1244
+ );
1245
+ // The compile() call above already refreshed it, so this is informational
1246
+ }
1247
+ }
1248
+
1249
+ if (compilation.status === "blocked") {
1250
+ if (jsonFlag) {
1251
+ console.log(
1252
+ JSON.stringify(
1253
+ { gate: "failed", errors: compilation.errors },
1254
+ null,
1255
+ 2
1256
+ )
1257
+ );
1258
+ } else {
1259
+ console.error(`Gate FAILED: ${compilation.errors.length} blocker(s)`);
1260
+ compilation.errors.forEach((e) =>
1261
+ console.error(` ${e.code}: ${e.message}`)
1262
+ );
1263
+ }
1264
+ process.exit(1);
1010
1265
  }
1011
- }
1012
1266
 
1013
- if (compilation.status === 'blocked') {
1014
1267
  if (jsonFlag) {
1015
- console.log(JSON.stringify({ gate: 'failed', errors: compilation.errors }, null, 2));
1268
+ console.log(
1269
+ JSON.stringify(
1270
+ {
1271
+ gate: "passed",
1272
+ active_claims: compilation.sprint_meta.active_claims,
1273
+ topics: Object.keys(compilation.coverage).length,
1274
+ hash: compilation.claims_hash,
1275
+ },
1276
+ null,
1277
+ 2
1278
+ )
1279
+ );
1016
1280
  } else {
1017
- console.error(`Gate FAILED: ${compilation.errors.length} blocker(s)`);
1018
- compilation.errors.forEach(e => console.error(` ${e.code}: ${e.message}`));
1281
+ // Print a one-line gate pass for audit
1282
+ console.log(
1283
+ `Gate PASSED: ${compilation.sprint_meta.active_claims} claims, ${
1284
+ Object.keys(compilation.coverage).length
1285
+ } topics, hash ${compilation.claims_hash}`
1286
+ );
1019
1287
  }
1020
- process.exit(1);
1288
+ process.exit(0);
1021
1289
  }
1022
1290
 
1023
- if (jsonFlag) {
1024
- console.log(JSON.stringify({ gate: 'passed', active_claims: compilation.sprint_meta.active_claims, topics: Object.keys(compilation.coverage).length, hash: compilation.claims_hash }, null, 2));
1025
- } else {
1026
- // Print a one-line gate pass for audit
1027
- console.log(`Gate PASSED: ${compilation.sprint_meta.active_claims} claims, ${Object.keys(compilation.coverage).length} topics, hash ${compilation.claims_hash}`);
1028
- }
1029
- process.exit(0);
1030
- }
1031
-
1032
- // ─── --next: Data-driven next action recommendation ──────────────────────────
1033
- if (args.includes('--next')) {
1034
- const n = parseInt(args[args.indexOf('--next') + 1]) || 1;
1035
- const actions = computeNextActions(compilation);
1036
- const top = actions.slice(0, n);
1037
-
1038
- if (top.length === 0) {
1039
- console.log('\nNo actions recommended — sprint looks complete.');
1040
- console.log('Consider: /brief to compile, /present to share, /calibrate after shipping.');
1041
- } else {
1042
- console.log(`\nNext ${top.length === 1 ? 'action' : top.length + ' actions'} (by Bran priority):`);
1043
- console.log('='.repeat(50));
1044
- top.forEach((a, i) => {
1045
- console.log(`\n${i + 1}. [${a.priority}] ${a.command}`);
1046
- console.log(` ${a.reason}`);
1047
- console.log(` Impact: ${a.impact}`);
1048
- });
1049
- console.log();
1050
- }
1051
- // Also output as JSON for programmatic use
1052
- if (args.includes('--json')) {
1053
- console.log(JSON.stringify(top, null, 2));
1291
+ // ─── --next: Data-driven next action recommendation ──────────────────────────
1292
+ if (args.includes("--next")) {
1293
+ const n = parseInt(args[args.indexOf("--next") + 1]) || 1;
1294
+ const actions = computeNextActions(compilation);
1295
+ const top = actions.slice(0, n);
1296
+
1297
+ if (top.length === 0) {
1298
+ console.log("\nNo actions recommended — sprint looks complete.");
1299
+ console.log(
1300
+ "Consider: /brief to compile, /present to share, /calibrate after shipping."
1301
+ );
1302
+ } else {
1303
+ console.log(
1304
+ `\nNext ${
1305
+ top.length === 1 ? "action" : top.length + " actions"
1306
+ } (by Bran priority):`
1307
+ );
1308
+ console.log("=".repeat(50));
1309
+ top.forEach((a, i) => {
1310
+ console.log(`\n${i + 1}. [${a.priority}] ${a.command}`);
1311
+ console.log(` ${a.reason}`);
1312
+ console.log(` Impact: ${a.impact}`);
1313
+ });
1314
+ console.log();
1315
+ }
1316
+ // Also output as JSON for programmatic use
1317
+ if (args.includes("--json")) {
1318
+ console.log(JSON.stringify(top, null, 2));
1319
+ }
1320
+ process.exit(0);
1054
1321
  }
1055
- process.exit(0);
1056
- }
1057
-
1058
1322
  } // end if (isMain)
1059
1323
 
1060
1324
  /**
@@ -1066,40 +1330,44 @@ function computeNextActions(comp) {
1066
1330
  const actions = [];
1067
1331
  const coverage = comp.coverage || {};
1068
1332
  const conflicts = comp.conflict_graph || { resolved: [], unresolved: [] };
1069
- const phase = comp.sprint_meta?.phase || 'init';
1333
+ const phase = comp.sprint_meta?.phase || "init";
1070
1334
  const phases = comp.phase_summary || {};
1071
1335
  const warnings = comp.warnings || [];
1072
1336
  const corroboration = comp.corroboration || {};
1073
1337
 
1074
1338
  // ── Priority 1: Unresolved conflicts (blocks compilation) ──────────────
1075
1339
  if (conflicts.unresolved.length > 0) {
1076
- conflicts.unresolved.forEach(c => {
1340
+ conflicts.unresolved.forEach((c) => {
1077
1341
  actions.push({
1078
- priority: 'P0-BLOCKER',
1342
+ priority: "P0-BLOCKER",
1079
1343
  score: 1000,
1080
1344
  command: `/resolve ${c.claim_a} ${c.claim_b}`,
1081
1345
  reason: `Unresolved conflict between ${c.claim_a} and ${c.claim_b} — blocks compilation.`,
1082
- impact: 'Unblocks compilation. Status changes from BLOCKED to READY.',
1346
+ impact: "Unblocks compilation. Status changes from BLOCKED to READY.",
1083
1347
  });
1084
1348
  });
1085
1349
  }
1086
1350
 
1087
1351
  // ── Priority 2: Phase progression ──────────────────────────────────────
1088
- const phaseFlow = ['init', 'define', 'research', 'prototype', 'evaluate'];
1352
+ const phaseFlow = ["init", "define", "research", "prototype", "evaluate"];
1089
1353
  const phaseIdx = phaseFlow.indexOf(phase);
1090
1354
 
1091
- if (phase === 'init') {
1355
+ if (phase === "init") {
1092
1356
  actions.push({
1093
- priority: 'P1-PHASE',
1357
+ priority: "P1-PHASE",
1094
1358
  score: 900,
1095
- command: '/init',
1096
- reason: 'Sprint not initialized. No question, constraints, or audience defined.',
1097
- impact: 'Establishes sprint question and seeds constraint claims.',
1359
+ command: "/init",
1360
+ reason:
1361
+ "Sprint not initialized. No question, constraints, or audience defined.",
1362
+ impact: "Establishes sprint question and seeds constraint claims.",
1098
1363
  });
1099
1364
  }
1100
1365
 
1101
1366
  // If in define, push toward research
1102
- if (phase === 'define' && (!phases.research || phases.research.claims === 0)) {
1367
+ if (
1368
+ phase === "define" &&
1369
+ (!phases.research || phases.research.claims === 0)
1370
+ ) {
1103
1371
  // Find topics with only constraint claims
1104
1372
  const constraintTopics = Object.entries(coverage)
1105
1373
  .filter(([, e]) => e.constraint_count === e.claims && e.claims > 0)
@@ -1110,11 +1378,11 @@ function computeNextActions(comp) {
1110
1378
  .map(([t]) => t)[0];
1111
1379
 
1112
1380
  actions.push({
1113
- priority: 'P1-PHASE',
1381
+ priority: "P1-PHASE",
1114
1382
  score: 850,
1115
- command: `/research "${researchTarget || 'core topic'}"`,
1383
+ command: `/research "${researchTarget || "core topic"}"`,
1116
1384
  reason: `Phase is define with no research claims yet. Need to advance to research.`,
1117
- impact: 'Adds web-level evidence. Moves sprint into research phase.',
1385
+ impact: "Adds web-level evidence. Moves sprint into research phase.",
1118
1386
  });
1119
1387
  }
1120
1388
 
@@ -1122,12 +1390,12 @@ function computeNextActions(comp) {
1122
1390
  if (phaseIdx >= 2 && (!phases.prototype || phases.prototype.claims === 0)) {
1123
1391
  // Find topic with most web claims — best candidate to upgrade
1124
1392
  const webHeavy = Object.entries(coverage)
1125
- .filter(([, e]) => e.max_evidence === 'web' && e.claims >= 2)
1393
+ .filter(([, e]) => e.max_evidence === "web" && e.claims >= 2)
1126
1394
  .sort((a, b) => b[1].claims - a[1].claims);
1127
1395
 
1128
1396
  if (webHeavy.length > 0) {
1129
1397
  actions.push({
1130
- priority: 'P1-PHASE',
1398
+ priority: "P1-PHASE",
1131
1399
  score: 800,
1132
1400
  command: `/prototype "${webHeavy[0][0]}"`,
1133
1401
  reason: `Topic "${webHeavy[0][0]}" has ${webHeavy[0][1].claims} claims at web-level. Prototyping upgrades to tested.`,
@@ -1137,8 +1405,19 @@ function computeNextActions(comp) {
1137
1405
  }
1138
1406
 
1139
1407
  // ── Priority 3: Weak evidence topics ───────────────────────────────────
1140
- const evidenceRank = { stated: 1, web: 2, documented: 3, tested: 4, production: 5 };
1141
- const phaseExpectation = { define: 1, research: 2, prototype: 4, evaluate: 4 };
1408
+ const evidenceRank = {
1409
+ stated: 1,
1410
+ web: 2,
1411
+ documented: 3,
1412
+ tested: 4,
1413
+ production: 5,
1414
+ };
1415
+ const phaseExpectation = {
1416
+ define: 1,
1417
+ research: 2,
1418
+ prototype: 4,
1419
+ evaluate: 4,
1420
+ };
1142
1421
  const expected = phaseExpectation[phase] || 2;
1143
1422
 
1144
1423
  Object.entries(coverage).forEach(([topic, entry]) => {
@@ -1150,11 +1429,11 @@ function computeNextActions(comp) {
1150
1429
 
1151
1430
  if (rank < expected) {
1152
1431
  const gap = expected - rank;
1153
- const score = 600 + (gap * 50) + entry.claims * 5;
1432
+ const score = 600 + gap * 50 + entry.claims * 5;
1154
1433
 
1155
1434
  if (rank <= 2 && expected >= 4) {
1156
1435
  actions.push({
1157
- priority: 'P2-EVIDENCE',
1436
+ priority: "P2-EVIDENCE",
1158
1437
  score,
1159
1438
  command: `/prototype "${topic}"`,
1160
1439
  reason: `Topic "${topic}" is at ${entry.max_evidence} (${entry.claims} claims) but phase is ${phase}. Needs tested-level evidence.`,
@@ -1162,7 +1441,7 @@ function computeNextActions(comp) {
1162
1441
  });
1163
1442
  } else if (rank <= 1) {
1164
1443
  actions.push({
1165
- priority: 'P2-EVIDENCE',
1444
+ priority: "P2-EVIDENCE",
1166
1445
  score,
1167
1446
  command: `/research "${topic}"`,
1168
1447
  reason: `Topic "${topic}" is at ${entry.max_evidence} (${entry.claims} claims). Needs deeper research.`,
@@ -1178,24 +1457,24 @@ function computeNextActions(comp) {
1178
1457
  if (constraintRatio > 0.5) return;
1179
1458
 
1180
1459
  if ((entry.type_diversity || 0) < 2 && entry.claims >= 2) {
1181
- const missing = (entry.missing_types || []).slice(0, 3).join(', ');
1460
+ const missing = (entry.missing_types || []).slice(0, 3).join(", ");
1182
1461
  actions.push({
1183
- priority: 'P3-DIVERSITY',
1462
+ priority: "P3-DIVERSITY",
1184
1463
  score: 400 + entry.claims * 3,
1185
1464
  command: `/challenge ${entry.claim_ids?.[0] || topic}`,
1186
1465
  reason: `Topic "${topic}" has ${entry.claims} claims but only ${entry.type_diversity} type(s). Missing: ${missing}.`,
1187
- impact: 'Adds risk/recommendation claims. Improves type diversity.',
1466
+ impact: "Adds risk/recommendation claims. Improves type diversity.",
1188
1467
  });
1189
1468
  }
1190
1469
 
1191
1470
  // Missing risk claims specifically
1192
- if (entry.claims >= 3 && !(entry.types || []).includes('risk')) {
1471
+ if (entry.claims >= 3 && !(entry.types || []).includes("risk")) {
1193
1472
  actions.push({
1194
- priority: 'P3-DIVERSITY',
1473
+ priority: "P3-DIVERSITY",
1195
1474
  score: 380,
1196
1475
  command: `/challenge ${entry.claim_ids?.[0] || topic}`,
1197
1476
  reason: `Topic "${topic}" has ${entry.claims} claims but zero risks. What could go wrong?`,
1198
- impact: 'Adds adversarial risk claims. Stress-tests assumptions.',
1477
+ impact: "Adds adversarial risk claims. Stress-tests assumptions.",
1199
1478
  });
1200
1479
  }
1201
1480
  });
@@ -1204,11 +1483,13 @@ function computeNextActions(comp) {
1204
1483
  Object.entries(coverage).forEach(([topic, entry]) => {
1205
1484
  if (entry.claims >= 3 && (entry.source_count || 1) === 1) {
1206
1485
  actions.push({
1207
- priority: 'P4-CORROBORATION',
1486
+ priority: "P4-CORROBORATION",
1208
1487
  score: 300 + entry.claims * 2,
1209
- command: `/witness ${entry.claim_ids?.[0] || ''} <url>`,
1210
- reason: `Topic "${topic}" has ${entry.claims} claims all from "${(entry.source_origins || ['unknown'])[0]}". Single source.`,
1211
- impact: 'Adds external corroboration. Breaks echo chamber.',
1488
+ command: `/witness ${entry.claim_ids?.[0] || ""} <url>`,
1489
+ reason: `Topic "${topic}" has ${entry.claims} claims all from "${
1490
+ (entry.source_origins || ["unknown"])[0]
1491
+ }". Single source.`,
1492
+ impact: "Adds external corroboration. Breaks echo chamber.",
1212
1493
  });
1213
1494
  }
1214
1495
  });
@@ -1221,16 +1502,16 @@ function computeNextActions(comp) {
1221
1502
  // Find tested claims with zero corroboration — highest value to witness
1222
1503
  if (uncorroborated.length > 0) {
1223
1504
  const testedUncorroborated = (comp.resolved_claims || [])
1224
- .filter(c => c.evidence === 'tested' && uncorroborated.includes(c.id))
1505
+ .filter((c) => c.evidence === "tested" && uncorroborated.includes(c.id))
1225
1506
  .slice(0, 1);
1226
1507
 
1227
1508
  if (testedUncorroborated.length > 0) {
1228
1509
  actions.push({
1229
- priority: 'P4-CORROBORATION',
1510
+ priority: "P4-CORROBORATION",
1230
1511
  score: 250,
1231
1512
  command: `/witness ${testedUncorroborated[0].id} <url>`,
1232
1513
  reason: `Tested claim "${testedUncorroborated[0].id}" has zero external corroboration.`,
1233
- impact: 'Adds external validation to highest-evidence claim.',
1514
+ impact: "Adds external validation to highest-evidence claim.",
1234
1515
  });
1235
1516
  }
1236
1517
  }
@@ -1243,19 +1524,21 @@ function computeNextActions(comp) {
1243
1524
 
1244
1525
  if (hasEvaluate && allTopicsTested && conflicts.unresolved.length === 0) {
1245
1526
  actions.push({
1246
- priority: 'P5-SHIP',
1527
+ priority: "P5-SHIP",
1247
1528
  score: 100,
1248
- command: '/brief',
1249
- reason: 'All non-constraint topics at tested evidence, evaluate phase complete, 0 conflicts.',
1250
- impact: 'Compiles the decision document. Sprint ready to ship.',
1529
+ command: "/brief",
1530
+ reason:
1531
+ "All non-constraint topics at tested evidence, evaluate phase complete, 0 conflicts.",
1532
+ impact: "Compiles the decision document. Sprint ready to ship.",
1251
1533
  });
1252
1534
  } else if (!hasEvaluate && phaseIdx >= 3) {
1253
1535
  actions.push({
1254
- priority: 'P1-PHASE',
1536
+ priority: "P1-PHASE",
1255
1537
  score: 750,
1256
- command: '/evaluate',
1538
+ command: "/evaluate",
1257
1539
  reason: `Phase is ${phase} but no evaluation claims exist. Time to test claims against reality.`,
1258
- impact: 'Validates claims, resolves conflicts, produces comparison dashboard.',
1540
+ impact:
1541
+ "Validates claims, resolves conflicts, produces comparison dashboard.",
1259
1542
  });
1260
1543
  }
1261
1544
 
@@ -1264,8 +1547,8 @@ function computeNextActions(comp) {
1264
1547
 
1265
1548
  // Deduplicate by command
1266
1549
  const seen = new Set();
1267
- return actions.filter(a => {
1268
- const key = a.command.split(' ').slice(0, 2).join(' ');
1550
+ return actions.filter((a) => {
1551
+ const key = a.command.split(" ").slice(0, 2).join(" ");
1269
1552
  if (seen.has(key)) return false;
1270
1553
  seen.add(key);
1271
1554
  return true;
@@ -1273,4 +1556,13 @@ function computeNextActions(comp) {
1273
1556
  }
1274
1557
 
1275
1558
  // Export for use as a library
1276
- export { compile, diffCompilations, computeNextActions, generateManifest, loadConfig, detectSprints, EVIDENCE_TIERS, VALID_TYPES };
1559
+ export {
1560
+ compile,
1561
+ diffCompilations,
1562
+ computeNextActions,
1563
+ generateManifest,
1564
+ loadConfig,
1565
+ detectSprints,
1566
+ EVIDENCE_TIERS,
1567
+ VALID_TYPES,
1568
+ };