musubix3 0.1.19 → 0.1.21

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 (80) hide show
  1. package/.github/plugin/marketplace.json +2 -2
  2. package/.github/skills/sdd-change/SKILL.md +28 -45
  3. package/CHANGELOG.md +126 -0
  4. package/README-ja.md +171 -5
  5. package/README.md +185 -8
  6. package/dist/packages/analysis/src/adapters.d.ts +2 -2
  7. package/dist/packages/analysis/src/adapters.js +33 -3
  8. package/dist/packages/analysis/src/adapters.js.map +1 -1
  9. package/dist/packages/analysis/src/approval-record.js +4 -0
  10. package/dist/packages/analysis/src/approval-record.js.map +1 -1
  11. package/dist/packages/analysis/src/approval.d.ts +51 -0
  12. package/dist/packages/analysis/src/approval.js +341 -2
  13. package/dist/packages/analysis/src/approval.js.map +1 -1
  14. package/dist/packages/analysis/src/attestation.js +4 -0
  15. package/dist/packages/analysis/src/attestation.js.map +1 -1
  16. package/dist/packages/analysis/src/change-evidence.d.ts +65 -8
  17. package/dist/packages/analysis/src/change-evidence.js +173 -46
  18. package/dist/packages/analysis/src/change-evidence.js.map +1 -1
  19. package/dist/packages/analysis/src/change-waiver.d.ts +46 -13
  20. package/dist/packages/analysis/src/change-waiver.js +481 -152
  21. package/dist/packages/analysis/src/change-waiver.js.map +1 -1
  22. package/dist/packages/analysis/src/change.d.ts +1 -0
  23. package/dist/packages/analysis/src/change.js +194 -66
  24. package/dist/packages/analysis/src/change.js.map +1 -1
  25. package/dist/packages/analysis/src/evidence-merge-guard.d.ts +9 -0
  26. package/dist/packages/analysis/src/evidence-merge-guard.js +62 -0
  27. package/dist/packages/analysis/src/evidence-merge-guard.js.map +1 -0
  28. package/dist/packages/analysis/src/evidence-merge.d.ts +55 -0
  29. package/dist/packages/analysis/src/evidence-merge.js +1182 -0
  30. package/dist/packages/analysis/src/evidence-merge.js.map +1 -0
  31. package/dist/packages/analysis/src/evidence-writer-lock.d.ts +189 -0
  32. package/dist/packages/analysis/src/evidence-writer-lock.js +843 -0
  33. package/dist/packages/analysis/src/evidence-writer-lock.js.map +1 -0
  34. package/dist/packages/analysis/src/files.js +11 -0
  35. package/dist/packages/analysis/src/files.js.map +1 -1
  36. package/dist/packages/analysis/src/formal.js +7 -0
  37. package/dist/packages/analysis/src/formal.js.map +1 -1
  38. package/dist/packages/analysis/src/gate.d.ts +1 -1
  39. package/dist/packages/analysis/src/gate.js +23 -6
  40. package/dist/packages/analysis/src/gate.js.map +1 -1
  41. package/dist/packages/analysis/src/graph.js +6 -0
  42. package/dist/packages/analysis/src/graph.js.map +1 -1
  43. package/dist/packages/analysis/src/index.d.ts +3 -0
  44. package/dist/packages/analysis/src/index.js +3 -0
  45. package/dist/packages/analysis/src/index.js.map +1 -1
  46. package/dist/packages/analysis/src/knowledge.js +4 -0
  47. package/dist/packages/analysis/src/knowledge.js.map +1 -1
  48. package/dist/packages/analysis/src/model-correspondence.js +5 -1
  49. package/dist/packages/analysis/src/model-correspondence.js.map +1 -1
  50. package/dist/packages/analysis/src/mutation.js +4 -0
  51. package/dist/packages/analysis/src/mutation.js.map +1 -1
  52. package/dist/packages/analysis/src/order.d.ts +16 -1
  53. package/dist/packages/analysis/src/order.js +10 -0
  54. package/dist/packages/analysis/src/order.js.map +1 -1
  55. package/dist/packages/analysis/src/performance.js +5 -1
  56. package/dist/packages/analysis/src/performance.js.map +1 -1
  57. package/dist/packages/analysis/src/quality-refresh.d.ts +18 -0
  58. package/dist/packages/analysis/src/quality-refresh.js +312 -0
  59. package/dist/packages/analysis/src/quality-refresh.js.map +1 -0
  60. package/dist/packages/analysis/src/tdd.d.ts +86 -3
  61. package/dist/packages/analysis/src/tdd.js +604 -52
  62. package/dist/packages/analysis/src/tdd.js.map +1 -1
  63. package/dist/packages/analysis/src/trace.d.ts +24 -0
  64. package/dist/packages/analysis/src/trace.js +101 -3
  65. package/dist/packages/analysis/src/trace.js.map +1 -1
  66. package/dist/packages/analysis/src/workflow-waiver.d.ts +6 -2
  67. package/dist/packages/analysis/src/workflow-waiver.js +9 -5
  68. package/dist/packages/analysis/src/workflow-waiver.js.map +1 -1
  69. package/dist/packages/analysis/src/workflow.d.ts +1 -1
  70. package/dist/packages/analysis/src/workflow.js +103 -21
  71. package/dist/packages/analysis/src/workflow.js.map +1 -1
  72. package/dist/packages/cli/src/install.js +51 -6
  73. package/dist/packages/cli/src/install.js.map +1 -1
  74. package/dist/packages/cli/src/main.js +252 -79
  75. package/dist/packages/cli/src/main.js.map +1 -1
  76. package/dist/packages/domain/src/design.js +8 -1
  77. package/dist/packages/domain/src/design.js.map +1 -1
  78. package/dist/packages/domain/src/types.d.ts +1 -0
  79. package/package.json +7 -2
  80. package/plugin.json +1 -1
@@ -1,8 +1,9 @@
1
1
  import { error } from '../../domain/src/index.js';
2
2
  import { digest, exists, readText, within, writeJson } from './files.js';
3
- import { batchFor, batchForKey, batchKey, completenessTddUnsatisfiedCondition, designUnchangedCondition, effectiveBatches, greenUnprovenCondition, implementationUnchangedCondition, loadChangeEvidence, orderMigrationRequiredBatchCondition, orderMigrationRequiredPhaseCondition, orderMigrationRequiredRequirementCondition, phaseMissingCondition, recordMissingCondition, redUnprovenCondition, relevantImplementationUnchangedCondition, requirementsUnchangedCondition, testChangedAfterRedCondition, testsUnchangedCondition, } from './change-evidence.js';
4
- import { loadTddEvidence } from './tdd.js';
3
+ import { batchFor, batchesForKey, batchKey, designUnchangedCondition, currentRequirementIdsForBatch, effectiveBatches, implementationUnchangedCondition, loadChangeEvidence, orderMigrationRequiredBatchCondition, orderMigrationRequiredPhaseCondition, phaseMissingCondition, phaseOrderBatchCondition, phaseOrderPhaseCondition, recordMissingCondition, relevantImplementationUnchangedCondition, requirementsUnchangedCondition, testChangedAfterRedCondition, testsUnchangedCondition, } from './change-evidence.js';
4
+ import { loadTddEvidence, validlyVoidedTddCycles, validlyArchivedTddCycles, completenessTddUnsatisfiedCondition, greenUnprovenCondition, orderMigrationRequiredRequirementCondition, redUnprovenCondition, voidedCycleOrdersInCurrentWindow, } from './tdd.js';
5
5
  import { appendEvidenceOrder, evidenceOrderRecord, inspectEvidenceOrder } from './order.js';
