@ecoma-io/archkeep 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/cli.mjs +156 -66
  2. package/package.json +1 -1
  3. package/src/analysis/contract.md +32 -5
  4. package/src/analysis/source-util.mjs +107 -0
  5. package/src/analysis/typescript.mjs +86 -5
  6. package/src/commands/change.mjs +59 -28
  7. package/src/commands/check.mjs +65 -26
  8. package/src/commands/completeness.mjs +708 -0
  9. package/src/commands/context-command.mjs +13 -5
  10. package/src/commands/context.mjs +31 -4
  11. package/src/commands/coverage-verdict.mjs +184 -0
  12. package/src/commands/debt.mjs +18 -15
  13. package/src/commands/delta-classify.mjs +13 -18
  14. package/src/commands/delta.mjs +95 -33
  15. package/src/commands/diff.mjs +31 -24
  16. package/src/commands/discover.mjs +30 -10
  17. package/src/commands/drift.mjs +21 -21
  18. package/src/commands/edge-constraints.mjs +47 -1
  19. package/src/commands/evaluation-primitives.mjs +691 -0
  20. package/src/commands/evolution.mjs +27 -10
  21. package/src/commands/explain.mjs +14 -13
  22. package/src/commands/fitness.mjs +20 -19
  23. package/src/commands/graph.mjs +14 -5
  24. package/src/commands/health.mjs +12 -5
  25. package/src/commands/history.mjs +29 -15
  26. package/src/commands/impact-statement.mjs +31 -409
  27. package/src/commands/impact.mjs +18 -18
  28. package/src/commands/plan-context-command.mjs +10 -5
  29. package/src/commands/provenance-command.mjs +33 -2
  30. package/src/commands/reconcile.mjs +14 -17
  31. package/src/commands/scenario-evaluation.mjs +363 -198
  32. package/src/commands/scenario.mjs +32 -21
  33. package/src/commands/waivers.mjs +36 -28
  34. package/src/governance/evolution-event.mjs +62 -9
  35. package/src/governance/provenance-graph.mjs +479 -0
  36. package/src/intent/intent-manifest.json +83 -39
  37. package/src/report/json.mjs +32 -5
  38. package/src/report/provenance-text.mjs +30 -7
  39. package/src/report/text.mjs +82 -12
  40. package/src/verdict.mjs +78 -36
  41. package/src/workspace.mjs +126 -2
