scene-capability-engine 3.3.17 → 3.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1244 @@
1
+ const crypto = require('crypto');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const chalk = require('chalk');
5
+ const Table = require('cli-table3');
6
+
7
+ const ERRORBOOK_INDEX_API_VERSION = 'sce.errorbook.index/v0.1';
8
+ const ERRORBOOK_ENTRY_API_VERSION = 'sce.errorbook.entry/v0.1';
9
+ const ERRORBOOK_STATUSES = Object.freeze(['candidate', 'verified', 'promoted', 'deprecated']);
10
+ const STATUS_RANK = Object.freeze({
11
+ deprecated: 0,
12
+ candidate: 1,
13
+ verified: 2,
14
+ promoted: 3
15
+ });
16
+ const ERRORBOOK_ONTOLOGY_TAGS = Object.freeze([
17
+ 'entity',
18
+ 'relation',
19
+ 'business_rule',
20
+ 'decision_policy',
21
+ 'execution_flow'
22
+ ]);
23
+ const ONTOLOGY_TAG_ALIASES = Object.freeze({
24
+ entities: 'entity',
25
+ relations: 'relation',
26
+ rule: 'business_rule',
27
+ rules: 'business_rule',
28
+ business_rules: 'business_rule',
29
+ decision: 'decision_policy',
30
+ decisions: 'decision_policy',
31
+ policy: 'decision_policy',
32
+ policies: 'decision_policy',
33
+ execution: 'execution_flow',
34
+ flow: 'execution_flow',
35
+ workflow: 'execution_flow',
36
+ workflows: 'execution_flow',
37
+ action_chain: 'execution_flow'
38
+ });
39
+ const DEFAULT_PROMOTE_MIN_QUALITY = 75;
40
+ const ERRORBOOK_RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);
41
+ const HIGH_RISK_SIGNAL_TAGS = Object.freeze([
42
+ 'release-blocker',
43
+ 'security',
44
+ 'auth',
45
+ 'payment',
46
+ 'data-loss',
47
+ 'integrity',
48
+ 'compliance',
49
+ 'incident'
50
+ ]);
51
+
52
+ function resolveErrorbookPaths(projectPath = process.cwd()) {
53
+ const baseDir = path.join(projectPath, '.sce', 'errorbook');
54
+ return {
55
+ projectPath,
56
+ baseDir,
57
+ entriesDir: path.join(baseDir, 'entries'),
58
+ indexFile: path.join(baseDir, 'index.json')
59
+ };
60
+ }
61
+
62
+ function nowIso() {
63
+ return new Date().toISOString();
64
+ }
65
+
66
+ function normalizeText(value) {
67
+ if (typeof value !== 'string') {
68
+ return '';
69
+ }
70
+
71
+ return value.trim();
72
+ }
73
+
74
+ function normalizeCsv(value) {
75
+ if (Array.isArray(value)) {
76
+ return value;
77
+ }
78
+
79
+ if (typeof value !== 'string' || !value.trim()) {
80
+ return [];
81
+ }
82
+
83
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
84
+ }
85
+
86
+ function normalizeStringList(...rawInputs) {
87
+ const merged = [];
88
+ for (const raw of rawInputs) {
89
+ if (Array.isArray(raw)) {
90
+ for (const item of raw) {
91
+ const normalized = normalizeText(`${item}`);
92
+ if (normalized) {
93
+ merged.push(normalized);
94
+ }
95
+ }
96
+ continue;
97
+ }
98
+
99
+ if (typeof raw === 'string') {
100
+ for (const item of normalizeCsv(raw)) {
101
+ const normalized = normalizeText(item);
102
+ if (normalized) {
103
+ merged.push(normalized);
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ return Array.from(new Set(merged));
110
+ }
111
+
112
+ function normalizeOntologyTags(...rawInputs) {
113
+ const normalized = normalizeStringList(...rawInputs).map((item) => item.toLowerCase());
114
+ const mapped = normalized.map((item) => ONTOLOGY_TAG_ALIASES[item] || item);
115
+ const valid = mapped.filter((item) => ERRORBOOK_ONTOLOGY_TAGS.includes(item));
116
+ return Array.from(new Set(valid));
117
+ }
118
+
119
+ function normalizeStatus(input, fallback = 'candidate') {
120
+ const normalized = normalizeText(`${input || ''}`).toLowerCase();
121
+ if (!normalized) {
122
+ return fallback;
123
+ }
124
+
125
+ if (!ERRORBOOK_STATUSES.includes(normalized)) {
126
+ throw new Error(`status must be one of: ${ERRORBOOK_STATUSES.join(', ')}`);
127
+ }
128
+
129
+ return normalized;
130
+ }
131
+
132
+ function selectStatus(...candidates) {
133
+ let selected = 'candidate';
134
+ for (const candidate of candidates) {
135
+ const status = normalizeStatus(candidate, 'candidate');
136
+ if ((STATUS_RANK[status] || 0) > (STATUS_RANK[selected] || 0)) {
137
+ selected = status;
138
+ }
139
+ }
140
+ return selected;
141
+ }
142
+
143
+ function createFingerprint(input = {}) {
144
+ const explicit = normalizeText(input.fingerprint);
145
+ if (explicit) {
146
+ return explicit;
147
+ }
148
+
149
+ const basis = [
150
+ normalizeText(input.title).toLowerCase(),
151
+ normalizeText(input.symptom).toLowerCase(),
152
+ normalizeText(input.root_cause || input.rootCause).toLowerCase()
153
+ ].join('|');
154
+
155
+ const digest = crypto.createHash('sha1').update(basis).digest('hex').slice(0, 16);
156
+ return `fp-${digest}`;
157
+ }
158
+
159
+ function buildDefaultIndex() {
160
+ return {
161
+ api_version: ERRORBOOK_INDEX_API_VERSION,
162
+ updated_at: nowIso(),
163
+ total_entries: 0,
164
+ entries: []
165
+ };
166
+ }
167
+
168
+ async function ensureErrorbookStorage(paths, fileSystem = fs) {
169
+ await fileSystem.ensureDir(paths.entriesDir);
170
+ if (!await fileSystem.pathExists(paths.indexFile)) {
171
+ await fileSystem.writeJson(paths.indexFile, buildDefaultIndex(), { spaces: 2 });
172
+ }
173
+ }
174
+
175
+ async function readErrorbookIndex(paths, fileSystem = fs) {
176
+ await ensureErrorbookStorage(paths, fileSystem);
177
+ const index = await fileSystem.readJson(paths.indexFile);
178
+ if (!index || typeof index !== 'object' || !Array.isArray(index.entries)) {
179
+ return buildDefaultIndex();
180
+ }
181
+
182
+ return {
183
+ api_version: index.api_version || ERRORBOOK_INDEX_API_VERSION,
184
+ updated_at: index.updated_at || nowIso(),
185
+ total_entries: Number.isInteger(index.total_entries) ? index.total_entries : index.entries.length,
186
+ entries: index.entries
187
+ };
188
+ }
189
+
190
+ async function writeErrorbookIndex(paths, index, fileSystem = fs) {
191
+ const payload = {
192
+ ...index,
193
+ api_version: ERRORBOOK_INDEX_API_VERSION,
194
+ updated_at: nowIso(),
195
+ total_entries: Array.isArray(index.entries) ? index.entries.length : 0
196
+ };
197
+ await fileSystem.ensureDir(path.dirname(paths.indexFile));
198
+ await fileSystem.writeJson(paths.indexFile, payload, { spaces: 2 });
199
+ return payload;
200
+ }
201
+
202
+ function buildEntryFilePath(paths, entryId) {
203
+ return path.join(paths.entriesDir, `${entryId}.json`);
204
+ }
205
+
206
+ async function readErrorbookEntry(paths, entryId, fileSystem = fs) {
207
+ const entryPath = buildEntryFilePath(paths, entryId);
208
+ if (!await fileSystem.pathExists(entryPath)) {
209
+ return null;
210
+ }
211
+ return fileSystem.readJson(entryPath);
212
+ }
213
+
214
+ async function writeErrorbookEntry(paths, entry, fileSystem = fs) {
215
+ const entryPath = buildEntryFilePath(paths, entry.id);
216
+ await fileSystem.ensureDir(path.dirname(entryPath));
217
+ await fileSystem.writeJson(entryPath, entry, { spaces: 2 });
218
+ return entryPath;
219
+ }
220
+
221
+ function scoreQuality(entry = {}) {
222
+ let score = 0;
223
+
224
+ if (normalizeText(entry.title)) {
225
+ score += 10;
226
+ }
227
+ if (normalizeText(entry.symptom)) {
228
+ score += 10;
229
+ }
230
+ if (normalizeText(entry.fingerprint)) {
231
+ score += 10;
232
+ }
233
+ if (normalizeText(entry.root_cause)) {
234
+ score += 20;
235
+ }
236
+ if (Array.isArray(entry.fix_actions) && entry.fix_actions.length > 0) {
237
+ score += 20;
238
+ }
239
+ if (Array.isArray(entry.verification_evidence) && entry.verification_evidence.length > 0) {
240
+ score += 20;
241
+ }
242
+ if (Array.isArray(entry.ontology_tags) && entry.ontology_tags.length > 0) {
243
+ score += 5;
244
+ }
245
+ if (Array.isArray(entry.tags) && entry.tags.length > 0) {
246
+ score += 3;
247
+ }
248
+ if (normalizeText(entry.symptom).length >= 24 && normalizeText(entry.root_cause).length >= 24) {
249
+ score += 2;
250
+ }
251
+
252
+ return Math.max(0, Math.min(100, score));
253
+ }
254
+
255
+ function validateRecordPayload(payload) {
256
+ if (!normalizeText(payload.title)) {
257
+ throw new Error('--title is required');
258
+ }
259
+ if (!normalizeText(payload.symptom)) {
260
+ throw new Error('--symptom is required');
261
+ }
262
+ if (!normalizeText(payload.root_cause)) {
263
+ throw new Error('--root-cause is required');
264
+ }
265
+ if (!Array.isArray(payload.fix_actions) || payload.fix_actions.length === 0) {
266
+ throw new Error('at least one --fix-action is required');
267
+ }
268
+
269
+ const status = normalizeStatus(payload.status, 'candidate');
270
+ if (status === 'promoted') {
271
+ throw new Error('record does not accept status=promoted. Use "sce errorbook promote <id>"');
272
+ }
273
+ if (status === 'verified' && (!Array.isArray(payload.verification_evidence) || payload.verification_evidence.length === 0)) {
274
+ throw new Error('status=verified requires at least one --verification evidence');
275
+ }
276
+ }
277
+
278
+ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
279
+ const payload = {
280
+ title: normalizeText(options.title || fromFilePayload.title),
281
+ symptom: normalizeText(options.symptom || fromFilePayload.symptom),
282
+ root_cause: normalizeText(options.rootCause || options.root_cause || fromFilePayload.root_cause || fromFilePayload.rootCause),
283
+ fix_actions: normalizeStringList(fromFilePayload.fix_actions, fromFilePayload.fixActions, options.fixAction, options.fixActions),
284
+ verification_evidence: normalizeStringList(
285
+ fromFilePayload.verification_evidence,
286
+ fromFilePayload.verificationEvidence,
287
+ options.verification,
288
+ options.verificationEvidence
289
+ ),
290
+ tags: normalizeStringList(fromFilePayload.tags, options.tags),
291
+ ontology_tags: normalizeOntologyTags(fromFilePayload.ontology_tags, fromFilePayload.ontology, options.ontology),
292
+ status: normalizeStatus(options.status || fromFilePayload.status || 'candidate'),
293
+ source: {
294
+ spec: normalizeText(options.spec || fromFilePayload?.source?.spec),
295
+ files: normalizeStringList(fromFilePayload?.source?.files, options.files),
296
+ tests: normalizeStringList(fromFilePayload?.source?.tests, options.tests)
297
+ },
298
+ notes: normalizeText(options.notes || fromFilePayload.notes),
299
+ fingerprint: createFingerprint({
300
+ fingerprint: options.fingerprint || fromFilePayload.fingerprint,
301
+ title: options.title || fromFilePayload.title,
302
+ symptom: options.symptom || fromFilePayload.symptom,
303
+ root_cause: options.rootCause || options.root_cause || fromFilePayload.root_cause || fromFilePayload.rootCause
304
+ })
305
+ };
306
+
307
+ return payload;
308
+ }
309
+
310
+ function createEntryId() {
311
+ return `eb-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
312
+ }
313
+
314
+ function buildIndexSummary(entry) {
315
+ return {
316
+ id: entry.id,
317
+ fingerprint: entry.fingerprint,
318
+ title: entry.title,
319
+ status: entry.status,
320
+ quality_score: entry.quality_score,
321
+ tags: entry.tags,
322
+ ontology_tags: entry.ontology_tags,
323
+ occurrences: entry.occurrences || 1,
324
+ created_at: entry.created_at,
325
+ updated_at: entry.updated_at
326
+ };
327
+ }
328
+
329
+ function findSummaryById(index, id) {
330
+ const normalized = normalizeText(id);
331
+ if (!normalized) {
332
+ return null;
333
+ }
334
+
335
+ const exact = index.entries.find((item) => item.id === normalized);
336
+ if (exact) {
337
+ return exact;
338
+ }
339
+
340
+ const startsWith = index.entries.filter((item) => item.id.startsWith(normalized));
341
+ if (startsWith.length === 1) {
342
+ return startsWith[0];
343
+ }
344
+ if (startsWith.length > 1) {
345
+ throw new Error(`entry id prefix "${normalized}" is ambiguous (${startsWith.length} matches)`);
346
+ }
347
+ return null;
348
+ }
349
+
350
+ function mergeEntry(existingEntry, incomingPayload) {
351
+ const merged = {
352
+ ...existingEntry,
353
+ title: normalizeText(incomingPayload.title) || existingEntry.title,
354
+ symptom: normalizeText(incomingPayload.symptom) || existingEntry.symptom,
355
+ root_cause: normalizeText(incomingPayload.root_cause) || existingEntry.root_cause,
356
+ fix_actions: normalizeStringList(existingEntry.fix_actions, incomingPayload.fix_actions),
357
+ verification_evidence: normalizeStringList(existingEntry.verification_evidence, incomingPayload.verification_evidence),
358
+ tags: normalizeStringList(existingEntry.tags, incomingPayload.tags),
359
+ ontology_tags: normalizeOntologyTags(existingEntry.ontology_tags, incomingPayload.ontology_tags),
360
+ status: selectStatus(existingEntry.status, incomingPayload.status),
361
+ notes: normalizeText(incomingPayload.notes) || existingEntry.notes || '',
362
+ source: {
363
+ spec: normalizeText(incomingPayload?.source?.spec) || normalizeText(existingEntry?.source?.spec),
364
+ files: normalizeStringList(existingEntry?.source?.files, incomingPayload?.source?.files),
365
+ tests: normalizeStringList(existingEntry?.source?.tests, incomingPayload?.source?.tests)
366
+ },
367
+ occurrences: Number(existingEntry.occurrences || 1) + 1,
368
+ updated_at: nowIso()
369
+ };
370
+ merged.quality_score = scoreQuality(merged);
371
+ return merged;
372
+ }
373
+
374
+ async function loadRecordPayloadFromFile(projectPath, sourcePath, fileSystem = fs) {
375
+ const normalized = normalizeText(sourcePath);
376
+ if (!normalized) {
377
+ return {};
378
+ }
379
+
380
+ const absolutePath = path.isAbsolute(normalized)
381
+ ? normalized
382
+ : path.join(projectPath, normalized);
383
+
384
+ if (!await fileSystem.pathExists(absolutePath)) {
385
+ throw new Error(`record source file not found: ${sourcePath}`);
386
+ }
387
+
388
+ try {
389
+ const payload = await fileSystem.readJson(absolutePath);
390
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
391
+ throw new Error('record source JSON must be an object');
392
+ }
393
+ return payload;
394
+ } catch (error) {
395
+ throw new Error(`failed to parse record source file (${sourcePath}): ${error.message}`);
396
+ }
397
+ }
398
+
399
+ function printRecordSummary(result) {
400
+ const action = result.created ? 'Recorded new entry' : 'Updated duplicate fingerprint';
401
+ console.log(chalk.green(`✓ ${action}`));
402
+ console.log(chalk.gray(` id: ${result.entry.id}`));
403
+ console.log(chalk.gray(` status: ${result.entry.status}`));
404
+ console.log(chalk.gray(` quality: ${result.entry.quality_score}`));
405
+ console.log(chalk.gray(` fingerprint: ${result.entry.fingerprint}`));
406
+ }
407
+
408
+ function printListSummary(payload) {
409
+ if (payload.entries.length === 0) {
410
+ console.log(chalk.gray('No errorbook entries found'));
411
+ return;
412
+ }
413
+
414
+ const table = new Table({
415
+ head: ['ID', 'Status', 'Quality', 'Title', 'Updated', 'Occurrences'].map((item) => chalk.cyan(item)),
416
+ colWidths: [16, 12, 10, 44, 22, 12]
417
+ });
418
+
419
+ payload.entries.forEach((entry) => {
420
+ table.push([
421
+ entry.id,
422
+ entry.status,
423
+ entry.quality_score,
424
+ entry.title.length > 40 ? `${entry.title.slice(0, 40)}...` : entry.title,
425
+ entry.updated_at,
426
+ entry.occurrences || 1
427
+ ]);
428
+ });
429
+
430
+ console.log(table.toString());
431
+ console.log(chalk.gray(`Total: ${payload.total_results} (stored: ${payload.total_entries})`));
432
+ }
433
+
434
+ function scoreSearchMatch(entry, queryTokens) {
435
+ const title = normalizeText(entry.title).toLowerCase();
436
+ const symptom = normalizeText(entry.symptom).toLowerCase();
437
+ const rootCause = normalizeText(entry.root_cause).toLowerCase();
438
+ const fingerprint = normalizeText(entry.fingerprint).toLowerCase();
439
+ const tagText = normalizeStringList(entry.tags, entry.ontology_tags).join(' ').toLowerCase();
440
+ const fixText = normalizeStringList(entry.fix_actions).join(' ').toLowerCase();
441
+
442
+ let score = 0;
443
+ for (const token of queryTokens) {
444
+ if (!token) {
445
+ continue;
446
+ }
447
+ if (title.includes(token)) {
448
+ score += 8;
449
+ }
450
+ if (symptom.includes(token)) {
451
+ score += 5;
452
+ }
453
+ if (rootCause.includes(token)) {
454
+ score += 5;
455
+ }
456
+ if (fixText.includes(token)) {
457
+ score += 3;
458
+ }
459
+ if (tagText.includes(token)) {
460
+ score += 2;
461
+ }
462
+ if (fingerprint.includes(token)) {
463
+ score += 1;
464
+ }
465
+ }
466
+
467
+ score += (Number(entry.quality_score) || 0) / 20;
468
+ score += STATUS_RANK[entry.status] || 0;
469
+ return Number(score.toFixed(3));
470
+ }
471
+
472
+ function normalizeRiskLevel(value, fallback = 'high') {
473
+ const normalized = normalizeText(`${value || ''}`).toLowerCase();
474
+ if (!normalized) {
475
+ return fallback;
476
+ }
477
+ if (!ERRORBOOK_RISK_LEVELS.includes(normalized)) {
478
+ throw new Error(`risk level must be one of: ${ERRORBOOK_RISK_LEVELS.join(', ')}`);
479
+ }
480
+ return normalized;
481
+ }
482
+
483
+ function riskRank(level) {
484
+ const normalized = normalizeRiskLevel(level, 'high');
485
+ if (normalized === 'high') {
486
+ return 3;
487
+ }
488
+ if (normalized === 'medium') {
489
+ return 2;
490
+ }
491
+ return 1;
492
+ }
493
+
494
+ function evaluateEntryRisk(entry = {}) {
495
+ const status = normalizeStatus(entry.status, 'candidate');
496
+ if (status === 'promoted' || status === 'deprecated') {
497
+ return 'low';
498
+ }
499
+
500
+ const qualityScore = Number(entry.quality_score || 0);
501
+ const tags = normalizeStringList(entry.tags).map((item) => item.toLowerCase());
502
+ const ontologyTags = normalizeOntologyTags(entry.ontology_tags);
503
+ const hasHighRiskTag = tags.some((tag) => HIGH_RISK_SIGNAL_TAGS.includes(tag));
504
+
505
+ if (hasHighRiskTag) {
506
+ return 'high';
507
+ }
508
+ if (status === 'candidate' && qualityScore >= 85) {
509
+ return 'high';
510
+ }
511
+ if (status === 'candidate' && qualityScore >= 75 && ontologyTags.includes('decision_policy')) {
512
+ return 'high';
513
+ }
514
+ if (status === 'candidate') {
515
+ return 'medium';
516
+ }
517
+ if (qualityScore >= 85 && ontologyTags.includes('decision_policy')) {
518
+ return 'high';
519
+ }
520
+ return 'medium';
521
+ }
522
+
523
+ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
524
+ const projectPath = dependencies.projectPath || process.cwd();
525
+ const fileSystem = dependencies.fileSystem || fs;
526
+ const paths = resolveErrorbookPaths(projectPath);
527
+ const index = await readErrorbookIndex(paths, fileSystem);
528
+ const minRisk = normalizeRiskLevel(options.minRisk || options.min_risk || 'high', 'high');
529
+ const includeVerified = options.includeVerified === true;
530
+
531
+ const inspected = [];
532
+ for (const summary of index.entries) {
533
+ const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
534
+ if (!entry) {
535
+ continue;
536
+ }
537
+
538
+ const status = normalizeStatus(entry.status, 'candidate');
539
+ const unresolved = status === 'candidate' || (includeVerified && status === 'verified');
540
+ if (!unresolved) {
541
+ continue;
542
+ }
543
+
544
+ const risk = evaluateEntryRisk(entry);
545
+ inspected.push({
546
+ id: entry.id,
547
+ title: entry.title,
548
+ status,
549
+ risk,
550
+ quality_score: Number(entry.quality_score || 0),
551
+ tags: normalizeStringList(entry.tags),
552
+ updated_at: entry.updated_at
553
+ });
554
+ }
555
+
556
+ const blocked = inspected
557
+ .filter((item) => riskRank(item.risk) >= riskRank(minRisk))
558
+ .sort((left, right) => {
559
+ const riskDiff = riskRank(right.risk) - riskRank(left.risk);
560
+ if (riskDiff !== 0) {
561
+ return riskDiff;
562
+ }
563
+ const qualityDiff = Number(right.quality_score || 0) - Number(left.quality_score || 0);
564
+ if (qualityDiff !== 0) {
565
+ return qualityDiff;
566
+ }
567
+ return `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`);
568
+ });
569
+
570
+ return {
571
+ mode: 'errorbook-release-gate',
572
+ gate: {
573
+ min_risk: minRisk,
574
+ include_verified: includeVerified
575
+ },
576
+ passed: blocked.length === 0,
577
+ inspected_count: inspected.length,
578
+ blocked_count: blocked.length,
579
+ blocked_entries: blocked
580
+ };
581
+ }
582
+
583
+ function validatePromoteCandidate(entry, minQuality = DEFAULT_PROMOTE_MIN_QUALITY) {
584
+ const missing = [];
585
+ if (!normalizeText(entry.root_cause)) {
586
+ missing.push('root_cause');
587
+ }
588
+ if (!Array.isArray(entry.fix_actions) || entry.fix_actions.length === 0) {
589
+ missing.push('fix_actions');
590
+ }
591
+ if (!Array.isArray(entry.verification_evidence) || entry.verification_evidence.length === 0) {
592
+ missing.push('verification_evidence');
593
+ }
594
+ if (!Array.isArray(entry.ontology_tags) || entry.ontology_tags.length === 0) {
595
+ missing.push('ontology_tags');
596
+ }
597
+ if ((Number(entry.quality_score) || 0) < minQuality) {
598
+ missing.push(`quality_score>=${minQuality}`);
599
+ }
600
+ if (entry.status === 'deprecated') {
601
+ missing.push('status!=deprecated');
602
+ }
603
+ return missing;
604
+ }
605
+
606
+ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
607
+ const projectPath = dependencies.projectPath || process.cwd();
608
+ const fileSystem = dependencies.fileSystem || fs;
609
+ const paths = resolveErrorbookPaths(projectPath);
610
+
611
+ const fromFilePayload = await loadRecordPayloadFromFile(projectPath, options.from, fileSystem);
612
+ const normalized = normalizeRecordPayload(options, fromFilePayload);
613
+ validateRecordPayload(normalized);
614
+
615
+ const index = await readErrorbookIndex(paths, fileSystem);
616
+ const existingSummary = index.entries.find((entry) => entry.fingerprint === normalized.fingerprint);
617
+ const timestamp = nowIso();
618
+
619
+ let entry;
620
+ let created = false;
621
+ let deduplicated = false;
622
+
623
+ if (existingSummary) {
624
+ const existingEntry = await readErrorbookEntry(paths, existingSummary.id, fileSystem);
625
+ if (!existingEntry) {
626
+ throw new Error(`errorbook index references missing entry: ${existingSummary.id}`);
627
+ }
628
+ entry = mergeEntry(existingEntry, normalized);
629
+ deduplicated = true;
630
+ } else {
631
+ entry = {
632
+ api_version: ERRORBOOK_ENTRY_API_VERSION,
633
+ id: createEntryId(),
634
+ created_at: timestamp,
635
+ updated_at: timestamp,
636
+ fingerprint: normalized.fingerprint,
637
+ title: normalized.title,
638
+ symptom: normalized.symptom,
639
+ root_cause: normalized.root_cause,
640
+ fix_actions: normalized.fix_actions,
641
+ verification_evidence: normalized.verification_evidence,
642
+ tags: normalized.tags,
643
+ ontology_tags: normalized.ontology_tags,
644
+ status: normalized.status,
645
+ source: normalized.source,
646
+ notes: normalized.notes || '',
647
+ occurrences: 1
648
+ };
649
+ entry.quality_score = scoreQuality(entry);
650
+ created = true;
651
+ }
652
+
653
+ entry.updated_at = nowIso();
654
+ entry.quality_score = scoreQuality(entry);
655
+ await writeErrorbookEntry(paths, entry, fileSystem);
656
+
657
+ const summary = buildIndexSummary(entry);
658
+ const summaryIndex = index.entries.findIndex((item) => item.id === summary.id);
659
+ if (summaryIndex >= 0) {
660
+ index.entries[summaryIndex] = summary;
661
+ } else {
662
+ index.entries.push(summary);
663
+ }
664
+ index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
665
+ await writeErrorbookIndex(paths, index, fileSystem);
666
+
667
+ const result = {
668
+ mode: 'errorbook-record',
669
+ created,
670
+ deduplicated,
671
+ entry
672
+ };
673
+
674
+ if (options.json) {
675
+ console.log(JSON.stringify(result, null, 2));
676
+ } else if (!options.silent) {
677
+ printRecordSummary(result);
678
+ }
679
+
680
+ return result;
681
+ }
682
+
683
+ async function runErrorbookListCommand(options = {}, dependencies = {}) {
684
+ const projectPath = dependencies.projectPath || process.cwd();
685
+ const fileSystem = dependencies.fileSystem || fs;
686
+ const paths = resolveErrorbookPaths(projectPath);
687
+ const index = await readErrorbookIndex(paths, fileSystem);
688
+
689
+ const requestedStatus = options.status ? normalizeStatus(options.status) : null;
690
+ const requestedTag = normalizeText(options.tag).toLowerCase();
691
+ const requestedOntology = normalizeOntologyTags(options.ontology)[0] || '';
692
+ const minQuality = Number.isFinite(Number(options.minQuality)) ? Number(options.minQuality) : null;
693
+ const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
694
+ ? Number(options.limit)
695
+ : 20;
696
+
697
+ let filtered = [...index.entries];
698
+ if (requestedStatus) {
699
+ filtered = filtered.filter((entry) => entry.status === requestedStatus);
700
+ }
701
+ if (requestedTag) {
702
+ filtered = filtered.filter((entry) => normalizeStringList(entry.tags).some((tag) => tag.toLowerCase() === requestedTag));
703
+ }
704
+ if (requestedOntology) {
705
+ filtered = filtered.filter((entry) => normalizeOntologyTags(entry.ontology_tags).includes(requestedOntology));
706
+ }
707
+ if (minQuality !== null) {
708
+ filtered = filtered.filter((entry) => Number(entry.quality_score || 0) >= minQuality);
709
+ }
710
+
711
+ filtered.sort((left, right) => {
712
+ const qualityDiff = Number(right.quality_score || 0) - Number(left.quality_score || 0);
713
+ if (qualityDiff !== 0) {
714
+ return qualityDiff;
715
+ }
716
+ return `${right.updated_at}`.localeCompare(`${left.updated_at}`);
717
+ });
718
+
719
+ const result = {
720
+ mode: 'errorbook-list',
721
+ total_entries: index.entries.length,
722
+ total_results: filtered.length,
723
+ entries: filtered.slice(0, limit).map((entry) => ({
724
+ ...entry,
725
+ tags: normalizeStringList(entry.tags),
726
+ ontology_tags: normalizeOntologyTags(entry.ontology_tags)
727
+ }))
728
+ };
729
+
730
+ if (options.json) {
731
+ console.log(JSON.stringify(result, null, 2));
732
+ } else if (!options.silent) {
733
+ printListSummary(result);
734
+ }
735
+
736
+ return result;
737
+ }
738
+
739
+ async function runErrorbookShowCommand(options = {}, dependencies = {}) {
740
+ const projectPath = dependencies.projectPath || process.cwd();
741
+ const fileSystem = dependencies.fileSystem || fs;
742
+ const paths = resolveErrorbookPaths(projectPath);
743
+ const index = await readErrorbookIndex(paths, fileSystem);
744
+
745
+ const id = normalizeText(options.id || options.entryId);
746
+ if (!id) {
747
+ throw new Error('entry id is required');
748
+ }
749
+
750
+ const summary = findSummaryById(index, id);
751
+ if (!summary) {
752
+ throw new Error(`errorbook entry not found: ${id}`);
753
+ }
754
+
755
+ const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
756
+ if (!entry) {
757
+ throw new Error(`errorbook entry file not found: ${summary.id}`);
758
+ }
759
+
760
+ const result = {
761
+ mode: 'errorbook-show',
762
+ entry
763
+ };
764
+
765
+ if (options.json) {
766
+ console.log(JSON.stringify(result, null, 2));
767
+ } else if (!options.silent) {
768
+ console.log(chalk.cyan.bold(entry.title));
769
+ console.log(chalk.gray(`id: ${entry.id}`));
770
+ console.log(chalk.gray(`status: ${entry.status}`));
771
+ console.log(chalk.gray(`quality: ${entry.quality_score}`));
772
+ console.log(chalk.gray(`fingerprint: ${entry.fingerprint}`));
773
+ console.log(chalk.gray(`symptom: ${entry.symptom}`));
774
+ console.log(chalk.gray(`root_cause: ${entry.root_cause}`));
775
+ console.log(chalk.gray(`fix_actions: ${entry.fix_actions.join(' | ')}`));
776
+ console.log(chalk.gray(`verification: ${entry.verification_evidence.join(' | ') || '(none)'}`));
777
+ console.log(chalk.gray(`ontology: ${entry.ontology_tags.join(', ') || '(none)'}`));
778
+ }
779
+
780
+ return result;
781
+ }
782
+
783
+ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
784
+ const projectPath = dependencies.projectPath || process.cwd();
785
+ const fileSystem = dependencies.fileSystem || fs;
786
+ const paths = resolveErrorbookPaths(projectPath);
787
+ const index = await readErrorbookIndex(paths, fileSystem);
788
+
789
+ const query = normalizeText(options.query);
790
+ if (!query) {
791
+ throw new Error('--query is required');
792
+ }
793
+
794
+ const requestedStatus = options.status ? normalizeStatus(options.status) : null;
795
+ const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
796
+ ? Number(options.limit)
797
+ : 10;
798
+ const tokens = query.toLowerCase().split(/[\s,;|]+/).map((item) => item.trim()).filter(Boolean);
799
+
800
+ const candidates = [];
801
+ for (const summary of index.entries) {
802
+ if (requestedStatus && summary.status !== requestedStatus) {
803
+ continue;
804
+ }
805
+ const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
806
+ if (!entry) {
807
+ continue;
808
+ }
809
+ const matchScore = scoreSearchMatch(entry, tokens);
810
+ if (matchScore <= 0) {
811
+ continue;
812
+ }
813
+ candidates.push({
814
+ id: entry.id,
815
+ status: entry.status,
816
+ quality_score: entry.quality_score,
817
+ title: entry.title,
818
+ fingerprint: entry.fingerprint,
819
+ tags: normalizeStringList(entry.tags),
820
+ ontology_tags: normalizeOntologyTags(entry.ontology_tags),
821
+ match_score: matchScore,
822
+ updated_at: entry.updated_at
823
+ });
824
+ }
825
+
826
+ candidates.sort((left, right) => {
827
+ const scoreDiff = Number(right.match_score) - Number(left.match_score);
828
+ if (scoreDiff !== 0) {
829
+ return scoreDiff;
830
+ }
831
+ return `${right.updated_at}`.localeCompare(`${left.updated_at}`);
832
+ });
833
+
834
+ const result = {
835
+ mode: 'errorbook-find',
836
+ query,
837
+ total_results: candidates.length,
838
+ entries: candidates.slice(0, limit)
839
+ };
840
+
841
+ if (options.json) {
842
+ console.log(JSON.stringify(result, null, 2));
843
+ } else if (!options.silent) {
844
+ printListSummary({
845
+ entries: result.entries,
846
+ total_results: result.total_results,
847
+ total_entries: index.entries.length
848
+ });
849
+ }
850
+
851
+ return result;
852
+ }
853
+
854
+ async function runErrorbookPromoteCommand(options = {}, dependencies = {}) {
855
+ const projectPath = dependencies.projectPath || process.cwd();
856
+ const fileSystem = dependencies.fileSystem || fs;
857
+ const paths = resolveErrorbookPaths(projectPath);
858
+ const index = await readErrorbookIndex(paths, fileSystem);
859
+
860
+ const id = normalizeText(options.id || options.entryId);
861
+ if (!id) {
862
+ throw new Error('entry id is required');
863
+ }
864
+
865
+ const summary = findSummaryById(index, id);
866
+ if (!summary) {
867
+ throw new Error(`errorbook entry not found: ${id}`);
868
+ }
869
+
870
+ const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
871
+ if (!entry) {
872
+ throw new Error(`errorbook entry file not found: ${summary.id}`);
873
+ }
874
+
875
+ entry.quality_score = scoreQuality(entry);
876
+ const missing = validatePromoteCandidate(entry, DEFAULT_PROMOTE_MIN_QUALITY);
877
+ if (missing.length > 0) {
878
+ throw new Error(`promote gate failed: ${missing.join(', ')}`);
879
+ }
880
+
881
+ entry.status = 'promoted';
882
+ entry.promoted_at = nowIso();
883
+ entry.updated_at = entry.promoted_at;
884
+ await writeErrorbookEntry(paths, entry, fileSystem);
885
+
886
+ const updatedSummary = buildIndexSummary(entry);
887
+ const targetIndex = index.entries.findIndex((item) => item.id === entry.id);
888
+ if (targetIndex >= 0) {
889
+ index.entries[targetIndex] = updatedSummary;
890
+ } else {
891
+ index.entries.push(updatedSummary);
892
+ }
893
+ index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
894
+ await writeErrorbookIndex(paths, index, fileSystem);
895
+
896
+ const result = {
897
+ mode: 'errorbook-promote',
898
+ promoted: true,
899
+ entry
900
+ };
901
+
902
+ if (options.json) {
903
+ console.log(JSON.stringify(result, null, 2));
904
+ } else if (!options.silent) {
905
+ console.log(chalk.green('✓ Promoted errorbook entry'));
906
+ console.log(chalk.gray(` id: ${entry.id}`));
907
+ console.log(chalk.gray(` quality: ${entry.quality_score}`));
908
+ console.log(chalk.gray(` promoted_at: ${entry.promoted_at}`));
909
+ }
910
+
911
+ return result;
912
+ }
913
+
914
+ async function runErrorbookReleaseGateCommand(options = {}, dependencies = {}) {
915
+ const payload = await evaluateErrorbookReleaseGate(options, dependencies);
916
+
917
+ if (options.json) {
918
+ console.log(JSON.stringify(payload, null, 2));
919
+ } else if (!options.silent) {
920
+ if (payload.passed) {
921
+ console.log(chalk.green('✓ Errorbook release gate passed'));
922
+ console.log(chalk.gray(` inspected: ${payload.inspected_count}`));
923
+ return payload;
924
+ }
925
+ console.log(chalk.red('✗ Errorbook release gate blocked'));
926
+ console.log(chalk.gray(` blocked: ${payload.blocked_count}`));
927
+ payload.blocked_entries.slice(0, 10).forEach((item) => {
928
+ console.log(chalk.gray(` - ${item.id} [${item.risk}] ${item.title}`));
929
+ });
930
+ }
931
+
932
+ if (options.failOnBlock && !payload.passed) {
933
+ throw new Error(
934
+ `errorbook release gate blocked: ${payload.blocked_count} unresolved entries (min-risk=${payload.gate.min_risk})`
935
+ );
936
+ }
937
+
938
+ return payload;
939
+ }
940
+
941
+ async function runErrorbookDeprecateCommand(options = {}, dependencies = {}) {
942
+ const projectPath = dependencies.projectPath || process.cwd();
943
+ const fileSystem = dependencies.fileSystem || fs;
944
+ const paths = resolveErrorbookPaths(projectPath);
945
+ const index = await readErrorbookIndex(paths, fileSystem);
946
+
947
+ const id = normalizeText(options.id || options.entryId);
948
+ if (!id) {
949
+ throw new Error('entry id is required');
950
+ }
951
+
952
+ const summary = findSummaryById(index, id);
953
+ if (!summary) {
954
+ throw new Error(`errorbook entry not found: ${id}`);
955
+ }
956
+
957
+ const reason = normalizeText(options.reason);
958
+ if (!reason) {
959
+ throw new Error('--reason is required for deprecate');
960
+ }
961
+
962
+ const replacement = normalizeText(options.replacement);
963
+ if (replacement && replacement === summary.id) {
964
+ throw new Error('--replacement cannot reference the same entry id');
965
+ }
966
+
967
+ const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
968
+ if (!entry) {
969
+ throw new Error(`errorbook entry file not found: ${summary.id}`);
970
+ }
971
+
972
+ entry.status = 'deprecated';
973
+ entry.updated_at = nowIso();
974
+ entry.deprecated_at = entry.updated_at;
975
+ entry.deprecation = {
976
+ reason,
977
+ replacement_id: replacement || null
978
+ };
979
+ entry.quality_score = scoreQuality(entry);
980
+ await writeErrorbookEntry(paths, entry, fileSystem);
981
+
982
+ const updatedSummary = buildIndexSummary(entry);
983
+ const targetIndex = index.entries.findIndex((item) => item.id === entry.id);
984
+ if (targetIndex >= 0) {
985
+ index.entries[targetIndex] = updatedSummary;
986
+ } else {
987
+ index.entries.push(updatedSummary);
988
+ }
989
+ index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
990
+ await writeErrorbookIndex(paths, index, fileSystem);
991
+
992
+ const result = {
993
+ mode: 'errorbook-deprecate',
994
+ deprecated: true,
995
+ entry
996
+ };
997
+
998
+ if (options.json) {
999
+ console.log(JSON.stringify(result, null, 2));
1000
+ } else if (!options.silent) {
1001
+ console.log(chalk.yellow('✓ Deprecated errorbook entry'));
1002
+ console.log(chalk.gray(` id: ${entry.id}`));
1003
+ console.log(chalk.gray(` reason: ${reason}`));
1004
+ if (replacement) {
1005
+ console.log(chalk.gray(` replacement: ${replacement}`));
1006
+ }
1007
+ }
1008
+
1009
+ return result;
1010
+ }
1011
+
1012
+ async function runErrorbookRequalifyCommand(options = {}, dependencies = {}) {
1013
+ const projectPath = dependencies.projectPath || process.cwd();
1014
+ const fileSystem = dependencies.fileSystem || fs;
1015
+ const paths = resolveErrorbookPaths(projectPath);
1016
+ const index = await readErrorbookIndex(paths, fileSystem);
1017
+
1018
+ const id = normalizeText(options.id || options.entryId);
1019
+ if (!id) {
1020
+ throw new Error('entry id is required');
1021
+ }
1022
+
1023
+ const summary = findSummaryById(index, id);
1024
+ if (!summary) {
1025
+ throw new Error(`errorbook entry not found: ${id}`);
1026
+ }
1027
+
1028
+ const status = normalizeStatus(options.status || 'verified');
1029
+ if (status === 'promoted') {
1030
+ throw new Error('requalify does not accept status=promoted. Use "sce errorbook promote <id>"');
1031
+ }
1032
+ if (status === 'deprecated') {
1033
+ throw new Error('requalify does not accept status=deprecated. Use "sce errorbook deprecate <id>"');
1034
+ }
1035
+
1036
+ const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
1037
+ if (!entry) {
1038
+ throw new Error(`errorbook entry file not found: ${summary.id}`);
1039
+ }
1040
+
1041
+ if (status === 'verified' && (!Array.isArray(entry.verification_evidence) || entry.verification_evidence.length === 0)) {
1042
+ throw new Error('requalify to verified requires verification_evidence');
1043
+ }
1044
+
1045
+ entry.status = status;
1046
+ entry.updated_at = nowIso();
1047
+ entry.requalified_at = entry.updated_at;
1048
+ if (entry.deprecation) {
1049
+ delete entry.deprecation;
1050
+ }
1051
+ entry.quality_score = scoreQuality(entry);
1052
+ await writeErrorbookEntry(paths, entry, fileSystem);
1053
+
1054
+ const updatedSummary = buildIndexSummary(entry);
1055
+ const targetIndex = index.entries.findIndex((item) => item.id === entry.id);
1056
+ if (targetIndex >= 0) {
1057
+ index.entries[targetIndex] = updatedSummary;
1058
+ } else {
1059
+ index.entries.push(updatedSummary);
1060
+ }
1061
+ index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
1062
+ await writeErrorbookIndex(paths, index, fileSystem);
1063
+
1064
+ const result = {
1065
+ mode: 'errorbook-requalify',
1066
+ requalified: true,
1067
+ entry
1068
+ };
1069
+
1070
+ if (options.json) {
1071
+ console.log(JSON.stringify(result, null, 2));
1072
+ } else if (!options.silent) {
1073
+ console.log(chalk.green('✓ Requalified errorbook entry'));
1074
+ console.log(chalk.gray(` id: ${entry.id}`));
1075
+ console.log(chalk.gray(` status: ${entry.status}`));
1076
+ }
1077
+
1078
+ return result;
1079
+ }
1080
+
1081
+ function collectOptionValue(value, previous = []) {
1082
+ const next = Array.isArray(previous) ? previous : [];
1083
+ next.push(value);
1084
+ return next;
1085
+ }
1086
+
1087
+ function emitCommandError(error, json) {
1088
+ if (json) {
1089
+ console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
1090
+ } else {
1091
+ console.error(chalk.red('Error:'), error.message);
1092
+ }
1093
+ process.exit(1);
1094
+ }
1095
+
1096
+ function registerErrorbookCommands(program) {
1097
+ const errorbook = program
1098
+ .command('errorbook')
1099
+ .description('Curated failure-remediation knowledge base');
1100
+
1101
+ errorbook
1102
+ .command('record')
1103
+ .description('Record a high-signal failure remediation entry')
1104
+ .option('--title <text>', 'Entry title')
1105
+ .option('--symptom <text>', 'Observed symptom')
1106
+ .option('--root-cause <text>', 'Validated root cause')
1107
+ .option('--fix-action <text>', 'Concrete fix action (repeatable)', collectOptionValue, [])
1108
+ .option('--verification <text>', 'Verification evidence (repeatable)', collectOptionValue, [])
1109
+ .option('--tags <csv>', 'Tags, comma-separated')
1110
+ .option('--ontology <csv>', `Ontology focus tags (${ERRORBOOK_ONTOLOGY_TAGS.join(', ')})`)
1111
+ .option('--status <status>', 'candidate|verified', 'candidate')
1112
+ .option('--fingerprint <text>', 'Custom deduplication fingerprint')
1113
+ .option('--from <path>', 'Load payload from JSON file')
1114
+ .option('--spec <spec>', 'Related spec id/name')
1115
+ .option('--json', 'Emit machine-readable JSON')
1116
+ .action(async (options) => {
1117
+ try {
1118
+ await runErrorbookRecordCommand(options);
1119
+ } catch (error) {
1120
+ emitCommandError(error, options.json);
1121
+ }
1122
+ });
1123
+
1124
+ errorbook
1125
+ .command('list')
1126
+ .description('List curated errorbook entries')
1127
+ .option('--status <status>', `Filter by status (${ERRORBOOK_STATUSES.join(', ')})`)
1128
+ .option('--tag <tag>', 'Filter by tag')
1129
+ .option('--ontology <tag>', `Filter by ontology tag (${ERRORBOOK_ONTOLOGY_TAGS.join(', ')})`)
1130
+ .option('--min-quality <n>', 'Minimum quality score', parseInt)
1131
+ .option('--limit <n>', 'Maximum entries returned', parseInt, 20)
1132
+ .option('--json', 'Emit machine-readable JSON')
1133
+ .action(async (options) => {
1134
+ try {
1135
+ await runErrorbookListCommand(options);
1136
+ } catch (error) {
1137
+ emitCommandError(error, options.json);
1138
+ }
1139
+ });
1140
+
1141
+ errorbook
1142
+ .command('show <id>')
1143
+ .description('Show a single errorbook entry')
1144
+ .option('--json', 'Emit machine-readable JSON')
1145
+ .action(async (id, options) => {
1146
+ try {
1147
+ await runErrorbookShowCommand({ ...options, id });
1148
+ } catch (error) {
1149
+ emitCommandError(error, options.json);
1150
+ }
1151
+ });
1152
+
1153
+ errorbook
1154
+ .command('find')
1155
+ .description('Search curated entries with ranking')
1156
+ .requiredOption('--query <text>', 'Search query')
1157
+ .option('--status <status>', `Filter by status (${ERRORBOOK_STATUSES.join(', ')})`)
1158
+ .option('--limit <n>', 'Maximum entries returned', parseInt, 10)
1159
+ .option('--json', 'Emit machine-readable JSON')
1160
+ .action(async (options) => {
1161
+ try {
1162
+ await runErrorbookFindCommand(options);
1163
+ } catch (error) {
1164
+ emitCommandError(error, options.json);
1165
+ }
1166
+ });
1167
+
1168
+ errorbook
1169
+ .command('promote <id>')
1170
+ .description('Promote entry after strict quality gate')
1171
+ .option('--json', 'Emit machine-readable JSON')
1172
+ .action(async (id, options) => {
1173
+ try {
1174
+ await runErrorbookPromoteCommand({ ...options, id });
1175
+ } catch (error) {
1176
+ emitCommandError(error, options.json);
1177
+ }
1178
+ });
1179
+
1180
+ errorbook
1181
+ .command('release-gate')
1182
+ .description('Block release on unresolved high-risk candidate entries')
1183
+ .option('--min-risk <level>', 'Risk threshold (low|medium|high)', 'high')
1184
+ .option('--include-verified', 'Also inspect verified (non-promoted) entries')
1185
+ .option('--fail-on-block', 'Exit with error when gate is blocked')
1186
+ .option('--json', 'Emit machine-readable JSON')
1187
+ .action(async (options) => {
1188
+ try {
1189
+ await runErrorbookReleaseGateCommand(options);
1190
+ } catch (error) {
1191
+ emitCommandError(error, options.json);
1192
+ }
1193
+ });
1194
+
1195
+ errorbook
1196
+ .command('deprecate <id>')
1197
+ .description('Deprecate low-value or obsolete entry')
1198
+ .requiredOption('--reason <text>', 'Deprecation reason')
1199
+ .option('--replacement <id>', 'Replacement entry id')
1200
+ .option('--json', 'Emit machine-readable JSON')
1201
+ .action(async (id, options) => {
1202
+ try {
1203
+ await runErrorbookDeprecateCommand({ ...options, id });
1204
+ } catch (error) {
1205
+ emitCommandError(error, options.json);
1206
+ }
1207
+ });
1208
+
1209
+ errorbook
1210
+ .command('requalify <id>')
1211
+ .description('Requalify deprecated/candidate entry back to candidate|verified')
1212
+ .option('--status <status>', 'candidate|verified', 'verified')
1213
+ .option('--json', 'Emit machine-readable JSON')
1214
+ .action(async (id, options) => {
1215
+ try {
1216
+ await runErrorbookRequalifyCommand({ ...options, id });
1217
+ } catch (error) {
1218
+ emitCommandError(error, options.json);
1219
+ }
1220
+ });
1221
+ }
1222
+
1223
+ module.exports = {
1224
+ ERRORBOOK_STATUSES,
1225
+ ERRORBOOK_ONTOLOGY_TAGS,
1226
+ ERRORBOOK_RISK_LEVELS,
1227
+ HIGH_RISK_SIGNAL_TAGS,
1228
+ DEFAULT_PROMOTE_MIN_QUALITY,
1229
+ resolveErrorbookPaths,
1230
+ normalizeOntologyTags,
1231
+ normalizeRecordPayload,
1232
+ scoreQuality,
1233
+ evaluateEntryRisk,
1234
+ evaluateErrorbookReleaseGate,
1235
+ runErrorbookRecordCommand,
1236
+ runErrorbookListCommand,
1237
+ runErrorbookShowCommand,
1238
+ runErrorbookFindCommand,
1239
+ runErrorbookPromoteCommand,
1240
+ runErrorbookReleaseGateCommand,
1241
+ runErrorbookDeprecateCommand,
1242
+ runErrorbookRequalifyCommand,
1243
+ registerErrorbookCommands
1244
+ };