6
+ import { withEvidenceWriterLock } from './evidence-writer-lock.js';
6
7
  const WAIVER_PATH = '.musubix/evidence/change-waivers.json';
7
8
  const GENESIS_SHA256 = '0'.repeat(64);
8
9
  const SHA256_RE = /^[a-f0-9]{64}$/i;
@@ -24,6 +25,7 @@ export const WAIVABLE_CODES = [
24
25
  'CHANGE_IMPLEMENTATION_UNCHANGED',
25
26
  'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED',
26
27
  'CHANGE_TEST_CHANGED_AFTER_RED',
28
+ 'CHANGE_PHASE_ORDER',
27
29
  ];
28
30
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-017
29
31
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-004 REQ-CHANGE-EVIDENCE-WAIVER-016
@@ -36,7 +38,7 @@ export const NEITHER_KEY_CODES = new Set(['CHANGE_REQUIREMENTS_UNCHANGED', 'CHAN
36
38
  export const REQUIREMENT_ONLY_CODES = new Set(['CHANGE_RED_UNPROVEN', 'CHANGE_GREEN_UNPROVEN', 'CHANGE_COMPLETENESS_TDD']);
37
39
  export const DETAIL_ONLY_CODES = new Set([
38
40
  'CHANGE_PHASE_MISSING', 'CHANGE_ORDER_MIGRATION_REQUIRED', 'CHANGE_TESTS_UNCHANGED',
39
- 'CHANGE_IMPLEMENTATION_UNCHANGED', 'CHANGE_TEST_CHANGED_AFTER_RED',
41
+ 'CHANGE_IMPLEMENTATION_UNCHANGED', 'CHANGE_TEST_CHANGED_AFTER_RED', 'CHANGE_PHASE_ORDER',
40
42
  ]);
41
43
  export const BOTH_KEYS_CODES = new Set(['CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED']);
42
44
  /** Retained purely for backward-compatible naming inside this module. */
@@ -64,6 +66,49 @@ export function canonicalJson(value) {
64
66
  }
65
67
  return JSON.stringify(value) ?? 'null';
66
68
  }
69
+ function changeRecordMissingApprovalNoteLines(changeId, waiverRecord) {
70
+ return [
71
+ 'Code: CHANGE_RECORD_MISSING',
72
+ `Approver: ${waiverRecord.approver}`,
73
+ `Recorded at: ${waiverRecord.recordedAt}`,
74
+ `Snapshot hash: ${waiverRecord.snapshotHash}`,
75
+ 'Files:',
76
+ `- .musubix/changes/${changeId}.md`,
77
+ '- .musubix/evidence/changes.json',
78
+ '- .musubix/evidence/order.json',
79
+ ];
80
+ }
81
+ function trimTrailingApprovalSeparator(prefix) {
82
+ if (prefix.endsWith('\r\n\r\n'))
83
+ return prefix.slice(0, -2);
84
+ if (prefix.endsWith('\n\n'))
85
+ return prefix.slice(0, -1);
86
+ return prefix;
87
+ }
88
+ /** @id CODE-CHANGE-EVIDENCE-WAIVER-026
89
+ * @implements REQ-CHANGE-EVIDENCE-WAIVER-011
90
+ * @design DES-CHANGE-EVIDENCE-WAIVER-002
91
+ */
92
+ export function normalizeChangeRecordMissingDocument(text, changeId, waiverRecord) {
93
+ if (!waiverRecord || waiverRecord.code !== 'CHANGE_RECORD_MISSING')
94
+ return text;
95
+ const headings = [...text.matchAll(/^## [^\r\n]+$/gm)];
96
+ const lastHeading = headings.at(-1);
97
+ if (!lastHeading || lastHeading[0] !== '## Debt Remediation Approval' || lastHeading.index === undefined) {
98
+ return text;
99
+ }
100
+ const sectionStart = lastHeading.index;
101
+ const afterHeading = text.slice(sectionStart + lastHeading[0].length).replace(/^\r?\n/, '');
102
+ const nonBlankLines = afterHeading
103
+ .split(/\r?\n/)
104
+ .filter((line) => line.length > 0);
105
+ const expectedLines = changeRecordMissingApprovalNoteLines(changeId, waiverRecord);
106
+ if (nonBlankLines.length !== expectedLines.length)
107
+ return text;
108
+ if (!nonBlankLines.every((line, index) => line === expectedLines[index]))
109
+ return text;
110
+ return trimTrailingApprovalSeparator(text.slice(0, sectionStart));
111
+ }
67
112
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-002
68
113
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-007
69
114
  * @design DES-CHANGE-EVIDENCE-WAIVER-001
@@ -76,7 +121,7 @@ export async function loadChangeWaiverEvidence(root) {
76
121
  if (value.schemaVersion !== 1 || !Array.isArray(value.waivers)) {
77
122
  return { schemaVersion: 1, waivers: [], malformed: true };
78
123
  }
79
- return { schemaVersion: 1, waivers: value.waivers };
124
+ return { ...value, schemaVersion: 1, waivers: value.waivers };
80
125
  }
81
126
  catch {
82
127
  return { schemaVersion: 1, waivers: [], malformed: true };
@@ -114,8 +159,23 @@ export function diagnosticDetail(code, context) {
114
159
  || code === 'CHANGE_TEST_CHANGED_AFTER_RED' || code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED') {
115
160
  return context.batch !== undefined ? batchKey(context.batch.requirementIds) : undefined;
116
161
  }
162
+ if (code === 'CHANGE_PHASE_ORDER') {
163
+ if (context.batchPhaseName !== undefined && context.batch !== undefined) {
164
+ return `batch:${context.batchPhaseName}:${batchKey(context.batch.requirementIds)}`;
165
+ }
166
+ if (context.phaseName !== undefined)
167
+ return `phase:${context.phaseName}`;
168
+ return undefined;
169
+ }
117
170
  return undefined;
118
171
  }
172
+ function validBatchKey(key) {
173
+ const ids = key.split(',');
174
+ return ids.length > 0
175
+ && ids.every((id) => id.length > 0)
176
+ && new Set(ids).size === ids.length
177
+ && batchKey(ids) === key;
178
+ }
119
179
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-019
120
180
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-016
121
181
  * @design DES-CHANGE-EVIDENCE-WAIVER-002
@@ -125,31 +185,71 @@ export function diagnosticDetail(code, context) {
125
185
  export function parseDetail(code, detail) {
126
186
  if (detail === undefined)
127
187
  return null;
128
- if (detail.startsWith('phase:'))
129
- return { kind: 'phase', phaseName: detail.slice('phase:'.length) };
188
+ if (detail.startsWith('phase:')) {
189
+ const phaseName = detail.slice('phase:'.length);
190
+ const allowed = code === 'CHANGE_PHASE_MISSING'
191
+ ? ['impact', 'requirements', 'design', 'quality', ...tddBatchPhaseNames]
192
+ : code === 'CHANGE_ORDER_MIGRATION_REQUIRED'
193
+ ? ['impact', 'requirements', 'design']
194
+ : code === 'CHANGE_PHASE_ORDER'
195
+ ? ['requirements', 'design']
196
+ : [];
197
+ return allowed.includes(phaseName) ? { kind: 'phase', phaseName } : null;
198
+ }
130
199
  if (detail.startsWith('batch:')) {
200
+ if (code !== 'CHANGE_ORDER_MIGRATION_REQUIRED' && code !== 'CHANGE_PHASE_ORDER')
201
+ return null;
131
202
  const rest = detail.slice('batch:'.length);
132
203
  const separator = rest.indexOf(':');
133
204
  if (separator === -1)
134
205
  return null;
135
- return { kind: 'batch', batchPhaseName: rest.slice(0, separator), batchKey: rest.slice(separator + 1) };
206
+ const batchPhaseName = rest.slice(0, separator);
207
+ const key = rest.slice(separator + 1);
208
+ const allowedBatchPhaseNames = code === 'CHANGE_PHASE_ORDER'
209
+ ? [...tddBatchPhaseNames, 'quality']
210
+ : tddBatchPhaseNames;
211
+ return allowedBatchPhaseNames.includes(batchPhaseName) && validBatchKey(key)
212
+ ? { kind: 'batch', batchPhaseName, batchKey: key }
213
+ : null;
214
+ }
215
+ if (detail.startsWith('requirement:')) {
216
+ if (code !== 'CHANGE_ORDER_MIGRATION_REQUIRED')
217
+ return null;
218
+ const parsedRequirementId = detail.slice('requirement:'.length);
219
+ return parsedRequirementId ? { kind: 'requirement', requirementId: parsedRequirementId } : null;
136
220
  }
137
- if (detail.startsWith('requirement:'))
138
- return { kind: 'requirement', requirementId: detail.slice('requirement:'.length) };
139
221
  if (code === 'CHANGE_TESTS_UNCHANGED' || code === 'CHANGE_IMPLEMENTATION_UNCHANGED'
140
222
  || code === 'CHANGE_TEST_CHANGED_AFTER_RED' || code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED') {
141
- return { kind: 'batchKey', batchKey: detail };
223
+ return validBatchKey(detail) ? { kind: 'batchKey', batchKey: detail } : null;
142
224
  }
143
225
  return null;
144
226
  }
227
+ function compareSnapshotCycles(left, right, orderOf, idOf, serializedOf) {
228
+ const rank = (value) => Number.isInteger(value) ? 0 : value === undefined || value === null ? 2 : 1;
229
+ const leftOrder = orderOf(left);
230
+ const rightOrder = orderOf(right);
231
+ const rankDifference = rank(leftOrder) - rank(rightOrder);
232
+ if (rankDifference !== 0)
233
+ return rankDifference;
234
+ if (Number.isInteger(leftOrder) && Number.isInteger(rightOrder) && leftOrder !== rightOrder) {
235
+ return leftOrder - rightOrder;
236
+ }
237
+ if (!Number.isInteger(leftOrder) && leftOrder != null && rightOrder != null) {
238
+ const orderComparison = canonicalJson(leftOrder).localeCompare(canonicalJson(rightOrder));
239
+ if (orderComparison !== 0)
240
+ return orderComparison;
241
+ }
242
+ const idComparison = (idOf(left) ?? '').localeCompare(idOf(right) ?? '');
243
+ if (idComparison !== 0)
244
+ return idComparison;
245
+ return canonicalJson(serializedOf(left)).localeCompare(canonicalJson(serializedOf(right)));
246
+ }
145
247
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-004
146
248
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-011 REQ-CHANGE-EVIDENCE-WAIVER-016
147
249
  * @design DES-CHANGE-EVIDENCE-WAIVER-002
148
250
  */
149
- export async function snapshotPayload(root, evidence, tdd, order, changeId, code, requirementId, detail) {
251
+ export async function snapshotPayload(root, evidence, tdd, order, changeId, code, requirementId, detail, waiverRecord) {
150
252
  const changePath = `.musubix/changes/${changeId}.md`;
151
- if (!await exists(within(root, changePath)))
152
- return null;
153
253
  const change = evidence?.changes.find((entry) => entry.changeId === changeId);
154
254
  if (code === 'CHANGE_REQUIREMENTS_UNCHANGED') {
155
255
  const impact = change?.phases.impact;
@@ -169,8 +269,11 @@ export async function snapshotPayload(root, evidence, tdd, order, changeId, code
169
269
  };
170
270
  }
171
271
  if (code === 'CHANGE_RECORD_MISSING') {
272
+ if (!await exists(within(root, changePath)))
273
+ return null;
274
+ const documentText = await readText(root, changePath);
172
275
  return {
173
- documentDigest: digest(await readText(root, changePath)),
276
+ documentDigest: digest(normalizeChangeRecordMissingDocument(documentText, changeId, waiverRecord)),
174
277
  everRecorded: [...order.records.values()].some((record) => record.kind === 'change' && record.entityId === changeId && record.phase !== 'waiver'),
175
278
  currentEntry: change ? digest(canonicalJson(change)) : null,
176
279
  };
@@ -184,23 +287,25 @@ export async function snapshotPayload(root, evidence, tdd, order, changeId, code
184
287
  .filter((cycle) => cycle.requirementId === requirementId)
185
288
  .map((cycle) => ({
186
289
  cycleId: cycle.cycleId ?? '',
187
- red: { valid: cycle.red.valid, order: cycle.red.order ?? null },
188
- green: cycle.green ? { valid: cycle.green.valid, order: cycle.green.order ?? null } : null,
290
+ red: { valid: cycle.red.valid, order: cycle.red.order },
291
+ green: cycle.green ? { valid: cycle.green.valid, order: cycle.green.order } : null,
189
292
  }))
190
- .sort((a, b) => {
191
- const orderA = a.red.order ?? Number.MAX_SAFE_INTEGER;
192
- const orderB = b.red.order ?? Number.MAX_SAFE_INTEGER;
193
- if (orderA !== orderB)
194
- return orderA - orderB;
195
- return a.cycleId < b.cycleId ? -1 : a.cycleId > b.cycleId ? 1 : 0;
196
- })
197
- .map(({ cycleId: _cycleId, ...rest }) => rest);
293
+ .sort((a, b) => compareSnapshotCycles(a, b, (item) => item.red.order, (item) => item.cycleId, ({ cycleId: _id, ...item }) => item))
294
+ .map(({ cycleId: _cycleId, ...rest }) => ({
295
+ red: { valid: rest.red.valid, order: rest.red.order ?? null },
296
+ green: rest.green ? { valid: rest.green.valid, order: rest.green.order ?? null } : null,
297
+ }));
298
+ const validlyVoided = validlyVoidedTddCycles(tdd, order);
299
+ const voidedCycleOrders = change
300
+ ? voidedCycleOrdersInCurrentWindow(change, requirementId, tdd, validlyVoided)
301
+ : [];
198
302
  return {
199
303
  requirementsOrder: change?.phases.requirements?.order ?? null,
200
304
  red: phaseItem(batch?.red),
201
305
  implementation: phaseItem(batch?.implementation),
202
306
  green: phaseItem(batch?.green),
203
307
  cycles,
308
+ ...(voidedCycleOrders.length ? { voidedCycleOrders } : {}),
204
309
  };
205
310
  }
206
311
  const parsed = parseDetail(code, detail);
@@ -236,41 +341,118 @@ export async function snapshotPayload(root, evidence, tdd, order, changeId, code
236
341
  };
237
342
  }
238
343
  if (parsed.kind === 'batch') {
239
- const batch = change ? batchForKey(effectiveBatches(change), parsed.batchKey) : undefined;
240
- const item = batch?.[parsed.batchPhaseName];
241
- return {
242
- phaseItemPresent: item != null,
243
- orderIsInteger: Number.isInteger(item?.order),
244
- phaseOrder: Number.isInteger(item?.order) ? item.order : null,
344
+ const matches = change ? batchesForKey(effectiveBatches(change), parsed.batchKey) : [];
345
+ const state = (batch) => {
346
+ const item = batch?.[parsed.batchPhaseName];
347
+ return {
348
+ phaseItemPresent: item != null,
349
+ orderIsInteger: Number.isInteger(item?.order),
350
+ phaseOrder: Number.isInteger(item?.order) ? item.order : null,
351
+ };
245
352
  };
353
+ return matches.length <= 1 ? state(matches[0]) : { matchingBatches: matches.map((batch) => state(batch)) };
246
354
  }
247
355
  if (parsed.kind === 'requirement') {
356
+ const validlyVoided = validlyVoidedTddCycles(tdd, order);
248
357
  const cycles = (tdd?.cycles ?? [])
249
358
  .filter((cycle) => cycle.requirementId === parsed.requirementId)
250
- .map((cycle) => ({ redOrder: cycle.red.order ?? null, greenOrder: cycle.green?.order ?? null }))
251
- .sort((a, b) => (a.redOrder ?? Number.MAX_SAFE_INTEGER) - (b.redOrder ?? Number.MAX_SAFE_INTEGER));
252
- return { cycles };
359
+ .map((cycle) => ({ cycleId: cycle.cycleId ?? '', redOrder: cycle.red.order, greenOrder: cycle.green?.order }))
360
+ .sort((a, b) => compareSnapshotCycles(a, b, (item) => item.redOrder, (item) => item.cycleId, ({ cycleId: _id, ...item }) => item))
361
+ .map(({ cycleId: _cycleId, redOrder, greenOrder }) => ({
362
+ redOrder: redOrder ?? null,
363
+ greenOrder: greenOrder ?? null,
364
+ }));
365
+ const voidedCycleOrders = change
366
+ ? voidedCycleOrdersInCurrentWindow(change, parsed.requirementId, tdd, validlyVoided)
367
+ : [];
368
+ return { cycles, ...(voidedCycleOrders.length ? { voidedCycleOrders } : {}) };
369
+ }
370
+ return null;
371
+ }
372
+ if (code === 'CHANGE_PHASE_ORDER') {
373
+ if (parsed.kind === 'phase') {
374
+ const laterItem = change?.phases[parsed.phaseName];
375
+ const earlierPhaseName = parsed.phaseName === 'requirements' ? 'impact' : 'requirements';
376
+ const earlierItem = change?.phases[earlierPhaseName];
377
+ return {
378
+ firstOrder: Number.isInteger(earlierItem?.order) ? earlierItem.order : null,
379
+ secondOrder: Number.isInteger(laterItem?.order) ? laterItem.order : null,
380
+ };
381
+ }
382
+ if (parsed.kind === 'batch') {
383
+ const matches = change ? batchesForKey(effectiveBatches(change), parsed.batchKey) : [];
384
+ const state = (batch) => {
385
+ const batchPhaseName = parsed.batchPhaseName;
386
+ const earlierItem = batchPhaseName === 'red' ? change?.phases.design
387
+ : batchPhaseName === 'implementation' ? batch?.red
388
+ : batchPhaseName === 'green' ? batch?.implementation
389
+ : batch?.green;
390
+ const laterItem = batchPhaseName === 'quality' ? change?.phases.quality : batch?.[batchPhaseName];
391
+ return {
392
+ firstOrder: Number.isInteger(earlierItem?.order) ? earlierItem.order : null,
393
+ secondOrder: Number.isInteger(laterItem?.order) ? laterItem.order : null,
394
+ };
395
+ };
396
+ return matches.length <= 1 ? state(matches[0]) : { matchingBatches: matches.map((batch) => state(batch)) };
253
397
  }
254
398
  return null;
255
399
  }
256
400
  if (parsed.kind !== 'batchKey')
257
401
  return null;
258
- const batch = change ? batchForKey(effectiveBatches(change), parsed.batchKey) : undefined;
402
+ const batches = change ? effectiveBatches(change) : [];
403
+ const matches = batchesForKey(batches, parsed.batchKey);
404
+ const batch = matches[0];
259
405
  if (code === 'CHANGE_TESTS_UNCHANGED') {
260
- return { designTests: change?.phases.design?.fingerprints.tests ?? null, batchRedTests: batch?.red?.fingerprints.tests ?? null };
406
+ const designTests = change?.phases.design?.fingerprints.tests ?? null;
407
+ if (matches.length <= 1)
408
+ return { designTests, batchRedTests: batch?.red?.fingerprints.tests ?? null };
409
+ return {
410
+ designTests,
411
+ matchingBatches: matches.map((candidate) => ({
412
+ ownedRequirementIds: change ? currentRequirementIdsForBatch(batches, candidate, change.requirementIds).sort() : [],
413
+ batchRedTests: candidate.red?.fingerprints.tests ?? null,
414
+ })),
415
+ };
261
416
  }
262
417
  if (code === 'CHANGE_IMPLEMENTATION_UNCHANGED') {
418
+ if (matches.length > 1) {
419
+ return {
420
+ matchingBatches: matches.map((candidate) => ({
421
+ ownedRequirementIds: change ? currentRequirementIdsForBatch(batches, candidate, change.requirementIds).sort() : [],
422
+ redImplementation: candidate.red?.fingerprints.implementation ?? null,
423
+ implementationImplementation: candidate.implementation?.fingerprints.implementation ?? null,
424
+ })),
425
+ };
426
+ }
263
427
  return {
264
428
  redImplementation: batch?.red?.fingerprints.implementation ?? null,
265
429
  implementationImplementation: batch?.implementation?.fingerprints.implementation ?? null,
266
430
  };
267
431
  }
268
432
  if (code === 'CHANGE_TEST_CHANGED_AFTER_RED') {
433
+ if (matches.length > 1) {
434
+ return {
435
+ matchingBatches: matches.map((candidate) => ({
436
+ ownedRequirementIds: change ? currentRequirementIdsForBatch(batches, candidate, change.requirementIds).sort() : [],
437
+ redTests: candidate.red?.fingerprints.tests ?? null,
438
+ greenTests: candidate.green?.fingerprints.tests ?? null,
439
+ })),
440
+ };
441
+ }
269
442
  return { redTests: batch?.red?.fingerprints.tests ?? null, greenTests: batch?.green?.fingerprints.tests ?? null };
270
443
  }
271
444
  if (code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED') {
272
445
  if (requirementId === undefined)
273
446
  return null;
447
+ if (matches.length > 1) {
448
+ return {
449
+ matchingBatches: matches.map((candidate) => ({
450
+ ownedRequirementIds: change ? currentRequirementIdsForBatch(batches, candidate, change.requirementIds).sort() : [],
451
+ redRequirementImplementation: candidate.red?.fingerprints.requirementImplementations?.[requirementId] ?? null,
452
+ implementationRequirementImplementation: candidate.implementation?.fingerprints.requirementImplementations?.[requirementId] ?? null,
453
+ })),
454
+ };
455
+ }
274
456
  return {
275
457
  redRequirementImplementation: batch?.red?.fingerprints.requirementImplementations?.[requirementId] ?? null,
276
458
  implementationRequirementImplementation: batch?.implementation?.fingerprints.requirementImplementations?.[requirementId] ?? null,
@@ -328,7 +510,7 @@ export function waiverChainValid(waivers, index) {
328
510
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-016
329
511
  * @design DES-CHANGE-EVIDENCE-WAIVER-002
330
512
  */
331
- export async function waiverLinkage(root, evidence, order, waivers, index) {
513
+ export function waiverLinkage(order, waivers, index) {
332
514
  const record = waivers[index];
333
515
  if (!record)
334
516
  return { valid: false, reason: 'Waiver record does not exist.' };
@@ -336,9 +518,6 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
336
518
  return { valid: false, reason: 'Waiver record has an invalid shape.' };
337
519
  if (!waiverChainValid(waivers, index))
338
520
  return { valid: false, reason: 'Waiver record breaks the evidence chain.' };
339
- if (!await exists(within(root, `.musubix/changes/${record.changeId}.md`))) {
340
- return { valid: false, reason: `${record.changeId} has no change document.` };
341
- }
342
521
  if (!WAIVABLE_CODES.includes(record.code)) {
343
522
  return { valid: false, reason: `${record.code} is not a waivable code.` };
344
523
  }
@@ -349,20 +528,11 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
349
528
  if (requiresDetail(code) !== (record.detail !== undefined)) {
350
529
  return { valid: false, reason: `${code}'s detail scope does not match its regime.` };
351
530
  }
352
- if (code === 'CHANGE_RECORD_MISSING') {
353
- const stillMissing = evidence === null || !evidence.changes.some((entry) => entry.changeId === record.changeId);
354
- if (!stillMissing)
355
- return { valid: false, reason: `${record.changeId} now has a chronology record.` };
356
- }
357
- else {
358
- if (evidence === null)
359
- return { valid: false, reason: 'No change evidence is available to link this waiver.' };
360
- const change = evidence.changes.find((entry) => entry.changeId === record.changeId);
361
- if (!change)
362
- return { valid: false, reason: `${record.changeId} is not a known change.` };
363
- if (record.requirementId !== undefined && !change.requirementIds.includes(record.requirementId)) {
364
- return { valid: false, reason: `${record.requirementId} is not declared by ${record.changeId}.` };
365
- }
531
+ if (record.detail !== undefined && !parseDetail(code, record.detail)) {
532
+ return { valid: false, reason: `${record.detail} is not valid detail grammar for ${code}.` };
533
+ }
534
+ if (waivers.filter((candidate) => candidate.order === record.order).length !== 1) {
535
+ return { valid: false, reason: `Waiver order ${record.order} is claimed by more than one record.` };
366
536
  }
367
537
  if (!order.valid)
368
538
  return { valid: false, reason: 'Monotonic evidence order is invalid.' };
@@ -377,6 +547,100 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
377
547
  }
378
548
  return { valid: true };
379
549
  }
550
+ export async function evaluateWaiverCondition(root, evidence, tdd, order, scope) {
551
+ const change = evidence?.changes.find((entry) => entry.changeId === scope.changeId);
552
+ if (scope.code === 'CHANGE_RECORD_MISSING') {
553
+ if (!await exists(within(root, `.musubix/changes/${scope.changeId}.md`)))
554
+ return 'indeterminate';
555
+ return await recordMissingCondition(root, evidence, scope.changeId) ? 'true' : 'false';
556
+ }
557
+ if (!change)
558
+ return 'indeterminate';
559
+ if (scope.requirementId !== undefined && !change.requirementIds.includes(scope.requirementId))
560
+ return 'indeterminate';
561
+ const validlyVoided = validlyVoidedTddCycles(tdd, order);
562
+ // Issue #64 / REQ-CHANGE-EVIDENCE-WAIVER-018: mirror the gate's exclusion
563
+ // of validly archived cycles so a waiver's "is this condition still true"
564
+ // re-check never disagrees with validateChangeEvidence.
565
+ const validlyArchived = validlyArchivedTddCycles(tdd, order);
566
+ const result = (value) => value ? 'true' : 'false';
567
+ switch (scope.code) {
568
+ case 'CHANGE_REQUIREMENTS_UNCHANGED':
569
+ return result(requirementsUnchangedCondition(change));
570
+ case 'CHANGE_DESIGN_UNCHANGED':
571
+ return result(designUnchangedCondition(change));
572
+ case 'CHANGE_RED_UNPROVEN':
573
+ return result(!change.qualityHistory?.length
574
+ && redUnprovenCondition(change, scope.requirementId, tdd, validlyVoided));
575
+ case 'CHANGE_GREEN_UNPROVEN':
576
+ return result(!change.qualityHistory?.length
577
+ && greenUnprovenCondition(change, scope.requirementId, tdd, validlyVoided));
578
+ case 'CHANGE_COMPLETENESS_TDD':
579
+ return result(completenessTddUnsatisfiedCondition(change, scope.requirementId, tdd, validlyVoided));
580
+ default:
581
+ break;
582
+ }
583
+ const parsed = parseDetail(scope.code, scope.detail);
584
+ if (!parsed)
585
+ return 'indeterminate';
586
+ if (scope.code === 'CHANGE_PHASE_MISSING') {
587
+ return parsed.kind === 'phase' ? result(phaseMissingCondition(change, parsed.phaseName)) : 'indeterminate';
588
+ }
589
+ if (scope.code === 'CHANGE_ORDER_MIGRATION_REQUIRED') {
590
+ if (parsed.kind === 'phase')
591
+ return result(orderMigrationRequiredPhaseCondition(change, parsed.phaseName));
592
+ if (parsed.kind === 'requirement') {
593
+ if (!change.requirementIds.includes(parsed.requirementId))
594
+ return 'indeterminate';
595
+ return result(orderMigrationRequiredRequirementCondition(change, parsed.requirementId, tdd, validlyVoided, validlyArchived));
596
+ }
597
+ if (parsed.kind === 'batch') {
598
+ const matches = batchesForKey(effectiveBatches(change), parsed.batchKey);
599
+ if (!matches.length)
600
+ return 'indeterminate';
601
+ return result(orderMigrationRequiredBatchCondition(change, parsed.batchPhaseName, parsed.batchKey));
602
+ }
603
+ return 'indeterminate';
604
+ }
605
+ if (scope.code === 'CHANGE_PHASE_ORDER') {
606
+ if (parsed.kind === 'phase')
607
+ return result(phaseOrderPhaseCondition(change, parsed.phaseName));
608
+ if (parsed.kind === 'batch') {
609
+ const matches = batchesForKey(effectiveBatches(change), parsed.batchKey);
610
+ if (!matches.length)
611
+ return 'indeterminate';
612
+ return result(phaseOrderBatchCondition(change, parsed.batchPhaseName, parsed.batchKey));
613
+ }
614
+ return 'indeterminate';
615
+ }
616
+ if (parsed.kind !== 'batchKey')
617
+ return 'indeterminate';
618
+ const batches = effectiveBatches(change);
619
+ const matches = batchesForKey(batches, parsed.batchKey);
620
+ if (!matches.length)
621
+ return 'indeterminate';
622
+ const owned = matches.filter((batch) => {
623
+ const requirementIds = currentRequirementIdsForBatch(batches, batch, change.requirementIds);
624
+ return scope.code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED'
625
+ ? scope.requirementId !== undefined && requirementIds.includes(scope.requirementId)
626
+ : requirementIds.length > 0;
627
+ });
628
+ if (!owned.length)
629
+ return 'false';
630
+ if (scope.code === 'CHANGE_TESTS_UNCHANGED') {
631
+ return result(owned.some((batch) => testsUnchangedCondition(change, batch)));
632
+ }
633
+ if (scope.code === 'CHANGE_IMPLEMENTATION_UNCHANGED') {
634
+ return result(owned.some((batch) => implementationUnchangedCondition(batch)));
635
+ }
636
+ if (scope.code === 'CHANGE_TEST_CHANGED_AFTER_RED') {
637
+ return result(owned.some((batch) => testChangedAfterRedCondition(batch)));
638
+ }
639
+ if (scope.code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED') {
640
+ return result(owned.some((batch) => relevantImplementationUnchangedCondition(batch, scope.requirementId)));
641
+ }
642
+ return 'indeterminate';
643
+ }
380
644
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-020
381
645
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-007 REQ-CHANGE-EVIDENCE-WAIVER-010 REQ-CHANGE-EVIDENCE-WAIVER-011
382
646
  * @design DES-CHANGE-EVIDENCE-WAIVER-004
@@ -384,28 +648,45 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
384
648
  * consults only this fully resolved value, so no diagnostic-emission call
385
649
  * site or final malformed/stale pass needs to itself `await` anything.
386
650
  */
387
- export async function buildWaiverContext(root, evidence, tdd) {
388
- const loaded = await loadChangeWaiverEvidence(root);
389
- const order = await inspectEvidenceOrder(root);
651
+ export async function buildWaiverContext(root, evidence, tdd, precomputed = {}) {
652
+ const loaded = precomputed.loaded !== undefined ? precomputed.loaded : await loadChangeWaiverEvidence(root);
653
+ const order = precomputed.order ?? await inspectEvidenceOrder(root);
390
654
  const linkage = [];
391
655
  const currentHash = [];
656
+ const condition = [];
392
657
  if (loaded && !loaded.malformed) {
393
658
  for (let index = 0; index < loaded.waivers.length; index++) {
394
- const recordLinkage = await waiverLinkage(root, evidence, order, loaded.waivers, index);
659
+ const recordLinkage = waiverLinkage(order, loaded.waivers, index);
395
660
  linkage.push(recordLinkage);
396
661
  if (recordLinkage.valid) {
397
662
  const record = loaded.waivers[index];
398
- currentHash.push(digest(canonicalJson(await snapshotPayload(root, evidence, tdd, order, record.changeId, record.code, record.requirementId, record.detail))));
663
+ currentHash.push(digest(canonicalJson(await snapshotPayload(root, evidence, tdd, order, record.changeId, record.code, record.requirementId, record.detail, record))));
664
+ condition.push(await evaluateWaiverCondition(root, evidence, tdd, order, {
665
+ changeId: record.changeId,
666
+ code: record.code,
667
+ ...(record.requirementId !== undefined ? { requirementId: record.requirementId } : {}),
668
+ ...(record.detail !== undefined ? { detail: record.detail } : {}),
669
+ }));
399
670
  }
400
671
  else {
401
672
  currentHash.push(undefined);
673
+ condition.push(undefined);
402
674
  }
403
675
  }
404
676
  }
405
- return { loaded, order, linkage, currentHash };
677
+ return { loaded, order, linkage, currentHash, condition };
406
678
  }
407
- function nonStale(record, index, waiverContext) {
408
- return record.snapshotVersion === CURRENT_SNAPSHOT_VERSION && record.snapshotHash === waiverContext.currentHash[index];
679
+ export function isWaiverStale(waiverContext, index) {
680
+ const loaded = waiverContext.loaded;
681
+ if (!loaded || loaded.malformed)
682
+ return true;
683
+ const record = loaded.waivers[index];
684
+ return !record
685
+ || record.snapshotVersion !== CURRENT_SNAPSHOT_VERSION
686
+ || record.snapshotHash !== waiverContext.currentHash[index]
687
+ || waiverContext.condition[index] === 'false'
688
+ || waiverContext.condition[index] === 'indeterminate'
689
+ || waiverContext.condition[index] === undefined;
409
690
  }
410
691
  function scopeMatches(record, changeId, code, requirementId, detail) {
411
692
  return record.changeId === changeId && record.code === code && record.requirementId === requirementId && record.detail === detail;
@@ -415,7 +696,7 @@ function scopeMatches(record, changeId, code, requirementId, detail) {
415
696
  * `changeId`/`code`/`requirementId`/`detail` scope group (REQ-006's
416
697
  * supersession rule), returning its index, or -1 if none match/link.
417
698
  */
418
- function authoritativeIndex(waiverContext, changeId, code, requirementId, detail) {
699
+ export function authoritativeWaiverIndex(waiverContext, scope) {
419
700
  const loaded = waiverContext.loaded;
420
701
  if (!loaded || loaded.malformed)
421
702
  return -1;
@@ -424,7 +705,7 @@ function authoritativeIndex(waiverContext, changeId, code, requirementId, detail
424
705
  const record = loaded.waivers[index];
425
706
  if (!waiverContext.linkage[index]?.valid)
426
707
  continue;
427
- if (!scopeMatches(record, changeId, code, requirementId, detail))
708
+ if (!scopeMatches(record, scope.changeId, scope.code, scope.requirementId, scope.detail))
428
709
  continue;
429
710
  if (best === -1 || record.order > loaded.waivers[best].order)
430
711
  best = index;
@@ -440,11 +721,16 @@ export function waivedDiagnostic(waiverContext, code, message, changeId, require
440
721
  const loaded = waiverContext.loaded;
441
722
  if (!loaded || loaded.malformed)
442
723
  return base;
443
- const index = authoritativeIndex(waiverContext, changeId, code, requirementId, detail);
724
+ const index = authoritativeWaiverIndex(waiverContext, {
725
+ changeId,
726
+ code,
727
+ ...(requirementId !== undefined ? { requirementId } : {}),
728
+ ...(detail !== undefined ? { detail } : {}),
729
+ });
444
730
  if (index === -1)
445
731
  return base;
446
732
  const record = loaded.waivers[index];
447
- if (!nonStale(record, index, waiverContext))
733
+ if (isWaiverStale(waiverContext, index))
448
734
  return base;
449
735
  return {
450
736
  ...base,
@@ -456,7 +742,7 @@ export function waivedDiagnostic(waiverContext, code, message, changeId, require
456
742
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-007 REQ-CHANGE-EVIDENCE-WAIVER-011
457
743
  * @design DES-CHANGE-EVIDENCE-WAIVER-004
458
744
  */
459
- export function reportWaiverEvidenceDiagnostics(waiverContext, _evidence, _tdd) {
745
+ export function reportWaiverEvidenceDiagnostics(waiverContext) {
460
746
  const loaded = waiverContext.loaded;
461
747
  if (!loaded)
462
748
  return [];
@@ -477,12 +763,29 @@ export function reportWaiverEvidenceDiagnostics(waiverContext, _evidence, _tdd)
477
763
  if (groupsSeen.has(scopeKey))
478
764
  continue;
479
765
  groupsSeen.add(scopeKey);
480
- const authoritative = authoritativeIndex(waiverContext, record.changeId, record.code, record.requirementId, record.detail);
766
+ const authoritative = authoritativeWaiverIndex(waiverContext, {
767
+ changeId: record.changeId,
768
+ code: record.code,
769
+ ...(record.requirementId !== undefined ? { requirementId: record.requirementId } : {}),
770
+ ...(record.detail !== undefined ? { detail: record.detail } : {}),
771
+ });
481
772
  if (authoritative === -1)
482
773
  continue;
483
774
  const authoritativeRecord = loaded.waivers[authoritative];
484
- if (!nonStale(authoritativeRecord, authoritative, waiverContext)) {
485
- diagnostics.push(error('CHANGE_WAIVER_STALE', `Waiver for ${label(authoritativeRecord)} is stale.`, WAIVER_PATH));
775
+ if (isWaiverStale(waiverContext, authoritative)) {
776
+ const condition = waiverContext.condition[authoritative];
777
+ const remediation = condition === 'false'
778
+ ? 'The target code is no longer reported for this scope (condition=false); a replacement waiver is not required.'
779
+ : condition === 'true'
780
+ ? 'The waived condition still exists (condition=true); resolve the debt or record an allowed replacement waiver.'
781
+ : 'The waived condition cannot be evaluated; restore an evaluable change, requirement, or batch scope before retrying.';
782
+ diagnostics.push({
783
+ ...error('CHANGE_WAIVER_STALE', `Waiver for ${label(authoritativeRecord)} is stale. ${remediation}`, WAIVER_PATH),
784
+ severity: condition === 'false' ? 'warning' : 'error',
785
+ changeId: authoritativeRecord.changeId,
786
+ ...(authoritativeRecord.requirementId !== undefined ? { requirementId: authoritativeRecord.requirementId } : {}),
787
+ ...(authoritativeRecord.detail !== undefined ? { detail: authoritativeRecord.detail } : {}),
788
+ });
486
789
  }
487
790
  }
488
791
  return diagnostics;
@@ -492,6 +795,9 @@ export function reportWaiverEvidenceDiagnostics(waiverContext, _evidence, _tdd)
492
795
  * @design DES-CHANGE-EVIDENCE-WAIVER-001
493
796
  */
494
797
  export async function recordChangeWaiver(root, changeId, code, requirementId, detail, approver, reason) {
798
+ return withEvidenceWriterLock(root, 'change waiver record', () => recordChangeWaiverUnlocked(root, changeId, code, requirementId, detail, approver, reason));
799
+ }
800
+ async function recordChangeWaiverUnlocked(root, changeId, code, requirementId, detail, approver, reason) {
495
801
  const loaded = await loadChangeWaiverEvidence(root);
496
802
  const existing = loaded === null ? { schemaVersion: 1, waivers: [] } : loaded;
497
803
  if (loaded !== null && loaded.malformed) {
@@ -502,7 +808,7 @@ export async function recordChangeWaiver(root, changeId, code, requirementId, de
502
808
  for (let index = 0; index < existing.waivers.length; index++) {
503
809
  if (!waiverRecordShapeValid(existing.waivers[index])
504
810
  || !waiverChainValid(existing.waivers, index)
505
- || !(await waiverLinkage(root, evidence, order, existing.waivers, index)).valid) {
811
+ || !waiverLinkage(order, existing.waivers, index).valid) {
506
812
  throw new Error('An existing waiver record is invalid; repair the evidence chain before recording a new waiver.');
507
813
  }
508
814
  }
@@ -523,82 +829,35 @@ export async function recordChangeWaiver(root, changeId, code, requirementId, de
523
829
  if (!approver.trim() || !reason.trim()) {
524
830
  throw new Error('A non-empty --approver and --reason are required.');
525
831
  }
526
- if (!await exists(within(root, `.musubix/changes/${changeId}.md`))) {
527
- throw new Error(`${changeId} has no change document.`);
528
- }
529
- const change = evidence?.changes.find((entry) => entry.changeId === changeId);
530
- if (waivableCode !== 'CHANGE_RECORD_MISSING') {
531
- if (!change)
532
- throw new Error(`${changeId} is not a known change.`);
533
- if (requirementId !== undefined && !change.requirementIds.includes(requirementId)) {
534
- throw new Error(`${requirementId} is not declared by ${changeId}.`);
535
- }
536
- }
537
832
  const tdd = await loadTddEvidence(root);
538
833
  const parsed = detail !== undefined ? parseDetail(waivableCode, detail) : null;
539
834
  if (requiresDetail(waivableCode) && !parsed) {
540
835
  throw new Error(`${detail} is not a valid --detail value for ${waivableCode}.`);
541
836
  }
542
- const currentlyReported = await (async () => {
543
- switch (waivableCode) {
544
- case 'CHANGE_REQUIREMENTS_UNCHANGED': return requirementsUnchangedCondition(change);
545
- case 'CHANGE_DESIGN_UNCHANGED': return designUnchangedCondition(change);
546
- case 'CHANGE_RED_UNPROVEN': return redUnprovenCondition(change, requirementId, tdd);
547
- case 'CHANGE_GREEN_UNPROVEN': return greenUnprovenCondition(change, requirementId, tdd);
548
- case 'CHANGE_COMPLETENESS_TDD': return completenessTddUnsatisfiedCondition(change, requirementId, tdd);
549
- case 'CHANGE_RECORD_MISSING': return recordMissingCondition(root, evidence, changeId);
550
- case 'CHANGE_PHASE_MISSING':
551
- return parsed?.kind === 'phase' ? phaseMissingCondition(change, parsed.phaseName) : false;
552
- case 'CHANGE_ORDER_MIGRATION_REQUIRED': {
553
- if (!parsed)
554
- return false;
555
- if (parsed.kind === 'phase')
556
- return orderMigrationRequiredPhaseCondition(change, parsed.phaseName);
557
- if (parsed.kind === 'batch')
558
- return orderMigrationRequiredBatchCondition(change, parsed.batchPhaseName, parsed.batchKey);
559
- if (parsed.kind === 'requirement')
560
- return orderMigrationRequiredRequirementCondition(change, parsed.requirementId, tdd);
561
- return false;
562
- }
563
- case 'CHANGE_TESTS_UNCHANGED': {
564
- if (parsed?.kind !== 'batchKey')
565
- return false;
566
- const batch = batchForKey(effectiveBatches(change), parsed.batchKey);
567
- return !!batch && testsUnchangedCondition(change, batch);
568
- }
569
- case 'CHANGE_IMPLEMENTATION_UNCHANGED': {
570
- if (parsed?.kind !== 'batchKey')
571
- return false;
572
- const batch = batchForKey(effectiveBatches(change), parsed.batchKey);
573
- return !!batch && implementationUnchangedCondition(batch);
574
- }
575
- case 'CHANGE_TEST_CHANGED_AFTER_RED': {
576
- if (parsed?.kind !== 'batchKey')
577
- return false;
578
- const batch = batchForKey(effectiveBatches(change), parsed.batchKey);
579
- return !!batch && testChangedAfterRedCondition(batch);
580
- }
581
- case 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED': {
582
- if (parsed?.kind !== 'batchKey' || requirementId === undefined)
583
- return false;
584
- const batch = batchForKey(effectiveBatches(change), parsed.batchKey);
585
- return !!batch && relevantImplementationUnchangedCondition(batch, requirementId);
586
- }
587
- default: return false;
837
+ const scope = {
838
+ changeId,
839
+ code: waivableCode,
840
+ ...(requirementId !== undefined ? { requirementId } : {}),
841
+ ...(detail !== undefined ? { detail } : {}),
842
+ };
843
+ const condition = await evaluateWaiverCondition(root, evidence, tdd, order, scope);
844
+ if (condition === 'indeterminate') {
845
+ const change = evidence?.changes.find((entry) => entry.changeId === changeId);
846
+ if (requirementId !== undefined && change && !change.requirementIds.includes(requirementId)) {
847
+ throw new Error(`The ${waivableCode} scope cannot be evaluated because ${requirementId} is not declared by ${changeId}.`);
588
848
  }
589
- })();
590
- if (!currentlyReported) {
591
- throw new Error(`No matching ${waivableCode} diagnostic is currently reported for ${changeId}${requirementId ? `:${requirementId}` : ''}${detail ? `:${detail}` : ''}.`);
849
+ throw new Error(`The ${waivableCode} scope for ${changeId}${requirementId ? `:${requirementId}` : ''}${detail ? `:${detail}` : ''} cannot be evaluated.`);
592
850
  }
593
- for (let index = 0; index < existing.waivers.length; index++) {
594
- const record = existing.waivers[index];
595
- if (scopeMatches(record, changeId, waivableCode, requirementId, detail)
596
- && (await waiverLinkage(root, evidence, order, existing.waivers, index)).valid) {
597
- const hash = digest(canonicalJson(await snapshotPayload(root, evidence, tdd, order, changeId, waivableCode, requirementId, detail)));
598
- if (record.snapshotVersion === CURRENT_SNAPSHOT_VERSION && record.snapshotHash === hash) {
599
- throw new Error(`${changeId}:${waivableCode}${requirementId ? `:${requirementId}` : ''}${detail ? `:${detail}` : ''} already has an active waiver.`);
600
- }
601
- }
851
+ if (condition === 'false') {
852
+ throw new Error(`No matching ${waivableCode} diagnostic is currently reported for ${changeId}${requirementId ? `:${requirementId}` : ''}${detail ? `:${detail}` : ''}; the evaluated condition is false.`);
853
+ }
854
+ const waiverContext = await buildWaiverContext(root, evidence, tdd, {
855
+ loaded: existing,
856
+ order,
857
+ });
858
+ const authoritative = authoritativeWaiverIndex(waiverContext, scope);
859
+ if (authoritative !== -1 && !isWaiverStale(waiverContext, authoritative)) {
860
+ throw new Error(`${changeId}:${waivableCode}${requirementId ? `:${requirementId}` : ''}${detail ? `:${detail}` : ''} already has an active waiver.`);
602
861
  }
603
862
  const snapshotHash = digest(canonicalJson(await snapshotPayload(root, evidence, tdd, order, changeId, waivableCode, requirementId, detail)));
604
863
  const orderRecord = await appendEvidenceOrder(root, {
@@ -639,13 +898,11 @@ export async function recordChangeWaiver(root, changeId, code, requirementId, de
639
898
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-012 REQ-CHANGE-EVIDENCE-WAIVER-016
640
899
  * @design DES-CHANGE-EVIDENCE-WAIVER-005
641
900
  */
642
- export async function activeWaivers(root) {
643
- const loaded = await loadChangeWaiverEvidence(root);
901
+ export async function activeWaivers(root, precomputed) {
902
+ const waiverContext = precomputed ?? await buildWaiverContext(root, await loadChangeEvidence(root), await loadTddEvidence(root));
903
+ const loaded = waiverContext.loaded;
644
904
  if (!loaded || loaded.malformed)
645
905
  return [];
646
- const evidence = await loadChangeEvidence(root);
647
- const tdd = await loadTddEvidence(root);
648
- const waiverContext = await buildWaiverContext(root, evidence, tdd);
649
906
  const results = [];
650
907
  const groupsSeen = new Set();
651
908
  for (let index = 0; index < loaded.waivers.length; index++) {
@@ -656,11 +913,16 @@ export async function activeWaivers(root) {
656
913
  if (groupsSeen.has(scopeKey))
657
914
  continue;
658
915
  groupsSeen.add(scopeKey);
659
- const authoritative = authoritativeIndex(waiverContext, record.changeId, record.code, record.requirementId, record.detail);
916
+ const authoritative = authoritativeWaiverIndex(waiverContext, {
917
+ changeId: record.changeId,
918
+ code: record.code,
919
+ ...(record.requirementId !== undefined ? { requirementId: record.requirementId } : {}),
920
+ ...(record.detail !== undefined ? { detail: record.detail } : {}),
921
+ });
660
922
  if (authoritative === -1)
661
923
  continue;
662
924
  const authoritativeRecord = loaded.waivers[authoritative];
663
- if (!nonStale(authoritativeRecord, authoritative, waiverContext))
925
+ if (isWaiverStale(waiverContext, authoritative))
664
926
  continue;
665
927
  results.push({
666
928
  changeId: authoritativeRecord.changeId,
@@ -674,15 +936,82 @@ export async function activeWaivers(root) {
674
936
  }
675
937
  return results;
676
938
  }
939
+ export async function evaluateChangeWaiverState(root, evidence, tdd, order, waivers) {
940
+ const linkage = [];
941
+ const currentHash = [];
942
+ for (let index = 0; index < waivers.waivers.length; index++) {
943
+ const recordLinkage = waiverLinkage(order, waivers.waivers, index);
944
+ linkage.push(recordLinkage);
945
+ if (recordLinkage.valid) {
946
+ const record = waivers.waivers[index];
947
+ currentHash.push(digest(canonicalJson(await snapshotPayload(root, evidence, tdd, order, record.changeId, record.code, record.requirementId, record.detail))));
948
+ }
949
+ else {
950
+ currentHash.push(undefined);
951
+ }
952
+ }
953
+ const condition = [];
954
+ for (let index = 0; index < waivers.waivers.length; index++) {
955
+ const record = waivers.waivers[index];
956
+ condition.push(linkage[index]?.valid
957
+ ? await evaluateWaiverCondition(root, evidence, tdd, order, {
958
+ changeId: record.changeId,
959
+ code: record.code,
960
+ ...(record.requirementId !== undefined ? { requirementId: record.requirementId } : {}),
961
+ ...(record.detail !== undefined ? { detail: record.detail } : {}),
962
+ })
963
+ : undefined);
964
+ }
965
+ const context = {
966
+ loaded: { schemaVersion: 1, waivers: waivers.waivers, malformed: false },
967
+ order,
968
+ linkage,
969
+ currentHash,
970
+ condition,
971
+ };
972
+ const authoritative = new Map();
973
+ const stale = [];
974
+ for (const record of waivers.waivers) {
975
+ const scope = JSON.stringify([record.changeId, record.code, record.requirementId, record.detail]);
976
+ if (authoritative.has(scope))
977
+ continue;
978
+ const index = authoritativeWaiverIndex(context, {
979
+ changeId: record.changeId,
980
+ code: record.code,
981
+ ...(record.requirementId !== undefined ? { requirementId: record.requirementId } : {}),
982
+ ...(record.detail !== undefined ? { detail: record.detail } : {}),
983
+ });
984
+ if (index === -1)
985
+ continue;
986
+ const selected = waivers.waivers[index];
987
+ if (isWaiverStale(context, index)) {
988
+ stale.push({
989
+ changeId: selected.changeId,
990
+ code: selected.code,
991
+ ...(selected.requirementId !== undefined ? { requirementId: selected.requirementId } : {}),
992
+ ...(selected.detail !== undefined ? { detail: selected.detail } : {}),
993
+ });
994
+ }
995
+ authoritative.set(scope, JSON.stringify([
996
+ selected.changeId,
997
+ selected.code,
998
+ selected.requirementId,
999
+ selected.detail,
1000
+ selected.approver,
1001
+ selected.reason,
1002
+ selected.recordedAt,
1003
+ selected.snapshotVersion,
1004
+ selected.snapshotHash,
1005
+ ]));
1006
+ }
1007
+ return { stale, authoritative };
1008
+ }
677
1009
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-012
678
1010
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-007 REQ-CHANGE-EVIDENCE-WAIVER-011
679
1011
  * @design DES-CHANGE-EVIDENCE-WAIVER-005
680
1012
  */
681
- export async function waiverEvidenceDiagnostics(root) {
682
- const loaded = await loadChangeWaiverEvidence(root);
683
- const evidence = await loadChangeEvidence(root);
684
- const tdd = await loadTddEvidence(root);
685
- const waiverContext = await buildWaiverContext(root, evidence, tdd);
686
- return reportWaiverEvidenceDiagnostics({ ...waiverContext, loaded }, evidence, tdd);
1013
+ export async function waiverEvidenceDiagnostics(root, precomputed) {
1014
+ const waiverContext = precomputed ?? await buildWaiverContext(root, await loadChangeEvidence(root), await loadTddEvidence(root));
1015
+ return reportWaiverEvidenceDiagnostics(waiverContext);
687
1016
  }
688
1017
  //# sourceMappingURL=change-waiver.js.map