scene-capability-engine 3.3.21 → 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
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.3.22] - 2026-02-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Errorbook now supports governed temporary mitigation records (stop-bleeding only):
|
|
14
|
+
- `--temporary-mitigation`
|
|
15
|
+
- `--mitigation-reason`
|
|
16
|
+
- `--mitigation-exit`
|
|
17
|
+
- `--mitigation-cleanup`
|
|
18
|
+
- `--mitigation-deadline`
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `errorbook release-gate` now blocks release on temporary mitigation policy violations in addition to risk threshold violations:
|
|
22
|
+
- missing exit criteria / cleanup task / deadline
|
|
23
|
+
- expired mitigation deadline
|
|
24
|
+
- Steering baseline strengthened with explicit anti-workaround rules:
|
|
25
|
+
- core-path fail-fast (no silent swallow-and-continue)
|
|
26
|
+
- temporary fallback must be governed and time-bounded
|
|
27
|
+
- release must be blocked until fallback cleanup is completed
|
|
28
|
+
- Command reference and release checklists updated (EN/ZH) for temporary mitigation governance.
|
|
29
|
+
|
|
10
30
|
## [3.3.21] - 2026-02-27
|
|
11
31
|
|
|
12
32
|
### Fixed
|
|
@@ -361,6 +361,19 @@ sce errorbook deprecate <entry-id> --reason "superseded by v2 policy" --json
|
|
|
361
361
|
# Requalify deprecated entry after remediation review
|
|
362
362
|
sce errorbook requalify <entry-id> --status verified --json
|
|
363
363
|
|
|
364
|
+
# Record controlled temporary mitigation (stop-bleeding only, must include governance fields)
|
|
365
|
+
sce errorbook record \
|
|
366
|
+
--title "Temporary fallback for order approval lock contention" \
|
|
367
|
+
--symptom "Fallback path enabled to keep approval flow available" \
|
|
368
|
+
--root-cause "Primary lock ordering fix is in progress" \
|
|
369
|
+
--fix-action "Ship lock ordering fix and remove fallback path" \
|
|
370
|
+
--temporary-mitigation \
|
|
371
|
+
--mitigation-reason "Emergency stop-bleeding in production" \
|
|
372
|
+
--mitigation-exit "Primary path concurrency tests are green" \
|
|
373
|
+
--mitigation-cleanup "spec/remove-order-approval-fallback" \
|
|
374
|
+
--mitigation-deadline 2026-03-15T00:00:00Z \
|
|
375
|
+
--json
|
|
376
|
+
|
|
364
377
|
# Release hard gate (default in prepublish and studio release preflight)
|
|
365
378
|
sce errorbook release-gate --min-risk high --fail-on-block --json
|
|
366
379
|
|
|
@@ -380,6 +393,13 @@ Curated quality policy (`宁缺毋滥,优胜略汰`) defaults:
|
|
|
380
393
|
- `deprecate` requires explicit `--reason` to preserve elimination traceability.
|
|
381
394
|
- `requalify` only accepts `candidate|verified`; `promoted` must still go through `promote` gate.
|
|
382
395
|
- `release-gate` blocks release when unresolved high-risk `candidate` entries remain.
|
|
396
|
+
- Temporary mitigation is allowed only as stop-bleeding and must include:
|
|
397
|
+
- `mitigation_exit` (exit criteria)
|
|
398
|
+
- `mitigation_cleanup` (cleanup task/spec)
|
|
399
|
+
- `mitigation_deadline` (deadline)
|
|
400
|
+
- `release-gate` also blocks when temporary mitigation policy is violated:
|
|
401
|
+
- missing exit/cleanup/deadline metadata
|
|
402
|
+
- expired mitigation deadline
|
|
383
403
|
- `git-managed-gate` blocks release when:
|
|
384
404
|
- worktree has uncommitted changes
|
|
385
405
|
- branch has no upstream
|
|
@@ -107,6 +107,7 @@ Verify:
|
|
|
107
107
|
- If GitHub/GitLab remote exists, current branch is upstream-tracked and fully synced (ahead=0, behind=0).
|
|
108
108
|
- If customer has no GitHub/GitLab, gate can be bypassed by policy (`SCE_GIT_MANAGEMENT_ALLOW_NO_REMOTE=1`, default).
|
|
109
109
|
- In CI/tag detached-HEAD contexts, branch/upstream sync checks are relaxed by default; enforce strict mode with `SCE_GIT_MANAGEMENT_STRICT_CI=1` when needed.
|
|
110
|
+
- Errorbook release gate also enforces temporary mitigation governance: active fallback entries must include cleanup task + exit criteria + deadline, and must not be expired.
|
|
110
111
|
|
|
111
112
|
---
|
|
112
113
|
|
|
@@ -92,6 +92,7 @@ node scripts/git-managed-gate.js --fail-on-violation --json
|
|
|
92
92
|
- 若配置了 GitHub/GitLab 远端:当前分支必须已设置 upstream 且与远端完全同步(ahead=0, behind=0)。
|
|
93
93
|
- 若客户确实没有 GitHub/GitLab:可通过策略放行(`SCE_GIT_MANAGEMENT_ALLOW_NO_REMOTE=1`,默认开启)。
|
|
94
94
|
- 在 CI/tag 的 detached HEAD 场景下,默认放宽分支/upstream 同步检查;如需强制严格校验,设置 `SCE_GIT_MANAGEMENT_STRICT_CI=1`。
|
|
95
|
+
- Errorbook release gate 同时强制临时兜底治理:活动中的兜底记录必须包含退出条件、清理任务和截止时间,且不得过期。
|
|
95
96
|
|
|
96
97
|
---
|
|
97
98
|
|
|
@@ -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,
|
|
@@ -71,6 +72,23 @@ function normalizeText(value) {
|
|
|
71
72
|
return value.trim();
|
|
72
73
|
}
|
|
73
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
|
+
|
|
74
92
|
function normalizeCsv(value) {
|
|
75
93
|
if (Array.isArray(value)) {
|
|
76
94
|
return value;
|
|
@@ -109,6 +127,18 @@ function normalizeStringList(...rawInputs) {
|
|
|
109
127
|
return Array.from(new Set(merged));
|
|
110
128
|
}
|
|
111
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
|
+
|
|
112
142
|
function normalizeOntologyTags(...rawInputs) {
|
|
113
143
|
const normalized = normalizeStringList(...rawInputs).map((item) => item.toLowerCase());
|
|
114
144
|
const mapped = normalized.map((item) => ONTOLOGY_TAG_ALIASES[item] || item);
|
|
@@ -116,6 +146,140 @@ function normalizeOntologyTags(...rawInputs) {
|
|
|
116
146
|
return Array.from(new Set(valid));
|
|
117
147
|
}
|
|
118
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
|
+
|
|
119
283
|
function normalizeStatus(input, fallback = 'candidate') {
|
|
120
284
|
const normalized = normalizeText(`${input || ''}`).toLowerCase();
|
|
121
285
|
if (!normalized) {
|
|
@@ -273,9 +437,22 @@ function validateRecordPayload(payload) {
|
|
|
273
437
|
if (status === 'verified' && (!Array.isArray(payload.verification_evidence) || payload.verification_evidence.length === 0)) {
|
|
274
438
|
throw new Error('status=verified requires at least one --verification evidence');
|
|
275
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
|
+
}
|
|
276
452
|
}
|
|
277
453
|
|
|
278
454
|
function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
455
|
+
const temporaryMitigation = normalizeTemporaryMitigation(options, fromFilePayload);
|
|
279
456
|
const payload = {
|
|
280
457
|
title: normalizeText(options.title || fromFilePayload.title),
|
|
281
458
|
symptom: normalizeText(options.symptom || fromFilePayload.symptom),
|
|
@@ -287,7 +464,11 @@ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
|
287
464
|
options.verification,
|
|
288
465
|
options.verificationEvidence
|
|
289
466
|
),
|
|
290
|
-
tags: normalizeStringList(
|
|
467
|
+
tags: normalizeStringList(
|
|
468
|
+
fromFilePayload.tags,
|
|
469
|
+
options.tags,
|
|
470
|
+
temporaryMitigation.enabled ? TEMPORARY_MITIGATION_TAG : []
|
|
471
|
+
),
|
|
291
472
|
ontology_tags: normalizeOntologyTags(fromFilePayload.ontology_tags, fromFilePayload.ontology, options.ontology),
|
|
292
473
|
status: normalizeStatus(options.status || fromFilePayload.status || 'candidate'),
|
|
293
474
|
source: {
|
|
@@ -295,6 +476,7 @@ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
|
295
476
|
files: normalizeStringList(fromFilePayload?.source?.files, options.files),
|
|
296
477
|
tests: normalizeStringList(fromFilePayload?.source?.tests, options.tests)
|
|
297
478
|
},
|
|
479
|
+
temporary_mitigation: temporaryMitigation,
|
|
298
480
|
notes: normalizeText(options.notes || fromFilePayload.notes),
|
|
299
481
|
fingerprint: createFingerprint({
|
|
300
482
|
fingerprint: options.fingerprint || fromFilePayload.fingerprint,
|
|
@@ -312,6 +494,8 @@ function createEntryId() {
|
|
|
312
494
|
}
|
|
313
495
|
|
|
314
496
|
function buildIndexSummary(entry) {
|
|
497
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
498
|
+
const mitigationActive = mitigation.enabled && !normalizeText(mitigation.resolved_at);
|
|
315
499
|
return {
|
|
316
500
|
id: entry.id,
|
|
317
501
|
fingerprint: entry.fingerprint,
|
|
@@ -320,6 +504,8 @@ function buildIndexSummary(entry) {
|
|
|
320
504
|
quality_score: entry.quality_score,
|
|
321
505
|
tags: entry.tags,
|
|
322
506
|
ontology_tags: entry.ontology_tags,
|
|
507
|
+
temporary_mitigation_active: mitigationActive,
|
|
508
|
+
temporary_mitigation_deadline_at: mitigationActive ? normalizeText(mitigation.deadline_at) : '',
|
|
323
509
|
occurrences: entry.occurrences || 1,
|
|
324
510
|
created_at: entry.created_at,
|
|
325
511
|
updated_at: entry.updated_at
|
|
@@ -348,6 +534,12 @@ function findSummaryById(index, id) {
|
|
|
348
534
|
}
|
|
349
535
|
|
|
350
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
|
+
);
|
|
351
543
|
const merged = {
|
|
352
544
|
...existingEntry,
|
|
353
545
|
title: normalizeText(incomingPayload.title) || existingEntry.title,
|
|
@@ -355,7 +547,7 @@ function mergeEntry(existingEntry, incomingPayload) {
|
|
|
355
547
|
root_cause: normalizeText(incomingPayload.root_cause) || existingEntry.root_cause,
|
|
356
548
|
fix_actions: normalizeStringList(existingEntry.fix_actions, incomingPayload.fix_actions),
|
|
357
549
|
verification_evidence: normalizeStringList(existingEntry.verification_evidence, incomingPayload.verification_evidence),
|
|
358
|
-
tags:
|
|
550
|
+
tags: mergedTags,
|
|
359
551
|
ontology_tags: normalizeOntologyTags(existingEntry.ontology_tags, incomingPayload.ontology_tags),
|
|
360
552
|
status: selectStatus(existingEntry.status, incomingPayload.status),
|
|
361
553
|
notes: normalizeText(incomingPayload.notes) || existingEntry.notes || '',
|
|
@@ -364,6 +556,7 @@ function mergeEntry(existingEntry, incomingPayload) {
|
|
|
364
556
|
files: normalizeStringList(existingEntry?.source?.files, incomingPayload?.source?.files),
|
|
365
557
|
tests: normalizeStringList(existingEntry?.source?.tests, incomingPayload?.source?.tests)
|
|
366
558
|
},
|
|
559
|
+
temporary_mitigation: temporaryMitigation,
|
|
367
560
|
occurrences: Number(existingEntry.occurrences || 1) + 1,
|
|
368
561
|
updated_at: nowIso()
|
|
369
562
|
};
|
|
@@ -529,6 +722,8 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
529
722
|
const includeVerified = options.includeVerified === true;
|
|
530
723
|
|
|
531
724
|
const inspected = [];
|
|
725
|
+
const mitigationInspected = [];
|
|
726
|
+
const mitigationBlocked = [];
|
|
532
727
|
for (const summary of index.entries) {
|
|
533
728
|
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
534
729
|
if (!entry) {
|
|
@@ -536,12 +731,34 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
536
731
|
}
|
|
537
732
|
|
|
538
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
|
+
|
|
539
757
|
const unresolved = status === 'candidate' || (includeVerified && status === 'verified');
|
|
540
758
|
if (!unresolved) {
|
|
541
759
|
continue;
|
|
542
760
|
}
|
|
543
761
|
|
|
544
|
-
const risk = evaluateEntryRisk(entry);
|
|
545
762
|
inspected.push({
|
|
546
763
|
id: entry.id,
|
|
547
764
|
title: entry.title,
|
|
@@ -553,9 +770,43 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
553
770
|
});
|
|
554
771
|
}
|
|
555
772
|
|
|
556
|
-
const
|
|
773
|
+
const riskBlocked = inspected
|
|
557
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())
|
|
558
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
|
+
}
|
|
559
810
|
const riskDiff = riskRank(right.risk) - riskRank(left.risk);
|
|
560
811
|
if (riskDiff !== 0) {
|
|
561
812
|
return riskDiff;
|
|
@@ -571,10 +822,14 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
571
822
|
mode: 'errorbook-release-gate',
|
|
572
823
|
gate: {
|
|
573
824
|
min_risk: minRisk,
|
|
574
|
-
include_verified: includeVerified
|
|
825
|
+
include_verified: includeVerified,
|
|
826
|
+
mitigation_policy_enforced: true
|
|
575
827
|
},
|
|
576
828
|
passed: blocked.length === 0,
|
|
577
829
|
inspected_count: inspected.length,
|
|
830
|
+
risk_blocked_count: riskBlocked.length,
|
|
831
|
+
mitigation_inspected_count: mitigationInspected.length,
|
|
832
|
+
mitigation_blocked_count: mitigationBlocked.length,
|
|
578
833
|
blocked_count: blocked.length,
|
|
579
834
|
blocked_entries: blocked
|
|
580
835
|
};
|
|
@@ -628,6 +883,15 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
628
883
|
entry = mergeEntry(existingEntry, normalized);
|
|
629
884
|
deduplicated = true;
|
|
630
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 };
|
|
631
895
|
entry = {
|
|
632
896
|
api_version: ERRORBOOK_ENTRY_API_VERSION,
|
|
633
897
|
id: createEntryId(),
|
|
@@ -643,6 +907,7 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
643
907
|
ontology_tags: normalized.ontology_tags,
|
|
644
908
|
status: normalized.status,
|
|
645
909
|
source: normalized.source,
|
|
910
|
+
temporary_mitigation: mitigationPayload,
|
|
646
911
|
notes: normalized.notes || '',
|
|
647
912
|
occurrences: 1
|
|
648
913
|
};
|
|
@@ -765,6 +1030,7 @@ async function runErrorbookShowCommand(options = {}, dependencies = {}) {
|
|
|
765
1030
|
if (options.json) {
|
|
766
1031
|
console.log(JSON.stringify(result, null, 2));
|
|
767
1032
|
} else if (!options.silent) {
|
|
1033
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
768
1034
|
console.log(chalk.cyan.bold(entry.title));
|
|
769
1035
|
console.log(chalk.gray(`id: ${entry.id}`));
|
|
770
1036
|
console.log(chalk.gray(`status: ${entry.status}`));
|
|
@@ -775,6 +1041,16 @@ async function runErrorbookShowCommand(options = {}, dependencies = {}) {
|
|
|
775
1041
|
console.log(chalk.gray(`fix_actions: ${entry.fix_actions.join(' | ')}`));
|
|
776
1042
|
console.log(chalk.gray(`verification: ${entry.verification_evidence.join(' | ') || '(none)'}`));
|
|
777
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
|
+
}
|
|
778
1054
|
}
|
|
779
1055
|
|
|
780
1056
|
return result;
|
|
@@ -880,6 +1156,7 @@ async function runErrorbookPromoteCommand(options = {}, dependencies = {}) {
|
|
|
880
1156
|
|
|
881
1157
|
entry.status = 'promoted';
|
|
882
1158
|
entry.promoted_at = nowIso();
|
|
1159
|
+
entry.temporary_mitigation = markTemporaryMitigationResolved(entry, entry.promoted_at);
|
|
883
1160
|
entry.updated_at = entry.promoted_at;
|
|
884
1161
|
await writeErrorbookEntry(paths, entry, fileSystem);
|
|
885
1162
|
|
|
@@ -971,6 +1248,7 @@ async function runErrorbookDeprecateCommand(options = {}, dependencies = {}) {
|
|
|
971
1248
|
|
|
972
1249
|
entry.status = 'deprecated';
|
|
973
1250
|
entry.updated_at = nowIso();
|
|
1251
|
+
entry.temporary_mitigation = markTemporaryMitigationResolved(entry, entry.updated_at);
|
|
974
1252
|
entry.deprecated_at = entry.updated_at;
|
|
975
1253
|
entry.deprecation = {
|
|
976
1254
|
reason,
|
|
@@ -1109,6 +1387,11 @@ function registerErrorbookCommands(program) {
|
|
|
1109
1387
|
.option('--tags <csv>', 'Tags, comma-separated')
|
|
1110
1388
|
.option('--ontology <csv>', `Ontology focus tags (${ERRORBOOK_ONTOLOGY_TAGS.join(', ')})`)
|
|
1111
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)')
|
|
1112
1395
|
.option('--fingerprint <text>', 'Custom deduplication fingerprint')
|
|
1113
1396
|
.option('--from <path>', 'Load payload from JSON file')
|
|
1114
1397
|
.option('--spec <spec>', 'Related spec id/name')
|
|
@@ -1179,7 +1462,7 @@ function registerErrorbookCommands(program) {
|
|
|
1179
1462
|
|
|
1180
1463
|
errorbook
|
|
1181
1464
|
.command('release-gate')
|
|
1182
|
-
.description('Block release on unresolved high-risk
|
|
1465
|
+
.description('Block release on unresolved high-risk entries and temporary-mitigation policy violations')
|
|
1183
1466
|
.option('--min-risk <level>', 'Risk threshold (low|medium|high)', 'high')
|
|
1184
1467
|
.option('--include-verified', 'Also inspect verified (non-promoted) entries')
|
|
1185
1468
|
.option('--fail-on-block', 'Exit with error when gate is blocked')
|
|
@@ -1224,6 +1507,7 @@ module.exports = {
|
|
|
1224
1507
|
ERRORBOOK_STATUSES,
|
|
1225
1508
|
ERRORBOOK_ONTOLOGY_TAGS,
|
|
1226
1509
|
ERRORBOOK_RISK_LEVELS,
|
|
1510
|
+
TEMPORARY_MITIGATION_TAG,
|
|
1227
1511
|
HIGH_RISK_SIGNAL_TAGS,
|
|
1228
1512
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
1229
1513
|
resolveErrorbookPaths,
|
package/package.json
CHANGED
|
@@ -147,6 +147,15 @@
|
|
|
147
147
|
|
|
148
148
|
**硬规则**: ❌ 禁止通过关闭校验、跳过测试、降级关键路径、屏蔽异常等方式“假修复”
|
|
149
149
|
|
|
150
|
+
**失败显式原则**: 核心路径禁止吞错继续。无法确认安全修复时,必须 `fail-fast + 明确告警`,不得用静默回退伪装成功
|
|
151
|
+
|
|
152
|
+
**兜底治理原则**: 允许临时兜底仅用于止血,不得替代根因修复;临时兜底必须结构化记录:
|
|
153
|
+
1) 退出条件(exit criteria)
|
|
154
|
+
2) 清理任务(cleanup task/spec)
|
|
155
|
+
3) 截止时间(deadline)
|
|
156
|
+
|
|
157
|
+
**发布门禁**: 若存在临时兜底但缺少上述治理信息,或已超过截止时间未清理,默认阻断发布(`errorbook release-gate`)
|
|
158
|
+
|
|
150
159
|
**复杂问题定位方法**: 优先使用 debug 日志与可观测信号定位(输入、输出、关键分支、异常栈、上下文参数),先还原执行路径再下结论
|
|
151
160
|
|
|
152
161
|
**修复后清理要求**: 问题修复并验证通过后,必须清理临时 debug 日志、临时埋点、一次性脚本和调试开关;需要长期保留的日志必须转为可配置观测项且默认关闭
|
|
@@ -182,4 +191,4 @@
|
|
|
182
191
|
|
|
183
192
|
---
|
|
184
193
|
|
|
185
|
-
|
|
194
|
+
v18.0 | 2026-02-27 | 强化根因修复与临时兜底发布门禁原则
|