musubix3 0.1.20 → 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 (63) hide show
  1. package/.github/plugin/marketplace.json +2 -2
  2. package/.github/skills/sdd-change/SKILL.md +28 -45
  3. package/CHANGELOG.md +88 -0
  4. package/README-ja.md +103 -3
  5. package/README.md +113 -3
  6. package/dist/packages/analysis/src/approval.d.ts +51 -0
  7. package/dist/packages/analysis/src/approval.js +341 -2
  8. package/dist/packages/analysis/src/approval.js.map +1 -1
  9. package/dist/packages/analysis/src/attestation.js +4 -0
  10. package/dist/packages/analysis/src/attestation.js.map +1 -1
  11. package/dist/packages/analysis/src/change-evidence.d.ts +56 -13
  12. package/dist/packages/analysis/src/change-evidence.js +142 -81
  13. package/dist/packages/analysis/src/change-evidence.js.map +1 -1
  14. package/dist/packages/analysis/src/change-waiver.d.ts +37 -13
  15. package/dist/packages/analysis/src/change-waiver.js +425 -174
  16. package/dist/packages/analysis/src/change-waiver.js.map +1 -1
  17. package/dist/packages/analysis/src/change.d.ts +1 -0
  18. package/dist/packages/analysis/src/change.js +176 -23
  19. package/dist/packages/analysis/src/change.js.map +1 -1
  20. package/dist/packages/analysis/src/evidence-merge-guard.d.ts +5 -0
  21. package/dist/packages/analysis/src/evidence-merge-guard.js +15 -0
  22. package/dist/packages/analysis/src/evidence-merge-guard.js.map +1 -1
  23. package/dist/packages/analysis/src/evidence-merge.d.ts +2 -0
  24. package/dist/packages/analysis/src/evidence-merge.js +173 -38
  25. package/dist/packages/analysis/src/evidence-merge.js.map +1 -1
  26. package/dist/packages/analysis/src/evidence-writer-lock.d.ts +9 -2
  27. package/dist/packages/analysis/src/evidence-writer-lock.js +75 -21
  28. package/dist/packages/analysis/src/evidence-writer-lock.js.map +1 -1
  29. package/dist/packages/analysis/src/files.js +5 -0
  30. package/dist/packages/analysis/src/files.js.map +1 -1
  31. package/dist/packages/analysis/src/gate.d.ts +1 -1
  32. package/dist/packages/analysis/src/gate.js +17 -4
  33. package/dist/packages/analysis/src/gate.js.map +1 -1
  34. package/dist/packages/analysis/src/index.d.ts +1 -0
  35. package/dist/packages/analysis/src/index.js +1 -0
  36. package/dist/packages/analysis/src/index.js.map +1 -1
  37. package/dist/packages/analysis/src/order.d.ts +16 -1
  38. package/dist/packages/analysis/src/order.js +6 -0
  39. package/dist/packages/analysis/src/order.js.map +1 -1
  40. package/dist/packages/analysis/src/quality-refresh.d.ts +18 -0
  41. package/dist/packages/analysis/src/quality-refresh.js +312 -0
  42. package/dist/packages/analysis/src/quality-refresh.js.map +1 -0
  43. package/dist/packages/analysis/src/tdd.d.ts +84 -3
  44. package/dist/packages/analysis/src/tdd.js +589 -50
  45. package/dist/packages/analysis/src/tdd.js.map +1 -1
  46. package/dist/packages/analysis/src/trace.d.ts +24 -0
  47. package/dist/packages/analysis/src/trace.js +95 -3
  48. package/dist/packages/analysis/src/trace.js.map +1 -1
  49. package/dist/packages/analysis/src/workflow-waiver.d.ts +6 -2
  50. package/dist/packages/analysis/src/workflow-waiver.js +9 -5
  51. package/dist/packages/analysis/src/workflow-waiver.js.map +1 -1
  52. package/dist/packages/analysis/src/workflow.d.ts +1 -1
  53. package/dist/packages/analysis/src/workflow.js +4 -4
  54. package/dist/packages/analysis/src/workflow.js.map +1 -1
  55. package/dist/packages/cli/src/install.js +27 -4
  56. package/dist/packages/cli/src/install.js.map +1 -1
  57. package/dist/packages/cli/src/main.js +77 -18
  58. package/dist/packages/cli/src/main.js.map +1 -1
  59. package/dist/packages/domain/src/design.js +8 -1
  60. package/dist/packages/domain/src/design.js.map +1 -1
  61. package/dist/packages/domain/src/types.d.ts +1 -0
  62. package/package.json +7 -2
  63. package/plugin.json +1 -1
