@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.
- package/LICENSE +1 -1
- package/README.md +32 -31
- package/bin/wheat.js +63 -40
- package/compiler/detect-sprints.js +108 -66
- package/compiler/generate-manifest.js +116 -69
- package/compiler/wheat-compiler.js +763 -471
- package/lib/compiler.js +11 -6
- package/lib/connect.js +273 -134
- package/lib/defaults.js +32 -0
- package/lib/disconnect.js +61 -40
- package/lib/guard.js +20 -17
- package/lib/index.js +8 -8
- package/lib/init.js +260 -142
- package/lib/install-prompt.js +26 -26
- package/lib/load-claims.js +88 -0
- package/lib/quickstart.js +225 -111
- package/lib/serve-mcp.js +495 -180
- package/lib/server.js +198 -111
- package/lib/stats.js +65 -39
- package/lib/status.js +65 -34
- package/lib/update.js +13 -11
- package/package.json +8 -4
- package/templates/claude.md +31 -17
- package/templates/commands/blind-spot.md +9 -2
- package/templates/commands/brief.md +11 -1
- package/templates/commands/calibrate.md +3 -1
- package/templates/commands/challenge.md +4 -1
- package/templates/commands/connect.md +12 -1
- package/templates/commands/evaluate.md +4 -0
- package/templates/commands/feedback.md +3 -1
- package/templates/commands/handoff.md +11 -7
- package/templates/commands/init.md +4 -1
- package/templates/commands/merge.md +4 -1
- package/templates/commands/next.md +1 -0
- package/templates/commands/present.md +3 -0
- package/templates/commands/prototype.md +2 -0
- package/templates/commands/pull.md +103 -0
- package/templates/commands/replay.md +8 -0
- package/templates/commands/research.md +1 -0
- package/templates/commands/resolve.md +4 -1
- package/templates/commands/status.md +4 -0
- package/templates/commands/sync.md +94 -0
- 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
|
|
18
|
-
import crypto from
|
|
19
|
-
import path from
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import crypto from "crypto";
|
|
19
|
+
import path from "path";
|
|
20
20
|
|
|
21
|
-
import { fileURLToPath } from
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
22
|
|
|
23
23
|
// Sprint detection — git-based, no config pointer needed (p013/f001)
|
|
24
|
-
import { detectSprints } from
|
|
24
|
+
import { detectSprints } from "./detect-sprints.js";
|
|
25
25
|
// Direct manifest generation — avoids subprocess + redundant detectSprints call
|
|
26
|
-
import { buildManifest } from
|
|
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(
|
|
33
|
-
const TARGET_DIR =
|
|
34
|
-
|
|
35
|
-
|
|
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,
|
|
41
|
+
const configPath = path.join(dir, "wheat.config.json");
|
|
41
42
|
const defaults = {
|
|
42
|
-
dirs: {
|
|
43
|
-
|
|
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,
|
|
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:
|
|
63
|
-
web:
|
|
69
|
+
stated: 1,
|
|
70
|
+
web: 2,
|
|
64
71
|
documented: 3,
|
|
65
|
-
tested:
|
|
72
|
+
tested: 4,
|
|
66
73
|
production: 5,
|
|
67
74
|
};
|
|
68
75
|
|
|
69
76
|
/** @type {string[]} Allowed claim type values */
|
|
70
|
-
const VALID_TYPES = [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 =
|
|
103
|
+
const BURN_PREFIX = "burn-";
|
|
77
104
|
|
|
78
105
|
// ─── Schema Migration Framework [r237] ──────────────────────────────────────
|
|
79
|
-
const CURRENT_SCHEMA =
|
|
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(
|
|
97
|
-
const pb = b.split(
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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 (
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
claimsData.
|
|
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
|
|
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 = [
|
|
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:
|
|
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 (
|
|
208
|
+
requiredFields.forEach((field) => {
|
|
209
|
+
if (
|
|
210
|
+
claim[field] === undefined ||
|
|
211
|
+
claim[field] === null ||
|
|
212
|
+
claim[field] === ""
|
|
213
|
+
) {
|
|
162
214
|
errors.push({
|
|
163
|
-
code:
|
|
164
|
-
message: `Claim ${
|
|
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:
|
|
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:
|
|
192
|
-
message: `Claim ${claim.id}: invalid type "${
|
|
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:
|
|
200
|
-
message: `Claim ${claim.id}: invalid evidence tier "${
|
|
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:
|
|
208
|
-
message: `Claim ${claim.id}: invalid status "${
|
|
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 =
|
|
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 ||
|
|
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(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
291
|
-
claimB.status =
|
|
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(
|
|
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:
|
|
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 ===
|
|
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 !==
|
|
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 (
|
|
349
|
-
|
|
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)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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:
|
|
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)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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:
|
|
411
|
-
message: `Topic "${topic}"
|
|
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
|
-
}
|
|
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:
|
|
417
|
-
message: `Topic "${topic}" has
|
|
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 !==
|
|
461
|
-
if (Array.isArray(obj)) return
|
|
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
|
|
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
|
|
552
|
+
const hash = crypto
|
|
553
|
+
.createHash("sha256")
|
|
469
554
|
.update(canonicalJSON(claimsData))
|
|
470
|
-
.digest(
|
|
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 => {
|
|
599
|
+
(before.resolved_claims || []).forEach((c) => {
|
|
600
|
+
beforeClaimsMap[c.id] = c;
|
|
601
|
+
});
|
|
515
602
|
const afterClaimsMap = {};
|
|
516
|
-
(after.resolved_claims || []).forEach(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([
|
|
532
|
-
|
|
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:
|
|
628
|
+
delta.coverage_changes.push({ topic, type: "added", after: ac });
|
|
537
629
|
} else if (bc && !ac) {
|
|
538
|
-
delta.coverage_changes.push({ topic, type:
|
|
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)
|
|
542
|
-
|
|
543
|
-
if (bc.
|
|
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:
|
|
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(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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)
|
|
563
|
-
|
|
564
|
-
|
|
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 (
|
|
567
|
-
delta.meta_changes.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(
|
|
706
|
+
if (result && process.argv.includes("--summary")) {
|
|
584
707
|
console.log(`\nManifest: wheat-manifest.json generated`);
|
|
585
|
-
console.log(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
650
|
-
|
|
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 ?
|
|
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(),
|
|
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,
|
|
696
|
-
|
|
697
|
-
|
|
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 ===
|
|
712
|
-
conflicted_claims: claims.filter(c => c.status ===
|
|
713
|
-
superseded_claims: claims.filter(c => c.status ===
|
|
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 = [
|
|
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
|
|
885
|
+
return "init";
|
|
740
886
|
}
|
|
741
887
|
|
|
742
888
|
// ─── Self-Containment Scanner ────────────────────────────────────────────────
|
|
743
889
|
function scanSelfContainment(dirs) {
|
|
744
|
-
const extPattern =
|
|
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(
|
|
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),
|
|
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
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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 =
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
796
|
-
}
|
|
945
|
+
process.exit(0);
|
|
946
|
+
}
|
|
797
947
|
|
|
798
|
-
// --scan mode: check HTML artifacts for external dependencies
|
|
799
|
-
if (args.includes(
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
.
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
// --
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
c.sprints.
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
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
|
-
|
|
975
|
-
|
|
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(
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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(
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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(
|
|
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
|
-
|
|
1018
|
-
|
|
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(
|
|
1288
|
+
process.exit(0);
|
|
1021
1289
|
}
|
|
1022
1290
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
console.log(
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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 ||
|
|
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:
|
|
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:
|
|
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 = [
|
|
1352
|
+
const phaseFlow = ["init", "define", "research", "prototype", "evaluate"];
|
|
1089
1353
|
const phaseIdx = phaseFlow.indexOf(phase);
|
|
1090
1354
|
|
|
1091
|
-
if (phase ===
|
|
1355
|
+
if (phase === "init") {
|
|
1092
1356
|
actions.push({
|
|
1093
|
-
priority:
|
|
1357
|
+
priority: "P1-PHASE",
|
|
1094
1358
|
score: 900,
|
|
1095
|
-
command:
|
|
1096
|
-
reason:
|
|
1097
|
-
|
|
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 (
|
|
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:
|
|
1381
|
+
priority: "P1-PHASE",
|
|
1114
1382
|
score: 850,
|
|
1115
|
-
command: `/research "${researchTarget ||
|
|
1383
|
+
command: `/research "${researchTarget || "core topic"}"`,
|
|
1116
1384
|
reason: `Phase is define with no research claims yet. Need to advance to research.`,
|
|
1117
|
-
impact:
|
|
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 ===
|
|
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:
|
|
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 = {
|
|
1141
|
-
|
|
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 +
|
|
1432
|
+
const score = 600 + gap * 50 + entry.claims * 5;
|
|
1154
1433
|
|
|
1155
1434
|
if (rank <= 2 && expected >= 4) {
|
|
1156
1435
|
actions.push({
|
|
1157
|
-
priority:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
1471
|
+
if (entry.claims >= 3 && !(entry.types || []).includes("risk")) {
|
|
1193
1472
|
actions.push({
|
|
1194
|
-
priority:
|
|
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:
|
|
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:
|
|
1486
|
+
priority: "P4-CORROBORATION",
|
|
1208
1487
|
score: 300 + entry.claims * 2,
|
|
1209
|
-
command: `/witness ${entry.claim_ids?.[0] ||
|
|
1210
|
-
reason: `Topic "${topic}" has ${entry.claims} claims all from "${
|
|
1211
|
-
|
|
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 ===
|
|
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:
|
|
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:
|
|
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:
|
|
1527
|
+
priority: "P5-SHIP",
|
|
1247
1528
|
score: 100,
|
|
1248
|
-
command:
|
|
1249
|
-
reason:
|
|
1250
|
-
|
|
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:
|
|
1536
|
+
priority: "P1-PHASE",
|
|
1255
1537
|
score: 750,
|
|
1256
|
-
command:
|
|
1538
|
+
command: "/evaluate",
|
|
1257
1539
|
reason: `Phase is ${phase} but no evaluation claims exist. Time to test claims against reality.`,
|
|
1258
|
-
impact:
|
|
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(
|
|
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 {
|
|
1559
|
+
export {
|
|
1560
|
+
compile,
|
|
1561
|
+
diffCompilations,
|
|
1562
|
+
computeNextActions,
|
|
1563
|
+
generateManifest,
|
|
1564
|
+
loadConfig,
|
|
1565
|
+
detectSprints,
|
|
1566
|
+
EVIDENCE_TIERS,
|
|
1567
|
+
VALID_TYPES,
|
|
1568
|
+
};
|