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.
@@ -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(fromFilePayload.tags, options.tags),
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: normalizeStringList(existingEntry.tags, incomingPayload.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