@@ -1,7 +1,7 @@
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, currentRequirementIdsForBatch, effectiveBatches, greenUnprovenCondition, implementationUnchangedCondition, loadChangeEvidence, orderMigrationRequiredBatchCondition, orderMigrationRequiredPhaseCondition, orderMigrationRequiredRequirementCondition, phaseMissingCondition, recordMissingCondition, redUnprovenCondition, relevantImplementationUnchangedCondition, requirementsUnchangedCondition, testChangedAfterRedCondition, testsUnchangedCondition, voidedCycleOrdersInCurrentWindow, } from './change-evidence.js';
4
- import { loadTddEvidence, validlyVoidedTddCycles } 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
6
  import { withEvidenceWriterLock } from './evidence-writer-lock.js';
7
7
  const WAIVER_PATH = '.musubix/evidence/change-waivers.json';
@@ -25,6 +25,7 @@ export const WAIVABLE_CODES = [
25
25
  'CHANGE_IMPLEMENTATION_UNCHANGED',
26
26
  'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED',
27
27
  'CHANGE_TEST_CHANGED_AFTER_RED',
28
+ 'CHANGE_PHASE_ORDER',
28
29
  ];
29
30
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-017
30
31
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-004 REQ-CHANGE-EVIDENCE-WAIVER-016
@@ -37,7 +38,7 @@ export const NEITHER_KEY_CODES = new Set(['CHANGE_REQUIREMENTS_UNCHANGED', 'CHAN
37
38
  export const REQUIREMENT_ONLY_CODES = new Set(['CHANGE_RED_UNPROVEN', 'CHANGE_GREEN_UNPROVEN', 'CHANGE_COMPLETENESS_TDD']);
38
39
  export const DETAIL_ONLY_CODES = new Set([
39
40
  'CHANGE_PHASE_MISSING', 'CHANGE_ORDER_MIGRATION_REQUIRED', 'CHANGE_TESTS_UNCHANGED',
40
- 'CHANGE_IMPLEMENTATION_UNCHANGED', 'CHANGE_TEST_CHANGED_AFTER_RED',
41
+ 'CHANGE_IMPLEMENTATION_UNCHANGED', 'CHANGE_TEST_CHANGED_AFTER_RED', 'CHANGE_PHASE_ORDER',
41
42
  ]);
42
43
  export const BOTH_KEYS_CODES = new Set(['CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED']);
43
44
  /** Retained purely for backward-compatible naming inside this module. */
@@ -65,6 +66,49 @@ export function canonicalJson(value) {
65
66
  }
66
67
  return JSON.stringify(value) ?? 'null';
67
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
+ }
68
112
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-002
69
113
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-007
70
114
  * @design DES-CHANGE-EVIDENCE-WAIVER-001
@@ -115,8 +159,23 @@ export function diagnosticDetail(code, context) {
115
159
  || code === 'CHANGE_TEST_CHANGED_AFTER_RED' || code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED') {
116
160
  return context.batch !== undefined ? batchKey(context.batch.requirementIds) : undefined;
117
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
+ }
118
170
  return undefined;
119
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
+ }
120
179
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-019
121
180
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-016
122
181
  * @design DES-CHANGE-EVIDENCE-WAIVER-002
