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(fromFilePayload.tags, options.tags),
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: normalizeStringList(existingEntry.tags, incomingPayload.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 blocked = inspected
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 candidate entries')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.3.21",
3
+ "version": "3.3.22",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
- v17.0 | 2026-02-26 | 新增 Scene 主会话强制治理原则
194
+ v18.0 | 2026-02-27 | 强化根因修复与临时兜底发布门禁原则