@grainulation/wheat 1.0.2 → 1.0.4

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 (42) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -31
  3. package/bin/wheat.js +47 -36
  4. package/compiler/detect-sprints.js +126 -92
  5. package/compiler/generate-manifest.js +116 -69
  6. package/compiler/wheat-compiler.js +789 -468
  7. package/lib/compiler.js +11 -6
  8. package/lib/connect.js +273 -134
  9. package/lib/disconnect.js +61 -40
  10. package/lib/guard.js +20 -17
  11. package/lib/index.js +8 -8
  12. package/lib/init.js +217 -142
  13. package/lib/install-prompt.js +26 -26
  14. package/lib/load-claims.js +88 -0
  15. package/lib/quickstart.js +225 -111
  16. package/lib/serve-mcp.js +495 -180
  17. package/lib/server.js +198 -111
  18. package/lib/stats.js +65 -39
  19. package/lib/status.js +65 -34
  20. package/lib/update.js +13 -11
  21. package/package.json +8 -4
  22. package/templates/claude.md +31 -17
  23. package/templates/commands/blind-spot.md +9 -2
  24. package/templates/commands/brief.md +11 -1
  25. package/templates/commands/calibrate.md +3 -1
  26. package/templates/commands/challenge.md +4 -1
  27. package/templates/commands/connect.md +12 -1
  28. package/templates/commands/evaluate.md +4 -0
  29. package/templates/commands/feedback.md +3 -1
  30. package/templates/commands/handoff.md +11 -7
  31. package/templates/commands/init.md +4 -1
  32. package/templates/commands/merge.md +4 -1
  33. package/templates/commands/next.md +1 -0
  34. package/templates/commands/present.md +3 -0
  35. package/templates/commands/prototype.md +2 -0
  36. package/templates/commands/pull.md +103 -0
  37. package/templates/commands/replay.md +8 -0
  38. package/templates/commands/research.md +1 -0
  39. package/templates/commands/resolve.md +4 -1
  40. package/templates/commands/status.md +4 -0
  41. package/templates/commands/sync.md +94 -0
  42. 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
