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.
- package/.github/plugin/marketplace.json +2 -2
- package/.github/skills/sdd-change/SKILL.md +28 -45
- package/CHANGELOG.md +88 -0
- package/README-ja.md +103 -3
- package/README.md +113 -3
- package/dist/packages/analysis/src/approval.d.ts +51 -0
- package/dist/packages/analysis/src/approval.js +341 -2
- package/dist/packages/analysis/src/approval.js.map +1 -1
- package/dist/packages/analysis/src/attestation.js +4 -0
- package/dist/packages/analysis/src/attestation.js.map +1 -1
- package/dist/packages/analysis/src/change-evidence.d.ts +56 -13
- package/dist/packages/analysis/src/change-evidence.js +142 -81
- package/dist/packages/analysis/src/change-evidence.js.map +1 -1
- package/dist/packages/analysis/src/change-waiver.d.ts +37 -13
- package/dist/packages/analysis/src/change-waiver.js +425 -174
- package/dist/packages/analysis/src/change-waiver.js.map +1 -1
- package/dist/packages/analysis/src/change.d.ts +1 -0
- package/dist/packages/analysis/src/change.js +176 -23
- package/dist/packages/analysis/src/change.js.map +1 -1
- package/dist/packages/analysis/src/evidence-merge-guard.d.ts +5 -0
- package/dist/packages/analysis/src/evidence-merge-guard.js +15 -0
- package/dist/packages/analysis/src/evidence-merge-guard.js.map +1 -1
- package/dist/packages/analysis/src/evidence-merge.d.ts +2 -0
- package/dist/packages/analysis/src/evidence-merge.js +173 -38
- package/dist/packages/analysis/src/evidence-merge.js.map +1 -1
- package/dist/packages/analysis/src/evidence-writer-lock.d.ts +9 -2
- package/dist/packages/analysis/src/evidence-writer-lock.js +75 -21
- package/dist/packages/analysis/src/evidence-writer-lock.js.map +1 -1
- package/dist/packages/analysis/src/files.js +5 -0
- package/dist/packages/analysis/src/files.js.map +1 -1
- package/dist/packages/analysis/src/gate.d.ts +1 -1
- package/dist/packages/analysis/src/gate.js +17 -4
- package/dist/packages/analysis/src/gate.js.map +1 -1
- package/dist/packages/analysis/src/index.d.ts +1 -0
- package/dist/packages/analysis/src/index.js +1 -0
- package/dist/packages/analysis/src/index.js.map +1 -1
- package/dist/packages/analysis/src/order.d.ts +16 -1
- package/dist/packages/analysis/src/order.js +6 -0
- package/dist/packages/analysis/src/order.js.map +1 -1
- package/dist/packages/analysis/src/quality-refresh.d.ts +18 -0
- package/dist/packages/analysis/src/quality-refresh.js +312 -0
- package/dist/packages/analysis/src/quality-refresh.js.map +1 -0
- package/dist/packages/analysis/src/tdd.d.ts +84 -3
- package/dist/packages/analysis/src/tdd.js +589 -50
- package/dist/packages/analysis/src/tdd.js.map +1 -1
- package/dist/packages/analysis/src/trace.d.ts +24 -0
- package/dist/packages/analysis/src/trace.js +95 -3
- package/dist/packages/analysis/src/trace.js.map +1 -1
- package/dist/packages/analysis/src/workflow-waiver.d.ts +6 -2
- package/dist/packages/analysis/src/workflow-waiver.js +9 -5
- package/dist/packages/analysis/src/workflow-waiver.js.map +1 -1
- package/dist/packages/analysis/src/workflow.d.ts +1 -1
- package/dist/packages/analysis/src/workflow.js +4 -4
- package/dist/packages/analysis/src/workflow.js.map +1 -1
- package/dist/packages/cli/src/install.js +27 -4
- package/dist/packages/cli/src/install.js.map +1 -1
- package/dist/packages/cli/src/main.js +77 -18
- package/dist/packages/cli/src/main.js.map +1 -1
- package/dist/packages/domain/src/design.js +8 -1
- package/dist/packages/domain/src/design.js.map +1 -1
- package/dist/packages/domain/src/types.d.ts +1 -0
- package/package.json +7 -2
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
189
|
-
green: cycle.green ? { valid: cycle.green.valid, order: cycle.green.order
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
258
|
-
.sort((a, b) => (a
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 =
|
|
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
|
|
418
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
|
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 =
|
|
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 (
|
|
495
|
-
|
|
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
|
-
|| !
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
761
|
-
|
|
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
|