@@ -126,31 +185,71 @@ export function diagnosticDetail(code, context) {
126
185
  export function parseDetail(code, detail) {
127
186
  if (detail === undefined)
128
187
  return null;
129
- if (detail.startsWith('phase:'))
130
- 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
+ }
131
199
  if (detail.startsWith('batch:')) {
200
+ if (code !== 'CHANGE_ORDER_MIGRATION_REQUIRED' && code !== 'CHANGE_PHASE_ORDER')
201
+ return null;
132
202
  const rest = detail.slice('batch:'.length);
133
203
  const separator = rest.indexOf(':');
134
204
  if (separator === -1)
135
205
  return null;
136
- 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;
137
220
  }
138
- if (detail.startsWith('requirement:'))
139
- return { kind: 'requirement', requirementId: detail.slice('requirement:'.length) };
140
221
  if (code === 'CHANGE_TESTS_UNCHANGED' || code === 'CHANGE_IMPLEMENTATION_UNCHANGED'
141
222
  || code === 'CHANGE_TEST_CHANGED_AFTER_RED' || code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED') {
142
- return { kind: 'batchKey', batchKey: detail };
223
+ return validBatchKey(detail) ? { kind: 'batchKey', batchKey: detail } : null;
143
224
  }
144
225
  return null;
145
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
+ }
146
247
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-004
147
248
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-011 REQ-CHANGE-EVIDENCE-WAIVER-016
148
249
  * @design DES-CHANGE-EVIDENCE-WAIVER-002
149
250
  */
150
- 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) {
151
252
  const changePath = `.musubix/changes/${changeId}.md`;
152
- if (!await exists(within(root, changePath)))
153
- return null;
154
253
  const change = evidence?.changes.find((entry) => entry.changeId === changeId);
155
254
  if (code === 'CHANGE_REQUIREMENTS_UNCHANGED') {
156
255
  const impact = change?.phases.impact;
@@ -170,8 +269,11 @@ export async function snapshotPayload(root, evidence, tdd, order, changeId, code
170
269
  };
171
270
  }
172
271
  if (code === 'CHANGE_RECORD_MISSING') {
272
+ if (!await exists(within(root, changePath)))
273
+ return null;
274
+ const documentText = await readText(root, changePath);
173
275
  return {
174
- documentDigest: digest(await readText(root, changePath)),
276
+ documentDigest: digest(normalizeChangeRecordMissingDocument(documentText, changeId, waiverRecord)),
175
277
  everRecorded: [...order.records.values()].some((record) => record.kind === 'change' && record.entityId === changeId && record.phase !== 'waiver'),
176
278
  currentEntry: change ? digest(canonicalJson(change)) : null,
177
279
  };
@@ -185,17 +287,14 @@ export async function snapshotPayload(root, evidence, tdd, order, changeId, code
185
287
  .filter((cycle) => cycle.requirementId === requirementId)
186
288
  .map((cycle) => ({
187
289
  cycleId: cycle.cycleId ?? '',
188
- red: { valid: cycle.red.valid, order: cycle.red.order ?? null },
189
- 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,
190
292
  }))
191
- .sort((a, b) => {
192
- const orderA = a.red.order ?? Number.MAX_SAFE_INTEGER;
193
- const orderB = b.red.order ?? Number.MAX_SAFE_INTEGER;
194
- if (orderA !== orderB)
195
- return orderA - orderB;
196
- return a.cycleId < b.cycleId ? -1 : a.cycleId > b.cycleId ? 1 : 0;
197
- })
198
- .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
+ }));
199
298
  const validlyVoided = validlyVoidedTddCycles(tdd, order);
200
299
  const voidedCycleOrders = change
201
300
  ? voidedCycleOrdersInCurrentWindow(change, requirementId, tdd, validlyVoided)
@@ -242,20 +341,27 @@ export async function snapshotPayload(root, evidence, tdd, order, changeId, code
242
341
  };
243
342
  }
244
343
  if (parsed.kind === 'batch') {
245
- const batch = change ? batchForKey(effectiveBatches(change), parsed.batchKey) : undefined;
246
- const item = batch?.[parsed.batchPhaseName];
247
- return {
248
- phaseItemPresent: item != null,
249
- orderIsInteger: Number.isInteger(item?.order),
250
- 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
+ };
251
352
  };
