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