@@ -597,24 +722,31 @@ function generateManifest(compilation, dir, sprintsInfo) {
597
722
  * @param {string|null} outputPath - Path to write compilation.json (null = default from config)
598
723
  * @returns {object} The compiled output object
599
724
  */
600
- function compile(inputPath, outputPath, dir) {
601
- const compilerVersion = '0.2.0';
725
+ function compile(inputPath, outputPath, dir, opts = {}) {
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,59 +771,76 @@ function compile(inputPath, outputPath, dir) {
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);
661
801
 
662
802
  // ── Sprint detection (git-based, non-fatal) ──────────────────────────────
663
803
  let sprintsInfo = { active: null, sprints: [] };
664
- try {
665
- sprintsInfo = detectSprints(baseDir);
666
- } catch (err) {
667
- // Non-fatal: sprint detection failure should not block compilation
668
- console.error(`Warning: sprint detection failed — ${err.message}`);
669
- }
804
+ let sprintSummaries = [];
805
+ if (!opts.skipSprintDetection) {
806
+ try {
807
+ sprintsInfo = detectSprints(baseDir);
808
+ } catch (err) {
809
+ // Non-fatal: sprint detection failure should not block compilation
810
+ console.error(`Warning: sprint detection failed — ${err.message}`);
811
+ }
670
812
 
671
- // Build sprint summaries: active sprint gets full compilation, others get summary entries
672
- const sprintSummaries = sprintsInfo.sprints.map(s => ({
673
- name: s.name,
674
- path: s.path,
675
- status: s.status,
676
- phase: s.phase,
677
- question: s.question,
678
- claims_count: s.claims_count,
679
- active_claims: s.active_claims,
680
- last_git_activity: s.last_git_activity,
681
- git_commit_count: s.git_commit_count,
682
- }));
813
+ // Build sprint summaries: active sprint gets full compilation, others get summary entries
814
+ sprintSummaries = sprintsInfo.sprints.map((s) => ({
815
+ name: s.name,
816
+ path: s.path,
817
+ status: s.status,
818
+ phase: s.phase,
819
+ question: s.question,
820
+ claims_count: s.claims_count,
821
+ active_claims: s.active_claims,
822
+ last_git_activity: s.last_git_activity,
823
+ git_commit_count: s.git_commit_count,
824
+ }));
825
+ }
683
826
 
684
827
  const compilation = {
685
- compiled_at: new Date().toISOString(), // Non-deterministic metadata (excluded from certificate)
828
+ compiled_at: new Date().toISOString(), // Non-deterministic metadata (excluded from certificate)
686
829
  claims_hash: certificate.input_hash.slice(7, 14),
687
830
  compiler_version: compilerVersion,
688
831
  status,
689
832
  errors: readiness.blockers,
690
833
  warnings: readiness.warnings,
691
- resolved_claims: resolvedClaims.map(c => ({
692
- id: c.id, type: c.type, topic: c.topic,
693
- evidence: c.evidence, status: c.status, phase_added: c.phase_added,
694
- 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,
695
844
  tags: c.tags,
696
845
  })),
697
846
  conflict_graph: conflictGraph,
@@ -700,14 +849,14 @@ function compile(inputPath, outputPath, dir) {
700
849
  phase_summary: phaseSummary,
701
850
  sprints: sprintSummaries,
702
851
  sprint_meta: {
703
- question: meta.question || '',
852
+ question: meta.question || "",
704
853
  audience: meta.audience || [],
705
- initiated: meta.initiated || '',
854
+ initiated: meta.initiated || "",
706
855
  phase: currentPhase,
707
856
  total_claims: claims.length,
708
- active_claims: claims.filter(c => c.status === 'active').length,
709
- conflicted_claims: claims.filter(c => c.status === 'conflicted').length,
710
- 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,
711
860
  connectors: meta.connectors || [],
712
861
  },
713
862
  compilation_certificate: certificate,
@@ -718,38 +867,43 @@ function compile(inputPath, outputPath, dir) {
718
867
 
719
868
  // Generate topic-map manifest (wheat-manifest.json)
720
869
  // Pass sprintsInfo to avoid re-running detectSprints in manifest generator
721
- generateManifest(compilation, baseDir, sprintsInfo);
870
+ if (!opts.skipSprintDetection) {
871
+ generateManifest(compilation, baseDir, sprintsInfo);
872
+ }
722
873
 
723
874
  return compilation;
724
875
  }
725
876
 
726
877
  function inferPhase(phaseSummary) {
727
878
  // Walk backwards through phases to find the latest completed one
728
- const phases = ['evaluate', 'prototype', 'research', 'define'];
879
+ const phases = ["evaluate", "prototype", "research", "define"];
729
880
  for (const phase of phases) {
730
881
  if (phaseSummary[phase] && phaseSummary[phase].complete) {
731
882
  return phase;
732
883
  }
733
884
  }
734
- return 'init';
885
+ return "init";
735
886
  }
736
887
 
737
888
  // ─── Self-Containment Scanner ────────────────────────────────────────────────
738
889
  function scanSelfContainment(dirs) {
739
- 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;
740
892
  const results = [];
741
893
  for (const dir of dirs) {
742
894
  if (!fs.existsSync(dir)) continue;
743
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.html'));
895
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".html"));
744
896
  for (const file of files) {
745
- const raw = fs.readFileSync(path.join(dir, file), 'utf8');
897
+ const raw = fs.readFileSync(path.join(dir, file), "utf8");
746
898
  // Strip inline script/style bodies so URLs inside JS/CSS data aren't flagged.
747
899
  // Preserve <script src="..."> tags (external scripts we DO want to detect).
748
- const content = raw.replace(/(<script(?:\s[^>]*)?)>([\s\S]*?)<\/script>/gi, (_, open) => {
749
- return open + '></script>';
750
- }).replace(/(<style(?:\s[^>]*)?)>([\s\S]*?)<\/style>/gi, (_, open) => {
751
- return open + '></style>';
752
- });
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
+ });
753
907
  const matches = [];
754
908
  let m;
755
909
  while ((m = extPattern.exec(content)) !== null) {
@@ -762,15 +916,16 @@ function scanSelfContainment(dirs) {
762
916
  }
763
917
 
764
918
  // ─── CLI ─────────────────────────────────────────────────────────────────────
765
- 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]);
766
922
 
767
923
  if (isMain) {
924
+ const args = process.argv.slice(2);
768
925
 
769
- const args = process.argv.slice(2);
770
-
771
- // --help / -h
772
- if (args.includes('--help') || args.includes('-h')) {
773
- 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
774
929
 
775
930
  Usage:
776
931
  node wheat-compiler.js Compile claims.json → compilation.json
@@ -784,248 +939,386 @@ Usage:
784
939
 
785
940
  Options:
786
941
  --dir <path> Resolve all paths relative to <path> instead of script location
942
+ --quiet, -q One-liner output (for scripts and AI agents)
787
943
  --help, -h Show this help message
788
944
  --json Output as JSON (works with --summary, --check, --gate, --scan, --next)`);
789
- process.exit(0);
790
- }
945
+ process.exit(0);
946
+ }
791
947
 
792
- // --scan mode: check HTML artifacts for external dependencies
793
- if (args.includes('--scan')) {
794
- const scanDirs = ['output', 'research', 'evidence', 'prototypes'].map(d => path.join(TARGET_DIR, d));
795
- // Also scan nested dirs one level deep (e.g. prototypes/live-dashboard/)
796
- const allDirs = [...scanDirs];
797
- for (const d of scanDirs) {
798
- if (fs.existsSync(d)) {
799
- fs.readdirSync(d, { withFileTypes: true })
800
- .filter(e => e.isDirectory())
801
- .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
+ }
802
961
  }
803
- }
804
- const results = scanSelfContainment(allDirs);
805
- const clean = results.filter(r => r.external.length === 0);
806
- const dirty = results.filter(r => r.external.length > 0);
807
-
808
- const scanJsonFlag = args.includes('--json');
809
- if (scanJsonFlag) {
810
- console.log(JSON.stringify({ scanned: results.length, clean: clean.length, dirty: dirty.length, files: dirty }, null, 2));
811
- 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);
812
999
  }
813
1000
 
814
- console.log(`Self-Containment Scan`);
815
- console.log('='.repeat(50));
816
- console.log(`Scanned: ${results.length} HTML files`);
817
- console.log(`Clean: ${clean.length}`);
818
- console.log(`Dirty: ${dirty.length}`);
819
- if (dirty.length > 0) {
820
- console.log('\nExternal dependencies found:');
821
- dirty.forEach(r => {
822
- console.log(` ${r.file}:`);
823
- r.external.forEach(url => console.log(` → ${url}`));
824
- });
825
- process.exit(1);
826
- } else {
827
- 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);
828
1028
  }
829
- process.exit(0);
830
- }
831
1029
 
832
- // --diff mode: compare two compilation files
833
- if (args.includes('--diff')) {
834
- const diffIdx = args.indexOf('--diff');
835
- const fileA = args[diffIdx + 1];
836
- const fileB = args[diffIdx + 2];
837
- if (!fileA || !fileB) {
838
- console.error('Usage: node wheat-compiler.js --diff <before.json> <after.json>');
839
- 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]);
840
1040
  }
841
- let before, after;
842
- try { before = JSON.parse(fs.readFileSync(fileA, 'utf8')); }
843
- catch (e) { console.error(`Error: ${fileA} is not valid JSON — ${e.message}`); process.exit(1); }
844
- try { after = JSON.parse(fs.readFileSync(fileB, 'utf8')); }
845
- catch (e) { console.error(`Error: ${fileB} is not valid JSON — ${e.message}`); process.exit(1); }
846
- const delta = diffCompilations(before, after);
847
- console.log(JSON.stringify(delta, null, 2));
848
- process.exit(0);
849
- }
850
1041
 
851
- // Parse --input and --output flags
852
- let inputPath = null;
853
- let outputPath = null;
854
- const inputIdx = args.indexOf('--input');
855
- if (inputIdx !== -1 && args[inputIdx + 1]) {
856
- inputPath = path.resolve(args[inputIdx + 1]);
857
- }
858
- const outputIdx = args.indexOf('--output');
859
- if (outputIdx !== -1 && args[outputIdx + 1]) {
860
- outputPath = path.resolve(args[outputIdx + 1]);
861
- }
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
+ });
862
1047
 
863
- const compilation = compile(inputPath, outputPath);
864
- const jsonFlag = args.includes('--json');
865
-
866
- if (args.includes('--summary')) {
867
- const c = compilation;
868
- const statusIcon = c.status === 'ready' ? '\u2713' : '\u2717';
869
- console.log(`\nWheat Compiler v${c.compiler_version}`);
870
- console.log(`${'='.repeat(50)}`);
871
- console.log(`Sprint: ${c.sprint_meta.question || '(not initialized)'}`);
872
- console.log(`Phase: ${c.sprint_meta.phase}`);
873
- console.log(`Status: ${statusIcon} ${c.status.toUpperCase()}`);
874
- console.log(`Claims: ${c.sprint_meta.total_claims} total, ${c.sprint_meta.active_claims} active, ${c.sprint_meta.conflicted_claims} conflicted`);
875
-
876
- if (c.sprints && c.sprints.length > 0) {
877
- console.log(`Sprints: ${c.sprints.length} detected`);
878
- c.sprints.forEach(s => {
879
- const icon = s.status === 'active' ? '>>' : ' ';
880
- console.log(` ${icon} [${s.status.toUpperCase().padEnd(8)}] ${s.name} (${s.phase}, ${s.claims_count} claims)`);
881
- });
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);
882
1072
  }
883
- console.log();
884
-
885
- if (Object.keys(c.coverage).length > 0) {
886
- console.log('Coverage:');
887
- Object.entries(c.coverage).forEach(([topic, entry]) => {
888
- const bar = '\u2588'.repeat(Math.min(entry.claims, 10)) + '\u2591'.repeat(Math.max(0, 10 - entry.claims));
889
- const constraintDominated = (entry.constraint_count || 0) / entry.claims > 0.5;
890
- const icon = entry.status === 'strong' ? '\u2713' : entry.status === 'moderate' ? '~' : constraintDominated ? '\u2139' : '\u26A0';
891
- const srcInfo = entry.source_count !== undefined ? ` [${entry.source_count} src]` : '';
892
- const typeInfo = entry.type_diversity !== undefined ? ` [${entry.type_diversity}/${VALID_TYPES.length} types]` : '';
893
- console.log(` ${icon} ${topic.padEnd(20)} ${bar} ${entry.max_evidence} (${entry.claims} claims)${srcInfo}${typeInfo}`);
894
- });
1073
+
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
+ }
895
1097
  console.log();
896
- }
897
1098
 
898
- if (c.corroboration && Object.keys(c.corroboration).length > 0) {
899
- const corroborated = Object.entries(c.corroboration).filter(([, v]) => v > 0);
900
- if (corroborated.length > 0) {
901
- console.log('Corroborated claims:');
902
- corroborated.forEach(([id, count]) => {
903
- 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
+ );
904
1128
  });
905
1129
  console.log();
906
1130
  }
907
- }
908
1131
 
909
- if (c.errors.length > 0) {
910
- console.log('Errors:');
911
- c.errors.forEach(e => console.log(` ${e.code}: ${e.message}`));
912
- 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
+ }
913
1144
 
914
- // Show expected shape if schema errors exist
915
- const hasSchemaErrors = c.errors.some(e => e.code === 'E_SCHEMA' || e.code === 'E_TYPE' || e.code === 'E_EVIDENCE_TIER');
916
- if (hasSchemaErrors) {
917
- console.log(' Expected claim shape:');
918
- console.log(' {');
919
- console.log(' "id": "r001",');
920
- console.log(' "type": "constraint|factual|estimate|risk|recommendation|feedback",');
921
- console.log(' "topic": "topic-slug",');
922
- console.log(' "content": "The claim text",');
923
- console.log(' "source": { "origin": "research", "artifact": null, "connector": null },');
924
- console.log(' "evidence": "stated|web|documented|tested|production",');
925
- console.log(' "status": "active",');
926
- console.log(' "phase_added": "define|research|prototype|evaluate|feedback|challenge",');
927
- console.log(' "timestamp": "2026-01-01T00:00:00.000Z",');
928
- console.log(' "conflicts_with": [],');
929
- console.log(' "resolved_by": null,');
930
- console.log(' "tags": []');
931
- console.log(' }');
1145
+ if (c.errors.length > 0) {
1146
+ console.log("Errors:");
1147
+ c.errors.forEach((e) => console.log(` ${e.code}: ${e.message}`));
932
1148
  console.log();
933
- console.log(' Hint: Run "wheat init --headless --question ..." to generate a valid claims.json');
934
- }
935
- }
936
1149
 
937
- if (c.warnings.length > 0) {
938
- console.log('Warnings:');
939
- c.warnings.forEach(w => console.log(` ${w.code}: ${w.message}`));
940
- console.log();
941
- }
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
+ }
942
1187
 
943
- 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
+ }
944
1193
 
945
- if (jsonFlag) {
946
- console.log(JSON.stringify(c, null, 2));
947
- }
948
- }
1194
+ console.log(
1195
+ `Certificate: ${c.compilation_certificate.input_hash.slice(0, 20)}...`
1196
+ );
949
1197
 
950
- if (args.includes('--check')) {
951
- if (compilation.status === 'blocked') {
952
1198
  if (jsonFlag) {
953
- console.log(JSON.stringify({ status: 'blocked', errors: compilation.errors }, null, 2));
954
- } else {
955
- console.error(`Compilation blocked: ${compilation.errors.length} error(s)`);
956
- compilation.errors.forEach(e => console.error(` ${e.code}: ${e.message}`));
1199
+ console.log(JSON.stringify(c, null, 2));
957
1200
  }
958
- process.exit(1);
959
- } else {
960
- if (jsonFlag) {
961
- 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);
962
1222
  } else {
963
- 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);
964
1229
  }
965
- process.exit(0);
966
1230
  }
967
- }
968
1231
 
969
- if (args.includes('--gate')) {
970
- // Staleness check: is compilation.json older than claims.json?
971
- const compilationPath = path.join(TARGET_DIR, config.compiler.compilation);
972
- 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);
973
1236
 
974
- if (fs.existsSync(compilationPath) && fs.existsSync(claimsPath)) {
975
- const compilationMtime = fs.statSync(compilationPath).mtimeMs;
976
- 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;
977
1240
 
978
- if (claimsMtime > compilationMtime) {
979
- console.error('Gate FAILED: compilation.json is stale. Recompiling now...');
980
- // 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);
981
1265
  }
982
- }
983
1266
 
984
- if (compilation.status === 'blocked') {
985
1267
  if (jsonFlag) {
986
- 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
+ );
987
1280
  } else {
988
- console.error(`Gate FAILED: ${compilation.errors.length} blocker(s)`);
989
- 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
+ );
990
1287
  }
991
- process.exit(1);
1288
+ process.exit(0);
992
1289
  }
993
1290
 
994
- if (jsonFlag) {
995
- 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));
996
- } else {
997
- // Print a one-line gate pass for audit
998
- console.log(`Gate PASSED: ${compilation.sprint_meta.active_claims} claims, ${Object.keys(compilation.coverage).length} topics, hash ${compilation.claims_hash}`);
999
- }
1000
- process.exit(0);
1001
- }
1002
-
1003
- // ─── --next: Data-driven next action recommendation ──────────────────────────
1004
- if (args.includes('--next')) {
1005
- const n = parseInt(args[args.indexOf('--next') + 1]) || 1;
1006
- const actions = computeNextActions(compilation);
1007
- const top = actions.slice(0, n);
1008
-
1009
- if (top.length === 0) {
1010
- console.log('\nNo actions recommended — sprint looks complete.');
1011
- console.log('Consider: /brief to compile, /present to share, /calibrate after shipping.');
1012
- } else {
1013
- console.log(`\nNext ${top.length === 1 ? 'action' : top.length + ' actions'} (by Bran priority):`);
1014
- console.log('='.repeat(50));
1015
- top.forEach((a, i) => {
1016
- console.log(`\n${i + 1}. [${a.priority}] ${a.command}`);
1017
- console.log(` ${a.reason}`);
1018
- console.log(` Impact: ${a.impact}`);
1019
- });
1020
- console.log();
1021
- }
1022
- // Also output as JSON for programmatic use
1023
- if (args.includes('--json')) {
1024
- 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);
1025
1321
  }
1026
- process.exit(0);
1027
- }
1028
-
1029
1322
  } // end if (isMain)
1030
1323
 
1031
1324
  /**
@@ -1037,40 +1330,44 @@ function computeNextActions(comp) {
1037
1330
  const actions = [];
1038
1331
  const coverage = comp.coverage || {};
1039
1332
  const conflicts = comp.conflict_graph || { resolved: [], unresolved: [] };
1040
- const phase = comp.sprint_meta?.phase || 'init';
1333
+ const phase = comp.sprint_meta?.phase || "init";
1041
1334
  const phases = comp.phase_summary || {};
1042
1335
  const warnings = comp.warnings || [];
1043
1336
  const corroboration = comp.corroboration || {};
1044
1337
 
1045
1338
  // ── Priority 1: Unresolved conflicts (blocks compilation) ──────────────
1046
1339
  if (conflicts.unresolved.length > 0) {
1047
- conflicts.unresolved.forEach(c => {
1340
+ conflicts.unresolved.forEach((c) => {
1048
1341
  actions.push({
1049
- priority: 'P0-BLOCKER',
1342
+ priority: "P0-BLOCKER",
1050
1343
  score: 1000,
1051
1344
  command: `/resolve ${c.claim_a} ${c.claim_b}`,
1052
1345
  reason: `Unresolved conflict between ${c.claim_a} and ${c.claim_b} — blocks compilation.`,
1053
- impact: 'Unblocks compilation. Status changes from BLOCKED to READY.',
1346
+ impact: "Unblocks compilation. Status changes from BLOCKED to READY.",
1054
1347
  });
1055
1348
  });
1056
1349
  }
1057
1350
 
1058
1351
  // ── Priority 2: Phase progression ──────────────────────────────────────
1059
- const phaseFlow = ['init', 'define', 'research', 'prototype', 'evaluate'];
1352
+ const phaseFlow = ["init", "define", "research", "prototype", "evaluate"];
1060
1353
  const phaseIdx = phaseFlow.indexOf(phase);
1061
1354
 
1062
- if (phase === 'init') {
1355
+ if (phase === "init") {
1063
1356
  actions.push({
1064
- priority: 'P1-PHASE',
1357
+ priority: "P1-PHASE",
1065
1358
  score: 900,
1066
- command: '/init',
1067
- reason: 'Sprint not initialized. No question, constraints, or audience defined.',
1068
- 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.",
1069
1363
  });
1070
1364
  }
1071
1365
 
1072
1366
  // If in define, push toward research
1073
- if (phase === 'define' && (!phases.research || phases.research.claims === 0)) {
1367
+ if (
1368
+ phase === "define" &&
1369
+ (!phases.research || phases.research.claims === 0)
1370
+ ) {
1074
1371
  // Find topics with only constraint claims
1075
1372
  const constraintTopics = Object.entries(coverage)
1076
1373
  .filter(([, e]) => e.constraint_count === e.claims && e.claims > 0)
@@ -1081,11 +1378,11 @@ function computeNextActions(comp) {
1081
1378
  .map(([t]) => t)[0];
1082
1379
 
1083
1380
  actions.push({
1084
- priority: 'P1-PHASE',
1381
+ priority: "P1-PHASE",
1085
1382
  score: 850,
1086
- command: `/research "${researchTarget || 'core topic'}"`,
1383
+ command: `/research "${researchTarget || "core topic"}"`,
1087
1384
  reason: `Phase is define with no research claims yet. Need to advance to research.`,
1088
- impact: 'Adds web-level evidence. Moves sprint into research phase.',
1385
+ impact: "Adds web-level evidence. Moves sprint into research phase.",
1089
1386
  });
1090
1387
  }
1091
1388
 
@@ -1093,12 +1390,12 @@ function computeNextActions(comp) {
1093
1390
  if (phaseIdx >= 2 && (!phases.prototype || phases.prototype.claims === 0)) {
1094
1391
  // Find topic with most web claims — best candidate to upgrade
1095
1392
  const webHeavy = Object.entries(coverage)
1096
- .filter(([, e]) => e.max_evidence === 'web' && e.claims >= 2)
1393
+ .filter(([, e]) => e.max_evidence === "web" && e.claims >= 2)
1097
1394
  .sort((a, b) => b[1].claims - a[1].claims);
1098
1395
 
1099
1396
  if (webHeavy.length > 0) {
1100
1397
  actions.push({
1101
- priority: 'P1-PHASE',
1398
+ priority: "P1-PHASE",
1102
1399
  score: 800,
1103
1400
  command: `/prototype "${webHeavy[0][0]}"`,
1104
1401
  reason: `Topic "${webHeavy[0][0]}" has ${webHeavy[0][1].claims} claims at web-level. Prototyping upgrades to tested.`,
@@ -1108,8 +1405,19 @@ function computeNextActions(comp) {
1108
1405
  }
1109
1406
 
1110
1407
  // ── Priority 3: Weak evidence topics ───────────────────────────────────
1111
- const evidenceRank = { stated: 1, web: 2, documented: 3, tested: 4, production: 5 };
1112
- 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
+ };
1113
1421
  const expected = phaseExpectation[phase] || 2;
1114
1422
 
1115
1423
  Object.entries(coverage).forEach(([topic, entry]) => {
@@ -1121,11 +1429,11 @@ function computeNextActions(comp) {
1121
1429
 
1122
1430
  if (rank < expected) {
1123
1431
  const gap = expected - rank;
1124
- const score = 600 + (gap * 50) + entry.claims * 5;
1432
+ const score = 600 + gap * 50 + entry.claims * 5;
1125
1433
 
1126
1434
  if (rank <= 2 && expected >= 4) {
1127
1435
  actions.push({
1128
- priority: 'P2-EVIDENCE',
1436
+ priority: "P2-EVIDENCE",
1129
1437
  score,
1130
1438
  command: `/prototype "${topic}"`,
1131
1439
  reason: `Topic "${topic}" is at ${entry.max_evidence} (${entry.claims} claims) but phase is ${phase}. Needs tested-level evidence.`,
@@ -1133,7 +1441,7 @@ function computeNextActions(comp) {
1133
1441
  });
1134
1442
  } else if (rank <= 1) {
1135
1443
  actions.push({
1136
- priority: 'P2-EVIDENCE',
1444
+ priority: "P2-EVIDENCE",
1137
1445
  score,
1138
1446
  command: `/research "${topic}"`,
1139
1447
  reason: `Topic "${topic}" is at ${entry.max_evidence} (${entry.claims} claims). Needs deeper research.`,
@@ -1149,24 +1457,24 @@ function computeNextActions(comp) {
1149
1457
  if (constraintRatio > 0.5) return;
1150
1458
 
1151
1459
  if ((entry.type_diversity || 0) < 2 && entry.claims >= 2) {
1152
- const missing = (entry.missing_types || []).slice(0, 3).join(', ');
1460
+ const missing = (entry.missing_types || []).slice(0, 3).join(", ");
1153
1461
  actions.push({
1154
- priority: 'P3-DIVERSITY',
1462
+ priority: "P3-DIVERSITY",
1155
1463
  score: 400 + entry.claims * 3,
1156
1464
  command: `/challenge ${entry.claim_ids?.[0] || topic}`,
1157
1465
  reason: `Topic "${topic}" has ${entry.claims} claims but only ${entry.type_diversity} type(s). Missing: ${missing}.`,
1158
- impact: 'Adds risk/recommendation claims. Improves type diversity.',
1466
+ impact: "Adds risk/recommendation claims. Improves type diversity.",
1159
1467
  });
1160
1468
  }
1161
1469
 
1162
1470
  // Missing risk claims specifically
1163
- if (entry.claims >= 3 && !(entry.types || []).includes('risk')) {
1471
+ if (entry.claims >= 3 && !(entry.types || []).includes("risk")) {
1164
1472
  actions.push({
1165
- priority: 'P3-DIVERSITY',
1473
+ priority: "P3-DIVERSITY",
1166
1474
  score: 380,
1167
1475
  command: `/challenge ${entry.claim_ids?.[0] || topic}`,
1168
1476
  reason: `Topic "${topic}" has ${entry.claims} claims but zero risks. What could go wrong?`,
1169
- impact: 'Adds adversarial risk claims. Stress-tests assumptions.',
1477
+ impact: "Adds adversarial risk claims. Stress-tests assumptions.",
1170
1478
  });
1171
1479
  }
1172
1480
  });
@@ -1175,11 +1483,13 @@ function computeNextActions(comp) {
1175
1483
  Object.entries(coverage).forEach(([topic, entry]) => {
1176
1484
  if (entry.claims >= 3 && (entry.source_count || 1) === 1) {
1177
1485
  actions.push({
1178
- priority: 'P4-CORROBORATION',
1486
+ priority: "P4-CORROBORATION",
1179
1487
  score: 300 + entry.claims * 2,
1180
- command: `/witness ${entry.claim_ids?.[0] || ''} <url>`,
1181
- reason: `Topic "${topic}" has ${entry.claims} claims all from "${(entry.source_origins || ['unknown'])[0]}". Single source.`,
1182
- 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.",
1183
1493
  });
1184
1494
  }
1185
1495
  });
@@ -1192,16 +1502,16 @@ function computeNextActions(comp) {
1192
1502
  // Find tested claims with zero corroboration — highest value to witness
1193
1503
  if (uncorroborated.length > 0) {
1194
1504
  const testedUncorroborated = (comp.resolved_claims || [])
1195
- .filter(c => c.evidence === 'tested' && uncorroborated.includes(c.id))
1505
+ .filter((c) => c.evidence === "tested" && uncorroborated.includes(c.id))
1196
1506
  .slice(0, 1);
1197
1507
 
1198
1508
  if (testedUncorroborated.length > 0) {
1199
1509
  actions.push({
1200
- priority: 'P4-CORROBORATION',
1510
+ priority: "P4-CORROBORATION",
1201
1511
  score: 250,
1202
1512
  command: `/witness ${testedUncorroborated[0].id} <url>`,
1203
1513
  reason: `Tested claim "${testedUncorroborated[0].id}" has zero external corroboration.`,
1204
- impact: 'Adds external validation to highest-evidence claim.',
1514
+ impact: "Adds external validation to highest-evidence claim.",
1205
1515
  });
1206
1516
  }
1207
1517
  }
@@ -1214,19 +1524,21 @@ function computeNextActions(comp) {
1214
1524
 
1215
1525
  if (hasEvaluate && allTopicsTested && conflicts.unresolved.length === 0) {
1216
1526
  actions.push({
1217
- priority: 'P5-SHIP',
1527
+ priority: "P5-SHIP",
1218
1528
  score: 100,
1219
- command: '/brief',
1220
- reason: 'All non-constraint topics at tested evidence, evaluate phase complete, 0 conflicts.',
1221
- 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.",
1222
1533
  });
1223
1534
  } else if (!hasEvaluate && phaseIdx >= 3) {
1224
1535
  actions.push({
1225
- priority: 'P1-PHASE',
1536
+ priority: "P1-PHASE",
1226
1537
  score: 750,
1227
- command: '/evaluate',
1538
+ command: "/evaluate",
1228
1539
  reason: `Phase is ${phase} but no evaluation claims exist. Time to test claims against reality.`,
1229
- impact: 'Validates claims, resolves conflicts, produces comparison dashboard.',
1540
+ impact:
1541
+ "Validates claims, resolves conflicts, produces comparison dashboard.",
1230
1542
  });
1231
1543
  }
1232
1544
 
@@ -1235,8 +1547,8 @@ function computeNextActions(comp) {
1235
1547
 
1236
1548
  // Deduplicate by command
1237
1549
  const seen = new Set();
1238
- return actions.filter(a => {
1239
- const key = a.command.split(' ').slice(0, 2).join(' ');
1550
+ return actions.filter((a) => {
1551
+ const key = a.command.split(" ").slice(0, 2).join(" ");
1240
1552
  if (seen.has(key)) return false;
1241
1553
  seen.add(key);
1242
1554
  return true;
@@ -1244,4 +1556,13 @@ function computeNextActions(comp) {
1244
1556
  }
1245
1557
 
1246
1558
  // Export for use as a library
1247
- 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
+ };