353
+ return matches.length <= 1 ? state(matches[0]) : { matchingBatches: matches.map((batch) => state(batch)) };
252
354
  }
253
355
  if (parsed.kind === 'requirement') {
254
356
  const validlyVoided = validlyVoidedTddCycles(tdd, order);
255
357
  const cycles = (tdd?.cycles ?? [])
256
358
  .filter((cycle) => cycle.requirementId === parsed.requirementId)
257
- .map((cycle) => ({ redOrder: cycle.red.order ?? null, greenOrder: cycle.green?.order ?? null }))
258
- .sort((a, b) => (a.redOrder ?? Number.MAX_SAFE_INTEGER) - (b.redOrder ?? Number.MAX_SAFE_INTEGER));
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
+ }));
259
365
  const voidedCycleOrders = change
260
366
  ? voidedCycleOrdersInCurrentWindow(change, parsed.requirementId, tdd, validlyVoided)
261
367
  : [];
@@ -263,24 +369,90 @@ export async function snapshotPayload(root, evidence, tdd, order, changeId, code
263
369
  }
264
370
  return null;
265
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)) };
397
+ }
398
+ return null;
399
+ }
266
400
  if (parsed.kind !== 'batchKey')
267
401
  return null;
268
- 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];
269
405
  if (code === 'CHANGE_TESTS_UNCHANGED') {
270
- 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
+ };
271
416
  }
272
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
+ }
273
427
  return {
274
428
  redImplementation: batch?.red?.fingerprints.implementation ?? null,
275
429
  implementationImplementation: batch?.implementation?.fingerprints.implementation ?? null,
276
430
  };
277
431
  }
278
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
+ }
279
442
  return { redTests: batch?.red?.fingerprints.tests ?? null, greenTests: batch?.green?.fingerprints.tests ?? null };
280
443
  }
