scene-capability-engine 3.3.17 → 3.3.18
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 +17 -0
- package/bin/scene-capability-engine.js +4 -0
- package/docs/command-reference.md +43 -0
- package/lib/commands/errorbook.js +1075 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.3.18] - 2026-02-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Curated `errorbook` command set for high-signal failure remediation knowledge:
|
|
14
|
+
- `sce errorbook record`
|
|
15
|
+
- `sce errorbook list`
|
|
16
|
+
- `sce errorbook show <id>`
|
|
17
|
+
- `sce errorbook find --query <text>`
|
|
18
|
+
- `sce errorbook promote <id>`
|
|
19
|
+
- `sce errorbook deprecate <id> --reason <text>`
|
|
20
|
+
- `sce errorbook requalify <id> --status <candidate|verified>`
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Added strict curation/promotion policy to command reference (`宁缺毋滥,优胜略汰`):
|
|
24
|
+
- fingerprint-based deduplication on record
|
|
25
|
+
- promotion gate requires validated root cause, fix actions, verification evidence, ontology tags, and minimum quality score
|
|
26
|
+
|
|
10
27
|
## [3.3.17] - 2026-02-26
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -817,6 +817,10 @@ registerLockCommands(program);
|
|
|
817
817
|
const { registerKnowledgeCommands } = require('../lib/commands/knowledge');
|
|
818
818
|
registerKnowledgeCommands(program);
|
|
819
819
|
|
|
820
|
+
// Errorbook commands
|
|
821
|
+
const { registerErrorbookCommands } = require('../lib/commands/errorbook');
|
|
822
|
+
registerErrorbookCommands(program);
|
|
823
|
+
|
|
820
824
|
// Studio orchestration commands
|
|
821
825
|
const { registerStudioCommands } = require('../lib/commands/studio');
|
|
822
826
|
registerStudioCommands(program);
|
|
@@ -318,6 +318,49 @@ Rate-limit profiles:
|
|
|
318
318
|
- `balanced`: default profile for normal multi-agent runs
|
|
319
319
|
- `aggressive`: higher throughput with lower protection margins
|
|
320
320
|
|
|
321
|
+
### Errorbook (Curated Failure Knowledge)
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
# Record curated remediation entry
|
|
325
|
+
sce errorbook record \
|
|
326
|
+
--title "Order approval queue saturation" \
|
|
327
|
+
--symptom "Approval queue backlog exceeded SLA" \
|
|
328
|
+
--root-cause "Worker pool under-provisioned and retries amplified load" \
|
|
329
|
+
--fix-action "Increase workers from 4 to 8" \
|
|
330
|
+
--fix-action "Reduce retry burst window" \
|
|
331
|
+
--verification "Load test confirms p95 < SLA threshold" \
|
|
332
|
+
--ontology "entity,relation,decision_policy" \
|
|
333
|
+
--tags "moqui,order,performance" \
|
|
334
|
+
--status verified \
|
|
335
|
+
--json
|
|
336
|
+
|
|
337
|
+
# List/show/find entries
|
|
338
|
+
sce errorbook list --status promoted --min-quality 75 --json
|
|
339
|
+
sce errorbook show <entry-id> --json
|
|
340
|
+
sce errorbook find --query "approve order timeout" --limit 10 --json
|
|
341
|
+
|
|
342
|
+
# Promote only after strict gate checks pass
|
|
343
|
+
sce errorbook promote <entry-id> --json
|
|
344
|
+
|
|
345
|
+
# Eliminate obsolete/low-value entries (curation)
|
|
346
|
+
sce errorbook deprecate <entry-id> --reason "superseded by v2 policy" --json
|
|
347
|
+
|
|
348
|
+
# Requalify deprecated entry after remediation review
|
|
349
|
+
sce errorbook requalify <entry-id> --status verified --json
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Curated quality policy (`宁缺毋滥,优胜略汰`) defaults:
|
|
353
|
+
- `record` requires: `title`, `symptom`, `root_cause`, and at least one `fix_action`.
|
|
354
|
+
- Fingerprint dedup is automatic; repeated records merge evidence and increment occurrence count.
|
|
355
|
+
- `promote` enforces strict gate:
|
|
356
|
+
- `root_cause` present
|
|
357
|
+
- `fix_actions` non-empty
|
|
358
|
+
- `verification_evidence` non-empty
|
|
359
|
+
- `ontology_tags` non-empty
|
|
360
|
+
- `quality_score >= 75`
|
|
361
|
+
- `deprecate` requires explicit `--reason` to preserve elimination traceability.
|
|
362
|
+
- `requalify` only accepts `candidate|verified`; `promoted` must still go through `promote` gate.
|
|
363
|
+
|
|
321
364
|
### Studio Workflow
|
|
322
365
|
|
|
323
366
|
```bash
|
|
@@ -0,0 +1,1075 @@
|
|
|
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
|
+
|
|
41
|
+
function resolveErrorbookPaths(projectPath = process.cwd()) {
|
|
42
|
+
const baseDir = path.join(projectPath, '.sce', 'errorbook');
|
|
43
|
+
return {
|
|
44
|
+
projectPath,
|
|
45
|
+
baseDir,
|
|
46
|
+
entriesDir: path.join(baseDir, 'entries'),
|
|
47
|
+
indexFile: path.join(baseDir, 'index.json')
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nowIso() {
|
|
52
|
+
return new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeText(value) {
|
|
56
|
+
if (typeof value !== 'string') {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return value.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeCsv(value) {
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeStringList(...rawInputs) {
|
|
76
|
+
const merged = [];
|
|
77
|
+
for (const raw of rawInputs) {
|
|
78
|
+
if (Array.isArray(raw)) {
|
|
79
|
+
for (const item of raw) {
|
|
80
|
+
const normalized = normalizeText(`${item}`);
|
|
81
|
+
if (normalized) {
|
|
82
|
+
merged.push(normalized);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof raw === 'string') {
|
|
89
|
+
for (const item of normalizeCsv(raw)) {
|
|
90
|
+
const normalized = normalizeText(item);
|
|
91
|
+
if (normalized) {
|
|
92
|
+
merged.push(normalized);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Array.from(new Set(merged));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeOntologyTags(...rawInputs) {
|
|
102
|
+
const normalized = normalizeStringList(...rawInputs).map((item) => item.toLowerCase());
|
|
103
|
+
const mapped = normalized.map((item) => ONTOLOGY_TAG_ALIASES[item] || item);
|
|
104
|
+
const valid = mapped.filter((item) => ERRORBOOK_ONTOLOGY_TAGS.includes(item));
|
|
105
|
+
return Array.from(new Set(valid));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeStatus(input, fallback = 'candidate') {
|
|
109
|
+
const normalized = normalizeText(`${input || ''}`).toLowerCase();
|
|
110
|
+
if (!normalized) {
|
|
111
|
+
return fallback;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!ERRORBOOK_STATUSES.includes(normalized)) {
|
|
115
|
+
throw new Error(`status must be one of: ${ERRORBOOK_STATUSES.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function selectStatus(...candidates) {
|
|
122
|
+
let selected = 'candidate';
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const status = normalizeStatus(candidate, 'candidate');
|
|
125
|
+
if ((STATUS_RANK[status] || 0) > (STATUS_RANK[selected] || 0)) {
|
|
126
|
+
selected = status;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return selected;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createFingerprint(input = {}) {
|
|
133
|
+
const explicit = normalizeText(input.fingerprint);
|
|
134
|
+
if (explicit) {
|
|
135
|
+
return explicit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const basis = [
|
|
139
|
+
normalizeText(input.title).toLowerCase(),
|
|
140
|
+
normalizeText(input.symptom).toLowerCase(),
|
|
141
|
+
normalizeText(input.root_cause || input.rootCause).toLowerCase()
|
|
142
|
+
].join('|');
|
|
143
|
+
|
|
144
|
+
const digest = crypto.createHash('sha1').update(basis).digest('hex').slice(0, 16);
|
|
145
|
+
return `fp-${digest}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildDefaultIndex() {
|
|
149
|
+
return {
|
|
150
|
+
api_version: ERRORBOOK_INDEX_API_VERSION,
|
|
151
|
+
updated_at: nowIso(),
|
|
152
|
+
total_entries: 0,
|
|
153
|
+
entries: []
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function ensureErrorbookStorage(paths, fileSystem = fs) {
|
|
158
|
+
await fileSystem.ensureDir(paths.entriesDir);
|
|
159
|
+
if (!await fileSystem.pathExists(paths.indexFile)) {
|
|
160
|
+
await fileSystem.writeJson(paths.indexFile, buildDefaultIndex(), { spaces: 2 });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function readErrorbookIndex(paths, fileSystem = fs) {
|
|
165
|
+
await ensureErrorbookStorage(paths, fileSystem);
|
|
166
|
+
const index = await fileSystem.readJson(paths.indexFile);
|
|
167
|
+
if (!index || typeof index !== 'object' || !Array.isArray(index.entries)) {
|
|
168
|
+
return buildDefaultIndex();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
api_version: index.api_version || ERRORBOOK_INDEX_API_VERSION,
|
|
173
|
+
updated_at: index.updated_at || nowIso(),
|
|
174
|
+
total_entries: Number.isInteger(index.total_entries) ? index.total_entries : index.entries.length,
|
|
175
|
+
entries: index.entries
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function writeErrorbookIndex(paths, index, fileSystem = fs) {
|
|
180
|
+
const payload = {
|
|
181
|
+
...index,
|
|
182
|
+
api_version: ERRORBOOK_INDEX_API_VERSION,
|
|
183
|
+
updated_at: nowIso(),
|
|
184
|
+
total_entries: Array.isArray(index.entries) ? index.entries.length : 0
|
|
185
|
+
};
|
|
186
|
+
await fileSystem.ensureDir(path.dirname(paths.indexFile));
|
|
187
|
+
await fileSystem.writeJson(paths.indexFile, payload, { spaces: 2 });
|
|
188
|
+
return payload;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildEntryFilePath(paths, entryId) {
|
|
192
|
+
return path.join(paths.entriesDir, `${entryId}.json`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readErrorbookEntry(paths, entryId, fileSystem = fs) {
|
|
196
|
+
const entryPath = buildEntryFilePath(paths, entryId);
|
|
197
|
+
if (!await fileSystem.pathExists(entryPath)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return fileSystem.readJson(entryPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function writeErrorbookEntry(paths, entry, fileSystem = fs) {
|
|
204
|
+
const entryPath = buildEntryFilePath(paths, entry.id);
|
|
205
|
+
await fileSystem.ensureDir(path.dirname(entryPath));
|
|
206
|
+
await fileSystem.writeJson(entryPath, entry, { spaces: 2 });
|
|
207
|
+
return entryPath;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function scoreQuality(entry = {}) {
|
|
211
|
+
let score = 0;
|
|
212
|
+
|
|
213
|
+
if (normalizeText(entry.title)) {
|
|
214
|
+
score += 10;
|
|
215
|
+
}
|
|
216
|
+
if (normalizeText(entry.symptom)) {
|
|
217
|
+
score += 10;
|
|
218
|
+
}
|
|
219
|
+
if (normalizeText(entry.fingerprint)) {
|
|
220
|
+
score += 10;
|
|
221
|
+
}
|
|
222
|
+
if (normalizeText(entry.root_cause)) {
|
|
223
|
+
score += 20;
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(entry.fix_actions) && entry.fix_actions.length > 0) {
|
|
226
|
+
score += 20;
|
|
227
|
+
}
|
|
228
|
+
if (Array.isArray(entry.verification_evidence) && entry.verification_evidence.length > 0) {
|
|
229
|
+
score += 20;
|
|
230
|
+
}
|
|
231
|
+
if (Array.isArray(entry.ontology_tags) && entry.ontology_tags.length > 0) {
|
|
232
|
+
score += 5;
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(entry.tags) && entry.tags.length > 0) {
|
|
235
|
+
score += 3;
|
|
236
|
+
}
|
|
237
|
+
if (normalizeText(entry.symptom).length >= 24 && normalizeText(entry.root_cause).length >= 24) {
|
|
238
|
+
score += 2;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return Math.max(0, Math.min(100, score));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function validateRecordPayload(payload) {
|
|
245
|
+
if (!normalizeText(payload.title)) {
|
|
246
|
+
throw new Error('--title is required');
|
|
247
|
+
}
|
|
248
|
+
if (!normalizeText(payload.symptom)) {
|
|
249
|
+
throw new Error('--symptom is required');
|
|
250
|
+
}
|
|
251
|
+
if (!normalizeText(payload.root_cause)) {
|
|
252
|
+
throw new Error('--root-cause is required');
|
|
253
|
+
}
|
|
254
|
+
if (!Array.isArray(payload.fix_actions) || payload.fix_actions.length === 0) {
|
|
255
|
+
throw new Error('at least one --fix-action is required');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const status = normalizeStatus(payload.status, 'candidate');
|
|
259
|
+
if (status === 'promoted') {
|
|
260
|
+
throw new Error('record does not accept status=promoted. Use "sce errorbook promote <id>"');
|
|
261
|
+
}
|
|
262
|
+
if (status === 'verified' && (!Array.isArray(payload.verification_evidence) || payload.verification_evidence.length === 0)) {
|
|
263
|
+
throw new Error('status=verified requires at least one --verification evidence');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
268
|
+
const payload = {
|
|
269
|
+
title: normalizeText(options.title || fromFilePayload.title),
|
|
270
|
+
symptom: normalizeText(options.symptom || fromFilePayload.symptom),
|
|
271
|
+
root_cause: normalizeText(options.rootCause || options.root_cause || fromFilePayload.root_cause || fromFilePayload.rootCause),
|
|
272
|
+
fix_actions: normalizeStringList(fromFilePayload.fix_actions, fromFilePayload.fixActions, options.fixAction, options.fixActions),
|
|
273
|
+
verification_evidence: normalizeStringList(
|
|
274
|
+
fromFilePayload.verification_evidence,
|
|
275
|
+
fromFilePayload.verificationEvidence,
|
|
276
|
+
options.verification,
|
|
277
|
+
options.verificationEvidence
|
|
278
|
+
),
|
|
279
|
+
tags: normalizeStringList(fromFilePayload.tags, options.tags),
|
|
280
|
+
ontology_tags: normalizeOntologyTags(fromFilePayload.ontology_tags, fromFilePayload.ontology, options.ontology),
|
|
281
|
+
status: normalizeStatus(options.status || fromFilePayload.status || 'candidate'),
|
|
282
|
+
source: {
|
|
283
|
+
spec: normalizeText(options.spec || fromFilePayload?.source?.spec),
|
|
284
|
+
files: normalizeStringList(fromFilePayload?.source?.files, options.files),
|
|
285
|
+
tests: normalizeStringList(fromFilePayload?.source?.tests, options.tests)
|
|
286
|
+
},
|
|
287
|
+
notes: normalizeText(options.notes || fromFilePayload.notes),
|
|
288
|
+
fingerprint: createFingerprint({
|
|
289
|
+
fingerprint: options.fingerprint || fromFilePayload.fingerprint,
|
|
290
|
+
title: options.title || fromFilePayload.title,
|
|
291
|
+
symptom: options.symptom || fromFilePayload.symptom,
|
|
292
|
+
root_cause: options.rootCause || options.root_cause || fromFilePayload.root_cause || fromFilePayload.rootCause
|
|
293
|
+
})
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return payload;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function createEntryId() {
|
|
300
|
+
return `eb-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildIndexSummary(entry) {
|
|
304
|
+
return {
|
|
305
|
+
id: entry.id,
|
|
306
|
+
fingerprint: entry.fingerprint,
|
|
307
|
+
title: entry.title,
|
|
308
|
+
status: entry.status,
|
|
309
|
+
quality_score: entry.quality_score,
|
|
310
|
+
tags: entry.tags,
|
|
311
|
+
ontology_tags: entry.ontology_tags,
|
|
312
|
+
occurrences: entry.occurrences || 1,
|
|
313
|
+
created_at: entry.created_at,
|
|
314
|
+
updated_at: entry.updated_at
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function findSummaryById(index, id) {
|
|
319
|
+
const normalized = normalizeText(id);
|
|
320
|
+
if (!normalized) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const exact = index.entries.find((item) => item.id === normalized);
|
|
325
|
+
if (exact) {
|
|
326
|
+
return exact;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const startsWith = index.entries.filter((item) => item.id.startsWith(normalized));
|
|
330
|
+
if (startsWith.length === 1) {
|
|
331
|
+
return startsWith[0];
|
|
332
|
+
}
|
|
333
|
+
if (startsWith.length > 1) {
|
|
334
|
+
throw new Error(`entry id prefix "${normalized}" is ambiguous (${startsWith.length} matches)`);
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function mergeEntry(existingEntry, incomingPayload) {
|
|
340
|
+
const merged = {
|
|
341
|
+
...existingEntry,
|
|
342
|
+
title: normalizeText(incomingPayload.title) || existingEntry.title,
|
|
343
|
+
symptom: normalizeText(incomingPayload.symptom) || existingEntry.symptom,
|
|
344
|
+
root_cause: normalizeText(incomingPayload.root_cause) || existingEntry.root_cause,
|
|
345
|
+
fix_actions: normalizeStringList(existingEntry.fix_actions, incomingPayload.fix_actions),
|
|
346
|
+
verification_evidence: normalizeStringList(existingEntry.verification_evidence, incomingPayload.verification_evidence),
|
|
347
|
+
tags: normalizeStringList(existingEntry.tags, incomingPayload.tags),
|
|
348
|
+
ontology_tags: normalizeOntologyTags(existingEntry.ontology_tags, incomingPayload.ontology_tags),
|
|
349
|
+
status: selectStatus(existingEntry.status, incomingPayload.status),
|
|
350
|
+
notes: normalizeText(incomingPayload.notes) || existingEntry.notes || '',
|
|
351
|
+
source: {
|
|
352
|
+
spec: normalizeText(incomingPayload?.source?.spec) || normalizeText(existingEntry?.source?.spec),
|
|
353
|
+
files: normalizeStringList(existingEntry?.source?.files, incomingPayload?.source?.files),
|
|
354
|
+
tests: normalizeStringList(existingEntry?.source?.tests, incomingPayload?.source?.tests)
|
|
355
|
+
},
|
|
356
|
+
occurrences: Number(existingEntry.occurrences || 1) + 1,
|
|
357
|
+
updated_at: nowIso()
|
|
358
|
+
};
|
|
359
|
+
merged.quality_score = scoreQuality(merged);
|
|
360
|
+
return merged;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function loadRecordPayloadFromFile(projectPath, sourcePath, fileSystem = fs) {
|
|
364
|
+
const normalized = normalizeText(sourcePath);
|
|
365
|
+
if (!normalized) {
|
|
366
|
+
return {};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const absolutePath = path.isAbsolute(normalized)
|
|
370
|
+
? normalized
|
|
371
|
+
: path.join(projectPath, normalized);
|
|
372
|
+
|
|
373
|
+
if (!await fileSystem.pathExists(absolutePath)) {
|
|
374
|
+
throw new Error(`record source file not found: ${sourcePath}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const payload = await fileSystem.readJson(absolutePath);
|
|
379
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
380
|
+
throw new Error('record source JSON must be an object');
|
|
381
|
+
}
|
|
382
|
+
return payload;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new Error(`failed to parse record source file (${sourcePath}): ${error.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function printRecordSummary(result) {
|
|
389
|
+
const action = result.created ? 'Recorded new entry' : 'Updated duplicate fingerprint';
|
|
390
|
+
console.log(chalk.green(`✓ ${action}`));
|
|
391
|
+
console.log(chalk.gray(` id: ${result.entry.id}`));
|
|
392
|
+
console.log(chalk.gray(` status: ${result.entry.status}`));
|
|
393
|
+
console.log(chalk.gray(` quality: ${result.entry.quality_score}`));
|
|
394
|
+
console.log(chalk.gray(` fingerprint: ${result.entry.fingerprint}`));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function printListSummary(payload) {
|
|
398
|
+
if (payload.entries.length === 0) {
|
|
399
|
+
console.log(chalk.gray('No errorbook entries found'));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const table = new Table({
|
|
404
|
+
head: ['ID', 'Status', 'Quality', 'Title', 'Updated', 'Occurrences'].map((item) => chalk.cyan(item)),
|
|
405
|
+
colWidths: [16, 12, 10, 44, 22, 12]
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
payload.entries.forEach((entry) => {
|
|
409
|
+
table.push([
|
|
410
|
+
entry.id,
|
|
411
|
+
entry.status,
|
|
412
|
+
entry.quality_score,
|
|
413
|
+
entry.title.length > 40 ? `${entry.title.slice(0, 40)}...` : entry.title,
|
|
414
|
+
entry.updated_at,
|
|
415
|
+
entry.occurrences || 1
|
|
416
|
+
]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
console.log(table.toString());
|
|
420
|
+
console.log(chalk.gray(`Total: ${payload.total_results} (stored: ${payload.total_entries})`));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function scoreSearchMatch(entry, queryTokens) {
|
|
424
|
+
const title = normalizeText(entry.title).toLowerCase();
|
|
425
|
+
const symptom = normalizeText(entry.symptom).toLowerCase();
|
|
426
|
+
const rootCause = normalizeText(entry.root_cause).toLowerCase();
|
|
427
|
+
const fingerprint = normalizeText(entry.fingerprint).toLowerCase();
|
|
428
|
+
const tagText = normalizeStringList(entry.tags, entry.ontology_tags).join(' ').toLowerCase();
|
|
429
|
+
const fixText = normalizeStringList(entry.fix_actions).join(' ').toLowerCase();
|
|
430
|
+
|
|
431
|
+
let score = 0;
|
|
432
|
+
for (const token of queryTokens) {
|
|
433
|
+
if (!token) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (title.includes(token)) {
|
|
437
|
+
score += 8;
|
|
438
|
+
}
|
|
439
|
+
if (symptom.includes(token)) {
|
|
440
|
+
score += 5;
|
|
441
|
+
}
|
|
442
|
+
if (rootCause.includes(token)) {
|
|
443
|
+
score += 5;
|
|
444
|
+
}
|
|
445
|
+
if (fixText.includes(token)) {
|
|
446
|
+
score += 3;
|
|
447
|
+
}
|
|
448
|
+
if (tagText.includes(token)) {
|
|
449
|
+
score += 2;
|
|
450
|
+
}
|
|
451
|
+
if (fingerprint.includes(token)) {
|
|
452
|
+
score += 1;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
score += (Number(entry.quality_score) || 0) / 20;
|
|
457
|
+
score += STATUS_RANK[entry.status] || 0;
|
|
458
|
+
return Number(score.toFixed(3));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function validatePromoteCandidate(entry, minQuality = DEFAULT_PROMOTE_MIN_QUALITY) {
|
|
462
|
+
const missing = [];
|
|
463
|
+
if (!normalizeText(entry.root_cause)) {
|
|
464
|
+
missing.push('root_cause');
|
|
465
|
+
}
|
|
466
|
+
if (!Array.isArray(entry.fix_actions) || entry.fix_actions.length === 0) {
|
|
467
|
+
missing.push('fix_actions');
|
|
468
|
+
}
|
|
469
|
+
if (!Array.isArray(entry.verification_evidence) || entry.verification_evidence.length === 0) {
|
|
470
|
+
missing.push('verification_evidence');
|
|
471
|
+
}
|
|
472
|
+
if (!Array.isArray(entry.ontology_tags) || entry.ontology_tags.length === 0) {
|
|
473
|
+
missing.push('ontology_tags');
|
|
474
|
+
}
|
|
475
|
+
if ((Number(entry.quality_score) || 0) < minQuality) {
|
|
476
|
+
missing.push(`quality_score>=${minQuality}`);
|
|
477
|
+
}
|
|
478
|
+
if (entry.status === 'deprecated') {
|
|
479
|
+
missing.push('status!=deprecated');
|
|
480
|
+
}
|
|
481
|
+
return missing;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
485
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
486
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
487
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
488
|
+
|
|
489
|
+
const fromFilePayload = await loadRecordPayloadFromFile(projectPath, options.from, fileSystem);
|
|
490
|
+
const normalized = normalizeRecordPayload(options, fromFilePayload);
|
|
491
|
+
validateRecordPayload(normalized);
|
|
492
|
+
|
|
493
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
494
|
+
const existingSummary = index.entries.find((entry) => entry.fingerprint === normalized.fingerprint);
|
|
495
|
+
const timestamp = nowIso();
|
|
496
|
+
|
|
497
|
+
let entry;
|
|
498
|
+
let created = false;
|
|
499
|
+
let deduplicated = false;
|
|
500
|
+
|
|
501
|
+
if (existingSummary) {
|
|
502
|
+
const existingEntry = await readErrorbookEntry(paths, existingSummary.id, fileSystem);
|
|
503
|
+
if (!existingEntry) {
|
|
504
|
+
throw new Error(`errorbook index references missing entry: ${existingSummary.id}`);
|
|
505
|
+
}
|
|
506
|
+
entry = mergeEntry(existingEntry, normalized);
|
|
507
|
+
deduplicated = true;
|
|
508
|
+
} else {
|
|
509
|
+
entry = {
|
|
510
|
+
api_version: ERRORBOOK_ENTRY_API_VERSION,
|
|
511
|
+
id: createEntryId(),
|
|
512
|
+
created_at: timestamp,
|
|
513
|
+
updated_at: timestamp,
|
|
514
|
+
fingerprint: normalized.fingerprint,
|
|
515
|
+
title: normalized.title,
|
|
516
|
+
symptom: normalized.symptom,
|
|
517
|
+
root_cause: normalized.root_cause,
|
|
518
|
+
fix_actions: normalized.fix_actions,
|
|
519
|
+
verification_evidence: normalized.verification_evidence,
|
|
520
|
+
tags: normalized.tags,
|
|
521
|
+
ontology_tags: normalized.ontology_tags,
|
|
522
|
+
status: normalized.status,
|
|
523
|
+
source: normalized.source,
|
|
524
|
+
notes: normalized.notes || '',
|
|
525
|
+
occurrences: 1
|
|
526
|
+
};
|
|
527
|
+
entry.quality_score = scoreQuality(entry);
|
|
528
|
+
created = true;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
entry.updated_at = nowIso();
|
|
532
|
+
entry.quality_score = scoreQuality(entry);
|
|
533
|
+
await writeErrorbookEntry(paths, entry, fileSystem);
|
|
534
|
+
|
|
535
|
+
const summary = buildIndexSummary(entry);
|
|
536
|
+
const summaryIndex = index.entries.findIndex((item) => item.id === summary.id);
|
|
537
|
+
if (summaryIndex >= 0) {
|
|
538
|
+
index.entries[summaryIndex] = summary;
|
|
539
|
+
} else {
|
|
540
|
+
index.entries.push(summary);
|
|
541
|
+
}
|
|
542
|
+
index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
|
|
543
|
+
await writeErrorbookIndex(paths, index, fileSystem);
|
|
544
|
+
|
|
545
|
+
const result = {
|
|
546
|
+
mode: 'errorbook-record',
|
|
547
|
+
created,
|
|
548
|
+
deduplicated,
|
|
549
|
+
entry
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
if (options.json) {
|
|
553
|
+
console.log(JSON.stringify(result, null, 2));
|
|
554
|
+
} else if (!options.silent) {
|
|
555
|
+
printRecordSummary(result);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function runErrorbookListCommand(options = {}, dependencies = {}) {
|
|
562
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
563
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
564
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
565
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
566
|
+
|
|
567
|
+
const requestedStatus = options.status ? normalizeStatus(options.status) : null;
|
|
568
|
+
const requestedTag = normalizeText(options.tag).toLowerCase();
|
|
569
|
+
const requestedOntology = normalizeOntologyTags(options.ontology)[0] || '';
|
|
570
|
+
const minQuality = Number.isFinite(Number(options.minQuality)) ? Number(options.minQuality) : null;
|
|
571
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
572
|
+
? Number(options.limit)
|
|
573
|
+
: 20;
|
|
574
|
+
|
|
575
|
+
let filtered = [...index.entries];
|
|
576
|
+
if (requestedStatus) {
|
|
577
|
+
filtered = filtered.filter((entry) => entry.status === requestedStatus);
|
|
578
|
+
}
|
|
579
|
+
if (requestedTag) {
|
|
580
|
+
filtered = filtered.filter((entry) => normalizeStringList(entry.tags).some((tag) => tag.toLowerCase() === requestedTag));
|
|
581
|
+
}
|
|
582
|
+
if (requestedOntology) {
|
|
583
|
+
filtered = filtered.filter((entry) => normalizeOntologyTags(entry.ontology_tags).includes(requestedOntology));
|
|
584
|
+
}
|
|
585
|
+
if (minQuality !== null) {
|
|
586
|
+
filtered = filtered.filter((entry) => Number(entry.quality_score || 0) >= minQuality);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
filtered.sort((left, right) => {
|
|
590
|
+
const qualityDiff = Number(right.quality_score || 0) - Number(left.quality_score || 0);
|
|
591
|
+
if (qualityDiff !== 0) {
|
|
592
|
+
return qualityDiff;
|
|
593
|
+
}
|
|
594
|
+
return `${right.updated_at}`.localeCompare(`${left.updated_at}`);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const result = {
|
|
598
|
+
mode: 'errorbook-list',
|
|
599
|
+
total_entries: index.entries.length,
|
|
600
|
+
total_results: filtered.length,
|
|
601
|
+
entries: filtered.slice(0, limit).map((entry) => ({
|
|
602
|
+
...entry,
|
|
603
|
+
tags: normalizeStringList(entry.tags),
|
|
604
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags)
|
|
605
|
+
}))
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
if (options.json) {
|
|
609
|
+
console.log(JSON.stringify(result, null, 2));
|
|
610
|
+
} else if (!options.silent) {
|
|
611
|
+
printListSummary(result);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function runErrorbookShowCommand(options = {}, dependencies = {}) {
|
|
618
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
619
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
620
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
621
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
622
|
+
|
|
623
|
+
const id = normalizeText(options.id || options.entryId);
|
|
624
|
+
if (!id) {
|
|
625
|
+
throw new Error('entry id is required');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const summary = findSummaryById(index, id);
|
|
629
|
+
if (!summary) {
|
|
630
|
+
throw new Error(`errorbook entry not found: ${id}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
634
|
+
if (!entry) {
|
|
635
|
+
throw new Error(`errorbook entry file not found: ${summary.id}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const result = {
|
|
639
|
+
mode: 'errorbook-show',
|
|
640
|
+
entry
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
if (options.json) {
|
|
644
|
+
console.log(JSON.stringify(result, null, 2));
|
|
645
|
+
} else if (!options.silent) {
|
|
646
|
+
console.log(chalk.cyan.bold(entry.title));
|
|
647
|
+
console.log(chalk.gray(`id: ${entry.id}`));
|
|
648
|
+
console.log(chalk.gray(`status: ${entry.status}`));
|
|
649
|
+
console.log(chalk.gray(`quality: ${entry.quality_score}`));
|
|
650
|
+
console.log(chalk.gray(`fingerprint: ${entry.fingerprint}`));
|
|
651
|
+
console.log(chalk.gray(`symptom: ${entry.symptom}`));
|
|
652
|
+
console.log(chalk.gray(`root_cause: ${entry.root_cause}`));
|
|
653
|
+
console.log(chalk.gray(`fix_actions: ${entry.fix_actions.join(' | ')}`));
|
|
654
|
+
console.log(chalk.gray(`verification: ${entry.verification_evidence.join(' | ') || '(none)'}`));
|
|
655
|
+
console.log(chalk.gray(`ontology: ${entry.ontology_tags.join(', ') || '(none)'}`));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
662
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
663
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
664
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
665
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
666
|
+
|
|
667
|
+
const query = normalizeText(options.query);
|
|
668
|
+
if (!query) {
|
|
669
|
+
throw new Error('--query is required');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const requestedStatus = options.status ? normalizeStatus(options.status) : null;
|
|
673
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
674
|
+
? Number(options.limit)
|
|
675
|
+
: 10;
|
|
676
|
+
const tokens = query.toLowerCase().split(/[\s,;|]+/).map((item) => item.trim()).filter(Boolean);
|
|
677
|
+
|
|
678
|
+
const candidates = [];
|
|
679
|
+
for (const summary of index.entries) {
|
|
680
|
+
if (requestedStatus && summary.status !== requestedStatus) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
684
|
+
if (!entry) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const matchScore = scoreSearchMatch(entry, tokens);
|
|
688
|
+
if (matchScore <= 0) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
candidates.push({
|
|
692
|
+
id: entry.id,
|
|
693
|
+
status: entry.status,
|
|
694
|
+
quality_score: entry.quality_score,
|
|
695
|
+
title: entry.title,
|
|
696
|
+
fingerprint: entry.fingerprint,
|
|
697
|
+
tags: normalizeStringList(entry.tags),
|
|
698
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
699
|
+
match_score: matchScore,
|
|
700
|
+
updated_at: entry.updated_at
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
candidates.sort((left, right) => {
|
|
705
|
+
const scoreDiff = Number(right.match_score) - Number(left.match_score);
|
|
706
|
+
if (scoreDiff !== 0) {
|
|
707
|
+
return scoreDiff;
|
|
708
|
+
}
|
|
709
|
+
return `${right.updated_at}`.localeCompare(`${left.updated_at}`);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const result = {
|
|
713
|
+
mode: 'errorbook-find',
|
|
714
|
+
query,
|
|
715
|
+
total_results: candidates.length,
|
|
716
|
+
entries: candidates.slice(0, limit)
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
if (options.json) {
|
|
720
|
+
console.log(JSON.stringify(result, null, 2));
|
|
721
|
+
} else if (!options.silent) {
|
|
722
|
+
printListSummary({
|
|
723
|
+
entries: result.entries,
|
|
724
|
+
total_results: result.total_results,
|
|
725
|
+
total_entries: index.entries.length
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function runErrorbookPromoteCommand(options = {}, dependencies = {}) {
|
|
733
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
734
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
735
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
736
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
737
|
+
|
|
738
|
+
const id = normalizeText(options.id || options.entryId);
|
|
739
|
+
if (!id) {
|
|
740
|
+
throw new Error('entry id is required');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const summary = findSummaryById(index, id);
|
|
744
|
+
if (!summary) {
|
|
745
|
+
throw new Error(`errorbook entry not found: ${id}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
749
|
+
if (!entry) {
|
|
750
|
+
throw new Error(`errorbook entry file not found: ${summary.id}`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
entry.quality_score = scoreQuality(entry);
|
|
754
|
+
const missing = validatePromoteCandidate(entry, DEFAULT_PROMOTE_MIN_QUALITY);
|
|
755
|
+
if (missing.length > 0) {
|
|
756
|
+
throw new Error(`promote gate failed: ${missing.join(', ')}`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
entry.status = 'promoted';
|
|
760
|
+
entry.promoted_at = nowIso();
|
|
761
|
+
entry.updated_at = entry.promoted_at;
|
|
762
|
+
await writeErrorbookEntry(paths, entry, fileSystem);
|
|
763
|
+
|
|
764
|
+
const updatedSummary = buildIndexSummary(entry);
|
|
765
|
+
const targetIndex = index.entries.findIndex((item) => item.id === entry.id);
|
|
766
|
+
if (targetIndex >= 0) {
|
|
767
|
+
index.entries[targetIndex] = updatedSummary;
|
|
768
|
+
} else {
|
|
769
|
+
index.entries.push(updatedSummary);
|
|
770
|
+
}
|
|
771
|
+
index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
|
|
772
|
+
await writeErrorbookIndex(paths, index, fileSystem);
|
|
773
|
+
|
|
774
|
+
const result = {
|
|
775
|
+
mode: 'errorbook-promote',
|
|
776
|
+
promoted: true,
|
|
777
|
+
entry
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
if (options.json) {
|
|
781
|
+
console.log(JSON.stringify(result, null, 2));
|
|
782
|
+
} else if (!options.silent) {
|
|
783
|
+
console.log(chalk.green('✓ Promoted errorbook entry'));
|
|
784
|
+
console.log(chalk.gray(` id: ${entry.id}`));
|
|
785
|
+
console.log(chalk.gray(` quality: ${entry.quality_score}`));
|
|
786
|
+
console.log(chalk.gray(` promoted_at: ${entry.promoted_at}`));
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return result;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function runErrorbookDeprecateCommand(options = {}, dependencies = {}) {
|
|
793
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
794
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
795
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
796
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
797
|
+
|
|
798
|
+
const id = normalizeText(options.id || options.entryId);
|
|
799
|
+
if (!id) {
|
|
800
|
+
throw new Error('entry id is required');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const summary = findSummaryById(index, id);
|
|
804
|
+
if (!summary) {
|
|
805
|
+
throw new Error(`errorbook entry not found: ${id}`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const reason = normalizeText(options.reason);
|
|
809
|
+
if (!reason) {
|
|
810
|
+
throw new Error('--reason is required for deprecate');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const replacement = normalizeText(options.replacement);
|
|
814
|
+
if (replacement && replacement === summary.id) {
|
|
815
|
+
throw new Error('--replacement cannot reference the same entry id');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
819
|
+
if (!entry) {
|
|
820
|
+
throw new Error(`errorbook entry file not found: ${summary.id}`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
entry.status = 'deprecated';
|
|
824
|
+
entry.updated_at = nowIso();
|
|
825
|
+
entry.deprecated_at = entry.updated_at;
|
|
826
|
+
entry.deprecation = {
|
|
827
|
+
reason,
|
|
828
|
+
replacement_id: replacement || null
|
|
829
|
+
};
|
|
830
|
+
entry.quality_score = scoreQuality(entry);
|
|
831
|
+
await writeErrorbookEntry(paths, entry, fileSystem);
|
|
832
|
+
|
|
833
|
+
const updatedSummary = buildIndexSummary(entry);
|
|
834
|
+
const targetIndex = index.entries.findIndex((item) => item.id === entry.id);
|
|
835
|
+
if (targetIndex >= 0) {
|
|
836
|
+
index.entries[targetIndex] = updatedSummary;
|
|
837
|
+
} else {
|
|
838
|
+
index.entries.push(updatedSummary);
|
|
839
|
+
}
|
|
840
|
+
index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
|
|
841
|
+
await writeErrorbookIndex(paths, index, fileSystem);
|
|
842
|
+
|
|
843
|
+
const result = {
|
|
844
|
+
mode: 'errorbook-deprecate',
|
|
845
|
+
deprecated: true,
|
|
846
|
+
entry
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
if (options.json) {
|
|
850
|
+
console.log(JSON.stringify(result, null, 2));
|
|
851
|
+
} else if (!options.silent) {
|
|
852
|
+
console.log(chalk.yellow('✓ Deprecated errorbook entry'));
|
|
853
|
+
console.log(chalk.gray(` id: ${entry.id}`));
|
|
854
|
+
console.log(chalk.gray(` reason: ${reason}`));
|
|
855
|
+
if (replacement) {
|
|
856
|
+
console.log(chalk.gray(` replacement: ${replacement}`));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function runErrorbookRequalifyCommand(options = {}, dependencies = {}) {
|
|
864
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
865
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
866
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
867
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
868
|
+
|
|
869
|
+
const id = normalizeText(options.id || options.entryId);
|
|
870
|
+
if (!id) {
|
|
871
|
+
throw new Error('entry id is required');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const summary = findSummaryById(index, id);
|
|
875
|
+
if (!summary) {
|
|
876
|
+
throw new Error(`errorbook entry not found: ${id}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const status = normalizeStatus(options.status || 'verified');
|
|
880
|
+
if (status === 'promoted') {
|
|
881
|
+
throw new Error('requalify does not accept status=promoted. Use "sce errorbook promote <id>"');
|
|
882
|
+
}
|
|
883
|
+
if (status === 'deprecated') {
|
|
884
|
+
throw new Error('requalify does not accept status=deprecated. Use "sce errorbook deprecate <id>"');
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
888
|
+
if (!entry) {
|
|
889
|
+
throw new Error(`errorbook entry file not found: ${summary.id}`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (status === 'verified' && (!Array.isArray(entry.verification_evidence) || entry.verification_evidence.length === 0)) {
|
|
893
|
+
throw new Error('requalify to verified requires verification_evidence');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
entry.status = status;
|
|
897
|
+
entry.updated_at = nowIso();
|
|
898
|
+
entry.requalified_at = entry.updated_at;
|
|
899
|
+
if (entry.deprecation) {
|
|
900
|
+
delete entry.deprecation;
|
|
901
|
+
}
|
|
902
|
+
entry.quality_score = scoreQuality(entry);
|
|
903
|
+
await writeErrorbookEntry(paths, entry, fileSystem);
|
|
904
|
+
|
|
905
|
+
const updatedSummary = buildIndexSummary(entry);
|
|
906
|
+
const targetIndex = index.entries.findIndex((item) => item.id === entry.id);
|
|
907
|
+
if (targetIndex >= 0) {
|
|
908
|
+
index.entries[targetIndex] = updatedSummary;
|
|
909
|
+
} else {
|
|
910
|
+
index.entries.push(updatedSummary);
|
|
911
|
+
}
|
|
912
|
+
index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
|
|
913
|
+
await writeErrorbookIndex(paths, index, fileSystem);
|
|
914
|
+
|
|
915
|
+
const result = {
|
|
916
|
+
mode: 'errorbook-requalify',
|
|
917
|
+
requalified: true,
|
|
918
|
+
entry
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
if (options.json) {
|
|
922
|
+
console.log(JSON.stringify(result, null, 2));
|
|
923
|
+
} else if (!options.silent) {
|
|
924
|
+
console.log(chalk.green('✓ Requalified errorbook entry'));
|
|
925
|
+
console.log(chalk.gray(` id: ${entry.id}`));
|
|
926
|
+
console.log(chalk.gray(` status: ${entry.status}`));
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function collectOptionValue(value, previous = []) {
|
|
933
|
+
const next = Array.isArray(previous) ? previous : [];
|
|
934
|
+
next.push(value);
|
|
935
|
+
return next;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function emitCommandError(error, json) {
|
|
939
|
+
if (json) {
|
|
940
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
941
|
+
} else {
|
|
942
|
+
console.error(chalk.red('Error:'), error.message);
|
|
943
|
+
}
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function registerErrorbookCommands(program) {
|
|
948
|
+
const errorbook = program
|
|
949
|
+
.command('errorbook')
|
|
950
|
+
.description('Curated failure-remediation knowledge base');
|
|
951
|
+
|
|
952
|
+
errorbook
|
|
953
|
+
.command('record')
|
|
954
|
+
.description('Record a high-signal failure remediation entry')
|
|
955
|
+
.option('--title <text>', 'Entry title')
|
|
956
|
+
.option('--symptom <text>', 'Observed symptom')
|
|
957
|
+
.option('--root-cause <text>', 'Validated root cause')
|
|
958
|
+
.option('--fix-action <text>', 'Concrete fix action (repeatable)', collectOptionValue, [])
|
|
959
|
+
.option('--verification <text>', 'Verification evidence (repeatable)', collectOptionValue, [])
|
|
960
|
+
.option('--tags <csv>', 'Tags, comma-separated')
|
|
961
|
+
.option('--ontology <csv>', `Ontology focus tags (${ERRORBOOK_ONTOLOGY_TAGS.join(', ')})`)
|
|
962
|
+
.option('--status <status>', 'candidate|verified', 'candidate')
|
|
963
|
+
.option('--fingerprint <text>', 'Custom deduplication fingerprint')
|
|
964
|
+
.option('--from <path>', 'Load payload from JSON file')
|
|
965
|
+
.option('--spec <spec>', 'Related spec id/name')
|
|
966
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
967
|
+
.action(async (options) => {
|
|
968
|
+
try {
|
|
969
|
+
await runErrorbookRecordCommand(options);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
emitCommandError(error, options.json);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
errorbook
|
|
976
|
+
.command('list')
|
|
977
|
+
.description('List curated errorbook entries')
|
|
978
|
+
.option('--status <status>', `Filter by status (${ERRORBOOK_STATUSES.join(', ')})`)
|
|
979
|
+
.option('--tag <tag>', 'Filter by tag')
|
|
980
|
+
.option('--ontology <tag>', `Filter by ontology tag (${ERRORBOOK_ONTOLOGY_TAGS.join(', ')})`)
|
|
981
|
+
.option('--min-quality <n>', 'Minimum quality score', parseInt)
|
|
982
|
+
.option('--limit <n>', 'Maximum entries returned', parseInt, 20)
|
|
983
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
984
|
+
.action(async (options) => {
|
|
985
|
+
try {
|
|
986
|
+
await runErrorbookListCommand(options);
|
|
987
|
+
} catch (error) {
|
|
988
|
+
emitCommandError(error, options.json);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
errorbook
|
|
993
|
+
.command('show <id>')
|
|
994
|
+
.description('Show a single errorbook entry')
|
|
995
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
996
|
+
.action(async (id, options) => {
|
|
997
|
+
try {
|
|
998
|
+
await runErrorbookShowCommand({ ...options, id });
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
emitCommandError(error, options.json);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
errorbook
|
|
1005
|
+
.command('find')
|
|
1006
|
+
.description('Search curated entries with ranking')
|
|
1007
|
+
.requiredOption('--query <text>', 'Search query')
|
|
1008
|
+
.option('--status <status>', `Filter by status (${ERRORBOOK_STATUSES.join(', ')})`)
|
|
1009
|
+
.option('--limit <n>', 'Maximum entries returned', parseInt, 10)
|
|
1010
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1011
|
+
.action(async (options) => {
|
|
1012
|
+
try {
|
|
1013
|
+
await runErrorbookFindCommand(options);
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
emitCommandError(error, options.json);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
errorbook
|
|
1020
|
+
.command('promote <id>')
|
|
1021
|
+
.description('Promote entry after strict quality gate')
|
|
1022
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1023
|
+
.action(async (id, options) => {
|
|
1024
|
+
try {
|
|
1025
|
+
await runErrorbookPromoteCommand({ ...options, id });
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
emitCommandError(error, options.json);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
errorbook
|
|
1032
|
+
.command('deprecate <id>')
|
|
1033
|
+
.description('Deprecate low-value or obsolete entry')
|
|
1034
|
+
.requiredOption('--reason <text>', 'Deprecation reason')
|
|
1035
|
+
.option('--replacement <id>', 'Replacement entry id')
|
|
1036
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1037
|
+
.action(async (id, options) => {
|
|
1038
|
+
try {
|
|
1039
|
+
await runErrorbookDeprecateCommand({ ...options, id });
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
emitCommandError(error, options.json);
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
errorbook
|
|
1046
|
+
.command('requalify <id>')
|
|
1047
|
+
.description('Requalify deprecated/candidate entry back to candidate|verified')
|
|
1048
|
+
.option('--status <status>', 'candidate|verified', 'verified')
|
|
1049
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1050
|
+
.action(async (id, options) => {
|
|
1051
|
+
try {
|
|
1052
|
+
await runErrorbookRequalifyCommand({ ...options, id });
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
emitCommandError(error, options.json);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
module.exports = {
|
|
1060
|
+
ERRORBOOK_STATUSES,
|
|
1061
|
+
ERRORBOOK_ONTOLOGY_TAGS,
|
|
1062
|
+
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
1063
|
+
resolveErrorbookPaths,
|
|
1064
|
+
normalizeOntologyTags,
|
|
1065
|
+
normalizeRecordPayload,
|
|
1066
|
+
scoreQuality,
|
|
1067
|
+
runErrorbookRecordCommand,
|
|
1068
|
+
runErrorbookListCommand,
|
|
1069
|
+
runErrorbookShowCommand,
|
|
1070
|
+
runErrorbookFindCommand,
|
|
1071
|
+
runErrorbookPromoteCommand,
|
|
1072
|
+
runErrorbookDeprecateCommand,
|
|
1073
|
+
runErrorbookRequalifyCommand,
|
|
1074
|
+
registerErrorbookCommands
|
|
1075
|
+
};
|
package/package.json
CHANGED