@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.
- package/LICENSE +1 -1
- package/README.md +32 -31
- package/bin/wheat.js +47 -36
- package/compiler/detect-sprints.js +126 -92
- package/compiler/generate-manifest.js +116 -69
- package/compiler/wheat-compiler.js +789 -468
- package/lib/compiler.js +11 -6
- package/lib/connect.js +273 -134
- package/lib/disconnect.js +61 -40
- package/lib/guard.js +20 -17
- package/lib/index.js +8 -8
- package/lib/init.js +217 -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
|
|
@@ -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 =
|
|
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 =
|
|
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,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(
|
|
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);
|
|
661
801
|
|
|
662
802
|
// ── Sprint detection (git-based, non-fatal) ──────────────────────────────
|
|
663
803
|
let sprintsInfo = { active: null, sprints: [] };
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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(),
|
|
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,
|
|
693
|
-
|
|
694
|
-
|
|
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 ===
|
|
709
|
-
conflicted_claims: claims.filter(c => c.status ===
|
|
710
|
-
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,
|
|
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
|
-
|
|
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 = [
|
|
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
|
|
885
|
+
return "init";
|
|
735
886
|
}
|
|
736
887
|
|
|
737
888
|
// ─── Self-Containment Scanner ────────────────────────────────────────────────
|
|
738
889
|
function scanSelfContainment(dirs) {
|
|
739
|
-
const extPattern =
|
|
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(
|
|
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),
|
|
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
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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 =
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
790
|
-
}
|
|
945
|
+
process.exit(0);
|
|
946
|
+
}
|
|
791
947
|
|
|
792
|
-
// --scan mode: check HTML artifacts for external dependencies
|
|
793
|
-
if (args.includes(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
.
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
// --
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
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
|
-
|
|
946
|
-
|
|
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(
|
|
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
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
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(
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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(
|
|
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
|
-
|
|
989
|
-
|
|
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(
|
|
1288
|
+
process.exit(0);
|
|
992
1289
|
}
|
|
993
1290
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
console.log(
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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 ||
|
|
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:
|
|
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:
|
|
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 = [
|
|
1352
|
+
const phaseFlow = ["init", "define", "research", "prototype", "evaluate"];
|
|
1060
1353
|
const phaseIdx = phaseFlow.indexOf(phase);
|
|
1061
1354
|
|
|
1062
|
-
if (phase ===
|
|
1355
|
+
if (phase === "init") {
|
|
1063
1356
|
actions.push({
|
|
1064
|
-
priority:
|
|
1357
|
+
priority: "P1-PHASE",
|
|
1065
1358
|
score: 900,
|
|
1066
|
-
command:
|
|
1067
|
-
reason:
|
|
1068
|
-
|
|
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 (
|
|
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:
|
|
1381
|
+
priority: "P1-PHASE",
|
|
1085
1382
|
score: 850,
|
|
1086
|
-
command: `/research "${researchTarget ||
|
|
1383
|
+
command: `/research "${researchTarget || "core topic"}"`,
|
|
1087
1384
|
reason: `Phase is define with no research claims yet. Need to advance to research.`,
|
|
1088
|
-
impact:
|
|
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 ===
|
|
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:
|
|
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 = {
|
|
1112
|
-
|
|
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 +
|
|
1432
|
+
const score = 600 + gap * 50 + entry.claims * 5;
|
|
1125
1433
|
|
|
1126
1434
|
if (rank <= 2 && expected >= 4) {
|
|
1127
1435
|
actions.push({
|
|
1128
|
-
priority:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
1471
|
+
if (entry.claims >= 3 && !(entry.types || []).includes("risk")) {
|
|
1164
1472
|
actions.push({
|
|
1165
|
-
priority:
|
|
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:
|
|
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:
|
|
1486
|
+
priority: "P4-CORROBORATION",
|
|
1179
1487
|
score: 300 + entry.claims * 2,
|
|
1180
|
-
command: `/witness ${entry.claim_ids?.[0] ||
|
|
1181
|
-
reason: `Topic "${topic}" has ${entry.claims} claims all from "${
|
|
1182
|
-
|
|
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 ===
|
|
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:
|
|
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:
|
|
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:
|
|
1527
|
+
priority: "P5-SHIP",
|
|
1218
1528
|
score: 100,
|
|
1219
|
-
command:
|
|
1220
|
-
reason:
|
|
1221
|
-
|
|
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:
|
|
1536
|
+
priority: "P1-PHASE",
|
|
1226
1537
|
score: 750,
|
|
1227
|
-
command:
|
|
1538
|
+
command: "/evaluate",
|
|
1228
1539
|
reason: `Phase is ${phase} but no evaluation claims exist. Time to test claims against reality.`,
|
|
1229
|
-
impact:
|
|
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(
|
|
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 {
|
|
1559
|
+
export {
|
|
1560
|
+
compile,
|
|
1561
|
+
diffCompilations,
|
|
1562
|
+
computeNextActions,
|
|
1563
|
+
generateManifest,
|
|
1564
|
+
loadConfig,
|
|
1565
|
+
detectSprints,
|
|
1566
|
+
EVIDENCE_TIERS,
|
|
1567
|
+
VALID_TYPES,
|
|
1568
|
+
};
|