281
444
  if (code === 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED') {
282
445
  if (requirementId === undefined)
283
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
+ }
284
456
  return {
285
457
  redRequirementImplementation: batch?.red?.fingerprints.requirementImplementations?.[requirementId] ?? null,
286
458
  implementationRequirementImplementation: batch?.implementation?.fingerprints.requirementImplementations?.[requirementId] ?? null,
@@ -338,7 +510,7 @@ export function waiverChainValid(waivers, index) {
338
510
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-016
339
511
  * @design DES-CHANGE-EVIDENCE-WAIVER-002
340
512
  */
341
- export async function waiverLinkage(root, evidence, order, waivers, index) {
513
+ export function waiverLinkage(order, waivers, index) {
342
514
  const record = waivers[index];
343
515
  if (!record)
344
516
  return { valid: false, reason: 'Waiver record does not exist.' };
@@ -346,9 +518,6 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
346
518
  return { valid: false, reason: 'Waiver record has an invalid shape.' };
347
519
  if (!waiverChainValid(waivers, index))
348
520
  return { valid: false, reason: 'Waiver record breaks the evidence chain.' };
349
- if (!await exists(within(root, `.musubix/changes/${record.changeId}.md`))) {
350
- return { valid: false, reason: `${record.changeId} has no change document.` };
351
- }
352
521
  if (!WAIVABLE_CODES.includes(record.code)) {
353
522
  return { valid: false, reason: `${record.code} is not a waivable code.` };
354
523
  }
@@ -359,20 +528,11 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
359
528
  if (requiresDetail(code) !== (record.detail !== undefined)) {
360
529
  return { valid: false, reason: `${code}'s detail scope does not match its regime.` };
361
530
  }
362
- if (code === 'CHANGE_RECORD_MISSING') {
363
- const stillMissing = evidence === null || !evidence.changes.some((entry) => entry.changeId === record.changeId);
364
- if (!stillMissing)
365
- return { valid: false, reason: `${record.changeId} now has a chronology record.` };
366
- }
367
- else {
368
- if (evidence === null)
369
- return { valid: false, reason: 'No change evidence is available to link this waiver.' };
370
- const change = evidence.changes.find((entry) => entry.changeId === record.changeId);
371
- if (!change)
372
- return { valid: false, reason: `${record.changeId} is not a known change.` };
373
- if (record.requirementId !== undefined && !change.requirementIds.includes(record.requirementId)) {
374
- return { valid: false, reason: `${record.requirementId} is not declared by ${record.changeId}.` };
375
- }
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.` };
376
536
  }
377
537
  if (!order.valid)
378
538
  return { valid: false, reason: 'Monotonic evidence order is invalid.' };
@@ -387,6 +547,100 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
387
547
  }
388
548
  return { valid: true };
389
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
+ }
390
644
  /** @id CODE-CHANGE-EVIDENCE-WAIVER-020
391
645
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-007 REQ-CHANGE-EVIDENCE-WAIVER-010 REQ-CHANGE-EVIDENCE-WAIVER-011
392
646
  * @design DES-CHANGE-EVIDENCE-WAIVER-004
@@ -394,28 +648,45 @@ export async function waiverLinkage(root, evidence, order, waivers, index) {
394
648
  * consults only this fully resolved value, so no diagnostic-emission call
395
649
  * site or final malformed/stale pass needs to itself `await` anything.
396
650
  */
397
- export async function buildWaiverContext(root, evidence, tdd) {
398
- const loaded = await loadChangeWaiverEvidence(root);
399
- 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);
400
654
  const linkage = [];
401
655
  const currentHash = [];
656
+ const condition = [];
402
657
  if (loaded && !loaded.malformed) {
403
658
  for (let index = 0; index < loaded.waivers.length; index++) {
404
- const recordLinkage = await waiverLinkage(root, evidence, order, loaded.waivers, index);
659
+ const recordLinkage = waiverLinkage(order, loaded.waivers, index);
405
660
  linkage.push(recordLinkage);
406
661
  if (recordLinkage.valid) {
407
662
  const record = loaded.waivers[index];
408
- 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
+ }));
409
670
  }
410
671
  else {
411
672
  currentHash.push(undefined);
673
+ condition.push(undefined);
412
674
  }
413
675
  }
414
676
  }
415
- return { loaded, order, linkage, currentHash };
677
+ return { loaded, order, linkage, currentHash, condition };
416
678
  }
417
- function nonStale(record, index, waiverContext) {
418
- 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;
419
690
  }
420
691
  function scopeMatches(record, changeId, code, requirementId, detail) {
421
692
  return record.changeId === changeId && record.code === code && record.requirementId === requirementId && record.detail === detail;
@@ -425,7 +696,7 @@ function scopeMatches(record, changeId, code, requirementId, detail) {
425
696
  * `changeId`/`code`/`requirementId`/`detail` scope group (REQ-006's
426
697
  * supersession rule), returning its index, or -1 if none match/link.
427
698
  */
428
- function authoritativeIndex(waiverContext, changeId, code, requirementId, detail) {
699
+ export function authoritativeWaiverIndex(waiverContext, scope) {
429
700
  const loaded = waiverContext.loaded;
430
701
  if (!loaded || loaded.malformed)
431
702
  return -1;
@@ -434,7 +705,7 @@ function authoritativeIndex(waiverContext, changeId, code, requirementId, detail
434
705
  const record = loaded.waivers[index];
435
706
  if (!waiverContext.linkage[index]?.valid)
436
707
  continue;
437
- if (!scopeMatches(record, changeId, code, requirementId, detail))
708
+ if (!scopeMatches(record, scope.changeId, scope.code, scope.requirementId, scope.detail))
438
709
  continue;
439
710
  if (best === -1 || record.order > loaded.waivers[best].order)
440
711
  best = index;
@@ -450,11 +721,16 @@ export function waivedDiagnostic(waiverContext, code, message, changeId, require
450
721
  const loaded = waiverContext.loaded;
451
722
  if (!loaded || loaded.malformed)
452
723
  return base;
453
- 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
+ });
454
730
  if (index === -1)
455
731
  return base;
456
732
  const record = loaded.waivers[index];
457
- if (!nonStale(record, index, waiverContext))
733
+ if (isWaiverStale(waiverContext, index))
458
734
  return base;
459
735
  return {
460
736
  ...base,
@@ -466,7 +742,7 @@ export function waivedDiagnostic(waiverContext, code, message, changeId, require
466
742
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-007 REQ-CHANGE-EVIDENCE-WAIVER-011
467
743
  * @design DES-CHANGE-EVIDENCE-WAIVER-004
468
744
  */
469
- export function reportWaiverEvidenceDiagnostics(waiverContext, _evidence, _tdd) {
745
+ export function reportWaiverEvidenceDiagnostics(waiverContext) {
470
746
  const loaded = waiverContext.loaded;
471
747
  if (!loaded)
472
748
  return [];
@@ -487,12 +763,29 @@ export function reportWaiverEvidenceDiagnostics(waiverContext, _evidence, _tdd)
487
763
  if (groupsSeen.has(scopeKey))
488
764
  continue;
489
765
  groupsSeen.add(scopeKey);
490
- 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
+ });
491
772
  if (authoritative === -1)
492
773
  continue;
493
774
  const authoritativeRecord = loaded.waivers[authoritative];
494
- if (!nonStale(authoritativeRecord, authoritative, waiverContext)) {
495
- 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
+ });
496
789
  }
497
790
  }
498
791
  return diagnostics;
@@ -515,7 +808,7 @@ async function recordChangeWaiverUnlocked(root, changeId, code, requirementId, d
515
808
  for (let index = 0; index < existing.waivers.length; index++) {
516
809
  if (!waiverRecordShapeValid(existing.waivers[index])
517
810
  || !waiverChainValid(existing.waivers, index)
518
- || !(await waiverLinkage(root, evidence, order, existing.waivers, index)).valid) {
811
+ || !waiverLinkage(order, existing.waivers, index).valid) {
519
812
  throw new Error('An existing waiver record is invalid; repair the evidence chain before recording a new waiver.');
520
813
  }
521
814
  }
@@ -536,96 +829,35 @@ async function recordChangeWaiverUnlocked(root, changeId, code, requirementId, d
536
829
  if (!approver.trim() || !reason.trim()) {
537
830
  throw new Error('A non-empty --approver and --reason are required.');
538
831
  }
539
- if (!await exists(within(root, `.musubix/changes/${changeId}.md`))) {
540
- throw new Error(`${changeId} has no change document.`);
541
- }
542
- const change = evidence?.changes.find((entry) => entry.changeId === changeId);
543
- if (waivableCode !== 'CHANGE_RECORD_MISSING') {
544
- if (!change)
545
- throw new Error(`${changeId} is not a known change.`);
546
- if (requirementId !== undefined && !change.requirementIds.includes(requirementId)) {
547
- throw new Error(`${requirementId} is not declared by ${changeId}.`);
548
- }
549
- }
550
832
  const tdd = await loadTddEvidence(root);
551
- const validlyVoided = validlyVoidedTddCycles(tdd, order);
552
833
  const parsed = detail !== undefined ? parseDetail(waivableCode, detail) : null;
553
834
  if (requiresDetail(waivableCode) && !parsed) {
554
835
  throw new Error(`${detail} is not a valid --detail value for ${waivableCode}.`);
555
836
  }
556
- const currentlyReported = await (async () => {
557
- switch (waivableCode) {
558
- case 'CHANGE_REQUIREMENTS_UNCHANGED': return requirementsUnchangedCondition(change);
559
- case 'CHANGE_DESIGN_UNCHANGED': return designUnchangedCondition(change);
560
- case 'CHANGE_RED_UNPROVEN': return redUnprovenCondition(change, requirementId, tdd, validlyVoided);
561
- case 'CHANGE_GREEN_UNPROVEN': return greenUnprovenCondition(change, requirementId, tdd, validlyVoided);
562
- case 'CHANGE_COMPLETENESS_TDD': return completenessTddUnsatisfiedCondition(change, requirementId, tdd, validlyVoided);
563
- case 'CHANGE_RECORD_MISSING': return recordMissingCondition(root, evidence, changeId);
564
- case 'CHANGE_PHASE_MISSING':
565
- return parsed?.kind === 'phase' ? phaseMissingCondition(change, parsed.phaseName) : false;
566
- case 'CHANGE_ORDER_MIGRATION_REQUIRED': {
567
- if (!parsed)
568
- return false;
569
- if (parsed.kind === 'phase')
570
- return orderMigrationRequiredPhaseCondition(change, parsed.phaseName);
571
- if (parsed.kind === 'batch')
572
- return orderMigrationRequiredBatchCondition(change, parsed.batchPhaseName, parsed.batchKey);
573
- if (parsed.kind === 'requirement') {
574
- return orderMigrationRequiredRequirementCondition(change, parsed.requirementId, tdd, validlyVoided);
575
- }
576
- return false;
577
- }
578
- case 'CHANGE_TESTS_UNCHANGED': {
579
- if (parsed?.kind !== 'batchKey')
580
- return false;
581
- const batches = effectiveBatches(change);
582
- const batch = batchForKey(batches, parsed.batchKey);
583
- return !!batch
584
- && currentRequirementIdsForBatch(batches, batch, change.requirementIds).length > 0
585
- && testsUnchangedCondition(change, batch);
586
- }
587
- case 'CHANGE_IMPLEMENTATION_UNCHANGED': {
588
- if (parsed?.kind !== 'batchKey')
589
- return false;
590
- const batches = effectiveBatches(change);
591
- const batch = batchForKey(batches, parsed.batchKey);
592
- return !!batch
593
- && currentRequirementIdsForBatch(batches, batch, change.requirementIds).length > 0
594
- && implementationUnchangedCondition(batch);
595
- }
596
- case 'CHANGE_TEST_CHANGED_AFTER_RED': {
597
- if (parsed?.kind !== 'batchKey')
598
- return false;
599
- const batches = effectiveBatches(change);
600
- const batch = batchForKey(batches, parsed.batchKey);
601
- return !!batch
602
- && currentRequirementIdsForBatch(batches, batch, change.requirementIds).length > 0
603
- && testChangedAfterRedCondition(batch);
604
- }
605
- case 'CHANGE_RELEVANT_IMPLEMENTATION_UNCHANGED': {
606
- if (parsed?.kind !== 'batchKey' || requirementId === undefined)
607
- return false;
608
- const batches = effectiveBatches(change);
609
- const batch = batchForKey(batches, parsed.batchKey);
610
- return !!batch
611
- && currentRequirementIdsForBatch(batches, batch, change.requirementIds).includes(requirementId)
612
- && relevantImplementationUnchangedCondition(batch, requirementId);
613
- }
614
- 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}.`);
615
848
  }
616
- })();
617
- if (!currentlyReported) {
618
- 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.`);
619
850
  }
620
- for (let index = 0; index < existing.waivers.length; index++) {
621
- const record = existing.waivers[index];
622
- if (scopeMatches(record, changeId, waivableCode, requirementId, detail)
623
- && (await waiverLinkage(root, evidence, order, existing.waivers, index)).valid) {
624
- const hash = digest(canonicalJson(await snapshotPayload(root, evidence, tdd, order, changeId, waivableCode, requirementId, detail)));
625
- if (record.snapshotVersion === CURRENT_SNAPSHOT_VERSION && record.snapshotHash === hash) {
626
- throw new Error(`${changeId}:${waivableCode}${requirementId ? `:${requirementId}` : ''}${detail ? `:${detail}` : ''} already has an active waiver.`);
627
- }
628
- }
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.`);
629
861
  }
