@ecoma-io/archkeep 0.20.1 → 0.21.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecoma-io/archkeep",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript, Vue, Java and Kotlin",
5
5
  "keywords": [
6
6
  "architecture",
@@ -0,0 +1,601 @@
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
+ // Evidence-Complete gate names — the canonical roster
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * The canonical roster of required gates for Evidence-Complete.
54
+ * Each gate has a key, a human-readable label, and a type.
55
+ * @type {Readonly<{key: string, label: string, type: string}[]>}
56
+ */
57
+ export const EVIDENCE_COMPLETE_GATES = Object.freeze([
58
+ { key: "domainCoverage", label: "Domain coverage", type: "ratio" },
59
+ { key: "claimEvidenceCoverage", label: "Claim evidence coverage", type: "ratio" },
60
+ { key: "causalCoverage", label: "Causal coverage", type: "ratio" },
61
+ { key: "provenanceCoverage", label: "Provenance coverage", type: "ratio" },
62
+ { key: "mutationCoverage", label: "Mutation coverage", type: "ratio" },
63
+ { key: "surfaceParity", label: "Surface parity", type: "ratio" },
64
+ { key: "hiddenGapCount", label: "Hidden gap count", type: "count" },
65
+ { key: "falseCompleteCount", label: "False-complete count", type: "count" },
66
+ { key: "baseIdentityValid", label: "Base identity valid", type: "boolean" },
67
+ { key: "deterministic", label: "Deterministic", type: "boolean" },
68
+ ]);
69
+
70
+ /**
71
+ * @typedef {object} EvidenceCompleteContract
72
+ * @property {number} domainCoverage Ratio of evaluated required domains to required domains (0-1).
73
+ * @property {number} claimEvidenceCoverage Ratio of claims with valid evidence to material claims (0-1).
74
+ * @property {number} causalCoverage Ratio of consequences with complete causal chain to material consequences (0-1).
75
+ * @property {number} provenanceCoverage Ratio of authoritative inputs with verified provenance to total authoritative inputs (0-1).
76
+ * @property {number} mutationCoverage Ratio of mutations with explicit outcome and evidence to requested mutations (0-1).
77
+ * @property {number} surfaceParity Ratio of surfaces with equivalent semantic output to total surfaces (0-1).
78
+ * @property {number} hiddenGapCount Number of required domains/inputs/claims/consequences/mutations not actually evaluated or evidenced but not explicitly reported.
79
+ * @property {number} falseCompleteCount Number of times overallComplete was true while required gates failed.
80
+ * @property {boolean} baseIdentityValid Whether the base identity was verified (not merely attributed).
81
+ * @property {boolean} deterministic Whether repeated evaluations produce semantically equivalent results.
82
+ * @property {string} overallStatus Overall Evidence-Complete status: "complete" | "incomplete" | "not_evaluated".
83
+ * @property {boolean} overallComplete True only when ALL required gates pass.
84
+ * @property {object} gates Individual gate statuses, keyed by gate name.
85
+ * @property {object} gates.domainCoverage Gate status.
86
+ * @property {object} gates.claimEvidenceCoverage Gate status.
87
+ * @property {object} gates.causalCoverage Gate status.
88
+ * @property {object} gates.provenanceCoverage Gate status.
89
+ * @property {object} gates.mutationCoverage Gate status.
90
+ * @property {object} gates.surfaceParity Gate status.
91
+ * @property {object} gates.hiddenGapCount Gate status.
92
+ * @property {object} gates.falseCompleteCount Gate status.
93
+ * @property {object} gates.baseIdentityValid Gate status.
94
+ * @property {object} gates.deterministic Gate status.
95
+ */
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Helpers
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * Priority order for overall status computation (worst first).
103
+ * @type {string[]}
104
+ */
105
+ const STATUS_ORDER = [
106
+ EVALUATION_STATUS.REFUSED,
107
+ EVALUATION_STATUS.UNSUPPORTED,
108
+ EVALUATION_STATUS.NOT_EVALUATED,
109
+ EVALUATION_STATUS.PARTIAL,
110
+ EVALUATION_STATUS.EVALUATED,
111
+ ];
112
+
113
+ /**
114
+ * Returns the worst (highest-priority) status among the given statuses.
115
+ *
116
+ * @param {...string} statuses One or more EVALUATION_STATUS values.
117
+ * @returns {string} The worst status.
118
+ */
119
+ function worstStatus(...statuses) {
120
+ for (const candidate of STATUS_ORDER) {
121
+ if (statuses.includes(candidate)) return candidate;
122
+ }
123
+ return EVALUATION_STATUS.NOT_EVALUATED;
124
+ }
125
+
126
+ /**
127
+ * Creates a domain object from a status and optional note.
128
+ *
129
+ * @param {string} status One of EVALUATION_STATUS values.
130
+ * @param {string} [note] Optional explanatory note.
131
+ * @returns {{status: string, evaluated: boolean, partial: boolean, notEvaluated: boolean, unsupported: boolean, refused: boolean, note: string}}
132
+ */
133
+ export function createDomain(status, note = "") {
134
+ return {
135
+ status,
136
+ evaluated: status === EVALUATION_STATUS.EVALUATED || status === EVALUATION_STATUS.PARTIAL,
137
+ partial: status === EVALUATION_STATUS.PARTIAL,
138
+ notEvaluated: status === EVALUATION_STATUS.NOT_EVALUATED,
139
+ unsupported: status === EVALUATION_STATUS.UNSUPPORTED,
140
+ refused: status === EVALUATION_STATUS.REFUSED,
141
+ note,
142
+ };
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Domain-level evaluation status
147
+ // ---------------------------------------------------------------------------
148
+
149
+ /**
150
+ * Determines the evaluation status from a set of boolean flags.
151
+ *
152
+ * Priority order: refused > unsupported > notEvaluated > evaluated+partial.
153
+ * When no flag is set, defaults to NOT_EVALUATED.
154
+ *
155
+ * @param {object} [flags]
156
+ * @param {boolean} [flags.evaluated] Whether evaluation was performed.
157
+ * @param {boolean} [flags.partial] Whether evaluation was partial.
158
+ * @param {boolean} [flags.notEvaluated] Whether evaluation was not performed.
159
+ * @param {boolean} [flags.unsupported] Whether the domain is unsupported.
160
+ * @param {boolean} [flags.refused] Whether evaluation was refused.
161
+ * @returns {string} One of EVALUATION_STATUS values.
162
+ */
163
+ export function evaluationStatus({ evaluated, partial, notEvaluated, unsupported, refused } = {}) {
164
+ if (refused) return EVALUATION_STATUS.REFUSED;
165
+ if (unsupported) return EVALUATION_STATUS.UNSUPPORTED;
166
+ if (notEvaluated) return EVALUATION_STATUS.NOT_EVALUATED;
167
+ if (evaluated) {
168
+ return partial ? EVALUATION_STATUS.PARTIAL : EVALUATION_STATUS.EVALUATED;
169
+ }
170
+ return EVALUATION_STATUS.NOT_EVALUATED;
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Required domains — the declared evaluation contract
175
+ // ---------------------------------------------------------------------------
176
+
177
+ /**
178
+ * The authoritative set of required evaluation domains.
179
+ * Derived from repository doctrine: structural, constraints, boundaries,
180
+ * decisions, findings, debt, governance, evidence.
181
+ * @type {Readonly<string[]>}
182
+ */
183
+ export const REQUIRED_DOMAINS = Object.freeze([
184
+ "structural",
185
+ "constraint",
186
+ "boundary",
187
+ "decision",
188
+ "findings",
189
+ "debt",
190
+ "governance",
191
+ "evidence",
192
+ ]);
193
+
194
+ /**
195
+ * Returns true when the domain status counts as evaluated for coverage purposes.
196
+ * PARTIAL does NOT count as evaluated — only EVALUATED does.
197
+ *
198
+ * @param {string} status EVALUATION_STATUS value
199
+ * @returns {boolean}
200
+ */
201
+ export function isDomainEvaluated(status) {
202
+ return status === EVALUATION_STATUS.EVALUATED;
203
+ }
204
+
205
+ /**
206
+ * Computes domain coverage from the required domains and their statuses.
207
+ *
208
+ * domainCoverage = evaluatedRequiredDomains / requiredDomains
209
+ *
210
+ * @param {Object<string, string>} domainStatuses Map of domain name to EVALUATION_STATUS value.
211
+ * @param {readonly string[]} [requiredDomains] The required domain names (defaults to REQUIRED_DOMAINS).
212
+ * @returns {{coverage: number, evaluatedCount: number, requiredCount: number, failedDomains: string[]}}
213
+ */
214
+ export function computeDomainCoverage(domainStatuses, requiredDomains = REQUIRED_DOMAINS) {
215
+ const failedDomains = [];
216
+ let evaluatedCount = 0;
217
+
218
+ for (const domain of requiredDomains) {
219
+ const status = domainStatuses[domain];
220
+ if (status && isDomainEvaluated(status)) {
221
+ evaluatedCount++;
222
+ } else {
223
+ failedDomains.push(domain);
224
+ }
225
+ }
226
+
227
+ const requiredCount = requiredDomains.length;
228
+ const coverage = requiredCount > 0 ? evaluatedCount / requiredCount : 1;
229
+
230
+ return { coverage, evaluatedCount, requiredCount, failedDomains };
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Canonical completeness
235
+ // ---------------------------------------------------------------------------
236
+
237
+ /**
238
+ * @typedef {object} DomainStatus
239
+ * @property {string} status One of EVALUATION_STATUS values.
240
+ * @property {boolean} evaluated Whether evaluation was performed.
241
+ * @property {boolean} partial Whether evaluation was partial.
242
+ * @property {boolean} notEvaluated Whether evaluation was not performed.
243
+ * @property {boolean} unsupported Whether the domain is unsupported.
244
+ * @property {boolean} refused Whether evaluation was refused.
245
+ * @property {string} note Explanatory note about the status.
246
+ */
247
+
248
+ /**
249
+ * @typedef {object} CompletenessResult
250
+ * @property {object} domains Domain-level statuses, keyed by domain name.
251
+ * @property {DomainStatus} domains.structural
252
+ * @property {DomainStatus} domains.constraint
253
+ * @property {DomainStatus} domains.boundary
254
+ * @property {DomainStatus} domains.decision
255
+ * @property {DomainStatus} domains.findings
256
+ * @property {DomainStatus} domains.debt
257
+ * @property {DomainStatus} domains.governance
258
+ * @property {DomainStatus} domains.evidence
259
+ * @property {boolean} overallComplete True only when ALL applicable domains are EVALUATED
260
+ * AND the Evidence-Complete contract is satisfied.
261
+ * @property {string} overallStatus One of EVALUATION_STATUS values.
262
+ * @property {EvidenceCompleteContract} [evidenceComplete] The Evidence-Complete contract,
263
+ * present when computed.
264
+ * @property {number} [hiddenGapCount] Number of domains NOT_EVALUATED without a note.
265
+ * @property {number} [falseCompleteCount] Number of false-complete detections.
266
+
267
+ /**
268
+ * Builds canonical completeness from domain statuses.
269
+ * Now includes 8 domains (structural, constraint, boundary, decision,
270
+ * findings, debt, governance, evidence).
271
+ *
272
+ * @param {object} [input]
273
+ * @param {DomainStatus} [input.structural] The structural domain status.
274
+ * @param {DomainStatus} [input.constraint] The constraint domain status.
275
+ * @param {DomainStatus} [input.boundary] The boundary domain status.
276
+ * @param {DomainStatus} [input.decision] The decision domain status.
277
+ * @param {DomainStatus} [input.findings] The findings domain status.
278
+ * @param {DomainStatus} [input.debt] The debt domain status.
279
+ * @param {DomainStatus} [input.governance] The governance domain status.
280
+ * @param {DomainStatus} [input.evidence] The evidence domain status.
281
+ * @param {EvidenceCompleteContract} [input.evidenceComplete] Optional Evidence-Complete contract.
282
+ * @returns {CompletenessResult}
283
+ */
284
+ export function buildCompleteness({
285
+ structural,
286
+ constraint,
287
+ boundary,
288
+ decision,
289
+ findings,
290
+ debt,
291
+ governance,
292
+ evidence,
293
+ evidenceComplete,
294
+ } = {}) {
295
+ const domains = {
296
+ structural: structural ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
297
+ constraint: constraint ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
298
+ boundary: boundary ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
299
+ decision: decision ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
300
+ findings: findings ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
301
+ debt: debt ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
302
+ governance: governance ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
303
+ evidence: evidence ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED),
304
+ };
305
+
306
+ const statuses = Object.values(domains).map((d) => d.status);
307
+ const domainOverallComplete = statuses.every((s) => s === EVALUATION_STATUS.EVALUATED);
308
+
309
+ // If an Evidence-Complete contract is provided, enforce it as a gate
310
+ let ecComplete = true;
311
+ let falseCompleteCount = 0;
312
+ if (evidenceComplete) {
313
+ ecComplete = evidenceComplete.overallComplete;
314
+ falseCompleteCount = evidenceComplete.falseCompleteCount;
315
+ // Detect false complete: domain claims complete but Evidence-Complete fails
316
+ if (domainOverallComplete && !ecComplete) {
317
+ falseCompleteCount++;
318
+ }
319
+ }
320
+
321
+ // Detect hidden gaps: a domain is NOT_EVALUATED but no note explains why
322
+ let hiddenGapCount = 0;
323
+ for (const [, domain] of Object.entries(domains)) {
324
+ if (domain.status === EVALUATION_STATUS.NOT_EVALUATED && !domain.note) {
325
+ hiddenGapCount++;
326
+ }
327
+ }
328
+
329
+ const overallComplete = domainOverallComplete && ecComplete;
330
+ const overallStatus = worstStatus(...statuses, ecComplete ? EVALUATED : NOT_EVALUATED);
331
+
332
+ return {
333
+ domains,
334
+ overallComplete,
335
+ overallStatus,
336
+ ...(evidenceComplete ? { evidenceComplete: { ...evidenceComplete, falseCompleteCount } } : {}),
337
+ ...(hiddenGapCount > 0 ? { hiddenGapCount } : {}),
338
+ ...(falseCompleteCount > 0 ? { falseCompleteCount } : {}),
339
+ };
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Evidence-Complete gate
344
+ // ---------------------------------------------------------------------------
345
+
346
+ /**
347
+ * Builds an Evidence-Complete contract from the individual gate values.
348
+ *
349
+ * @param {object} gates
350
+ * @param {number} [gates.domainCoverage] Ratio (0-1).
351
+ * @param {number} [gates.claimEvidenceCoverage] Ratio (0-1).
352
+ * @param {number} [gates.causalCoverage] Ratio (0-1).
353
+ * @param {number} [gates.provenanceCoverage] Ratio (0-1).
354
+ * @param {number} [gates.mutationCoverage] Ratio (0-1).
355
+ * @param {number} [gates.surfaceParity] Ratio (0-1).
356
+ * @param {number} [gates.hiddenGapCount] Count (0 = pass).
357
+ * @param {number} [gates.falseCompleteCount] Count (0 = pass).
358
+ * @param {boolean} [gates.baseIdentityValid] Boolean (true = pass).
359
+ * @param {boolean} [gates.deterministic] Boolean (true = pass).
360
+ * @returns {EvidenceCompleteContract}
361
+ */
362
+ export function buildEvidenceComplete({
363
+ domainCoverage = 0,
364
+ claimEvidenceCoverage = 0,
365
+ causalCoverage = 0,
366
+ provenanceCoverage = 0,
367
+ mutationCoverage = 0,
368
+ surfaceParity = 0,
369
+ hiddenGapCount = -1,
370
+ falseCompleteCount = -1,
371
+ baseIdentityValid = false,
372
+ deterministic = false,
373
+ } = {}) {
374
+ const gates = {
375
+ domainCoverage: { value: domainCoverage, pass: domainCoverage === 1 },
376
+ claimEvidenceCoverage: { value: claimEvidenceCoverage, pass: claimEvidenceCoverage === 1 },
377
+ causalCoverage: { value: causalCoverage, pass: causalCoverage === 1 },
378
+ provenanceCoverage: { value: provenanceCoverage, pass: provenanceCoverage === 1 },
379
+ mutationCoverage: { value: mutationCoverage, pass: mutationCoverage === 1 },
380
+ surfaceParity: { value: surfaceParity, pass: surfaceParity === 1 },
381
+ hiddenGapCount: { value: hiddenGapCount, pass: hiddenGapCount === 0 },
382
+ falseCompleteCount: { value: falseCompleteCount, pass: falseCompleteCount === 0 },
383
+ baseIdentityValid: { value: baseIdentityValid, pass: baseIdentityValid === true },
384
+ deterministic: { value: deterministic, pass: deterministic === true },
385
+ };
386
+
387
+ const allPass = Object.values(gates).every((g) => g.pass);
388
+
389
+ return {
390
+ domainCoverage,
391
+ claimEvidenceCoverage,
392
+ causalCoverage,
393
+ provenanceCoverage,
394
+ mutationCoverage,
395
+ surfaceParity,
396
+ hiddenGapCount,
397
+ falseCompleteCount,
398
+ baseIdentityValid,
399
+ deterministic,
400
+ overallStatus: allPass ? "complete" : "incomplete",
401
+ overallComplete: allPass,
402
+ gates,
403
+ };
404
+ }
405
+
406
+ /**
407
+ * Asserts that the Evidence-Complete contract is satisfied.
408
+ * Throws with a detailed message listing every failing gate.
409
+ *
410
+ * @param {EvidenceCompleteContract} ec The Evidence-Complete contract to verify.
411
+ * @returns {void}
412
+ * @throws {Error} When any gate fails.
413
+ */
414
+ export function assertEvidenceComplete(ec) {
415
+ if (ec.overallComplete) return;
416
+
417
+ const failures = [];
418
+ for (const gate of EVIDENCE_COMPLETE_GATES) {
419
+ const g = ec.gates[gate.key];
420
+ if (!g.pass) {
421
+ failures.push(`${gate.label}: ${JSON.stringify(g.value)} (expected pass)`);
422
+ }
423
+ }
424
+
425
+ throw new Error(
426
+ `Evidence-Complete contract not satisfied.\n` +
427
+ ` Overall: ${ec.overallStatus}\n` +
428
+ ` Failed gates:\n ${failures.join("\n ")}`,
429
+ );
430
+ }
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // Governance completeness
434
+ // ---------------------------------------------------------------------------
435
+
436
+ /**
437
+ * @typedef {object} GovernanceCompletenessResult
438
+ * @property {DomainStatus} domain The governance domain status.
439
+ * @property {DomainStatus} findings The findings domain status.
440
+ * @property {DomainStatus} debt The debt domain status.
441
+ * @property {string} findingsStatus EVALUATION_STATUS for findings.
442
+ * @property {string} debtStatus EVALUATION_STATUS for debt.
443
+ * @property {number} findingsCount Number of findings.
444
+ * @property {number} debtCount Number of debt entries.
445
+ */
446
+
447
+ /**
448
+ * Builds governance completeness from findings and debt statuses.
449
+ *
450
+ * @param {object} [input]
451
+ * @param {string} [input.findingsStatus] EVALUATION_STATUS for findings.
452
+ * @param {string} [input.debtStatus] EVALUATION_STATUS for debt.
453
+ * @param {number} [input.findingsCount] Number of findings.
454
+ * @param {number} [input.debtCount] Number of debt entries.
455
+ * @returns {GovernanceCompletenessResult}
456
+ */
457
+ export function buildGovernanceCompleteness({
458
+ findingsStatus = EVALUATION_STATUS.NOT_EVALUATED,
459
+ debtStatus = EVALUATION_STATUS.NOT_EVALUATED,
460
+ findingsCount = 0,
461
+ debtCount = 0,
462
+ } = {}) {
463
+ const findingsDomain = createDomain(findingsStatus);
464
+ const debtDomain = createDomain(debtStatus);
465
+
466
+ const status = worstStatus(findingsStatus, debtStatus);
467
+
468
+ let note = "";
469
+ if (status !== EVALUATION_STATUS.EVALUATED) {
470
+ const parts = [];
471
+ if (findingsStatus !== EVALUATION_STATUS.EVALUATED) {
472
+ parts.push(`findings: ${findingsStatus}`);
473
+ }
474
+ if (debtStatus !== EVALUATION_STATUS.EVALUATED) {
475
+ parts.push(`debt: ${debtStatus}`);
476
+ }
477
+ note = `Governance incomplete — ${parts.join(", ")}`;
478
+ }
479
+
480
+ return {
481
+ domain: createDomain(status, note),
482
+ findings: findingsDomain,
483
+ debt: debtDomain,
484
+ findingsStatus,
485
+ debtStatus,
486
+ findingsCount,
487
+ debtCount,
488
+ };
489
+ }
490
+
491
+ // ---------------------------------------------------------------------------
492
+ // Scenario completeness
493
+ // ---------------------------------------------------------------------------
494
+
495
+ /**
496
+ * @typedef {object} ScenarioCompletenessResult
497
+ * @property {object} domains Domain-level statuses (standard 5 domains).
498
+ * @property {boolean} overallComplete True only when ALL domains are EVALUATED,
499
+ * including scenario-specific domains.
500
+ * @property {string} overallStatus One of EVALUATION_STATUS values.
501
+ * @property {object} scenarioDomains Scenario-specific domain statuses.
502
+ * @property {DomainStatus} scenarioDomains.changes Whether all changes were applied.
503
+ * @property {DomainStatus} scenarioDomains.base Whether the base revision identity was verified.
504
+ * @property {DomainStatus} scenarioDomains.mutationCoverage Mutation coverage status.
505
+ * @property {EvidenceCompleteContract} [evidenceComplete] The Evidence-Complete contract.
506
+ */
507
+
508
+ /**
509
+ * Builds scenario completeness, extending canonical completeness with
510
+ * scenario-specific domains (changes, base identity, mutation coverage).
511
+ *
512
+ * In a scenario context, standard domains (structural, constraint, boundary,
513
+ * decision) pass through from the evaluation — they are NOT automatically
514
+ * EVALUATED. The scenario-specific domains capture whether changes were
515
+ * fully applied, whether the base revision identity was verified, and
516
+ * whether mutation coverage was complete.
517
+ *
518
+ * @param {object} [input]
519
+ * @param {boolean} [input.changesComplete] Whether all scenario changes were applied.
520
+ * @param {boolean} [input.baseIdentityVerified] Whether the base revision identity was verified.
521
+ * @param {boolean} [input.mutationCoverageComplete] Whether all mutations have explicit outcomes.
522
+ * @param {GovernanceCompletenessResult} [input.governance] Governance completeness
523
+ * from buildGovernanceCompleteness.
524
+ * @param {EvidenceCompleteContract} [input.evidenceComplete] Optional Evidence-Complete contract.
525
+ * @param {object} [input.domains] Existing domain statuses to pass through.
526
+ * @returns {ScenarioCompletenessResult}
527
+ */
528
+ export function buildScenarioCompleteness({
529
+ changesComplete = true,
530
+ baseIdentityVerified = true,
531
+ mutationCoverageComplete = true,
532
+ governance,
533
+ evidenceComplete,
534
+ domains: existingDomains,
535
+ } = {}) {
536
+ const changesDomain = changesComplete
537
+ ? createDomain(EVALUATION_STATUS.EVALUATED)
538
+ : createDomain(EVALUATION_STATUS.PARTIAL, "Some changes could not be applied");
539
+
540
+ const baseDomain = baseIdentityVerified
541
+ ? createDomain(EVALUATION_STATUS.EVALUATED)
542
+ : createDomain(EVALUATION_STATUS.NOT_EVALUATED, "Base revision identity could not be verified");
543
+
544
+ const mutationDomain = mutationCoverageComplete
545
+ ? createDomain(EVALUATION_STATUS.EVALUATED)
546
+ : createDomain(EVALUATION_STATUS.PARTIAL, "Not all mutations have explicit outcomes");
547
+
548
+ const governanceDomain =
549
+ governance?.domain ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED, "Governance not evaluated");
550
+
551
+ // Use existing domains if provided, otherwise create defaults
552
+ const structuralDomain =
553
+ existingDomains?.structural ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
554
+ const constraintDomain =
555
+ existingDomains?.constraint ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
556
+ const boundaryDomain = existingDomains?.boundary ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
557
+ const decisionDomain = existingDomains?.decision ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
558
+ const findingsDomain = existingDomains?.findings ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
559
+ const debtDomain = existingDomains?.debt ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
560
+ const evidenceDomain = existingDomains?.evidence ?? createDomain(EVALUATION_STATUS.NOT_EVALUATED);
561
+
562
+ const baseResult = buildCompleteness({
563
+ structural: structuralDomain,
564
+ constraint: constraintDomain,
565
+ boundary: boundaryDomain,
566
+ decision: decisionDomain,
567
+ findings: findingsDomain,
568
+ debt: debtDomain,
569
+ governance: governanceDomain,
570
+ evidence: evidenceDomain,
571
+ evidenceComplete,
572
+ });
573
+
574
+ // Recompute overall with scenario domains included
575
+ const allStatuses = [
576
+ ...Object.values(baseResult.domains).map((d) => d.status),
577
+ changesDomain.status,
578
+ baseDomain.status,
579
+ mutationDomain.status,
580
+ ];
581
+ const overallComplete = allStatuses.every((s) => s === EVALUATION_STATUS.EVALUATED);
582
+ const overallStatus = worstStatus(...allStatuses);
583
+
584
+ return {
585
+ domains: baseResult.domains,
586
+ overallComplete,
587
+ overallStatus,
588
+ scenarioDomains: {
589
+ changes: changesDomain,
590
+ base: baseDomain,
591
+ mutationCoverage: mutationDomain,
592
+ },
593
+ ...(baseResult.evidenceComplete ? { evidenceComplete: baseResult.evidenceComplete } : {}),
594
+ ...(baseResult.hiddenGapCount !== undefined
595
+ ? { hiddenGapCount: baseResult.hiddenGapCount }
596
+ : {}),
597
+ ...(baseResult.falseCompleteCount !== undefined
598
+ ? { falseCompleteCount: baseResult.falseCompleteCount }
599
+ : {}),
600
+ };
601
+ }