scene-capability-engine 3.3.21 → 3.3.23
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 +20 -0
- package/docs/command-reference.md +35 -0
- package/docs/errorbook-registry.md +116 -0
- package/docs/release-checklist.md +1 -0
- package/docs/zh/release-checklist.md +1 -0
- package/lib/adoption/adoption-strategy.js +2 -0
- package/lib/adoption/backup-manager.js +2 -0
- package/lib/adoption/detection-engine.js +2 -0
- package/lib/adoption/file-classifier.js +3 -1
- package/lib/adoption/smart-orchestrator.js +2 -0
- package/lib/adoption/strategy-selector.js +2 -0
- package/lib/adoption/template-sync.js +2 -0
- package/lib/commands/errorbook.js +1000 -10
- package/package.json +1 -1
- package/template/.sce/config/errorbook-registry.json +13 -0
- package/template/.sce/steering/CORE_PRINCIPLES.md +10 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const https = require('https');
|
|
2
4
|
const path = require('path');
|
|
3
5
|
const fs = require('fs-extra');
|
|
4
6
|
const chalk = require('chalk');
|
|
@@ -6,7 +8,14 @@ const Table = require('cli-table3');
|
|
|
6
8
|
|
|
7
9
|
const ERRORBOOK_INDEX_API_VERSION = 'sce.errorbook.index/v0.1';
|
|
8
10
|
const ERRORBOOK_ENTRY_API_VERSION = 'sce.errorbook.entry/v0.1';
|
|
11
|
+
const ERRORBOOK_REGISTRY_API_VERSION = 'sce.errorbook.registry/v0.1';
|
|
12
|
+
const ERRORBOOK_REGISTRY_CACHE_API_VERSION = 'sce.errorbook.registry-cache/v0.1';
|
|
13
|
+
const ERRORBOOK_REGISTRY_INDEX_API_VERSION = 'sce.errorbook.registry-index/v0.1';
|
|
9
14
|
const ERRORBOOK_STATUSES = Object.freeze(['candidate', 'verified', 'promoted', 'deprecated']);
|
|
15
|
+
const TEMPORARY_MITIGATION_TAG = 'temporary-mitigation';
|
|
16
|
+
const DEFAULT_ERRORBOOK_REGISTRY_CONFIG = '.sce/config/errorbook-registry.json';
|
|
17
|
+
const DEFAULT_ERRORBOOK_REGISTRY_CACHE = '.sce/errorbook/registry-cache.json';
|
|
18
|
+
const DEFAULT_ERRORBOOK_REGISTRY_EXPORT = '.sce/errorbook/exports/errorbook-registry-export.json';
|
|
10
19
|
const STATUS_RANK = Object.freeze({
|
|
11
20
|
deprecated: 0,
|
|
12
21
|
candidate: 1,
|
|
@@ -59,6 +68,28 @@ function resolveErrorbookPaths(projectPath = process.cwd()) {
|
|
|
59
68
|
};
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
function resolveProjectPath(projectPath, maybeRelativePath, fallbackRelativePath) {
|
|
72
|
+
const normalized = normalizeText(maybeRelativePath || fallbackRelativePath || '');
|
|
73
|
+
if (!normalized) {
|
|
74
|
+
return path.resolve(projectPath, fallbackRelativePath || '');
|
|
75
|
+
}
|
|
76
|
+
return path.isAbsolute(normalized)
|
|
77
|
+
? normalized
|
|
78
|
+
: path.resolve(projectPath, normalized);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveErrorbookRegistryPaths(projectPath = process.cwd(), overrides = {}) {
|
|
82
|
+
const configFile = resolveProjectPath(projectPath, overrides.configPath, DEFAULT_ERRORBOOK_REGISTRY_CONFIG);
|
|
83
|
+
const cacheFile = resolveProjectPath(projectPath, overrides.cachePath, DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
84
|
+
const exportFile = resolveProjectPath(projectPath, overrides.exportPath, DEFAULT_ERRORBOOK_REGISTRY_EXPORT);
|
|
85
|
+
return {
|
|
86
|
+
projectPath,
|
|
87
|
+
configFile,
|
|
88
|
+
cacheFile,
|
|
89
|
+
exportFile
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
function nowIso() {
|
|
63
94
|
return new Date().toISOString();
|
|
64
95
|
}
|
|
@@ -71,6 +102,23 @@ function normalizeText(value) {
|
|
|
71
102
|
return value.trim();
|
|
72
103
|
}
|
|
73
104
|
|
|
105
|
+
function normalizeBoolean(value, fallback = false) {
|
|
106
|
+
if (typeof value === 'boolean') {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
110
|
+
if (!normalized) {
|
|
111
|
+
return fallback;
|
|
112
|
+
}
|
|
113
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return fallback;
|
|
120
|
+
}
|
|
121
|
+
|
|
74
122
|
function normalizeCsv(value) {
|
|
75
123
|
if (Array.isArray(value)) {
|
|
76
124
|
return value;
|
|
@@ -109,6 +157,18 @@ function normalizeStringList(...rawInputs) {
|
|
|
109
157
|
return Array.from(new Set(merged));
|
|
110
158
|
}
|
|
111
159
|
|
|
160
|
+
function normalizeIsoTimestamp(value, fieldName = 'datetime') {
|
|
161
|
+
const normalized = normalizeText(value);
|
|
162
|
+
if (!normalized) {
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
const parsed = Date.parse(normalized);
|
|
166
|
+
if (Number.isNaN(parsed)) {
|
|
167
|
+
throw new Error(`${fieldName} must be a valid ISO datetime`);
|
|
168
|
+
}
|
|
169
|
+
return new Date(parsed).toISOString();
|
|
170
|
+
}
|
|
171
|
+
|
|
112
172
|
function normalizeOntologyTags(...rawInputs) {
|
|
113
173
|
const normalized = normalizeStringList(...rawInputs).map((item) => item.toLowerCase());
|
|
114
174
|
const mapped = normalized.map((item) => ONTOLOGY_TAG_ALIASES[item] || item);
|
|
@@ -116,6 +176,140 @@ function normalizeOntologyTags(...rawInputs) {
|
|
|
116
176
|
return Array.from(new Set(valid));
|
|
117
177
|
}
|
|
118
178
|
|
|
179
|
+
function hasMitigationInput(options = {}, fromFilePayload = {}) {
|
|
180
|
+
const mitigation = fromFilePayload && typeof fromFilePayload.temporary_mitigation === 'object'
|
|
181
|
+
? fromFilePayload.temporary_mitigation
|
|
182
|
+
: {};
|
|
183
|
+
return Boolean(
|
|
184
|
+
options.temporaryMitigation === true ||
|
|
185
|
+
normalizeBoolean(mitigation.enabled, false) ||
|
|
186
|
+
normalizeText(options.mitigationReason || mitigation.reason || mitigation.notes) ||
|
|
187
|
+
normalizeText(options.mitigationExit || mitigation.exit_criteria || mitigation.exitCriteria) ||
|
|
188
|
+
normalizeText(options.mitigationCleanup || mitigation.cleanup_task || mitigation.cleanupTask) ||
|
|
189
|
+
normalizeText(options.mitigationDeadline || mitigation.deadline_at || mitigation.deadlineAt)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeTemporaryMitigation(options = {}, fromFilePayload = {}) {
|
|
194
|
+
const mitigationFromFile = fromFilePayload && typeof fromFilePayload.temporary_mitigation === 'object'
|
|
195
|
+
? fromFilePayload.temporary_mitigation
|
|
196
|
+
: {};
|
|
197
|
+
if (!hasMitigationInput(options, fromFilePayload)) {
|
|
198
|
+
return { enabled: false };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
enabled: true,
|
|
203
|
+
reason: normalizeText(options.mitigationReason || mitigationFromFile.reason || mitigationFromFile.notes),
|
|
204
|
+
exit_criteria: normalizeText(options.mitigationExit || mitigationFromFile.exit_criteria || mitigationFromFile.exitCriteria),
|
|
205
|
+
cleanup_task: normalizeText(options.mitigationCleanup || mitigationFromFile.cleanup_task || mitigationFromFile.cleanupTask),
|
|
206
|
+
deadline_at: normalizeIsoTimestamp(
|
|
207
|
+
options.mitigationDeadline || mitigationFromFile.deadline_at || mitigationFromFile.deadlineAt,
|
|
208
|
+
'--mitigation-deadline'
|
|
209
|
+
),
|
|
210
|
+
created_at: normalizeIsoTimestamp(mitigationFromFile.created_at || mitigationFromFile.createdAt, 'temporary_mitigation.created_at') || '',
|
|
211
|
+
updated_at: normalizeIsoTimestamp(mitigationFromFile.updated_at || mitigationFromFile.updatedAt, 'temporary_mitigation.updated_at') || '',
|
|
212
|
+
resolved_at: normalizeIsoTimestamp(mitigationFromFile.resolved_at || mitigationFromFile.resolvedAt, 'temporary_mitigation.resolved_at') || ''
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeExistingTemporaryMitigation(value = {}) {
|
|
217
|
+
if (!value || typeof value !== 'object' || normalizeBoolean(value.enabled, false) !== true) {
|
|
218
|
+
return { enabled: false };
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
enabled: true,
|
|
222
|
+
reason: normalizeText(value.reason || value.notes),
|
|
223
|
+
exit_criteria: normalizeText(value.exit_criteria || value.exitCriteria),
|
|
224
|
+
cleanup_task: normalizeText(value.cleanup_task || value.cleanupTask),
|
|
225
|
+
deadline_at: normalizeText(value.deadline_at || value.deadlineAt),
|
|
226
|
+
created_at: normalizeText(value.created_at || value.createdAt),
|
|
227
|
+
updated_at: normalizeText(value.updated_at || value.updatedAt),
|
|
228
|
+
resolved_at: normalizeText(value.resolved_at || value.resolvedAt)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveMergedTemporaryMitigation(existingEntry = {}, incomingPayload = {}) {
|
|
233
|
+
const existing = normalizeExistingTemporaryMitigation(existingEntry.temporary_mitigation);
|
|
234
|
+
const incoming = normalizeExistingTemporaryMitigation(incomingPayload.temporary_mitigation);
|
|
235
|
+
if (!incoming.enabled) {
|
|
236
|
+
return existing;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
enabled: true,
|
|
241
|
+
reason: normalizeText(incoming.reason) || existing.reason || '',
|
|
242
|
+
exit_criteria: normalizeText(incoming.exit_criteria) || existing.exit_criteria || '',
|
|
243
|
+
cleanup_task: normalizeText(incoming.cleanup_task) || existing.cleanup_task || '',
|
|
244
|
+
deadline_at: normalizeText(incoming.deadline_at) || existing.deadline_at || '',
|
|
245
|
+
created_at: normalizeText(existing.created_at) || nowIso(),
|
|
246
|
+
updated_at: nowIso(),
|
|
247
|
+
resolved_at: ''
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function markTemporaryMitigationResolved(entry = {}, resolvedAt = nowIso()) {
|
|
252
|
+
const current = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
253
|
+
if (!current.enabled || normalizeText(current.resolved_at)) {
|
|
254
|
+
return current.enabled ? {
|
|
255
|
+
...current,
|
|
256
|
+
resolved_at: normalizeText(current.resolved_at) || resolvedAt,
|
|
257
|
+
updated_at: normalizeText(current.updated_at) || resolvedAt
|
|
258
|
+
} : { enabled: false };
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...current,
|
|
262
|
+
resolved_at: resolvedAt,
|
|
263
|
+
updated_at: resolvedAt
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function evaluateTemporaryMitigationPolicy(entry = {}) {
|
|
268
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
269
|
+
const status = normalizeStatus(entry.status, 'candidate');
|
|
270
|
+
if (!mitigation.enabled || ['promoted', 'deprecated'].includes(status)) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (normalizeText(mitigation.resolved_at)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const policyViolations = [];
|
|
278
|
+
if (!normalizeText(mitigation.exit_criteria)) {
|
|
279
|
+
policyViolations.push('temporary_mitigation.exit_criteria');
|
|
280
|
+
}
|
|
281
|
+
if (!normalizeText(mitigation.cleanup_task)) {
|
|
282
|
+
policyViolations.push('temporary_mitigation.cleanup_task');
|
|
283
|
+
}
|
|
284
|
+
const deadlineAtRaw = normalizeText(mitigation.deadline_at);
|
|
285
|
+
let deadlineAt = deadlineAtRaw;
|
|
286
|
+
let deadlineExpired = false;
|
|
287
|
+
if (!deadlineAtRaw) {
|
|
288
|
+
policyViolations.push('temporary_mitigation.deadline_at');
|
|
289
|
+
} else {
|
|
290
|
+
const parsed = Date.parse(deadlineAtRaw);
|
|
291
|
+
if (Number.isNaN(parsed)) {
|
|
292
|
+
policyViolations.push('temporary_mitigation.deadline_at:invalid_datetime');
|
|
293
|
+
} else {
|
|
294
|
+
deadlineAt = new Date(parsed).toISOString();
|
|
295
|
+
if (parsed <= Date.now()) {
|
|
296
|
+
deadlineExpired = true;
|
|
297
|
+
policyViolations.push('temporary_mitigation.deadline_at:expired');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
enabled: true,
|
|
304
|
+
reason: mitigation.reason,
|
|
305
|
+
exit_criteria: mitigation.exit_criteria,
|
|
306
|
+
cleanup_task: mitigation.cleanup_task,
|
|
307
|
+
deadline_at: deadlineAt,
|
|
308
|
+
deadline_expired: deadlineExpired,
|
|
309
|
+
policy_violations: policyViolations
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
119
313
|
function normalizeStatus(input, fallback = 'candidate') {
|
|
120
314
|
const normalized = normalizeText(`${input || ''}`).toLowerCase();
|
|
121
315
|
if (!normalized) {
|
|
@@ -273,9 +467,22 @@ function validateRecordPayload(payload) {
|
|
|
273
467
|
if (status === 'verified' && (!Array.isArray(payload.verification_evidence) || payload.verification_evidence.length === 0)) {
|
|
274
468
|
throw new Error('status=verified requires at least one --verification evidence');
|
|
275
469
|
}
|
|
470
|
+
const mitigation = normalizeExistingTemporaryMitigation(payload.temporary_mitigation);
|
|
471
|
+
if (mitigation.enabled) {
|
|
472
|
+
if (!normalizeText(mitigation.exit_criteria)) {
|
|
473
|
+
throw new Error('temporary mitigation requires --mitigation-exit');
|
|
474
|
+
}
|
|
475
|
+
if (!normalizeText(mitigation.cleanup_task)) {
|
|
476
|
+
throw new Error('temporary mitigation requires --mitigation-cleanup');
|
|
477
|
+
}
|
|
478
|
+
if (!normalizeText(mitigation.deadline_at)) {
|
|
479
|
+
throw new Error('temporary mitigation requires --mitigation-deadline');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
276
482
|
}
|
|
277
483
|
|
|
278
484
|
function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
485
|
+
const temporaryMitigation = normalizeTemporaryMitigation(options, fromFilePayload);
|
|
279
486
|
const payload = {
|
|
280
487
|
title: normalizeText(options.title || fromFilePayload.title),
|
|
281
488
|
symptom: normalizeText(options.symptom || fromFilePayload.symptom),
|
|
@@ -287,7 +494,11 @@ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
|
287
494
|
options.verification,
|
|
288
495
|
options.verificationEvidence
|
|
289
496
|
),
|
|
290
|
-
tags: normalizeStringList(
|
|
497
|
+
tags: normalizeStringList(
|
|
498
|
+
fromFilePayload.tags,
|
|
499
|
+
options.tags,
|
|
500
|
+
temporaryMitigation.enabled ? TEMPORARY_MITIGATION_TAG : []
|
|
501
|
+
),
|
|
291
502
|
ontology_tags: normalizeOntologyTags(fromFilePayload.ontology_tags, fromFilePayload.ontology, options.ontology),
|
|
292
503
|
status: normalizeStatus(options.status || fromFilePayload.status || 'candidate'),
|
|
293
504
|
source: {
|
|
@@ -295,6 +506,7 @@ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
|
295
506
|
files: normalizeStringList(fromFilePayload?.source?.files, options.files),
|
|
296
507
|
tests: normalizeStringList(fromFilePayload?.source?.tests, options.tests)
|
|
297
508
|
},
|
|
509
|
+
temporary_mitigation: temporaryMitigation,
|
|
298
510
|
notes: normalizeText(options.notes || fromFilePayload.notes),
|
|
299
511
|
fingerprint: createFingerprint({
|
|
300
512
|
fingerprint: options.fingerprint || fromFilePayload.fingerprint,
|
|
@@ -312,6 +524,8 @@ function createEntryId() {
|
|
|
312
524
|
}
|
|
313
525
|
|
|
314
526
|
function buildIndexSummary(entry) {
|
|
527
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
528
|
+
const mitigationActive = mitigation.enabled && !normalizeText(mitigation.resolved_at);
|
|
315
529
|
return {
|
|
316
530
|
id: entry.id,
|
|
317
531
|
fingerprint: entry.fingerprint,
|
|
@@ -320,6 +534,8 @@ function buildIndexSummary(entry) {
|
|
|
320
534
|
quality_score: entry.quality_score,
|
|
321
535
|
tags: entry.tags,
|
|
322
536
|
ontology_tags: entry.ontology_tags,
|
|
537
|
+
temporary_mitigation_active: mitigationActive,
|
|
538
|
+
temporary_mitigation_deadline_at: mitigationActive ? normalizeText(mitigation.deadline_at) : '',
|
|
323
539
|
occurrences: entry.occurrences || 1,
|
|
324
540
|
created_at: entry.created_at,
|
|
325
541
|
updated_at: entry.updated_at
|
|
@@ -348,6 +564,12 @@ function findSummaryById(index, id) {
|
|
|
348
564
|
}
|
|
349
565
|
|
|
350
566
|
function mergeEntry(existingEntry, incomingPayload) {
|
|
567
|
+
const temporaryMitigation = resolveMergedTemporaryMitigation(existingEntry, incomingPayload);
|
|
568
|
+
const mergedTags = normalizeStringList(
|
|
569
|
+
existingEntry.tags,
|
|
570
|
+
incomingPayload.tags,
|
|
571
|
+
temporaryMitigation.enabled ? TEMPORARY_MITIGATION_TAG : []
|
|
572
|
+
);
|
|
351
573
|
const merged = {
|
|
352
574
|
...existingEntry,
|
|
353
575
|
title: normalizeText(incomingPayload.title) || existingEntry.title,
|
|
@@ -355,7 +577,7 @@ function mergeEntry(existingEntry, incomingPayload) {
|
|
|
355
577
|
root_cause: normalizeText(incomingPayload.root_cause) || existingEntry.root_cause,
|
|
356
578
|
fix_actions: normalizeStringList(existingEntry.fix_actions, incomingPayload.fix_actions),
|
|
357
579
|
verification_evidence: normalizeStringList(existingEntry.verification_evidence, incomingPayload.verification_evidence),
|
|
358
|
-
tags:
|
|
580
|
+
tags: mergedTags,
|
|
359
581
|
ontology_tags: normalizeOntologyTags(existingEntry.ontology_tags, incomingPayload.ontology_tags),
|
|
360
582
|
status: selectStatus(existingEntry.status, incomingPayload.status),
|
|
361
583
|
notes: normalizeText(incomingPayload.notes) || existingEntry.notes || '',
|
|
@@ -364,6 +586,7 @@ function mergeEntry(existingEntry, incomingPayload) {
|
|
|
364
586
|
files: normalizeStringList(existingEntry?.source?.files, incomingPayload?.source?.files),
|
|
365
587
|
tests: normalizeStringList(existingEntry?.source?.tests, incomingPayload?.source?.tests)
|
|
366
588
|
},
|
|
589
|
+
temporary_mitigation: temporaryMitigation,
|
|
367
590
|
occurrences: Number(existingEntry.occurrences || 1) + 1,
|
|
368
591
|
updated_at: nowIso()
|
|
369
592
|
};
|
|
@@ -396,6 +619,382 @@ async function loadRecordPayloadFromFile(projectPath, sourcePath, fileSystem = f
|
|
|
396
619
|
}
|
|
397
620
|
}
|
|
398
621
|
|
|
622
|
+
function normalizeStatusList(values = [], fallback = ['promoted']) {
|
|
623
|
+
const raw = Array.isArray(values) ? values : normalizeStringList(values);
|
|
624
|
+
const list = raw.length > 0 ? raw : fallback;
|
|
625
|
+
const normalized = list
|
|
626
|
+
.map((item) => normalizeText(item).toLowerCase())
|
|
627
|
+
.filter(Boolean);
|
|
628
|
+
const unique = Array.from(new Set(normalized));
|
|
629
|
+
for (const status of unique) {
|
|
630
|
+
if (!ERRORBOOK_STATUSES.includes(status)) {
|
|
631
|
+
throw new Error(`invalid status in list: ${status}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return unique;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizeRegistrySource(input = {}) {
|
|
638
|
+
const candidate = input || {};
|
|
639
|
+
const name = normalizeText(candidate.name) || 'default';
|
|
640
|
+
const url = normalizeText(candidate.url || candidate.source);
|
|
641
|
+
const file = normalizeText(candidate.file || candidate.path);
|
|
642
|
+
const source = url || file;
|
|
643
|
+
const indexUrl = normalizeText(candidate.index_url || candidate.indexUrl || candidate.registry_index || candidate.registryIndex);
|
|
644
|
+
return {
|
|
645
|
+
name,
|
|
646
|
+
source,
|
|
647
|
+
index_url: indexUrl,
|
|
648
|
+
enabled: candidate.enabled !== false
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function normalizeRegistryMode(value, fallback = 'cache') {
|
|
653
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
654
|
+
if (!normalized) {
|
|
655
|
+
return fallback;
|
|
656
|
+
}
|
|
657
|
+
if (['cache', 'remote', 'hybrid'].includes(normalized)) {
|
|
658
|
+
return normalized;
|
|
659
|
+
}
|
|
660
|
+
throw new Error('registry mode must be one of: cache, remote, hybrid');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function readErrorbookRegistryConfig(paths, fileSystem = fs) {
|
|
664
|
+
const fallback = {
|
|
665
|
+
enabled: false,
|
|
666
|
+
search_mode: 'cache',
|
|
667
|
+
cache_file: DEFAULT_ERRORBOOK_REGISTRY_CACHE,
|
|
668
|
+
sources: []
|
|
669
|
+
};
|
|
670
|
+
if (!await fileSystem.pathExists(paths.configFile)) {
|
|
671
|
+
return fallback;
|
|
672
|
+
}
|
|
673
|
+
const payload = await fileSystem.readJson(paths.configFile).catch(() => null);
|
|
674
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
675
|
+
return fallback;
|
|
676
|
+
}
|
|
677
|
+
const sources = Array.isArray(payload.sources)
|
|
678
|
+
? payload.sources.map((item) => normalizeRegistrySource(item)).filter((item) => item.enabled && item.source)
|
|
679
|
+
: [];
|
|
680
|
+
return {
|
|
681
|
+
enabled: normalizeBoolean(payload.enabled, true),
|
|
682
|
+
search_mode: normalizeRegistryMode(payload.search_mode || payload.searchMode, 'cache'),
|
|
683
|
+
cache_file: normalizeText(payload.cache_file || payload.cacheFile || DEFAULT_ERRORBOOK_REGISTRY_CACHE),
|
|
684
|
+
sources
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isHttpSource(source = '') {
|
|
689
|
+
return /^https?:\/\//i.test(normalizeText(source));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function fetchJsonFromHttp(source, timeoutMs = 15000) {
|
|
693
|
+
const normalized = normalizeText(source);
|
|
694
|
+
if (!normalized) {
|
|
695
|
+
return Promise.reject(new Error('registry source is required'));
|
|
696
|
+
}
|
|
697
|
+
const client = normalized.startsWith('https://') ? https : http;
|
|
698
|
+
return new Promise((resolve, reject) => {
|
|
699
|
+
const request = client.get(normalized, {
|
|
700
|
+
timeout: timeoutMs,
|
|
701
|
+
headers: {
|
|
702
|
+
Accept: 'application/json'
|
|
703
|
+
}
|
|
704
|
+
}, (response) => {
|
|
705
|
+
const chunks = [];
|
|
706
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
707
|
+
response.on('end', () => {
|
|
708
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
709
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
710
|
+
reject(new Error(`registry source responded ${response.statusCode}`));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
resolve(JSON.parse(body));
|
|
715
|
+
} catch (error) {
|
|
716
|
+
reject(new Error(`registry source returned invalid JSON: ${error.message}`));
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
request.on('timeout', () => {
|
|
721
|
+
request.destroy(new Error('registry source request timed out'));
|
|
722
|
+
});
|
|
723
|
+
request.on('error', reject);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function loadRegistryPayload(projectPath, source, fileSystem = fs) {
|
|
728
|
+
const normalized = normalizeText(source);
|
|
729
|
+
if (!normalized) {
|
|
730
|
+
throw new Error('registry source is required');
|
|
731
|
+
}
|
|
732
|
+
if (isHttpSource(normalized)) {
|
|
733
|
+
return fetchJsonFromHttp(normalized);
|
|
734
|
+
}
|
|
735
|
+
const absolutePath = path.isAbsolute(normalized)
|
|
736
|
+
? normalized
|
|
737
|
+
: path.resolve(projectPath, normalized);
|
|
738
|
+
if (!await fileSystem.pathExists(absolutePath)) {
|
|
739
|
+
throw new Error(`registry source file not found: ${source}`);
|
|
740
|
+
}
|
|
741
|
+
return fileSystem.readJson(absolutePath);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function normalizeRegistryEntry(entry = {}, sourceName = 'registry') {
|
|
745
|
+
const title = normalizeText(entry.title || entry.name);
|
|
746
|
+
const symptom = normalizeText(entry.symptom);
|
|
747
|
+
const rootCause = normalizeText(entry.root_cause || entry.rootCause);
|
|
748
|
+
const fingerprint = createFingerprint({
|
|
749
|
+
fingerprint: normalizeText(entry.fingerprint),
|
|
750
|
+
title,
|
|
751
|
+
symptom,
|
|
752
|
+
root_cause: rootCause
|
|
753
|
+
});
|
|
754
|
+
const statusRaw = normalizeText(entry.status || 'candidate').toLowerCase();
|
|
755
|
+
const status = ERRORBOOK_STATUSES.includes(statusRaw) ? statusRaw : 'candidate';
|
|
756
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
id: normalizeText(entry.id) || `registry-${fingerprint}`,
|
|
760
|
+
fingerprint,
|
|
761
|
+
title,
|
|
762
|
+
symptom,
|
|
763
|
+
root_cause: rootCause,
|
|
764
|
+
fix_actions: normalizeStringList(entry.fix_actions, entry.fixActions),
|
|
765
|
+
verification_evidence: normalizeStringList(entry.verification_evidence, entry.verificationEvidence),
|
|
766
|
+
tags: normalizeStringList(entry.tags, mitigation.enabled ? TEMPORARY_MITIGATION_TAG : []),
|
|
767
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
768
|
+
status,
|
|
769
|
+
quality_score: Number.isFinite(Number(entry.quality_score)) ? Number(entry.quality_score) : scoreQuality(entry),
|
|
770
|
+
updated_at: normalizeIsoTimestamp(entry.updated_at || entry.updatedAt, 'registry.updated_at') || nowIso(),
|
|
771
|
+
source: {
|
|
772
|
+
...entry.source,
|
|
773
|
+
registry: sourceName
|
|
774
|
+
},
|
|
775
|
+
temporary_mitigation: mitigation,
|
|
776
|
+
entry_source: 'registry'
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function extractRegistryEntries(payload = {}, sourceName = 'registry') {
|
|
781
|
+
const rawEntries = Array.isArray(payload)
|
|
782
|
+
? payload
|
|
783
|
+
: Array.isArray(payload.entries)
|
|
784
|
+
? payload.entries
|
|
785
|
+
: [];
|
|
786
|
+
const normalized = [];
|
|
787
|
+
for (const item of rawEntries) {
|
|
788
|
+
if (!item || typeof item !== 'object') {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const entry = normalizeRegistryEntry(item, sourceName);
|
|
792
|
+
if (!entry.title || !entry.fingerprint) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
normalized.push(entry);
|
|
796
|
+
}
|
|
797
|
+
const deduped = new Map();
|
|
798
|
+
for (const entry of normalized) {
|
|
799
|
+
const key = entry.fingerprint;
|
|
800
|
+
const existing = deduped.get(key);
|
|
801
|
+
if (!existing) {
|
|
802
|
+
deduped.set(key, entry);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if ((Number(entry.quality_score) || 0) >= (Number(existing.quality_score) || 0)) {
|
|
806
|
+
deduped.set(key, entry);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return Array.from(deduped.values());
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function loadRegistryCache(projectPath, cachePathInput = '', fileSystem = fs) {
|
|
813
|
+
const cachePath = resolveProjectPath(projectPath, cachePathInput, DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
814
|
+
if (!await fileSystem.pathExists(cachePath)) {
|
|
815
|
+
return {
|
|
816
|
+
cache_path: cachePath,
|
|
817
|
+
entries: []
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const payload = await fileSystem.readJson(cachePath).catch(() => null);
|
|
821
|
+
const entries = extractRegistryEntries(payload || {}, 'registry-cache');
|
|
822
|
+
return {
|
|
823
|
+
cache_path: cachePath,
|
|
824
|
+
entries
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function tokenizeQueryText(query = '') {
|
|
829
|
+
return normalizeText(query)
|
|
830
|
+
.toLowerCase()
|
|
831
|
+
.split(/[^a-z0-9_]+/i)
|
|
832
|
+
.map((item) => item.trim())
|
|
833
|
+
.filter((item) => item.length >= 2);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function normalizeRegistryIndex(payload = {}, sourceName = '') {
|
|
837
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
api_version: normalizeText(payload.api_version || payload.version || ERRORBOOK_REGISTRY_INDEX_API_VERSION),
|
|
842
|
+
source_name: sourceName || normalizeText(payload.source_name || payload.sourceName),
|
|
843
|
+
min_token_length: Number.isFinite(Number(payload.min_token_length))
|
|
844
|
+
? Number(payload.min_token_length)
|
|
845
|
+
: 2,
|
|
846
|
+
token_to_source: payload.token_to_source && typeof payload.token_to_source === 'object'
|
|
847
|
+
? payload.token_to_source
|
|
848
|
+
: {},
|
|
849
|
+
token_to_bucket: payload.token_to_bucket && typeof payload.token_to_bucket === 'object'
|
|
850
|
+
? payload.token_to_bucket
|
|
851
|
+
: {},
|
|
852
|
+
buckets: payload.buckets && typeof payload.buckets === 'object'
|
|
853
|
+
? payload.buckets
|
|
854
|
+
: {},
|
|
855
|
+
default_source: normalizeText(payload.default_source || payload.fallback_source || '')
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function collectRegistryShardSources(indexPayload, queryTokens = [], maxShards = 8) {
|
|
860
|
+
const index = normalizeRegistryIndex(indexPayload);
|
|
861
|
+
if (!index) {
|
|
862
|
+
return [];
|
|
863
|
+
}
|
|
864
|
+
const sources = [];
|
|
865
|
+
const minTokenLength = Number.isFinite(index.min_token_length) ? index.min_token_length : 2;
|
|
866
|
+
for (const token of queryTokens) {
|
|
867
|
+
const normalizedToken = normalizeText(token).toLowerCase();
|
|
868
|
+
if (!normalizedToken || normalizedToken.length < minTokenLength) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const direct = index.token_to_source[normalizedToken];
|
|
872
|
+
if (direct) {
|
|
873
|
+
const items = Array.isArray(direct) ? direct : [direct];
|
|
874
|
+
for (const item of items) {
|
|
875
|
+
sources.push(normalizeText(item));
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const bucket = normalizeText(index.token_to_bucket[normalizedToken]);
|
|
880
|
+
if (!bucket) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const bucketSource = normalizeText(index.buckets[bucket] || index.buckets[normalizedToken]);
|
|
884
|
+
if (bucketSource) {
|
|
885
|
+
sources.push(bucketSource);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const deduped = Array.from(new Set(sources.filter(Boolean)));
|
|
890
|
+
if (deduped.length > 0) {
|
|
891
|
+
return Number.isFinite(Number(maxShards)) && Number(maxShards) > 0
|
|
892
|
+
? deduped.slice(0, Number(maxShards))
|
|
893
|
+
: deduped;
|
|
894
|
+
}
|
|
895
|
+
if (index.default_source) {
|
|
896
|
+
return [index.default_source];
|
|
897
|
+
}
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function searchRegistryRemote(options = {}, dependencies = {}) {
|
|
902
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
903
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
904
|
+
const source = normalizeRegistrySource(options.source || {});
|
|
905
|
+
const query = normalizeText(options.query);
|
|
906
|
+
const queryTokens = Array.isArray(options.queryTokens) ? options.queryTokens : tokenizeQueryText(query);
|
|
907
|
+
const requestedStatus = options.requestedStatus || null;
|
|
908
|
+
const maxShards = Number.isFinite(Number(options.maxShards)) ? Number(options.maxShards) : 8;
|
|
909
|
+
const allowRemoteFullscan = options.allowRemoteFullscan === true;
|
|
910
|
+
|
|
911
|
+
if (!source.source) {
|
|
912
|
+
return {
|
|
913
|
+
source_name: source.name || 'registry',
|
|
914
|
+
shard_sources: [],
|
|
915
|
+
matched_count: 0,
|
|
916
|
+
candidates: [],
|
|
917
|
+
warnings: ['registry source is empty']
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const warnings = [];
|
|
922
|
+
let shardSources = [];
|
|
923
|
+
if (source.index_url) {
|
|
924
|
+
try {
|
|
925
|
+
const indexPayload = await loadRegistryPayload(projectPath, source.index_url, fileSystem);
|
|
926
|
+
shardSources = collectRegistryShardSources(indexPayload, queryTokens, maxShards);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
warnings.push(`failed to load registry index (${source.index_url}): ${error.message}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (shardSources.length === 0) {
|
|
933
|
+
if (allowRemoteFullscan) {
|
|
934
|
+
shardSources = [source.source];
|
|
935
|
+
warnings.push('remote index unavailable; fallback to full-source scan');
|
|
936
|
+
} else {
|
|
937
|
+
warnings.push('remote index unavailable and full-source scan disabled');
|
|
938
|
+
return {
|
|
939
|
+
source_name: source.name || 'registry',
|
|
940
|
+
shard_sources: [],
|
|
941
|
+
matched_count: 0,
|
|
942
|
+
candidates: [],
|
|
943
|
+
warnings
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const candidates = [];
|
|
949
|
+
for (const shardSource of shardSources) {
|
|
950
|
+
try {
|
|
951
|
+
const payload = await loadRegistryPayload(projectPath, shardSource, fileSystem);
|
|
952
|
+
const entries = extractRegistryEntries(payload, source.name || 'registry');
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
if (requestedStatus && normalizeStatus(entry.status, 'candidate') !== requestedStatus) {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const matchScore = scoreSearchMatch(entry, queryTokens);
|
|
958
|
+
if (matchScore <= 0) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
candidates.push({
|
|
962
|
+
id: entry.id,
|
|
963
|
+
entry_source: 'registry-remote',
|
|
964
|
+
registry_source: source.name || 'registry',
|
|
965
|
+
status: entry.status,
|
|
966
|
+
quality_score: entry.quality_score,
|
|
967
|
+
title: entry.title,
|
|
968
|
+
fingerprint: entry.fingerprint,
|
|
969
|
+
tags: normalizeStringList(entry.tags),
|
|
970
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
971
|
+
match_score: matchScore,
|
|
972
|
+
updated_at: entry.updated_at
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
} catch (error) {
|
|
976
|
+
warnings.push(`failed to load registry shard (${shardSource}): ${error.message}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const deduped = new Map();
|
|
981
|
+
for (const item of candidates) {
|
|
982
|
+
const key = normalizeText(item.fingerprint || item.id);
|
|
983
|
+
const existing = deduped.get(key);
|
|
984
|
+
if (!existing || Number(item.match_score || 0) >= Number(existing.match_score || 0)) {
|
|
985
|
+
deduped.set(key, item);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return {
|
|
990
|
+
source_name: source.name || 'registry',
|
|
991
|
+
shard_sources: shardSources,
|
|
992
|
+
matched_count: deduped.size,
|
|
993
|
+
candidates: Array.from(deduped.values()),
|
|
994
|
+
warnings
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
399
998
|
function printRecordSummary(result) {
|
|
400
999
|
const action = result.created ? 'Recorded new entry' : 'Updated duplicate fingerprint';
|
|
401
1000
|
console.log(chalk.green(`✓ ${action}`));
|
|
@@ -529,6 +1128,8 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
529
1128
|
const includeVerified = options.includeVerified === true;
|
|
530
1129
|
|
|
531
1130
|
const inspected = [];
|
|
1131
|
+
const mitigationInspected = [];
|
|
1132
|
+
const mitigationBlocked = [];
|
|
532
1133
|
for (const summary of index.entries) {
|
|
533
1134
|
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
534
1135
|
if (!entry) {
|
|
@@ -536,12 +1137,34 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
536
1137
|
}
|
|
537
1138
|
|
|
538
1139
|
const status = normalizeStatus(entry.status, 'candidate');
|
|
1140
|
+
const risk = evaluateEntryRisk(entry);
|
|
1141
|
+
const mitigation = evaluateTemporaryMitigationPolicy(entry);
|
|
1142
|
+
if (mitigation) {
|
|
1143
|
+
const mitigationItem = {
|
|
1144
|
+
id: entry.id,
|
|
1145
|
+
title: entry.title,
|
|
1146
|
+
status,
|
|
1147
|
+
risk,
|
|
1148
|
+
quality_score: Number(entry.quality_score || 0),
|
|
1149
|
+
tags: normalizeStringList(entry.tags),
|
|
1150
|
+
updated_at: entry.updated_at,
|
|
1151
|
+
temporary_mitigation: mitigation
|
|
1152
|
+
};
|
|
1153
|
+
mitigationInspected.push(mitigationItem);
|
|
1154
|
+
if (Array.isArray(mitigation.policy_violations) && mitigation.policy_violations.length > 0) {
|
|
1155
|
+
mitigationBlocked.push({
|
|
1156
|
+
...mitigationItem,
|
|
1157
|
+
block_reasons: ['temporary_mitigation_policy'],
|
|
1158
|
+
policy_violations: mitigation.policy_violations
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
539
1163
|
const unresolved = status === 'candidate' || (includeVerified && status === 'verified');
|
|
540
1164
|
if (!unresolved) {
|
|
541
1165
|
continue;
|
|
542
1166
|
}
|
|
543
1167
|
|
|
544
|
-
const risk = evaluateEntryRisk(entry);
|
|
545
1168
|
inspected.push({
|
|
546
1169
|
id: entry.id,
|
|
547
1170
|
title: entry.title,
|
|
@@ -553,9 +1176,43 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
553
1176
|
});
|
|
554
1177
|
}
|
|
555
1178
|
|
|
556
|
-
const
|
|
1179
|
+
const riskBlocked = inspected
|
|
557
1180
|
.filter((item) => riskRank(item.risk) >= riskRank(minRisk))
|
|
1181
|
+
.map((item) => ({
|
|
1182
|
+
...item,
|
|
1183
|
+
block_reasons: ['risk_threshold']
|
|
1184
|
+
}));
|
|
1185
|
+
|
|
1186
|
+
const blockedById = new Map();
|
|
1187
|
+
for (const item of riskBlocked) {
|
|
1188
|
+
blockedById.set(item.id, {
|
|
1189
|
+
...item,
|
|
1190
|
+
policy_violations: []
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
for (const item of mitigationBlocked) {
|
|
1194
|
+
const existing = blockedById.get(item.id);
|
|
1195
|
+
if (!existing) {
|
|
1196
|
+
blockedById.set(item.id, {
|
|
1197
|
+
...item,
|
|
1198
|
+
policy_violations: Array.isArray(item.policy_violations) ? item.policy_violations : []
|
|
1199
|
+
});
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
existing.block_reasons = normalizeStringList(existing.block_reasons, item.block_reasons);
|
|
1203
|
+
existing.policy_violations = normalizeStringList(existing.policy_violations, item.policy_violations);
|
|
1204
|
+
if (!existing.temporary_mitigation && item.temporary_mitigation) {
|
|
1205
|
+
existing.temporary_mitigation = item.temporary_mitigation;
|
|
1206
|
+
}
|
|
1207
|
+
blockedById.set(existing.id, existing);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const blocked = Array.from(blockedById.values())
|
|
558
1211
|
.sort((left, right) => {
|
|
1212
|
+
const mitigationDiff = Number((right.policy_violations || []).length > 0) - Number((left.policy_violations || []).length > 0);
|
|
1213
|
+
if (mitigationDiff !== 0) {
|
|
1214
|
+
return mitigationDiff;
|
|
1215
|
+
}
|
|
559
1216
|
const riskDiff = riskRank(right.risk) - riskRank(left.risk);
|
|
560
1217
|
if (riskDiff !== 0) {
|
|
561
1218
|
return riskDiff;
|
|
@@ -571,10 +1228,14 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
571
1228
|
mode: 'errorbook-release-gate',
|
|
572
1229
|
gate: {
|
|
573
1230
|
min_risk: minRisk,
|
|
574
|
-
include_verified: includeVerified
|
|
1231
|
+
include_verified: includeVerified,
|
|
1232
|
+
mitigation_policy_enforced: true
|
|
575
1233
|
},
|
|
576
1234
|
passed: blocked.length === 0,
|
|
577
1235
|
inspected_count: inspected.length,
|
|
1236
|
+
risk_blocked_count: riskBlocked.length,
|
|
1237
|
+
mitigation_inspected_count: mitigationInspected.length,
|
|
1238
|
+
mitigation_blocked_count: mitigationBlocked.length,
|
|
578
1239
|
blocked_count: blocked.length,
|
|
579
1240
|
blocked_entries: blocked
|
|
580
1241
|
};
|
|
@@ -628,6 +1289,15 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
628
1289
|
entry = mergeEntry(existingEntry, normalized);
|
|
629
1290
|
deduplicated = true;
|
|
630
1291
|
} else {
|
|
1292
|
+
const temporaryMitigation = normalizeExistingTemporaryMitigation(normalized.temporary_mitigation);
|
|
1293
|
+
const mitigationPayload = temporaryMitigation.enabled
|
|
1294
|
+
? {
|
|
1295
|
+
...temporaryMitigation,
|
|
1296
|
+
created_at: nowIso(),
|
|
1297
|
+
updated_at: nowIso(),
|
|
1298
|
+
resolved_at: ''
|
|
1299
|
+
}
|
|
1300
|
+
: { enabled: false };
|
|
631
1301
|
entry = {
|
|
632
1302
|
api_version: ERRORBOOK_ENTRY_API_VERSION,
|
|
633
1303
|
id: createEntryId(),
|
|
@@ -643,6 +1313,7 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
643
1313
|
ontology_tags: normalized.ontology_tags,
|
|
644
1314
|
status: normalized.status,
|
|
645
1315
|
source: normalized.source,
|
|
1316
|
+
temporary_mitigation: mitigationPayload,
|
|
646
1317
|
notes: normalized.notes || '',
|
|
647
1318
|
occurrences: 1
|
|
648
1319
|
};
|
|
@@ -680,6 +1351,154 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
680
1351
|
return result;
|
|
681
1352
|
}
|
|
682
1353
|
|
|
1354
|
+
async function runErrorbookExportCommand(options = {}, dependencies = {}) {
|
|
1355
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1356
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1357
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
1358
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1359
|
+
exportPath: options.out
|
|
1360
|
+
});
|
|
1361
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
1362
|
+
|
|
1363
|
+
const statuses = normalizeStatusList(options.statuses || options.status || 'promoted', ['promoted']);
|
|
1364
|
+
const minQuality = Number.isFinite(Number(options.minQuality))
|
|
1365
|
+
? Number(options.minQuality)
|
|
1366
|
+
: 75;
|
|
1367
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
1368
|
+
? Number(options.limit)
|
|
1369
|
+
: 0;
|
|
1370
|
+
|
|
1371
|
+
const selected = [];
|
|
1372
|
+
for (const summary of index.entries) {
|
|
1373
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
1374
|
+
if (!entry) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const status = normalizeStatus(entry.status, 'candidate');
|
|
1378
|
+
if (!statuses.includes(status)) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
if (Number(entry.quality_score || 0) < minQuality) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
selected.push({
|
|
1385
|
+
id: entry.id,
|
|
1386
|
+
fingerprint: entry.fingerprint,
|
|
1387
|
+
title: entry.title,
|
|
1388
|
+
symptom: entry.symptom,
|
|
1389
|
+
root_cause: entry.root_cause,
|
|
1390
|
+
fix_actions: normalizeStringList(entry.fix_actions),
|
|
1391
|
+
verification_evidence: normalizeStringList(entry.verification_evidence),
|
|
1392
|
+
tags: normalizeStringList(entry.tags),
|
|
1393
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
1394
|
+
status,
|
|
1395
|
+
quality_score: Number(entry.quality_score || 0),
|
|
1396
|
+
updated_at: entry.updated_at,
|
|
1397
|
+
source: entry.source || {},
|
|
1398
|
+
temporary_mitigation: normalizeExistingTemporaryMitigation(entry.temporary_mitigation)
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
selected.sort((left, right) => {
|
|
1403
|
+
const qualityDiff = Number(right.quality_score || 0) - Number(left.quality_score || 0);
|
|
1404
|
+
if (qualityDiff !== 0) {
|
|
1405
|
+
return qualityDiff;
|
|
1406
|
+
}
|
|
1407
|
+
return `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
const entries = limit > 0 ? selected.slice(0, limit) : selected;
|
|
1411
|
+
const payload = {
|
|
1412
|
+
api_version: ERRORBOOK_REGISTRY_API_VERSION,
|
|
1413
|
+
generated_at: nowIso(),
|
|
1414
|
+
source: {
|
|
1415
|
+
project: path.basename(projectPath),
|
|
1416
|
+
statuses,
|
|
1417
|
+
min_quality: minQuality
|
|
1418
|
+
},
|
|
1419
|
+
total_entries: entries.length,
|
|
1420
|
+
entries
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
await fileSystem.ensureDir(path.dirname(registryPaths.exportFile));
|
|
1424
|
+
await fileSystem.writeJson(registryPaths.exportFile, payload, { spaces: 2 });
|
|
1425
|
+
|
|
1426
|
+
const result = {
|
|
1427
|
+
mode: 'errorbook-export',
|
|
1428
|
+
out_file: registryPaths.exportFile,
|
|
1429
|
+
statuses,
|
|
1430
|
+
min_quality: minQuality,
|
|
1431
|
+
total_entries: entries.length
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
if (options.json) {
|
|
1435
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1436
|
+
} else if (!options.silent) {
|
|
1437
|
+
console.log(chalk.green('✓ Exported curated errorbook entries'));
|
|
1438
|
+
console.log(chalk.gray(` out: ${registryPaths.exportFile}`));
|
|
1439
|
+
console.log(chalk.gray(` total: ${entries.length}`));
|
|
1440
|
+
console.log(chalk.gray(` statuses: ${statuses.join(', ')}`));
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return result;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function runErrorbookSyncRegistryCommand(options = {}, dependencies = {}) {
|
|
1447
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1448
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1449
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1450
|
+
configPath: options.config,
|
|
1451
|
+
cachePath: options.cache
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
const config = await readErrorbookRegistryConfig(registryPaths, fileSystem);
|
|
1455
|
+
const sourceOption = normalizeText(options.source);
|
|
1456
|
+
const configuredSource = config.sources.find((item) => item.enabled && item.source);
|
|
1457
|
+
const source = sourceOption || (configuredSource ? configuredSource.source : '');
|
|
1458
|
+
if (!source) {
|
|
1459
|
+
throw new Error('registry source is required (use --source or configure .sce/config/errorbook-registry.json)');
|
|
1460
|
+
}
|
|
1461
|
+
const sourceName = normalizeText(options.sourceName)
|
|
1462
|
+
|| (configuredSource ? configuredSource.name : '')
|
|
1463
|
+
|| 'registry';
|
|
1464
|
+
|
|
1465
|
+
const payload = await loadRegistryPayload(projectPath, source, fileSystem);
|
|
1466
|
+
const entries = extractRegistryEntries(payload, sourceName);
|
|
1467
|
+
const cachePath = resolveProjectPath(projectPath, options.cache, config.cache_file || DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
1468
|
+
const cachePayload = {
|
|
1469
|
+
api_version: ERRORBOOK_REGISTRY_CACHE_API_VERSION,
|
|
1470
|
+
synced_at: nowIso(),
|
|
1471
|
+
source: {
|
|
1472
|
+
name: sourceName,
|
|
1473
|
+
uri: source
|
|
1474
|
+
},
|
|
1475
|
+
total_entries: entries.length,
|
|
1476
|
+
entries
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
await fileSystem.ensureDir(path.dirname(cachePath));
|
|
1480
|
+
await fileSystem.writeJson(cachePath, cachePayload, { spaces: 2 });
|
|
1481
|
+
|
|
1482
|
+
const result = {
|
|
1483
|
+
mode: 'errorbook-sync-registry',
|
|
1484
|
+
source,
|
|
1485
|
+
source_name: sourceName,
|
|
1486
|
+
cache_file: cachePath,
|
|
1487
|
+
total_entries: entries.length
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
if (options.json) {
|
|
1491
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1492
|
+
} else if (!options.silent) {
|
|
1493
|
+
console.log(chalk.green('✓ Synced external errorbook registry'));
|
|
1494
|
+
console.log(chalk.gray(` source: ${source}`));
|
|
1495
|
+
console.log(chalk.gray(` cache: ${cachePath}`));
|
|
1496
|
+
console.log(chalk.gray(` entries: ${entries.length}`));
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
return result;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
683
1502
|
async function runErrorbookListCommand(options = {}, dependencies = {}) {
|
|
684
1503
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
685
1504
|
const fileSystem = dependencies.fileSystem || fs;
|
|
@@ -765,6 +1584,7 @@ async function runErrorbookShowCommand(options = {}, dependencies = {}) {
|
|
|
765
1584
|
if (options.json) {
|
|
766
1585
|
console.log(JSON.stringify(result, null, 2));
|
|
767
1586
|
} else if (!options.silent) {
|
|
1587
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
768
1588
|
console.log(chalk.cyan.bold(entry.title));
|
|
769
1589
|
console.log(chalk.gray(`id: ${entry.id}`));
|
|
770
1590
|
console.log(chalk.gray(`status: ${entry.status}`));
|
|
@@ -775,6 +1595,16 @@ async function runErrorbookShowCommand(options = {}, dependencies = {}) {
|
|
|
775
1595
|
console.log(chalk.gray(`fix_actions: ${entry.fix_actions.join(' | ')}`));
|
|
776
1596
|
console.log(chalk.gray(`verification: ${entry.verification_evidence.join(' | ') || '(none)'}`));
|
|
777
1597
|
console.log(chalk.gray(`ontology: ${entry.ontology_tags.join(', ') || '(none)'}`));
|
|
1598
|
+
if (mitigation.enabled) {
|
|
1599
|
+
const active = !normalizeText(mitigation.resolved_at);
|
|
1600
|
+
console.log(chalk.gray(`temporary_mitigation: ${active ? 'active' : 'resolved'}`));
|
|
1601
|
+
console.log(chalk.gray(` exit: ${mitigation.exit_criteria || '(none)'}`));
|
|
1602
|
+
console.log(chalk.gray(` cleanup: ${mitigation.cleanup_task || '(none)'}`));
|
|
1603
|
+
console.log(chalk.gray(` deadline: ${mitigation.deadline_at || '(none)'}`));
|
|
1604
|
+
if (mitigation.resolved_at) {
|
|
1605
|
+
console.log(chalk.gray(` resolved_at: ${mitigation.resolved_at}`));
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
778
1608
|
}
|
|
779
1609
|
|
|
780
1610
|
return result;
|
|
@@ -795,9 +1625,15 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
795
1625
|
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
796
1626
|
? Number(options.limit)
|
|
797
1627
|
: 10;
|
|
798
|
-
const tokens = query
|
|
1628
|
+
const tokens = tokenizeQueryText(query);
|
|
1629
|
+
const includeRegistry = options.includeRegistry === true;
|
|
799
1630
|
|
|
800
1631
|
const candidates = [];
|
|
1632
|
+
let localMatched = 0;
|
|
1633
|
+
let registryMatched = 0;
|
|
1634
|
+
let registryCacheMatched = 0;
|
|
1635
|
+
let registryRemoteMatched = 0;
|
|
1636
|
+
const registryWarnings = [];
|
|
801
1637
|
for (const summary of index.entries) {
|
|
802
1638
|
if (requestedStatus && summary.status !== requestedStatus) {
|
|
803
1639
|
continue;
|
|
@@ -810,8 +1646,10 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
810
1646
|
if (matchScore <= 0) {
|
|
811
1647
|
continue;
|
|
812
1648
|
}
|
|
1649
|
+
localMatched += 1;
|
|
813
1650
|
candidates.push({
|
|
814
1651
|
id: entry.id,
|
|
1652
|
+
entry_source: 'local',
|
|
815
1653
|
status: entry.status,
|
|
816
1654
|
quality_score: entry.quality_score,
|
|
817
1655
|
title: entry.title,
|
|
@@ -823,7 +1661,96 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
823
1661
|
});
|
|
824
1662
|
}
|
|
825
1663
|
|
|
826
|
-
|
|
1664
|
+
if (includeRegistry) {
|
|
1665
|
+
const configPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1666
|
+
configPath: options.config,
|
|
1667
|
+
cachePath: options.registryCache
|
|
1668
|
+
});
|
|
1669
|
+
const registryConfig = await readErrorbookRegistryConfig(configPaths, fileSystem);
|
|
1670
|
+
const registryMode = normalizeRegistryMode(options.registryMode, registryConfig.search_mode || 'cache');
|
|
1671
|
+
const useCache = registryMode === 'cache' || registryMode === 'hybrid';
|
|
1672
|
+
const useRemote = registryMode === 'remote' || registryMode === 'hybrid';
|
|
1673
|
+
|
|
1674
|
+
if (useRemote) {
|
|
1675
|
+
const configuredSources = Array.isArray(registryConfig.sources)
|
|
1676
|
+
? registryConfig.sources.filter((item) => item.enabled && item.source)
|
|
1677
|
+
: [];
|
|
1678
|
+
const overrideSource = normalizeText(options.registrySource);
|
|
1679
|
+
const remoteSources = overrideSource
|
|
1680
|
+
? [normalizeRegistrySource({
|
|
1681
|
+
name: normalizeText(options.registrySourceName) || 'override',
|
|
1682
|
+
source: overrideSource,
|
|
1683
|
+
index_url: normalizeText(options.registryIndex)
|
|
1684
|
+
})]
|
|
1685
|
+
: configuredSources;
|
|
1686
|
+
|
|
1687
|
+
for (const source of remoteSources) {
|
|
1688
|
+
const remoteResult = await searchRegistryRemote({
|
|
1689
|
+
source,
|
|
1690
|
+
query,
|
|
1691
|
+
queryTokens: tokens,
|
|
1692
|
+
requestedStatus,
|
|
1693
|
+
maxShards: options.registryMaxShards,
|
|
1694
|
+
allowRemoteFullscan: options.allowRemoteFullscan === true
|
|
1695
|
+
}, {
|
|
1696
|
+
projectPath,
|
|
1697
|
+
fileSystem
|
|
1698
|
+
});
|
|
1699
|
+
registryRemoteMatched += Number(remoteResult.matched_count || 0);
|
|
1700
|
+
registryMatched += Number(remoteResult.matched_count || 0);
|
|
1701
|
+
if (Array.isArray(remoteResult.warnings)) {
|
|
1702
|
+
registryWarnings.push(...remoteResult.warnings);
|
|
1703
|
+
}
|
|
1704
|
+
if (Array.isArray(remoteResult.candidates)) {
|
|
1705
|
+
candidates.push(...remoteResult.candidates);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
if (useCache) {
|
|
1711
|
+
const cachePath = resolveProjectPath(
|
|
1712
|
+
projectPath,
|
|
1713
|
+
options.registryCache,
|
|
1714
|
+
registryConfig.cache_file || DEFAULT_ERRORBOOK_REGISTRY_CACHE
|
|
1715
|
+
);
|
|
1716
|
+
const registryCache = await loadRegistryCache(projectPath, cachePath, fileSystem);
|
|
1717
|
+
for (const entry of registryCache.entries) {
|
|
1718
|
+
if (requestedStatus && normalizeStatus(entry.status, 'candidate') !== requestedStatus) {
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
const matchScore = scoreSearchMatch(entry, tokens);
|
|
1722
|
+
if (matchScore <= 0) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
registryMatched += 1;
|
|
1726
|
+
registryCacheMatched += 1;
|
|
1727
|
+
candidates.push({
|
|
1728
|
+
id: entry.id,
|
|
1729
|
+
entry_source: 'registry-cache',
|
|
1730
|
+
status: entry.status,
|
|
1731
|
+
quality_score: entry.quality_score,
|
|
1732
|
+
title: entry.title,
|
|
1733
|
+
fingerprint: entry.fingerprint,
|
|
1734
|
+
tags: normalizeStringList(entry.tags),
|
|
1735
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
1736
|
+
match_score: matchScore,
|
|
1737
|
+
updated_at: entry.updated_at
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const dedupedCandidates = new Map();
|
|
1744
|
+
for (const item of candidates) {
|
|
1745
|
+
const key = normalizeText(item.fingerprint || item.id);
|
|
1746
|
+
const existing = dedupedCandidates.get(key);
|
|
1747
|
+
if (!existing || Number(item.match_score || 0) >= Number(existing.match_score || 0)) {
|
|
1748
|
+
dedupedCandidates.set(key, item);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const sortedCandidates = Array.from(dedupedCandidates.values());
|
|
1752
|
+
|
|
1753
|
+
sortedCandidates.sort((left, right) => {
|
|
827
1754
|
const scoreDiff = Number(right.match_score) - Number(left.match_score);
|
|
828
1755
|
if (scoreDiff !== 0) {
|
|
829
1756
|
return scoreDiff;
|
|
@@ -834,8 +1761,16 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
834
1761
|
const result = {
|
|
835
1762
|
mode: 'errorbook-find',
|
|
836
1763
|
query,
|
|
837
|
-
|
|
838
|
-
|
|
1764
|
+
include_registry: includeRegistry,
|
|
1765
|
+
source_breakdown: {
|
|
1766
|
+
local_results: localMatched,
|
|
1767
|
+
registry_results: registryMatched,
|
|
1768
|
+
registry_cache_results: registryCacheMatched,
|
|
1769
|
+
registry_remote_results: registryRemoteMatched
|
|
1770
|
+
},
|
|
1771
|
+
warnings: normalizeStringList(registryWarnings),
|
|
1772
|
+
total_results: sortedCandidates.length,
|
|
1773
|
+
entries: sortedCandidates.slice(0, limit)
|
|
839
1774
|
};
|
|
840
1775
|
|
|
841
1776
|
if (options.json) {
|
|
@@ -880,6 +1815,7 @@ async function runErrorbookPromoteCommand(options = {}, dependencies = {}) {
|
|
|
880
1815
|
|
|
881
1816
|
entry.status = 'promoted';
|
|
882
1817
|
entry.promoted_at = nowIso();
|
|
1818
|
+
entry.temporary_mitigation = markTemporaryMitigationResolved(entry, entry.promoted_at);
|
|
883
1819
|
entry.updated_at = entry.promoted_at;
|
|
884
1820
|
await writeErrorbookEntry(paths, entry, fileSystem);
|
|
885
1821
|
|
|
@@ -971,6 +1907,7 @@ async function runErrorbookDeprecateCommand(options = {}, dependencies = {}) {
|
|
|
971
1907
|
|
|
972
1908
|
entry.status = 'deprecated';
|
|
973
1909
|
entry.updated_at = nowIso();
|
|
1910
|
+
entry.temporary_mitigation = markTemporaryMitigationResolved(entry, entry.updated_at);
|
|
974
1911
|
entry.deprecated_at = entry.updated_at;
|
|
975
1912
|
entry.deprecation = {
|
|
976
1913
|
reason,
|
|
@@ -1109,6 +2046,11 @@ function registerErrorbookCommands(program) {
|
|
|
1109
2046
|
.option('--tags <csv>', 'Tags, comma-separated')
|
|
1110
2047
|
.option('--ontology <csv>', `Ontology focus tags (${ERRORBOOK_ONTOLOGY_TAGS.join(', ')})`)
|
|
1111
2048
|
.option('--status <status>', 'candidate|verified', 'candidate')
|
|
2049
|
+
.option('--temporary-mitigation', 'Mark this entry as temporary fallback/mitigation (requires governance fields)')
|
|
2050
|
+
.option('--mitigation-reason <text>', 'Temporary mitigation reason/context')
|
|
2051
|
+
.option('--mitigation-exit <text>', 'Exit criteria that define mitigation cleanup completion')
|
|
2052
|
+
.option('--mitigation-cleanup <text>', 'Cleanup task/spec to remove temporary mitigation')
|
|
2053
|
+
.option('--mitigation-deadline <iso>', 'Deadline for mitigation cleanup (ISO datetime)')
|
|
1112
2054
|
.option('--fingerprint <text>', 'Custom deduplication fingerprint')
|
|
1113
2055
|
.option('--from <path>', 'Load payload from JSON file')
|
|
1114
2056
|
.option('--spec <spec>', 'Related spec id/name')
|
|
@@ -1156,6 +2098,15 @@ function registerErrorbookCommands(program) {
|
|
|
1156
2098
|
.requiredOption('--query <text>', 'Search query')
|
|
1157
2099
|
.option('--status <status>', `Filter by status (${ERRORBOOK_STATUSES.join(', ')})`)
|
|
1158
2100
|
.option('--limit <n>', 'Maximum entries returned', parseInt, 10)
|
|
2101
|
+
.option('--include-registry', 'Include external registry entries in search')
|
|
2102
|
+
.option('--registry-mode <mode>', 'Registry lookup mode (cache|remote|hybrid)')
|
|
2103
|
+
.option('--registry-source <url-or-path>', 'Override registry source (for remote mode)')
|
|
2104
|
+
.option('--registry-source-name <name>', 'Override registry source label')
|
|
2105
|
+
.option('--registry-index <url-or-path>', 'Override registry index source (for remote mode)')
|
|
2106
|
+
.option('--registry-max-shards <n>', 'Max remote shards to fetch per query', parseInt, 8)
|
|
2107
|
+
.option('--allow-remote-fullscan', 'Allow remote full-source fallback when index is unavailable')
|
|
2108
|
+
.option('--registry-cache <path>', `Registry cache path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2109
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
1159
2110
|
.option('--json', 'Emit machine-readable JSON')
|
|
1160
2111
|
.action(async (options) => {
|
|
1161
2112
|
try {
|
|
@@ -1165,6 +2116,38 @@ function registerErrorbookCommands(program) {
|
|
|
1165
2116
|
}
|
|
1166
2117
|
});
|
|
1167
2118
|
|
|
2119
|
+
errorbook
|
|
2120
|
+
.command('export')
|
|
2121
|
+
.description('Export curated local entries for external registry publication')
|
|
2122
|
+
.option('--status <csv>', 'Statuses to include (csv, default: promoted)', 'promoted')
|
|
2123
|
+
.option('--min-quality <n>', 'Minimum quality score (default: 75)', parseInt)
|
|
2124
|
+
.option('--limit <n>', 'Maximum entries exported', parseInt)
|
|
2125
|
+
.option('--out <path>', `Output file (default: ${DEFAULT_ERRORBOOK_REGISTRY_EXPORT})`)
|
|
2126
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2127
|
+
.action(async (options) => {
|
|
2128
|
+
try {
|
|
2129
|
+
await runErrorbookExportCommand(options);
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
emitCommandError(error, options.json);
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
errorbook
|
|
2136
|
+
.command('sync-registry')
|
|
2137
|
+
.description('Sync external errorbook registry to local cache')
|
|
2138
|
+
.option('--source <url-or-path>', 'Registry source JSON (https://... or local file)')
|
|
2139
|
+
.option('--source-name <name>', 'Registry source name label')
|
|
2140
|
+
.option('--cache <path>', `Registry cache output path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2141
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
2142
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2143
|
+
.action(async (options) => {
|
|
2144
|
+
try {
|
|
2145
|
+
await runErrorbookSyncRegistryCommand(options);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
emitCommandError(error, options.json);
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
|
|
1168
2151
|
errorbook
|
|
1169
2152
|
.command('promote <id>')
|
|
1170
2153
|
.description('Promote entry after strict quality gate')
|
|
@@ -1179,7 +2162,7 @@ function registerErrorbookCommands(program) {
|
|
|
1179
2162
|
|
|
1180
2163
|
errorbook
|
|
1181
2164
|
.command('release-gate')
|
|
1182
|
-
.description('Block release on unresolved high-risk
|
|
2165
|
+
.description('Block release on unresolved high-risk entries and temporary-mitigation policy violations')
|
|
1183
2166
|
.option('--min-risk <level>', 'Risk threshold (low|medium|high)', 'high')
|
|
1184
2167
|
.option('--include-verified', 'Also inspect verified (non-promoted) entries')
|
|
1185
2168
|
.option('--fail-on-block', 'Exit with error when gate is blocked')
|
|
@@ -1224,15 +2207,22 @@ module.exports = {
|
|
|
1224
2207
|
ERRORBOOK_STATUSES,
|
|
1225
2208
|
ERRORBOOK_ONTOLOGY_TAGS,
|
|
1226
2209
|
ERRORBOOK_RISK_LEVELS,
|
|
2210
|
+
TEMPORARY_MITIGATION_TAG,
|
|
2211
|
+
DEFAULT_ERRORBOOK_REGISTRY_CONFIG,
|
|
2212
|
+
DEFAULT_ERRORBOOK_REGISTRY_CACHE,
|
|
2213
|
+
DEFAULT_ERRORBOOK_REGISTRY_EXPORT,
|
|
1227
2214
|
HIGH_RISK_SIGNAL_TAGS,
|
|
1228
2215
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
1229
2216
|
resolveErrorbookPaths,
|
|
2217
|
+
resolveErrorbookRegistryPaths,
|
|
1230
2218
|
normalizeOntologyTags,
|
|
1231
2219
|
normalizeRecordPayload,
|
|
1232
2220
|
scoreQuality,
|
|
1233
2221
|
evaluateEntryRisk,
|
|
1234
2222
|
evaluateErrorbookReleaseGate,
|
|
1235
2223
|
runErrorbookRecordCommand,
|
|
2224
|
+
runErrorbookExportCommand,
|
|
2225
|
+
runErrorbookSyncRegistryCommand,
|
|
1236
2226
|
runErrorbookListCommand,
|
|
1237
2227
|
runErrorbookShowCommand,
|
|
1238
2228
|
runErrorbookFindCommand,
|