630
862
  const snapshotHash = digest(canonicalJson(await snapshotPayload(root, evidence, tdd, order, changeId, waivableCode, requirementId, detail)));
631
863
  const orderRecord = await appendEvidenceOrder(root, {
@@ -666,13 +898,11 @@ async function recordChangeWaiverUnlocked(root, changeId, code, requirementId, d
666
898
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-006 REQ-CHANGE-EVIDENCE-WAIVER-012 REQ-CHANGE-EVIDENCE-WAIVER-016
667
899
  * @design DES-CHANGE-EVIDENCE-WAIVER-005
668
900
  */
669
- export async function activeWaivers(root) {
670
- 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;
671
904
  if (!loaded || loaded.malformed)
672
905
  return [];
673
- const evidence = await loadChangeEvidence(root);
674
- const tdd = await loadTddEvidence(root);
675
- const waiverContext = await buildWaiverContext(root, evidence, tdd);
676
906
  const results = [];
677
907
  const groupsSeen = new Set();
678
908
  for (let index = 0; index < loaded.waivers.length; index++) {
@@ -683,11 +913,16 @@ export async function activeWaivers(root) {
683
913
  if (groupsSeen.has(scopeKey))
684
914
  continue;
685
915
  groupsSeen.add(scopeKey);
686
- 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
+ });
687
922
  if (authoritative === -1)
688
923
  continue;
689
924
  const authoritativeRecord = loaded.waivers[authoritative];
690
- if (!nonStale(authoritativeRecord, authoritative, waiverContext))
925
+ if (isWaiverStale(waiverContext, authoritative))
691
926
  continue;
692
927
  results.push({
693
928
  changeId: authoritativeRecord.changeId,
@@ -705,7 +940,7 @@ export async function evaluateChangeWaiverState(root, evidence, tdd, order, waiv
705
940
  const linkage = [];
706
941
  const currentHash = [];
707
942
  for (let index = 0; index < waivers.waivers.length; index++) {
708
- const recordLinkage = await waiverLinkage(root, evidence, order, waivers.waivers, index);
943
+ const recordLinkage = waiverLinkage(order, waivers.waivers, index);
709
944
  linkage.push(recordLinkage);
710
945
  if (recordLinkage.valid) {
711
946
  const record = waivers.waivers[index];
@@ -715,29 +950,48 @@ export async function evaluateChangeWaiverState(root, evidence, tdd, order, waiv
715
950
  currentHash.push(undefined);
716
951
  }
717
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
+ }
718
965
  const context = {
719
966
  loaded: { schemaVersion: 1, waivers: waivers.waivers, malformed: false },
720
967
  order,
721
968
  linkage,
722
969
  currentHash,
970
+ condition,
723
971
  };
724
- const stale = waivers.waivers.flatMap((record, index) => linkage[index]?.valid && !nonStale(record, index, context)
725
- ? [{
726
- changeId: record.changeId,
727
- code: record.code,
728
- ...(record.requirementId !== undefined ? { requirementId: record.requirementId } : {}),
729
- ...(record.detail !== undefined ? { detail: record.detail } : {}),
730
- }]
731
- : []);
732
972
  const authoritative = new Map();
973
+ const stale = [];
733
974
  for (const record of waivers.waivers) {
734
975
  const scope = JSON.stringify([record.changeId, record.code, record.requirementId, record.detail]);
735
976
  if (authoritative.has(scope))
736
977
  continue;
737
- const index = authoritativeIndex(context, record.changeId, record.code, record.requirementId, record.detail);
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
+ });
738
984
  if (index === -1)
739
985
  continue;
740
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
+ }
741
995
  authoritative.set(scope, JSON.stringify([
742
996
  selected.changeId,
743
997
  selected.code,
@@ -756,11 +1010,8 @@ export async function evaluateChangeWaiverState(root, evidence, tdd, order, waiv
756
1010
  * @implements REQ-CHANGE-EVIDENCE-WAIVER-007 REQ-CHANGE-EVIDENCE-WAIVER-011
757
1011
  * @design DES-CHANGE-EVIDENCE-WAIVER-005
758
1012
  */
759
- export async function waiverEvidenceDiagnostics(root) {
760
- const loaded = await loadChangeWaiverEvidence(root);
761
- const evidence = await loadChangeEvidence(root);
762
- const tdd = await loadTddEvidence(root);
763
- const waiverContext = await buildWaiverContext(root, evidence, tdd);
764
- 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);
765
1016
  }
766
1017
  //# sourceMappingURL=change-waiver.js.map