scene-capability-engine 3.3.18 → 3.3.22
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/CHANGELOG.md +51 -0
- package/README.md +19 -10
- package/README.zh.md +21 -11
- package/docs/command-reference.md +71 -12
- package/docs/release-checklist.md +7 -0
- package/docs/zh/release-checklist.md +7 -0
- package/lib/commands/errorbook.js +455 -2
- package/lib/commands/spec-bootstrap.js +126 -51
- package/lib/commands/spec-gate.js +92 -25
- package/lib/commands/spec-pipeline.js +86 -7
- package/lib/commands/studio.js +265 -30
- package/lib/runtime/multi-spec-scene-session.js +147 -0
- package/lib/runtime/scene-session-binding.js +109 -0
- package/lib/runtime/session-store.js +475 -3
- package/package.json +4 -2
- package/template/.sce/steering/CORE_PRINCIPLES.md +35 -1
|
@@ -7,6 +7,7 @@ const Table = require('cli-table3');
|
|
|
7
7
|
const ERRORBOOK_INDEX_API_VERSION = 'sce.errorbook.index/v0.1';
|
|
8
8
|
const ERRORBOOK_ENTRY_API_VERSION = 'sce.errorbook.entry/v0.1';
|
|
9
9
|
const ERRORBOOK_STATUSES = Object.freeze(['candidate', 'verified', 'promoted', 'deprecated']);
|
|
10
|
+
const TEMPORARY_MITIGATION_TAG = 'temporary-mitigation';
|
|
10
11
|
const STATUS_RANK = Object.freeze({
|
|
11
12
|
deprecated: 0,
|
|
12
13
|
candidate: 1,
|
|
@@ -37,6 +38,17 @@ const ONTOLOGY_TAG_ALIASES = Object.freeze({
|
|
|
37
38
|
action_chain: 'execution_flow'
|
|
38
39
|
});
|
|
39
40
|
const DEFAULT_PROMOTE_MIN_QUALITY = 75;
|
|
41
|
+
const ERRORBOOK_RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);
|
|
42
|
+
const HIGH_RISK_SIGNAL_TAGS = Object.freeze([
|
|
43
|
+
'release-blocker',
|
|
44
|
+
'security',
|
|
45
|
+
'auth',
|
|
46
|
+
'payment',
|
|
47
|
+
'data-loss',
|
|
48
|
+
'integrity',
|
|
49
|
+
'compliance',
|
|
50
|
+
'incident'
|
|
51
|
+
]);
|
|
40
52
|
|
|
41
53
|
function resolveErrorbookPaths(projectPath = process.cwd()) {
|
|
42
54
|
const baseDir = path.join(projectPath, '.sce', 'errorbook');
|
|
@@ -60,6 +72,23 @@ function normalizeText(value) {
|
|
|
60
72
|
return value.trim();
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
function normalizeBoolean(value, fallback = false) {
|
|
76
|
+
if (typeof value === 'boolean') {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
80
|
+
if (!normalized) {
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
function normalizeCsv(value) {
|
|
64
93
|
if (Array.isArray(value)) {
|
|
65
94
|
return value;
|
|
@@ -98,6 +127,18 @@ function normalizeStringList(...rawInputs) {
|
|
|
98
127
|
return Array.from(new Set(merged));
|
|
99
128
|
}
|
|
100
129
|
|
|
130
|
+
function normalizeIsoTimestamp(value, fieldName = 'datetime') {
|
|
131
|
+
const normalized = normalizeText(value);
|
|
132
|
+
if (!normalized) {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
const parsed = Date.parse(normalized);
|
|
136
|
+
if (Number.isNaN(parsed)) {
|
|
137
|
+
throw new Error(`${fieldName} must be a valid ISO datetime`);
|
|
138
|
+
}
|
|
139
|
+
return new Date(parsed).toISOString();
|
|
140
|
+
}
|
|
141
|
+
|
|
101
142
|
function normalizeOntologyTags(...rawInputs) {
|
|
102
143
|
const normalized = normalizeStringList(...rawInputs).map((item) => item.toLowerCase());
|
|
103
144
|
const mapped = normalized.map((item) => ONTOLOGY_TAG_ALIASES[item] || item);
|
|
@@ -105,6 +146,140 @@ function normalizeOntologyTags(...rawInputs) {
|
|
|
105
146
|
return Array.from(new Set(valid));
|
|
106
147
|
}
|
|
107
148
|
|
|
149
|
+
function hasMitigationInput(options = {}, fromFilePayload = {}) {
|
|
150
|
+
const mitigation = fromFilePayload && typeof fromFilePayload.temporary_mitigation === 'object'
|
|
151
|
+
? fromFilePayload.temporary_mitigation
|
|
152
|
+
: {};
|
|
153
|
+
return Boolean(
|
|
154
|
+
options.temporaryMitigation === true ||
|
|
155
|
+
normalizeBoolean(mitigation.enabled, false) ||
|
|
156
|
+
normalizeText(options.mitigationReason || mitigation.reason || mitigation.notes) ||
|
|
157
|
+
normalizeText(options.mitigationExit || mitigation.exit_criteria || mitigation.exitCriteria) ||
|
|
158
|
+
normalizeText(options.mitigationCleanup || mitigation.cleanup_task || mitigation.cleanupTask) ||
|
|
159
|
+
normalizeText(options.mitigationDeadline || mitigation.deadline_at || mitigation.deadlineAt)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeTemporaryMitigation(options = {}, fromFilePayload = {}) {
|
|
164
|
+
const mitigationFromFile = fromFilePayload && typeof fromFilePayload.temporary_mitigation === 'object'
|
|
165
|
+
? fromFilePayload.temporary_mitigation
|
|
166
|
+
: {};
|
|
167
|
+
if (!hasMitigationInput(options, fromFilePayload)) {
|
|
168
|
+
return { enabled: false };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
enabled: true,
|
|
173
|
+
reason: normalizeText(options.mitigationReason || mitigationFromFile.reason || mitigationFromFile.notes),
|
|
174
|
+
exit_criteria: normalizeText(options.mitigationExit || mitigationFromFile.exit_criteria || mitigationFromFile.exitCriteria),
|
|
175
|
+
cleanup_task: normalizeText(options.mitigationCleanup || mitigationFromFile.cleanup_task || mitigationFromFile.cleanupTask),
|
|
176
|
+
deadline_at: normalizeIsoTimestamp(
|
|
177
|
+
options.mitigationDeadline || mitigationFromFile.deadline_at || mitigationFromFile.deadlineAt,
|
|
178
|
+
'--mitigation-deadline'
|
|
179
|
+
),
|
|
180
|
+
created_at: normalizeIsoTimestamp(mitigationFromFile.created_at || mitigationFromFile.createdAt, 'temporary_mitigation.created_at') || '',
|
|
181
|
+
updated_at: normalizeIsoTimestamp(mitigationFromFile.updated_at || mitigationFromFile.updatedAt, 'temporary_mitigation.updated_at') || '',
|
|
182
|
+
resolved_at: normalizeIsoTimestamp(mitigationFromFile.resolved_at || mitigationFromFile.resolvedAt, 'temporary_mitigation.resolved_at') || ''
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeExistingTemporaryMitigation(value = {}) {
|
|
187
|
+
if (!value || typeof value !== 'object' || normalizeBoolean(value.enabled, false) !== true) {
|
|
188
|
+
return { enabled: false };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
enabled: true,
|
|
192
|
+
reason: normalizeText(value.reason || value.notes),
|
|
193
|
+
exit_criteria: normalizeText(value.exit_criteria || value.exitCriteria),
|
|
194
|
+
cleanup_task: normalizeText(value.cleanup_task || value.cleanupTask),
|
|
195
|
+
deadline_at: normalizeText(value.deadline_at || value.deadlineAt),
|
|
196
|
+
created_at: normalizeText(value.created_at || value.createdAt),
|
|
197
|
+
updated_at: normalizeText(value.updated_at || value.updatedAt),
|
|
198
|
+
resolved_at: normalizeText(value.resolved_at || value.resolvedAt)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveMergedTemporaryMitigation(existingEntry = {}, incomingPayload = {}) {
|
|
203
|
+
const existing = normalizeExistingTemporaryMitigation(existingEntry.temporary_mitigation);
|
|
204
|
+
const incoming = normalizeExistingTemporaryMitigation(incomingPayload.temporary_mitigation);
|
|
205
|
+
if (!incoming.enabled) {
|
|
206
|
+
return existing;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
enabled: true,
|
|
211
|
+
reason: normalizeText(incoming.reason) || existing.reason || '',
|
|
212
|
+
exit_criteria: normalizeText(incoming.exit_criteria) || existing.exit_criteria || '',
|
|
213
|
+
cleanup_task: normalizeText(incoming.cleanup_task) || existing.cleanup_task || '',
|
|
214
|
+
deadline_at: normalizeText(incoming.deadline_at) || existing.deadline_at || '',
|
|
215
|
+
created_at: normalizeText(existing.created_at) || nowIso(),
|
|
216
|
+
updated_at: nowIso(),
|
|
217
|
+
resolved_at: ''
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function markTemporaryMitigationResolved(entry = {}, resolvedAt = nowIso()) {
|
|
222
|
+
const current = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
223
|
+
if (!current.enabled || normalizeText(current.resolved_at)) {
|
|
224
|
+
return current.enabled ? {
|
|
225
|
+
...current,
|
|
226
|
+
resolved_at: normalizeText(current.resolved_at) || resolvedAt,
|
|
227
|
+
updated_at: normalizeText(current.updated_at) || resolvedAt
|
|
228
|
+
} : { enabled: false };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
...current,
|
|
232
|
+
resolved_at: resolvedAt,
|
|
233
|
+
updated_at: resolvedAt
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function evaluateTemporaryMitigationPolicy(entry = {}) {
|
|
238
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
239
|
+
const status = normalizeStatus(entry.status, 'candidate');
|
|
240
|
+
if (!mitigation.enabled || ['promoted', 'deprecated'].includes(status)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (normalizeText(mitigation.resolved_at)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const policyViolations = [];
|
|
248
|
+
if (!normalizeText(mitigation.exit_criteria)) {
|
|
249
|
+
policyViolations.push('temporary_mitigation.exit_criteria');
|
|
250
|
+
}
|
|
251
|
+
if (!normalizeText(mitigation.cleanup_task)) {
|
|
252
|
+
policyViolations.push('temporary_mitigation.cleanup_task');
|
|
253
|
+
}
|
|
254
|
+
const deadlineAtRaw = normalizeText(mitigation.deadline_at);
|
|
255
|
+
let deadlineAt = deadlineAtRaw;
|
|
256
|
+
let deadlineExpired = false;
|
|
257
|
+
if (!deadlineAtRaw) {
|
|
258
|
+
policyViolations.push('temporary_mitigation.deadline_at');
|
|
259
|
+
} else {
|
|
260
|
+
const parsed = Date.parse(deadlineAtRaw);
|
|
261
|
+
if (Number.isNaN(parsed)) {
|
|
262
|
+
policyViolations.push('temporary_mitigation.deadline_at:invalid_datetime');
|
|
263
|
+
} else {
|
|
264
|
+
deadlineAt = new Date(parsed).toISOString();
|
|
265
|
+
if (parsed <= Date.now()) {
|
|
266
|
+
deadlineExpired = true;
|
|
267
|
+
policyViolations.push('temporary_mitigation.deadline_at:expired');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
enabled: true,
|
|
274
|
+
reason: mitigation.reason,
|
|
275
|
+
exit_criteria: mitigation.exit_criteria,
|
|
276
|
+
cleanup_task: mitigation.cleanup_task,
|
|
277
|
+
deadline_at: deadlineAt,
|
|
278
|
+
deadline_expired: deadlineExpired,
|
|
279
|
+
policy_violations: policyViolations
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
108
283
|
function normalizeStatus(input, fallback = 'candidate') {
|
|
109
284
|
const normalized = normalizeText(`${input || ''}`).toLowerCase();
|
|
110
285
|
if (!normalized) {
|
|
@@ -262,9 +437,22 @@ function validateRecordPayload(payload) {
|
|
|
262
437
|
if (status === 'verified' && (!Array.isArray(payload.verification_evidence) || payload.verification_evidence.length === 0)) {
|
|
263
438
|
throw new Error('status=verified requires at least one --verification evidence');
|
|
264
439
|
}
|
|
440
|
+
const mitigation = normalizeExistingTemporaryMitigation(payload.temporary_mitigation);
|
|
441
|
+
if (mitigation.enabled) {
|
|
442
|
+
if (!normalizeText(mitigation.exit_criteria)) {
|
|
443
|
+
throw new Error('temporary mitigation requires --mitigation-exit');
|
|
444
|
+
}
|
|
445
|
+
if (!normalizeText(mitigation.cleanup_task)) {
|
|
446
|
+
throw new Error('temporary mitigation requires --mitigation-cleanup');
|
|
447
|
+
}
|
|
448
|
+
if (!normalizeText(mitigation.deadline_at)) {
|
|
449
|
+
throw new Error('temporary mitigation requires --mitigation-deadline');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
265
452
|
}
|
|
266
453
|
|
|
267
454
|
function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
455
|
+
const temporaryMitigation = normalizeTemporaryMitigation(options, fromFilePayload);
|
|
268
456
|
const payload = {
|
|
269
457
|
title: normalizeText(options.title || fromFilePayload.title),
|
|
270
458
|
symptom: normalizeText(options.symptom || fromFilePayload.symptom),
|
|
@@ -276,7 +464,11 @@ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
|
276
464
|
options.verification,
|
|
277
465
|
options.verificationEvidence
|
|
278
466
|
),
|
|
279
|
-
tags: normalizeStringList(
|
|
467
|
+
tags: normalizeStringList(
|
|
468
|
+
fromFilePayload.tags,
|
|
469
|
+
options.tags,
|
|
470
|
+
temporaryMitigation.enabled ? TEMPORARY_MITIGATION_TAG : []
|
|
471
|
+
),
|
|
280
472
|
ontology_tags: normalizeOntologyTags(fromFilePayload.ontology_tags, fromFilePayload.ontology, options.ontology),
|
|
281
473
|
status: normalizeStatus(options.status || fromFilePayload.status || 'candidate'),
|
|
282
474
|
source: {
|
|
@@ -284,6 +476,7 @@ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
|
284
476
|
files: normalizeStringList(fromFilePayload?.source?.files, options.files),
|
|
285
477
|
tests: normalizeStringList(fromFilePayload?.source?.tests, options.tests)
|
|
286
478
|
},
|
|
479
|
+
temporary_mitigation: temporaryMitigation,
|
|
287
480
|
notes: normalizeText(options.notes || fromFilePayload.notes),
|
|
288
481
|
fingerprint: createFingerprint({
|
|
289
482
|
fingerprint: options.fingerprint || fromFilePayload.fingerprint,
|
|
@@ -301,6 +494,8 @@ function createEntryId() {
|
|
|
301
494
|
}
|
|
302
495
|
|
|
303
496
|
function buildIndexSummary(entry) {
|
|
497
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
498
|
+
const mitigationActive = mitigation.enabled && !normalizeText(mitigation.resolved_at);
|
|
304
499
|
return {
|
|
305
500
|
id: entry.id,
|
|
306
501
|
fingerprint: entry.fingerprint,
|
|
@@ -309,6 +504,8 @@ function buildIndexSummary(entry) {
|
|
|
309
504
|
quality_score: entry.quality_score,
|
|
310
505
|
tags: entry.tags,
|
|
311
506
|
ontology_tags: entry.ontology_tags,
|
|
507
|
+
temporary_mitigation_active: mitigationActive,
|
|
508
|
+
temporary_mitigation_deadline_at: mitigationActive ? normalizeText(mitigation.deadline_at) : '',
|
|
312
509
|
occurrences: entry.occurrences || 1,
|
|
313
510
|
created_at: entry.created_at,
|
|
314
511
|
updated_at: entry.updated_at
|
|
@@ -337,6 +534,12 @@ function findSummaryById(index, id) {
|
|
|
337
534
|
}
|
|
338
535
|
|
|
339
536
|
function mergeEntry(existingEntry, incomingPayload) {
|
|
537
|
+
const temporaryMitigation = resolveMergedTemporaryMitigation(existingEntry, incomingPayload);
|
|
538
|
+
const mergedTags = normalizeStringList(
|
|
539
|
+
existingEntry.tags,
|
|
540
|
+
incomingPayload.tags,
|
|
541
|
+
temporaryMitigation.enabled ? TEMPORARY_MITIGATION_TAG : []
|
|
542
|
+
);
|
|
340
543
|
const merged = {
|
|
341
544
|
...existingEntry,
|
|
342
545
|
title: normalizeText(incomingPayload.title) || existingEntry.title,
|
|
@@ -344,7 +547,7 @@ function mergeEntry(existingEntry, incomingPayload) {
|
|
|
344
547
|
root_cause: normalizeText(incomingPayload.root_cause) || existingEntry.root_cause,
|
|
345
548
|
fix_actions: normalizeStringList(existingEntry.fix_actions, incomingPayload.fix_actions),
|
|
346
549
|
verification_evidence: normalizeStringList(existingEntry.verification_evidence, incomingPayload.verification_evidence),
|
|
347
|
-
tags:
|
|
550
|
+
tags: mergedTags,
|
|
348
551
|
ontology_tags: normalizeOntologyTags(existingEntry.ontology_tags, incomingPayload.ontology_tags),
|
|
349
552
|
status: selectStatus(existingEntry.status, incomingPayload.status),
|
|
350
553
|
notes: normalizeText(incomingPayload.notes) || existingEntry.notes || '',
|
|
@@ -353,6 +556,7 @@ function mergeEntry(existingEntry, incomingPayload) {
|
|
|
353
556
|
files: normalizeStringList(existingEntry?.source?.files, incomingPayload?.source?.files),
|
|
354
557
|
tests: normalizeStringList(existingEntry?.source?.tests, incomingPayload?.source?.tests)
|
|
355
558
|
},
|
|
559
|
+
temporary_mitigation: temporaryMitigation,
|
|
356
560
|
occurrences: Number(existingEntry.occurrences || 1) + 1,
|
|
357
561
|
updated_at: nowIso()
|
|
358
562
|
};
|
|
@@ -458,6 +662,179 @@ function scoreSearchMatch(entry, queryTokens) {
|
|
|
458
662
|
return Number(score.toFixed(3));
|
|
459
663
|
}
|
|
460
664
|
|
|
665
|
+
function normalizeRiskLevel(value, fallback = 'high') {
|
|
666
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
667
|
+
if (!normalized) {
|
|
668
|
+
return fallback;
|
|
669
|
+
}
|
|
670
|
+
if (!ERRORBOOK_RISK_LEVELS.includes(normalized)) {
|
|
671
|
+
throw new Error(`risk level must be one of: ${ERRORBOOK_RISK_LEVELS.join(', ')}`);
|
|
672
|
+
}
|
|
673
|
+
return normalized;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function riskRank(level) {
|
|
677
|
+
const normalized = normalizeRiskLevel(level, 'high');
|
|
678
|
+
if (normalized === 'high') {
|
|
679
|
+
return 3;
|
|
680
|
+
}
|
|
681
|
+
if (normalized === 'medium') {
|
|
682
|
+
return 2;
|
|
683
|
+
}
|
|
684
|
+
return 1;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function evaluateEntryRisk(entry = {}) {
|
|
688
|
+
const status = normalizeStatus(entry.status, 'candidate');
|
|
689
|
+
if (status === 'promoted' || status === 'deprecated') {
|
|
690
|
+
return 'low';
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const qualityScore = Number(entry.quality_score || 0);
|
|
694
|
+
const tags = normalizeStringList(entry.tags).map((item) => item.toLowerCase());
|
|
695
|
+
const ontologyTags = normalizeOntologyTags(entry.ontology_tags);
|
|
696
|
+
const hasHighRiskTag = tags.some((tag) => HIGH_RISK_SIGNAL_TAGS.includes(tag));
|
|
697
|
+
|
|
698
|
+
if (hasHighRiskTag) {
|
|
699
|
+
return 'high';
|
|
700
|
+
}
|
|
701
|
+
if (status === 'candidate' && qualityScore >= 85) {
|
|
702
|
+
return 'high';
|
|
703
|
+
}
|
|
704
|
+
if (status === 'candidate' && qualityScore >= 75 && ontologyTags.includes('decision_policy')) {
|
|
705
|
+
return 'high';
|
|
706
|
+
}
|
|
707
|
+
if (status === 'candidate') {
|
|
708
|
+
return 'medium';
|
|
709
|
+
}
|
|
710
|
+
if (qualityScore >= 85 && ontologyTags.includes('decision_policy')) {
|
|
711
|
+
return 'high';
|
|
712
|
+
}
|
|
713
|
+
return 'medium';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
717
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
718
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
719
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
720
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
721
|
+
const minRisk = normalizeRiskLevel(options.minRisk || options.min_risk || 'high', 'high');
|
|
722
|
+
const includeVerified = options.includeVerified === true;
|
|
723
|
+
|
|
724
|
+
const inspected = [];
|
|
725
|
+
const mitigationInspected = [];
|
|
726
|
+
const mitigationBlocked = [];
|
|
727
|
+
for (const summary of index.entries) {
|
|
728
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
729
|
+
if (!entry) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const status = normalizeStatus(entry.status, 'candidate');
|
|
734
|
+
const risk = evaluateEntryRisk(entry);
|
|
735
|
+
const mitigation = evaluateTemporaryMitigationPolicy(entry);
|
|
736
|
+
if (mitigation) {
|
|
737
|
+
const mitigationItem = {
|
|
738
|
+
id: entry.id,
|
|
739
|
+
title: entry.title,
|
|
740
|
+
status,
|
|
741
|
+
risk,
|
|
742
|
+
quality_score: Number(entry.quality_score || 0),
|
|
743
|
+
tags: normalizeStringList(entry.tags),
|
|
744
|
+
updated_at: entry.updated_at,
|
|
745
|
+
temporary_mitigation: mitigation
|
|
746
|
+
};
|
|
747
|
+
mitigationInspected.push(mitigationItem);
|
|
748
|
+
if (Array.isArray(mitigation.policy_violations) && mitigation.policy_violations.length > 0) {
|
|
749
|
+
mitigationBlocked.push({
|
|
750
|
+
...mitigationItem,
|
|
751
|
+
block_reasons: ['temporary_mitigation_policy'],
|
|
752
|
+
policy_violations: mitigation.policy_violations
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const unresolved = status === 'candidate' || (includeVerified && status === 'verified');
|
|
758
|
+
if (!unresolved) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
inspected.push({
|
|
763
|
+
id: entry.id,
|
|
764
|
+
title: entry.title,
|
|
765
|
+
status,
|
|
766
|
+
risk,
|
|
767
|
+
quality_score: Number(entry.quality_score || 0),
|
|
768
|
+
tags: normalizeStringList(entry.tags),
|
|
769
|
+
updated_at: entry.updated_at
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const riskBlocked = inspected
|
|
774
|
+
.filter((item) => riskRank(item.risk) >= riskRank(minRisk))
|
|
775
|
+
.map((item) => ({
|
|
776
|
+
...item,
|
|
777
|
+
block_reasons: ['risk_threshold']
|
|
778
|
+
}));
|
|
779
|
+
|
|
780
|
+
const blockedById = new Map();
|
|
781
|
+
for (const item of riskBlocked) {
|
|
782
|
+
blockedById.set(item.id, {
|
|
783
|
+
...item,
|
|
784
|
+
policy_violations: []
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
for (const item of mitigationBlocked) {
|
|
788
|
+
const existing = blockedById.get(item.id);
|
|
789
|
+
if (!existing) {
|
|
790
|
+
blockedById.set(item.id, {
|
|
791
|
+
...item,
|
|
792
|
+
policy_violations: Array.isArray(item.policy_violations) ? item.policy_violations : []
|
|
793
|
+
});
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
existing.block_reasons = normalizeStringList(existing.block_reasons, item.block_reasons);
|
|
797
|
+
existing.policy_violations = normalizeStringList(existing.policy_violations, item.policy_violations);
|
|
798
|
+
if (!existing.temporary_mitigation && item.temporary_mitigation) {
|
|
799
|
+
existing.temporary_mitigation = item.temporary_mitigation;
|
|
800
|
+
}
|
|
801
|
+
blockedById.set(existing.id, existing);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const blocked = Array.from(blockedById.values())
|
|
805
|
+
.sort((left, right) => {
|
|
806
|
+
const mitigationDiff = Number((right.policy_violations || []).length > 0) - Number((left.policy_violations || []).length > 0);
|
|
807
|
+
if (mitigationDiff !== 0) {
|
|
808
|
+
return mitigationDiff;
|
|
809
|
+
}
|
|
810
|
+
const riskDiff = riskRank(right.risk) - riskRank(left.risk);
|
|
811
|
+
if (riskDiff !== 0) {
|
|
812
|
+
return riskDiff;
|
|
813
|
+
}
|
|
814
|
+
const qualityDiff = Number(right.quality_score || 0) - Number(left.quality_score || 0);
|
|
815
|
+
if (qualityDiff !== 0) {
|
|
816
|
+
return qualityDiff;
|
|
817
|
+
}
|
|
818
|
+
return `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
mode: 'errorbook-release-gate',
|
|
823
|
+
gate: {
|
|
824
|
+
min_risk: minRisk,
|
|
825
|
+
include_verified: includeVerified,
|
|
826
|
+
mitigation_policy_enforced: true
|
|
827
|
+
},
|
|
828
|
+
passed: blocked.length === 0,
|
|
829
|
+
inspected_count: inspected.length,
|
|
830
|
+
risk_blocked_count: riskBlocked.length,
|
|
831
|
+
mitigation_inspected_count: mitigationInspected.length,
|
|
832
|
+
mitigation_blocked_count: mitigationBlocked.length,
|
|
833
|
+
blocked_count: blocked.length,
|
|
834
|
+
blocked_entries: blocked
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
461
838
|
function validatePromoteCandidate(entry, minQuality = DEFAULT_PROMOTE_MIN_QUALITY) {
|
|
462
839
|
const missing = [];
|
|
463
840
|
if (!normalizeText(entry.root_cause)) {
|
|
@@ -506,6 +883,15 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
506
883
|
entry = mergeEntry(existingEntry, normalized);
|
|
507
884
|
deduplicated = true;
|
|
508
885
|
} else {
|
|
886
|
+
const temporaryMitigation = normalizeExistingTemporaryMitigation(normalized.temporary_mitigation);
|
|
887
|
+
const mitigationPayload = temporaryMitigation.enabled
|
|
888
|
+
? {
|
|
889
|
+
...temporaryMitigation,
|
|
890
|
+
created_at: nowIso(),
|
|
891
|
+
updated_at: nowIso(),
|
|
892
|
+
resolved_at: ''
|
|
893
|
+
}
|
|
894
|
+
: { enabled: false };
|
|
509
895
|
entry = {
|
|
510
896
|
api_version: ERRORBOOK_ENTRY_API_VERSION,
|
|
511
897
|
id: createEntryId(),
|
|
@@ -521,6 +907,7 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
521
907
|
ontology_tags: normalized.ontology_tags,
|
|
522
908
|
status: normalized.status,
|
|
523
909
|
source: normalized.source,
|
|
910
|
+
temporary_mitigation: mitigationPayload,
|
|
524
911
|
notes: normalized.notes || '',
|
|
525
912
|
occurrences: 1
|
|
526
913
|
};
|
|
@@ -643,6 +1030,7 @@ async function runErrorbookShowCommand(options = {}, dependencies = {}) {
|
|
|
643
1030
|
if (options.json) {
|
|
644
1031
|
console.log(JSON.stringify(result, null, 2));
|
|
645
1032
|
} else if (!options.silent) {
|
|
1033
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
646
1034
|
console.log(chalk.cyan.bold(entry.title));
|
|
647
1035
|
console.log(chalk.gray(`id: ${entry.id}`));
|
|
648
1036
|
console.log(chalk.gray(`status: ${entry.status}`));
|
|
@@ -653,6 +1041,16 @@ async function runErrorbookShowCommand(options = {}, dependencies = {}) {
|
|
|
653
1041
|
console.log(chalk.gray(`fix_actions: ${entry.fix_actions.join(' | ')}`));
|
|
654
1042
|
console.log(chalk.gray(`verification: ${entry.verification_evidence.join(' | ') || '(none)'}`));
|
|
655
1043
|
console.log(chalk.gray(`ontology: ${entry.ontology_tags.join(', ') || '(none)'}`));
|
|
1044
|
+
if (mitigation.enabled) {
|
|
1045
|
+
const active = !normalizeText(mitigation.resolved_at);
|
|
1046
|
+
console.log(chalk.gray(`temporary_mitigation: ${active ? 'active' : 'resolved'}`));
|
|
1047
|
+
console.log(chalk.gray(` exit: ${mitigation.exit_criteria || '(none)'}`));
|
|
1048
|
+
console.log(chalk.gray(` cleanup: ${mitigation.cleanup_task || '(none)'}`));
|
|
1049
|
+
console.log(chalk.gray(` deadline: ${mitigation.deadline_at || '(none)'}`));
|
|
1050
|
+
if (mitigation.resolved_at) {
|
|
1051
|
+
console.log(chalk.gray(` resolved_at: ${mitigation.resolved_at}`));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
656
1054
|
}
|
|
657
1055
|
|
|
658
1056
|
return result;
|
|
@@ -758,6 +1156,7 @@ async function runErrorbookPromoteCommand(options = {}, dependencies = {}) {
|
|
|
758
1156
|
|
|
759
1157
|
entry.status = 'promoted';
|
|
760
1158
|
entry.promoted_at = nowIso();
|
|
1159
|
+
entry.temporary_mitigation = markTemporaryMitigationResolved(entry, entry.promoted_at);
|
|
761
1160
|
entry.updated_at = entry.promoted_at;
|
|
762
1161
|
await writeErrorbookEntry(paths, entry, fileSystem);
|
|
763
1162
|
|
|
@@ -789,6 +1188,33 @@ async function runErrorbookPromoteCommand(options = {}, dependencies = {}) {
|
|
|
789
1188
|
return result;
|
|
790
1189
|
}
|
|
791
1190
|
|
|
1191
|
+
async function runErrorbookReleaseGateCommand(options = {}, dependencies = {}) {
|
|
1192
|
+
const payload = await evaluateErrorbookReleaseGate(options, dependencies);
|
|
1193
|
+
|
|
1194
|
+
if (options.json) {
|
|
1195
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1196
|
+
} else if (!options.silent) {
|
|
1197
|
+
if (payload.passed) {
|
|
1198
|
+
console.log(chalk.green('✓ Errorbook release gate passed'));
|
|
1199
|
+
console.log(chalk.gray(` inspected: ${payload.inspected_count}`));
|
|
1200
|
+
return payload;
|
|
1201
|
+
}
|
|
1202
|
+
console.log(chalk.red('✗ Errorbook release gate blocked'));
|
|
1203
|
+
console.log(chalk.gray(` blocked: ${payload.blocked_count}`));
|
|
1204
|
+
payload.blocked_entries.slice(0, 10).forEach((item) => {
|
|
1205
|
+
console.log(chalk.gray(` - ${item.id} [${item.risk}] ${item.title}`));
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (options.failOnBlock && !payload.passed) {
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
`errorbook release gate blocked: ${payload.blocked_count} unresolved entries (min-risk=${payload.gate.min_risk})`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return payload;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
792
1218
|
async function runErrorbookDeprecateCommand(options = {}, dependencies = {}) {
|
|
793
1219
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
794
1220
|
const fileSystem = dependencies.fileSystem || fs;
|
|
@@ -822,6 +1248,7 @@ async function runErrorbookDeprecateCommand(options = {}, dependencies = {}) {
|
|
|
822
1248
|
|
|
823
1249
|
entry.status = 'deprecated';
|
|
824
1250
|
entry.updated_at = nowIso();
|
|
1251
|
+
entry.temporary_mitigation = markTemporaryMitigationResolved(entry, entry.updated_at);
|
|
825
1252
|
entry.deprecated_at = entry.updated_at;
|
|
826
1253
|
entry.deprecation = {
|
|
827
1254
|
reason,
|
|
@@ -960,6 +1387,11 @@ function registerErrorbookCommands(program) {
|
|
|
960
1387
|
.option('--tags <csv>', 'Tags, comma-separated')
|
|
961
1388
|
.option('--ontology <csv>', `Ontology focus tags (${ERRORBOOK_ONTOLOGY_TAGS.join(', ')})`)
|
|
962
1389
|
.option('--status <status>', 'candidate|verified', 'candidate')
|
|
1390
|
+
.option('--temporary-mitigation', 'Mark this entry as temporary fallback/mitigation (requires governance fields)')
|
|
1391
|
+
.option('--mitigation-reason <text>', 'Temporary mitigation reason/context')
|
|
1392
|
+
.option('--mitigation-exit <text>', 'Exit criteria that define mitigation cleanup completion')
|
|
1393
|
+
.option('--mitigation-cleanup <text>', 'Cleanup task/spec to remove temporary mitigation')
|
|
1394
|
+
.option('--mitigation-deadline <iso>', 'Deadline for mitigation cleanup (ISO datetime)')
|
|
963
1395
|
.option('--fingerprint <text>', 'Custom deduplication fingerprint')
|
|
964
1396
|
.option('--from <path>', 'Load payload from JSON file')
|
|
965
1397
|
.option('--spec <spec>', 'Related spec id/name')
|
|
@@ -1028,6 +1460,21 @@ function registerErrorbookCommands(program) {
|
|
|
1028
1460
|
}
|
|
1029
1461
|
});
|
|
1030
1462
|
|
|
1463
|
+
errorbook
|
|
1464
|
+
.command('release-gate')
|
|
1465
|
+
.description('Block release on unresolved high-risk entries and temporary-mitigation policy violations')
|
|
1466
|
+
.option('--min-risk <level>', 'Risk threshold (low|medium|high)', 'high')
|
|
1467
|
+
.option('--include-verified', 'Also inspect verified (non-promoted) entries')
|
|
1468
|
+
.option('--fail-on-block', 'Exit with error when gate is blocked')
|
|
1469
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1470
|
+
.action(async (options) => {
|
|
1471
|
+
try {
|
|
1472
|
+
await runErrorbookReleaseGateCommand(options);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
emitCommandError(error, options.json);
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1031
1478
|
errorbook
|
|
1032
1479
|
.command('deprecate <id>')
|
|
1033
1480
|
.description('Deprecate low-value or obsolete entry')
|
|
@@ -1059,16 +1506,22 @@ function registerErrorbookCommands(program) {
|
|
|
1059
1506
|
module.exports = {
|
|
1060
1507
|
ERRORBOOK_STATUSES,
|
|
1061
1508
|
ERRORBOOK_ONTOLOGY_TAGS,
|
|
1509
|
+
ERRORBOOK_RISK_LEVELS,
|
|
1510
|
+
TEMPORARY_MITIGATION_TAG,
|
|
1511
|
+
HIGH_RISK_SIGNAL_TAGS,
|
|
1062
1512
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
1063
1513
|
resolveErrorbookPaths,
|
|
1064
1514
|
normalizeOntologyTags,
|
|
1065
1515
|
normalizeRecordPayload,
|
|
1066
1516
|
scoreQuality,
|
|
1517
|
+
evaluateEntryRisk,
|
|
1518
|
+
evaluateErrorbookReleaseGate,
|
|
1067
1519
|
runErrorbookRecordCommand,
|
|
1068
1520
|
runErrorbookListCommand,
|
|
1069
1521
|
runErrorbookShowCommand,
|
|
1070
1522
|
runErrorbookFindCommand,
|
|
1071
1523
|
runErrorbookPromoteCommand,
|
|
1524
|
+
runErrorbookReleaseGateCommand,
|
|
1072
1525
|
runErrorbookDeprecateCommand,
|
|
1073
1526
|
runErrorbookRequalifyCommand,
|
|
1074
1527
|
registerErrorbookCommands
|