@@ -0,0 +1,708 @@
1
+ /**
2
+ * Shared completeness model used by both impact-statement and scenario-evaluation.
3
+ *
4
+ * Both the impact statement (`./impact-statement.mjs`) and scenario evaluation
5
+ * (`./scenario-evaluation.mjs`) compose deterministic evaluations into a single
6
+ * statement about what a change touches. This module provides the shared
7
+ * completeness vocabulary so two callers cannot drift.
8
+ *
9
+ * ## Evidence-Complete contract
10
+ *
11
+ * An evaluation is Evidence-Complete only when ALL required gates pass:
12
+ *
13
+ * - domainCoverage === 1
14
+ * - claimEvidenceCoverage === 1
15
+ * - causalCoverage === 1
16
+ * - provenanceCoverage === 1
17
+ * - mutationCoverage === 1
18
+ * - surfaceParity === 1
19
+ * - hiddenGapCount === 0
20
+ * - falseCompleteCount === 0
21
+ * - baseIdentityValid === true
22
+ * - deterministic === true
23
+ *
24
+ * `overallComplete` implies ALL gates pass. Any failed gate MUST prevent
25
+ * `overallComplete = true`.
26
+ *
27
+ * @module
28
+ */
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Evaluation status constants
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** @type {Readonly<{EVALUATED: string, PARTIAL: string, NOT_EVALUATED: string, UNSUPPORTED: string, REFUSED: string}>} */
35
+ export const EVALUATION_STATUS = Object.freeze({
36
+ EVALUATED: "evaluated",
37
+ PARTIAL: "partial",
38
+ NOT_EVALUATED: "not_evaluated",
39
+ UNSUPPORTED: "unsupported",
40
+ REFUSED: "refused",
41
+ });
42
+ export const EVALUATED = EVALUATION_STATUS.EVALUATED;
43
+ export const PARTIAL = EVALUATION_STATUS.PARTIAL;
44
+ export const NOT_EVALUATED = EVALUATION_STATUS.NOT_EVALUATED;
45
+ export const UNSUPPORTED = EVALUATION_STATUS.UNSUPPORTED;
46
+ export const REFUSED = EVALUATION_STATUS.REFUSED;
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Evaluation contract types — which gates are required per evaluation type
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * The evaluation contract types that determine which Evidence-Complete gates
54
+ * are required for `overallComplete`.
55
+ *
56
+ * - `canonical`: Standard architecture evaluation (no mutations, no scenario).
57
+ * Gates NOT required: mutationCoverage, surfaceParity, baseIdentityValid.
58
+ * - `scenario`: Hypothetical scenario evaluation. ALL gates required.
59
+ *
60
+ * @type {Readonly<{CANONICAL: string, SCENARIO: string}>}
61
+ */
62
+ export const EVALUATION_CONTRACT_TYPES = Object.freeze({
63
+ CANONICAL: "canonical",
64
+ SCENARIO: "scenario",
65
+ });
66
+
67
+ /**
68
+ * Which Evidence-Complete gates are required for each contract type.
69
+ * A gate not listed here is still tracked and reported but does NOT block
70
+ * `overallComplete` — it is explicitly not applicable for that evaluation type.
71
+ *
72
+ * @type {Readonly<Object<string, ReadonlySet<string>>>}
73
+ */
74
+ export const REQUIRED_GATES_FOR_CONTRACT = Object.freeze({
75
+ [EVALUATION_CONTRACT_TYPES.CANONICAL]: Object.freeze(
76
+ new Set([
77
+ "domainCoverage",
78
+ "claimEvidenceCoverage",
79
+ "causalCoverage",
80
+ "provenanceCoverage",
81
+ "hiddenGapCount",
82
+ "falseCompleteCount",
83
+ "deterministic",
84
+ ]),
85
+ ),
86
+ [EVALUATION_CONTRACT_TYPES.SCENARIO]: Object.freeze(
87
+ new Set([
88
+ "domainCoverage",
89
+ "claimEvidenceCoverage",
90
+ "causalCoverage",
91
+ "provenanceCoverage",
92
+ "mutationCoverage",
93
+ "surfaceParity",
94
+ "hiddenGapCount",
95
+ "falseCompleteCount",
96
+ "baseIdentityValid",
97
+ "deterministic",
98
+ ]),
99
+ ),
100
+ });
101
+
102
+ /**
103
+ * Returns true when the given gate key is required for the given contract type.
104
+ *
105
+ * @param {string} gateKey The gate key (e.g. "domainCoverage").
106
+ * @param {string} [contractType] The evaluation contract type.
107
+ * Defaults to SCENARIO (most restrictive).
108
+ * @returns {boolean}
109
+ */
110
+ export function isGateRequired(gateKey, contractType = EVALUATION_CONTRACT_TYPES.SCENARIO) {
111
+ const required = REQUIRED_GATES_FOR_CONTRACT[contractType];
112
+ return required ? required.has(gateKey) : true;
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Evidence-Complete gate names — the canonical roster
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * The canonical roster of required gates for Evidence-Complete.
120
+ * Each gate has a key, a human-readable label, and a type.
121
+ * @type {Readonly<{key: string, label: string, type: string}[]>}
122
+ */
123
+ export const EVIDENCE_COMPLETE_GATES = Object.freeze([
124
+ { key: "domainCoverage", label: "Domain coverage", type: "ratio" },
125
+ { key: "claimEvidenceCoverage", label: "Claim evidence coverage", type: "ratio" },
126
+ { key: "causalCoverage", label: "Causal coverage", type: "ratio" },
127
+ { key: "provenanceCoverage", label: "Provenance coverage", type: "ratio" },
128
+ { key: "mutationCoverage", label: "Mutation coverage", type: "ratio" },
129
+ { key: "surfaceParity", label: "Surface parity", type: "ratio" },
130
+ { key: "hiddenGapCount", label: "Hidden gap count", type: "count" },
131
+ { key: "falseCompleteCount", label: "False-complete count", type: "count" },
132
+ { key: "baseIdentityValid", label: "Base identity valid", type: "boolean" },
133
+ { key: "deterministic", label: "Deterministic", type: "boolean" },
134
+ ]);
135
+
136
+ /**
137
+ * @typedef {object} EvidenceCompleteContract
138
+ * @property {string} contractType The evaluation contract type (canonical | scenario).
139
+ * @property {number} domainCoverage Ratio of evaluated required domains to required domains (0-1).
140
+ * @property {number} claimEvidenceCoverage Ratio of claims with valid evidence to material claims (0-1).
141
+ * @property {number} causalCoverage Ratio of consequences with complete causal chain to material consequences (0-1).
142
+ * @property {number} provenanceCoverage Ratio of authoritative inputs with verified provenance to total authoritative inputs (0-1).
143
+ * @property {number} mutationCoverage Ratio of mutations with explicit outcome and evidence to requested mutations (0-1).
144
+ * @property {number} surfaceParity Ratio of surfaces with equivalent semantic output to total surfaces (0-1).
145
+ * @property {number} hiddenGapCount Number of required domains/inputs/claims/consequences/mutations not actually evaluated or evidenced but not explicitly reported.
146
+ * @property {number} falseCompleteCount Number of times overallComplete was true while required gates failed.
147
+ * @property {boolean} baseIdentityValid Whether the base identity was verified (not merely attributed).
148
+ * @property {boolean} deterministic Whether repeated evaluations produce semantically equivalent results.
149
+ * @property {string} overallStatus Overall Evidence-Complete status: "complete" | "incomplete" | "not_evaluated".
150
+ * @property {boolean} overallComplete True only when ALL required gates pass.
151
+ * @property {object} gates Individual gate statuses, keyed by gate name.
152
+ * @property {object} gates.domainCoverage Gate status with {value, pass, required}.
153
+ * @property {object} gates.claimEvidenceCoverage Gate status with {value, pass, required}.
154
+ * @property {object} gates.causalCoverage Gate status with {value, pass, required}.
155
+ * @property {object} gates.provenanceCoverage Gate status with {value, pass, required}.
156
+ * @property {object} gates.mutationCoverage Gate status with {value, pass, required}.
157
+ * @property {object} gates.surfaceParity Gate status with {value, pass, required}.
158
+ * @property {object} gates.hiddenGapCount Gate status with {value, pass, required}.
159
+ * @property {object} gates.falseCompleteCount Gate status with {value, pass, required}.
160
+ * @property {object} gates.baseIdentityValid Gate status with {value, pass, required}.
161
+ * @property {object} gates.deterministic Gate status with {value, pass, required}.
162
+ */
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Helpers
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Priority order for overall status computation (worst first).
170
+ * @type {string[]}
171
+ */
172
+ const STATUS_ORDER = [
173
+ EVALUATION_STATUS.REFUSED,
174
+ EVALUATION_STATUS.UNSUPPORTED,
175
+ EVALUATION_STATUS.NOT_EVALUATED,
176
+ EVALUATION_STATUS.PARTIAL,
177
+ EVALUATION_STATUS.EVALUATED,
178
+ ];
179
+
180
+ /**
181
+ * Returns the worst (highest-priority) status among the given statuses.
182
+ *
183
+ * @param {...string} statuses One or more EVALUATION_STATUS values.
184
+ * @returns {string} The worst status.
185
+ */
186
+ function worstStatus(...statuses) {
187
+ for (const candidate of STATUS_ORDER) {
188
+ if (statuses.includes(candidate)) return candidate;
189
+ }
190
+ return EVALUATION_STATUS.NOT_EVALUATED;
191
+ }
192
+
193
+ /**
194
+ * Creates a domain object from a status and optional note.
195
+ *
196
+ * @param {string} status One of EVALUATION_STATUS values.
197
+ * @param {string} [note] Optional explanatory note.
198
+ * @returns {{status: string, evaluated: boolean, partial: boolean, notEvaluated: boolean, unsupported: boolean, refused: boolean, note: string}}
199
+ */
200
+ export function createDomain(status, note = "") {
201
+ return {
202
+ status,
203
+ evaluated: status === EVALUATION_STATUS.EVALUATED || status === EVALUATION_STATUS.PARTIAL,
204
+ partial: status === EVALUATION_STATUS.PARTIAL,
205
+ notEvaluated: status === EVALUATION_STATUS.NOT_EVALUATED,
206
+ unsupported: status === EVALUATION_STATUS.UNSUPPORTED,
207
+ refused: status === EVALUATION_STATUS.REFUSED,
208
+ note,
209
+ };
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Domain-level evaluation status
214
+ // ---------------------------------------------------------------------------
215
+
216
+ /**
217
+ * Determines the evaluation status from a set of boolean flags.
218
+ *
219
+ * Priority order: refused > unsupported > notEvaluated > evaluated+partial.
220
+ * When no flag is set, defaults to NOT_EVALUATED.
221
+ *
222
+ * @param {object} [flags]
223
+ * @param {boolean} [flags.evaluated] Whether evaluation was performed.
224
+ * @param {boolean} [flags.partial] Whether evaluation was partial.
225
+ * @param {boolean} [flags.notEvaluated] Whether evaluation was not performed.
226
+ * @param {boolean} [flags.unsupported] Whether the domain is unsupported.
227
+ * @param {boolean} [flags.refused] Whether evaluation was refused.
228
+ * @returns {string} One of EVALUATION_STATUS values.
229
+ */
230
+ export function evaluationStatus({ evaluated, partial, notEvaluated, unsupported, refused } = {}) {
231
+ if (refused) return EVALUATION_STATUS.REFUSED;
232
+ if (unsupported) return EVALUATION_STATUS.UNSUPPORTED;
233
+ if (notEvaluated) return EVALUATION_STATUS.NOT_EVALUATED;
234
+ if (evaluated) {
235
+ return partial ? EVALUATION_STATUS.PARTIAL : EVALUATION_STATUS.EVALUATED;
236
+ }
237
+ return EVALUATION_STATUS.NOT_EVALUATED;
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Required domains — the declared evaluation contract
242
+ // ---------------------------------------------------------------------------
243
+
244
+ /**
245
+ * The authoritative set of required evaluation domains.
246
+ * Derived from repository doctrine: structural, constraints, boundaries,
247
+ * decisions, findings, debt, governance, evidence.
248
+ * @type {Readonly<string[]>}
249
+ */
250
+ export const REQUIRED_DOMAINS = Object.freeze([
251
+ "structural",
252
+ "constraint",
253
+ "boundary",
254
+ "decision",
255
+ "findings",
256
+ "debt",
257
+ "governance",
258
+ "evidence",
259
+ ]);
260
+
261
+ /**
262
+ * Returns true when the domain status counts as evaluated for coverage purposes.
263
+ * PARTIAL does NOT count as evaluated — only EVALUATED does.
264
+ *
265
+ * @param {string} status EVALUATION_STATUS value
266
+ * @returns {boolean}
267
+ */
268
+ export function isDomainEvaluated(status) {
269
+ return status === EVALUATION_STATUS.EVALUATED;
270
+ }
271
+
272
+ /**
273
+ * Computes domain coverage from the required domains and their statuses.
274
+ *
275
+ * domainCoverage = evaluatedRequiredDomains / requiredDomains
276
+ *
277
+ * @param {Object<string, string>} domainStatuses Map of domain name to EVALUATION_STATUS value.
278
+ * @param {readonly string[]} [requiredDomains] The required domain names (defaults to REQUIRED_DOMAINS).
279
+ * @returns {{coverage: number, evaluatedCount: number, requiredCount: number, failedDomains: string[]}}
280
+ */
281
+ export function computeDomainCoverage(domainStatuses, requiredDomains = REQUIRED_DOMAINS) {
282
+ const failedDomains = [];
283
+ let evaluatedCount = 0;
284
+
285
+ for (const domain of requiredDomains) {
286
+ const status = domainStatuses[domain];
287
+ if (status && isDomainEvaluated(status)) {
288
+ evaluatedCount++;
289
+ } else {
290
+ failedDomains.push(domain);
291
+ }
292
+ }
293
+
294
+ const requiredCount = requiredDomains.length;
295
+ const coverage = requiredCount > 0 ? evaluatedCount / requiredCount : 1;
296
+
297
+ return { coverage, evaluatedCount, requiredCount, failedDomains };
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Canonical completeness
302
+ // ---------------------------------------------------------------------------
303
+
304
+ /**
305
+ * @typedef {object} DomainStatus
306
+ * @property {string} status One of EVALUATION_STATUS values.
307
+ * @property {boolean} evaluated Whether evaluation was performed.
308
+ * @property {boolean} partial Whether evaluation was partial.
309
+ * @property {boolean} notEvaluated Whether evaluation was not performed.
310
+ * @property {boolean} unsupported Whether the domain is unsupported.
311
+ * @property {boolean} refused Whether evaluation was refused.
312
+ * @property {string} note Explanatory note about the status.
313
+ */
314
+
315
+ /**
316
+ * @typedef {object} CompletenessResult
317
+ * @property {object} domains Domain-level statuses, keyed by domain name.
318
+ * @property {DomainStatus} domains.structural
319
+ * @property {DomainStatus} domains.constraint
320
+ * @property {DomainStatus} domains.boundary
321
+ * @property {DomainStatus} domains.decision
322
+ * @property {DomainStatus} domains.findings
323
+ * @property {DomainStatus} domains.debt
324
+ * @property {DomainStatus} domains.governance
325
+ * @property {DomainStatus} domains.evidence
326
+ * @property {boolean} overallComplete True only when ALL applicable domains are EVALUATED
327
+ * AND the Evidence-Complete contract is satisfied.
328
+ * @property {string} overallStatus One of EVALUATION_STATUS values.
329
+ * @property {EvidenceCompleteContract} [evidenceComplete] The Evidence-Complete contract,
330
+ * present when computed.
331
+ * @property {number} [hiddenGapCount] Number of domains NOT_EVALUATED without a note.
332
+ * @property {number} [falseCompleteCount] Number of false-complete detections.
333
+
334
+ /**
335
+ * Builds canonical completeness from domain statuses.
336
+ * Now includes 8 domains (structural, constraint, boundary, decision,
337
+ * findings, debt, governance, evidence).
338
+ *
339
+ * @param {object} [input]
340
+ * @param {DomainStatus} [input.structural] The structural domain status.
341
+ * @param {DomainStatus} [input.constraint] The constraint domain status.
342
+ * @param {DomainStatus} [input.boundary] The boundary domain status.
343
+ * @param {DomainStatus} [input.decision] The decision domain status.
344
+ * @param {DomainStatus} [input.findings] The findings domain status.
345
+ * @param {DomainStatus} [input.debt] The debt domain status.
346
+ * @param {DomainStatus} [input.governance] The governance domain status.
347
+ * @param {DomainStatus} [input.evidence] The evidence domain status.
348
+ * @param {EvidenceCompleteContract} [input.evidenceComplete] Optional Evidence-Complete contract.
349
+ * @returns {CompletenessResult}
350
+ */
351
+ export function buildCompleteness({
352
+ structural,
353
+ constraint,
354
+ boundary,
355
+ decision,
356
+ findings,
357
+ debt,
358
+ governance,
359
+ evidence,
360
+ evidenceComplete,
361
+ } = {}) {
362
+ const domains = {
363
+ structural: structural ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
364
+ constraint: constraint ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
365
+ boundary: boundary ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
366
+ decision: decision ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
367
+ findings: findings ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
368
+ debt: debt ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
369
+ governance: governance ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
370
+ evidence: evidence ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
371
+ };
372
+
373
+ const statuses = Object.values(domains).map((d) => d.status);
374
+ const domainOverallComplete = statuses.every((s) => s === EVALUATION_STATUS.EVALUATED);
375
+
376
+ // If an Evidence-Complete contract is provided, enforce it as a gate.
377
+ // When no contract is provided, overallComplete MUST be false — the
378
+ // evaluation has not proven its evidence gates.
379
+ let ecComplete = false;
380
+ let falseCompleteCount = 0;
381
+ if (evidenceComplete) {
382
+ ecComplete = evidenceComplete.overallComplete;
383
+ falseCompleteCount = evidenceComplete.falseCompleteCount;
384
+ // Detect false complete: domain claims complete but Evidence-Complete fails
385
+ if (domainOverallComplete && !ecComplete) {
386
+ falseCompleteCount++;
387
+ }
388
+ }
389
+
390
+ // Detect hidden gaps: a domain is NOT_EVALUATED but no note explains why
391
+ let hiddenGapCount = 0;
392
+ for (const [, domain] of Object.entries(domains)) {
393
+ if (domain.status === EVALUATION_STATUS.NOT_EVALUATED && !domain.note) {
394
+ hiddenGapCount++;
395
+ }
396
+ }
397
+
398
+ const overallComplete = domainOverallComplete && ecComplete;
399
+ const overallStatus = worstStatus(...statuses, ecComplete ? EVALUATED : NOT_EVALUATED);
400
+
401
+ return {
402
+ domains,
403
+ overallComplete,
404
+ overallStatus,
405
+ ...(evidenceComplete ? { evidenceComplete: { ...evidenceComplete, falseCompleteCount } } : {}),
406
+ ...(hiddenGapCount > 0 ? { hiddenGapCount } : {}),
407
+ ...(falseCompleteCount > 0 ? { falseCompleteCount } : {}),
408
+ };
409
+ }
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // Evidence-Complete gate
413
+ // ---------------------------------------------------------------------------
414
+
415
+ /**
416
+ * Builds an Evidence-Complete contract from the individual gate values.
417
+ *
418
+ * Only gates required for the given `contractType` are considered for
419
+ * `overallComplete`. Gates not required are still tracked and reported
420
+ * but do NOT block completeness.
421
+ *
422
+ * @param {object} gates
423
+ * @param {number} [gates.domainCoverage] Ratio (0-1).
424
+ * @param {number} [gates.claimEvidenceCoverage] Ratio (0-1).
425
+ * @param {number} [gates.causalCoverage] Ratio (0-1).
426
+ * @param {number} [gates.provenanceCoverage] Ratio (0-1).
427
+ * @param {number} [gates.mutationCoverage] Ratio (0-1).
428
+ * @param {number} [gates.surfaceParity] Ratio (0-1).
429
+ * @param {number} [gates.hiddenGapCount] Count (0 = pass).
430
+ * @param {number} [gates.falseCompleteCount] Count (0 = pass).
431
+ * @param {boolean} [gates.baseIdentityValid] Boolean (true = pass).
432
+ * @param {boolean} [gates.deterministic] Boolean (true = pass).
433
+ * @param {string} [gates.contractType] Evaluation contract type for gate
434
+ * requirements (defaults to SCENARIO, the most restrictive).
435
+ * @returns {EvidenceCompleteContract}
436
+ */
437
+ export function buildEvidenceComplete({
438
+ domainCoverage = 0,
439
+ claimEvidenceCoverage = 0,
440
+ causalCoverage = 0,
441
+ provenanceCoverage = 0,
442
+ mutationCoverage = 0,
443
+ surfaceParity = 0,
444
+ hiddenGapCount = -1,
445
+ falseCompleteCount = -1,
446
+ baseIdentityValid = false,
447
+ deterministic = false,
448
+ contractType = EVALUATION_CONTRACT_TYPES.SCENARIO,
449
+ } = {}) {
450
+ const rawGates = {
451
+ domainCoverage: { value: domainCoverage, pass: domainCoverage === 1 },
452
+ claimEvidenceCoverage: { value: claimEvidenceCoverage, pass: claimEvidenceCoverage === 1 },
453
+ causalCoverage: { value: causalCoverage, pass: causalCoverage === 1 },
454
+ provenanceCoverage: { value: provenanceCoverage, pass: provenanceCoverage === 1 },
455
+ mutationCoverage: { value: mutationCoverage, pass: mutationCoverage === 1 },
456
+ surfaceParity: { value: surfaceParity, pass: surfaceParity === 1 },
457
+ hiddenGapCount: { value: hiddenGapCount, pass: hiddenGapCount === 0 },
458
+ falseCompleteCount: { value: falseCompleteCount, pass: falseCompleteCount === 0 },
459
+ baseIdentityValid: { value: baseIdentityValid, pass: baseIdentityValid === true },
460
+ deterministic: { value: deterministic, pass: deterministic === true },
461
+ };
462
+
463
+ // Only required gates block overallComplete
464
+ const allRequiredPass = Object.keys(rawGates)
465
+ .filter((key) => isGateRequired(key, contractType))
466
+ .every((key) => rawGates[key].pass);
467
+ // Annotate each gate with whether it is required for this contract type
468
+ /** @type {any} */
469
+ const gates = Object.fromEntries(
470
+ Object.entries(rawGates).map(([key, gate]) => [
471
+ key,
472
+ { ...gate, required: isGateRequired(key, contractType) },
473
+ ]),
474
+ );
475
+
476
+ return {
477
+ contractType,
478
+ domainCoverage,
479
+ claimEvidenceCoverage,
480
+ causalCoverage,
481
+ provenanceCoverage,
482
+ mutationCoverage,
483
+ surfaceParity,
484
+ hiddenGapCount,
485
+ falseCompleteCount,
486
+ baseIdentityValid,
487
+ deterministic,
488
+ overallStatus: allRequiredPass ? "complete" : "incomplete",
489
+ overallComplete: allRequiredPass,
490
+ gates,
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Asserts that the Evidence-Complete contract is satisfied.
496
+ * Throws with a detailed message listing every failing gate.
497
+ *
498
+ * @param {EvidenceCompleteContract} ec The Evidence-Complete contract to verify.
499
+ * @returns {void}
500
+ * @throws {Error} When any gate fails.
501
+ */
502
+ export function assertEvidenceComplete(ec) {
503
+ if (ec.overallComplete) return;
504
+
505
+ const contractType = ec.contractType || EVALUATION_CONTRACT_TYPES.SCENARIO;
506
+ const failures = [];
507
+ for (const gate of EVIDENCE_COMPLETE_GATES) {
508
+ const g = ec.gates[gate.key];
509
+ // Skip non-required gates for this contract type
510
+ if (g.required === false) continue;
511
+ if (!g.pass) {
512
+ failures.push(`${gate.label}: ${JSON.stringify(g.value)} (expected pass)`);
513
+ }
514
+ }
515
+
516
+ if (failures.length === 0) return;
517
+
518
+ throw new Error(
519
+ `Evidence-Complete contract not satisfied.\n` +
520
+ ` Contract type: ${contractType}\n` +
521
+ ` Overall: ${ec.overallStatus}\n` +
522
+ ` Failed gates:\n ${failures.join("\n ")}`,
523
+ );
524
+ }
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // Governance completeness
528
+ // ---------------------------------------------------------------------------
529
+
530
+ /**
531
+ * @typedef {object} GovernanceCompletenessResult
532
+ * @property {DomainStatus} domain The governance domain status.
533
+ * @property {DomainStatus} findings The findings domain status.
534
+ * @property {DomainStatus} debt The debt domain status.
535
+ * @property {string} findingsStatus EVALUATION_STATUS for findings.
536
+ * @property {string} debtStatus EVALUATION_STATUS for debt.
537
+ * @property {number} findingsCount Number of findings.
538
+ * @property {number} debtCount Number of debt entries.
539
+ */
540
+
541
+ /**
542
+ * Builds governance completeness from findings and debt statuses.
543
+ *
544
+ * @param {object} [input]
545
+ * @param {string} [input.findingsStatus] EVALUATION_STATUS for findings.
546
+ * @param {string} [input.debtStatus] EVALUATION_STATUS for debt.
547
+ * @param {number} [input.findingsCount] Number of findings.
548
+ * @param {number} [input.debtCount] Number of debt entries.
549
+ * @returns {GovernanceCompletenessResult}
550
+ */
551
+ export function buildGovernanceCompleteness({
552
+ findingsStatus = EVALUATION_STATUS.NOT_EVALUATED,
553
+ debtStatus = EVALUATION_STATUS.NOT_EVALUATED,
554
+ findingsCount = 0,
555
+ debtCount = 0,
556
+ } = {}) {
557
+ const findingsDomain = createDomain(findingsStatus);
558
+ const debtDomain = createDomain(debtStatus);
559
+
560
+ const status = worstStatus(findingsStatus, debtStatus);
561
+
562
+ let note = "";
563
+ if (status !== EVALUATION_STATUS.EVALUATED) {
564
+ const parts = [];
565
+ if (findingsStatus !== EVALUATION_STATUS.EVALUATED) {
566
+ parts.push(`findings: ${findingsStatus}`);
567
+ }
568
+ if (debtStatus !== EVALUATION_STATUS.EVALUATED) {
569
+ parts.push(`debt: ${debtStatus}`);
570
+ }
571
+ note = `Governance incomplete — ${parts.join(", ")}`;
572
+ }
573
+
574
+ return {
575
+ domain: createDomain(status, note),
576
+ findings: findingsDomain,
577
+ debt: debtDomain,
578
+ findingsStatus,
579
+ debtStatus,
580
+ findingsCount,
581
+ debtCount,
582
+ };
583
+ }
584
+
585
+ // ---------------------------------------------------------------------------
586
+ // Scenario completeness
587
+ // ---------------------------------------------------------------------------
588
+
589
+ /**
590
+ * @typedef {object} ScenarioCompletenessResult
591
+ * @property {object} domains Domain-level statuses (standard 5 domains).
592
+ * @property {boolean} overallComplete True only when ALL domains are EVALUATED,
593
+ * including scenario-specific domains.
594
+ * @property {string} overallStatus One of EVALUATION_STATUS values.
595
+ * @property {object} scenarioDomains Scenario-specific domain statuses.
596
+ * @property {DomainStatus} scenarioDomains.changes Whether all changes were applied.
597
+ * @property {DomainStatus} scenarioDomains.base Whether the base revision identity was verified.
598
+ * @property {DomainStatus} scenarioDomains.mutationCoverage Mutation coverage status.
599
+ * @property {EvidenceCompleteContract} [evidenceComplete] The Evidence-Complete contract.
600
+ */
601
+
602
+ /**
603
+ * Builds scenario completeness, extending canonical completeness with
604
+ * scenario-specific domains (changes, base identity, mutation coverage).
605
+ *
606
+ * In a scenario context, standard domains (structural, constraint, boundary,
607
+ * decision) pass through from the evaluation — they are NOT automatically
608
+ * EVALUATED. The scenario-specific domains capture whether changes were
609
+ * fully applied, whether the base revision identity was verified, and
610
+ * whether mutation coverage was complete.
611
+ *
612
+ * @param {object} [input]
613
+ * @param {boolean} [input.changesComplete] Whether all scenario changes were applied.
614
+ * @param {boolean} [input.baseIdentityVerified] Whether the base revision identity was verified.
615
+ * @param {boolean} [input.mutationCoverageComplete] Whether all mutations have explicit outcomes.
616
+ * @param {GovernanceCompletenessResult} [input.governance] Governance completeness
617
+ * from buildGovernanceCompleteness.
618
+ * @param {EvidenceCompleteContract} [input.evidenceComplete] Optional Evidence-Complete contract.
619
+ * @param {object} [input.domains] Existing domain statuses to pass through.
620
+ * @returns {ScenarioCompletenessResult}
621
+ */
622
+ export function buildScenarioCompleteness({
623
+ changesComplete = true,
624
+ baseIdentityVerified = true,
625
+ mutationCoverageComplete = true,
626
+ governance,
627
+ evidenceComplete,
628
+ domains: existingDomains,
629
+ } = {}) {
630
+ const changesDomain = changesComplete
631
+ ? createDomain(EVALUATION_STATUS.EVALUATED)
632
+ : createDomain(EVALUATION_STATUS.PARTIAL, "Some changes could not be applied");
633
+
634
+ const baseDomain = baseIdentityVerified
635
+ ? createDomain(EVALUATION_STATUS.EVALUATED)
636
+ : createDomain(EVALUATION_STATUS.NOT_EVALUATED, "Base revision identity could not be verified");
637
+
638
+ const mutationDomain = mutationCoverageComplete
639
+ ? createDomain(EVALUATION_STATUS.EVALUATED)
640
+ : createDomain(EVALUATION_STATUS.PARTIAL, "Not all mutations have explicit outcomes");
641
+
642
+ const governanceDomain =
643
+ governance?.domain ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED, "Governance not evaluated");
644
+
645
+ // Use existing domains if provided, otherwise create defaults
646
+ const structuralDomain =
647
+ existingDomains?.structural ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
648
+ const constraintDomain =
649
+ existingDomains?.constraint ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
650
+ const boundaryDomain = existingDomains?.boundary ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
651
+ const decisionDomain = existingDomains?.decision ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
652
+ const findingsDomain = existingDomains?.findings ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
653
+ const debtDomain = existingDomains?.debt ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
654
+ const evidenceDomain = existingDomains?.evidence ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
655
+
656
+ const baseResult = buildCompleteness({
657
+ structural: structuralDomain,
658
+ constraint: constraintDomain,
659
+ boundary: boundaryDomain,
660
+ decision: decisionDomain,
661
+ findings: findingsDomain,
662
+ debt: debtDomain,
663
+ governance: governanceDomain,
664
+ evidence: evidenceDomain,
665
+ evidenceComplete,
666
+ });
667
+
668
+ // Recompute overall: base domains + EC gate + scenario domains.
669
+ // baseResult.overallComplete already includes the evidenceComplete gate,
670
+ // so reusing it prevents the silent-complete defect.
671
+ const overallComplete =
672
+ baseResult.overallComplete &&
673
+ changesDomain.status === EVALUATION_STATUS.EVALUATED &&
674
+ baseDomain.status === EVALUATION_STATUS.EVALUATED &&
675
+ mutationDomain.status === EVALUATION_STATUS.EVALUATED;
676
+ const allStatuses = [
677
+ ...Object.values(baseResult.domains).map((d) => d.status),
678
+ changesDomain.status,
679
+ baseDomain.status,
680
+ mutationDomain.status,
681
+ ];
682
+ // Include EC gate status in overall status: when evidenceComplete is
683
+ // provided and fails, overall status must reflect that.
684
+ const ecStatus = evidenceComplete
685
+ ? evidenceComplete.overallComplete
686
+ ? EVALUATION_STATUS.EVALUATED
687
+ : EVALUATION_STATUS.NOT_EVALUATED
688
+ : EVALUATION_STATUS.NOT_EVALUATED;
689
+ const overallStatus = worstStatus(...allStatuses, ecStatus);
690
+
691
+ return {
692
+ domains: baseResult.domains,
693
+ overallComplete,
694
+ overallStatus,
695
+ scenarioDomains: {
696
+ changes: changesDomain,
697
+ base: baseDomain,
698
+ mutationCoverage: mutationDomain,
699
+ },
700
+ ...(baseResult.evidenceComplete ? { evidenceComplete: baseResult.evidenceComplete } : {}),
701
+ ...(baseResult.hiddenGapCount !== undefined
702
+ ? { hiddenGapCount: baseResult.hiddenGapCount }
703
+ : {}),
704
+ ...(baseResult.falseCompleteCount !== undefined
705
+ ? { falseCompleteCount: baseResult.falseCompleteCount }
706
+ : {}),
707
+ };
708
+ }