scene-capability-engine 3.3.26 → 3.4.5
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 +62 -0
- package/README.md +81 -719
- package/README.zh.md +85 -586
- package/bin/scene-capability-engine.js +103 -0
- package/docs/README.md +47 -249
- package/docs/command-reference.md +49 -4
- package/docs/spec-workflow.md +35 -4
- package/docs/zh/README.md +44 -331
- package/lib/adoption/adoption-strategy.js +4 -0
- package/lib/adoption/detection-engine.js +4 -0
- package/lib/adoption/file-classifier.js +5 -1
- package/lib/adoption/smart-orchestrator.js +4 -0
- package/lib/commands/adopt.js +32 -0
- package/lib/commands/errorbook.js +409 -2
- package/lib/commands/spec-domain.js +78 -2
- package/lib/commands/studio.js +251 -3
- package/lib/commands/upgrade.js +16 -0
- package/lib/problem/problem-evaluator.js +620 -0
- package/lib/spec/domain-modeling.js +217 -1
- package/lib/workspace/takeover-baseline.js +446 -0
- package/package.json +1 -1
- package/template/.sce/config/problem-eval-policy.json +36 -0
- package/template/.sce/config/session-governance.json +8 -0
- package/template/.sce/config/spec-domain-policy.json +6 -0
- package/template/.sce/config/takeover-baseline.json +33 -0
|
@@ -11,6 +11,8 @@ const ERRORBOOK_ENTRY_API_VERSION = 'sce.errorbook.entry/v0.1';
|
|
|
11
11
|
const ERRORBOOK_REGISTRY_API_VERSION = 'sce.errorbook.registry/v0.1';
|
|
12
12
|
const ERRORBOOK_REGISTRY_CACHE_API_VERSION = 'sce.errorbook.registry-cache/v0.1';
|
|
13
13
|
const ERRORBOOK_REGISTRY_INDEX_API_VERSION = 'sce.errorbook.registry-index/v0.1';
|
|
14
|
+
const ERRORBOOK_INCIDENT_INDEX_API_VERSION = 'sce.errorbook.incident-index/v0.1';
|
|
15
|
+
const ERRORBOOK_INCIDENT_API_VERSION = 'sce.errorbook.incident/v0.1';
|
|
14
16
|
const ERRORBOOK_STATUSES = Object.freeze(['candidate', 'verified', 'promoted', 'deprecated']);
|
|
15
17
|
const TEMPORARY_MITIGATION_TAG = 'temporary-mitigation';
|
|
16
18
|
const DEFAULT_ERRORBOOK_REGISTRY_CONFIG = '.sce/config/errorbook-registry.json';
|
|
@@ -65,11 +67,16 @@ const HIGH_RISK_SIGNAL_TAGS = Object.freeze([
|
|
|
65
67
|
|
|
66
68
|
function resolveErrorbookPaths(projectPath = process.cwd()) {
|
|
67
69
|
const baseDir = path.join(projectPath, '.sce', 'errorbook');
|
|
70
|
+
const stagingDir = path.join(baseDir, 'staging');
|
|
68
71
|
return {
|
|
69
72
|
projectPath,
|
|
70
73
|
baseDir,
|
|
71
74
|
entriesDir: path.join(baseDir, 'entries'),
|
|
72
|
-
indexFile: path.join(baseDir, 'index.json')
|
|
75
|
+
indexFile: path.join(baseDir, 'index.json'),
|
|
76
|
+
stagingDir,
|
|
77
|
+
incidentsDir: path.join(stagingDir, 'incidents'),
|
|
78
|
+
resolvedDir: path.join(stagingDir, 'resolved'),
|
|
79
|
+
incidentIndexFile: path.join(stagingDir, 'index.json')
|
|
73
80
|
};
|
|
74
81
|
}
|
|
75
82
|
|
|
@@ -417,6 +424,247 @@ async function writeErrorbookEntry(paths, entry, fileSystem = fs) {
|
|
|
417
424
|
return entryPath;
|
|
418
425
|
}
|
|
419
426
|
|
|
427
|
+
function buildDefaultIncidentIndex() {
|
|
428
|
+
return {
|
|
429
|
+
api_version: ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
430
|
+
updated_at: nowIso(),
|
|
431
|
+
total_incidents: 0,
|
|
432
|
+
incidents: []
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function normalizeIncidentState(value, fallback = 'open') {
|
|
437
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
438
|
+
if (!normalized) {
|
|
439
|
+
return fallback;
|
|
440
|
+
}
|
|
441
|
+
if (normalized === 'open' || normalized === 'resolved') {
|
|
442
|
+
return normalized;
|
|
443
|
+
}
|
|
444
|
+
return fallback;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function shouldResolveIncidentByStatus(status = '') {
|
|
448
|
+
const normalized = normalizeStatus(status, 'candidate');
|
|
449
|
+
return normalized === 'verified' || normalized === 'promoted' || normalized === 'deprecated';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function createIncidentId() {
|
|
453
|
+
return `ebi-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function createIncidentAttemptId() {
|
|
457
|
+
return `attempt-${Date.now()}-${crypto.randomBytes(2).toString('hex')}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildIncidentFilePath(paths, incidentId) {
|
|
461
|
+
return path.join(paths.incidentsDir, `${incidentId}.json`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function buildIncidentResolvedSnapshotPath(paths, incidentId) {
|
|
465
|
+
return path.join(paths.resolvedDir, `${incidentId}.json`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function ensureIncidentStorage(paths, fileSystem = fs) {
|
|
469
|
+
await fileSystem.ensureDir(paths.incidentsDir);
|
|
470
|
+
await fileSystem.ensureDir(paths.resolvedDir);
|
|
471
|
+
if (!await fileSystem.pathExists(paths.incidentIndexFile)) {
|
|
472
|
+
await fileSystem.writeJson(paths.incidentIndexFile, buildDefaultIncidentIndex(), { spaces: 2 });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function readIncidentIndex(paths, fileSystem = fs) {
|
|
477
|
+
await ensureIncidentStorage(paths, fileSystem);
|
|
478
|
+
const payload = await fileSystem.readJson(paths.incidentIndexFile).catch(() => null);
|
|
479
|
+
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.incidents)) {
|
|
480
|
+
return buildDefaultIncidentIndex();
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
api_version: payload.api_version || ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
484
|
+
updated_at: normalizeText(payload.updated_at) || nowIso(),
|
|
485
|
+
total_incidents: Number.isInteger(payload.total_incidents) ? payload.total_incidents : payload.incidents.length,
|
|
486
|
+
incidents: payload.incidents
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function writeIncidentIndex(paths, index, fileSystem = fs) {
|
|
491
|
+
const payload = {
|
|
492
|
+
api_version: ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
493
|
+
updated_at: nowIso(),
|
|
494
|
+
incidents: Array.isArray(index.incidents) ? index.incidents : []
|
|
495
|
+
};
|
|
496
|
+
payload.total_incidents = payload.incidents.length;
|
|
497
|
+
await fileSystem.ensureDir(path.dirname(paths.incidentIndexFile));
|
|
498
|
+
await fileSystem.writeJson(paths.incidentIndexFile, payload, { spaces: 2 });
|
|
499
|
+
return payload;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function readIncident(paths, incidentId, fileSystem = fs) {
|
|
503
|
+
const filePath = buildIncidentFilePath(paths, incidentId);
|
|
504
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
return fileSystem.readJson(filePath);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function writeIncident(paths, incident, fileSystem = fs) {
|
|
511
|
+
const filePath = buildIncidentFilePath(paths, incident.id);
|
|
512
|
+
await fileSystem.ensureDir(path.dirname(filePath));
|
|
513
|
+
await fileSystem.writeJson(filePath, incident, { spaces: 2 });
|
|
514
|
+
return filePath;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function createIncidentAttemptSignature(payload = {}) {
|
|
518
|
+
const basis = JSON.stringify({
|
|
519
|
+
root_cause: normalizeText(payload.root_cause),
|
|
520
|
+
fix_actions: normalizeStringList(payload.fix_actions),
|
|
521
|
+
verification_evidence: normalizeStringList(payload.verification_evidence),
|
|
522
|
+
notes: normalizeText(payload.notes),
|
|
523
|
+
source: {
|
|
524
|
+
spec: normalizeText(payload?.source?.spec),
|
|
525
|
+
files: normalizeStringList(payload?.source?.files),
|
|
526
|
+
tests: normalizeStringList(payload?.source?.tests)
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return crypto.createHash('sha1').update(basis).digest('hex').slice(0, 16);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function createIncidentSummaryFromIncident(incident = {}) {
|
|
533
|
+
return {
|
|
534
|
+
id: incident.id,
|
|
535
|
+
fingerprint: normalizeText(incident.fingerprint),
|
|
536
|
+
title: normalizeText(incident.title),
|
|
537
|
+
symptom: normalizeText(incident.symptom),
|
|
538
|
+
state: normalizeIncidentState(incident.state, 'open'),
|
|
539
|
+
attempt_count: Number(incident.attempt_count || 0),
|
|
540
|
+
created_at: normalizeText(incident.created_at),
|
|
541
|
+
updated_at: normalizeText(incident.updated_at),
|
|
542
|
+
last_attempt_at: normalizeText(incident.last_attempt_at),
|
|
543
|
+
resolved_at: normalizeText(incident.resolved_at),
|
|
544
|
+
linked_entry_id: normalizeText(incident?.resolution?.entry_id || '')
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function syncIncidentLoopForRecord(paths, payload = {}, entry = {}, options = {}, fileSystem = fs) {
|
|
549
|
+
await ensureIncidentStorage(paths, fileSystem);
|
|
550
|
+
const incidentIndex = await readIncidentIndex(paths, fileSystem);
|
|
551
|
+
const fingerprint = normalizeText(payload.fingerprint || entry.fingerprint);
|
|
552
|
+
const title = normalizeText(payload.title || entry.title);
|
|
553
|
+
const symptom = normalizeText(payload.symptom || entry.symptom);
|
|
554
|
+
const currentTime = normalizeText(options.nowIso) || nowIso();
|
|
555
|
+
|
|
556
|
+
let incidentSummary = incidentIndex.incidents.find((item) => item.fingerprint === fingerprint) || null;
|
|
557
|
+
let incident = incidentSummary ? await readIncident(paths, incidentSummary.id, fileSystem) : null;
|
|
558
|
+
|
|
559
|
+
if (!incident) {
|
|
560
|
+
const incidentId = incidentSummary ? incidentSummary.id : createIncidentId();
|
|
561
|
+
incident = {
|
|
562
|
+
api_version: ERRORBOOK_INCIDENT_API_VERSION,
|
|
563
|
+
id: incidentId,
|
|
564
|
+
fingerprint,
|
|
565
|
+
title,
|
|
566
|
+
symptom,
|
|
567
|
+
state: 'open',
|
|
568
|
+
created_at: currentTime,
|
|
569
|
+
updated_at: currentTime,
|
|
570
|
+
last_attempt_at: '',
|
|
571
|
+
resolved_at: '',
|
|
572
|
+
attempt_count: 0,
|
|
573
|
+
attempts: [],
|
|
574
|
+
resolution: {
|
|
575
|
+
entry_id: '',
|
|
576
|
+
status: '',
|
|
577
|
+
quality_score: 0,
|
|
578
|
+
resolved_at: ''
|
|
579
|
+
},
|
|
580
|
+
tags: [],
|
|
581
|
+
ontology_tags: []
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const existingAttempts = Array.isArray(incident.attempts) ? incident.attempts : [];
|
|
586
|
+
const attemptNo = existingAttempts.length + 1;
|
|
587
|
+
const attemptSignature = createIncidentAttemptSignature(payload);
|
|
588
|
+
const duplicateOf = existingAttempts.find((item) => normalizeText(item.signature) === attemptSignature);
|
|
589
|
+
const attempt = {
|
|
590
|
+
id: createIncidentAttemptId(),
|
|
591
|
+
attempt_no: attemptNo,
|
|
592
|
+
recorded_at: currentTime,
|
|
593
|
+
signature: attemptSignature,
|
|
594
|
+
duplicate_of_attempt_no: duplicateOf ? Number(duplicateOf.attempt_no || 0) : 0,
|
|
595
|
+
entry_status: normalizeStatus(entry.status, 'candidate'),
|
|
596
|
+
quality_score: Number(entry.quality_score || scoreQuality(entry)),
|
|
597
|
+
root_cause: normalizeText(payload.root_cause || entry.root_cause),
|
|
598
|
+
fix_actions: normalizeStringList(payload.fix_actions || entry.fix_actions),
|
|
599
|
+
verification_evidence: normalizeStringList(payload.verification_evidence || entry.verification_evidence),
|
|
600
|
+
tags: normalizeStringList(payload.tags || entry.tags),
|
|
601
|
+
ontology_tags: normalizeOntologyTags(payload.ontology_tags || entry.ontology_tags),
|
|
602
|
+
notes: normalizeText(payload.notes || entry.notes),
|
|
603
|
+
source: {
|
|
604
|
+
spec: normalizeText(payload?.source?.spec || entry?.source?.spec),
|
|
605
|
+
files: normalizeStringList(payload?.source?.files || entry?.source?.files),
|
|
606
|
+
tests: normalizeStringList(payload?.source?.tests || entry?.source?.tests)
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
incident.attempts = [...existingAttempts, attempt];
|
|
611
|
+
incident.attempt_count = incident.attempts.length;
|
|
612
|
+
incident.title = title || incident.title;
|
|
613
|
+
incident.symptom = symptom || incident.symptom;
|
|
614
|
+
incident.tags = normalizeStringList(incident.tags, attempt.tags);
|
|
615
|
+
incident.ontology_tags = normalizeOntologyTags(incident.ontology_tags, attempt.ontology_tags);
|
|
616
|
+
incident.last_attempt_at = currentTime;
|
|
617
|
+
incident.updated_at = currentTime;
|
|
618
|
+
|
|
619
|
+
const resolveIncident = shouldResolveIncidentByStatus(entry.status);
|
|
620
|
+
if (resolveIncident) {
|
|
621
|
+
incident.state = 'resolved';
|
|
622
|
+
incident.resolved_at = currentTime;
|
|
623
|
+
incident.resolution = {
|
|
624
|
+
entry_id: normalizeText(entry.id),
|
|
625
|
+
status: normalizeStatus(entry.status, 'candidate'),
|
|
626
|
+
quality_score: Number(entry.quality_score || scoreQuality(entry)),
|
|
627
|
+
resolved_at: currentTime
|
|
628
|
+
};
|
|
629
|
+
} else {
|
|
630
|
+
incident.state = 'open';
|
|
631
|
+
incident.resolved_at = '';
|
|
632
|
+
incident.resolution = {
|
|
633
|
+
entry_id: '',
|
|
634
|
+
status: '',
|
|
635
|
+
quality_score: 0,
|
|
636
|
+
resolved_at: ''
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
await writeIncident(paths, incident, fileSystem);
|
|
641
|
+
if (incident.state === 'resolved') {
|
|
642
|
+
const resolvedSnapshotPath = buildIncidentResolvedSnapshotPath(paths, incident.id);
|
|
643
|
+
await fileSystem.ensureDir(path.dirname(resolvedSnapshotPath));
|
|
644
|
+
await fileSystem.writeJson(resolvedSnapshotPath, incident, { spaces: 2 });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const summary = createIncidentSummaryFromIncident(incident);
|
|
648
|
+
const summaryIndex = incidentIndex.incidents.findIndex((item) => item.id === summary.id);
|
|
649
|
+
if (summaryIndex >= 0) {
|
|
650
|
+
incidentIndex.incidents[summaryIndex] = summary;
|
|
651
|
+
} else {
|
|
652
|
+
incidentIndex.incidents.push(summary);
|
|
653
|
+
}
|
|
654
|
+
incidentIndex.incidents.sort((left, right) => `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`));
|
|
655
|
+
await writeIncidentIndex(paths, incidentIndex, fileSystem);
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
incident: summary,
|
|
659
|
+
latest_attempt: {
|
|
660
|
+
id: attempt.id,
|
|
661
|
+
attempt_no: attempt.attempt_no,
|
|
662
|
+
duplicate_of_attempt_no: attempt.duplicate_of_attempt_no,
|
|
663
|
+
signature: attempt.signature
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
420
668
|
function scoreQuality(entry = {}) {
|
|
421
669
|
let score = 0;
|
|
422
670
|
|
|
@@ -1380,12 +1628,16 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
1380
1628
|
}
|
|
1381
1629
|
index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
|
|
1382
1630
|
await writeErrorbookIndex(paths, index, fileSystem);
|
|
1631
|
+
const incidentLoop = await syncIncidentLoopForRecord(paths, normalized, entry, {
|
|
1632
|
+
nowIso: entry.updated_at
|
|
1633
|
+
}, fileSystem);
|
|
1383
1634
|
|
|
1384
1635
|
const result = {
|
|
1385
1636
|
mode: 'errorbook-record',
|
|
1386
1637
|
created,
|
|
1387
1638
|
deduplicated,
|
|
1388
|
-
entry
|
|
1639
|
+
entry,
|
|
1640
|
+
incident_loop: incidentLoop
|
|
1389
1641
|
};
|
|
1390
1642
|
|
|
1391
1643
|
if (options.json) {
|
|
@@ -1735,6 +1987,127 @@ async function runErrorbookRegistryHealthCommand(options = {}, dependencies = {}
|
|
|
1735
1987
|
return result;
|
|
1736
1988
|
}
|
|
1737
1989
|
|
|
1990
|
+
function findIncidentSummaryById(index, id) {
|
|
1991
|
+
const normalized = normalizeText(id);
|
|
1992
|
+
if (!normalized) {
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
const exact = index.incidents.find((item) => item.id === normalized);
|
|
1996
|
+
if (exact) {
|
|
1997
|
+
return exact;
|
|
1998
|
+
}
|
|
1999
|
+
const startsWith = index.incidents.filter((item) => item.id.startsWith(normalized));
|
|
2000
|
+
if (startsWith.length === 1) {
|
|
2001
|
+
return startsWith[0];
|
|
2002
|
+
}
|
|
2003
|
+
if (startsWith.length > 1) {
|
|
2004
|
+
throw new Error(`incident id prefix "${normalized}" is ambiguous (${startsWith.length} matches)`);
|
|
2005
|
+
}
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
async function runErrorbookIncidentListCommand(options = {}, dependencies = {}) {
|
|
2010
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
2011
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
2012
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
2013
|
+
const index = await readIncidentIndex(paths, fileSystem);
|
|
2014
|
+
|
|
2015
|
+
const stateFilter = normalizeText(options.state).toLowerCase();
|
|
2016
|
+
if (stateFilter && !['open', 'resolved'].includes(stateFilter)) {
|
|
2017
|
+
throw new Error('incident state must be one of: open, resolved');
|
|
2018
|
+
}
|
|
2019
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
2020
|
+
? Number(options.limit)
|
|
2021
|
+
: 20;
|
|
2022
|
+
|
|
2023
|
+
let incidents = [...index.incidents];
|
|
2024
|
+
if (stateFilter) {
|
|
2025
|
+
incidents = incidents.filter((item) => normalizeIncidentState(item.state) === stateFilter);
|
|
2026
|
+
}
|
|
2027
|
+
incidents.sort((left, right) => `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`));
|
|
2028
|
+
|
|
2029
|
+
const result = {
|
|
2030
|
+
mode: 'errorbook-incident-list',
|
|
2031
|
+
total_incidents: index.incidents.length,
|
|
2032
|
+
total_results: incidents.length,
|
|
2033
|
+
incidents: incidents.slice(0, limit)
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
if (options.json) {
|
|
2037
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2038
|
+
} else if (!options.silent) {
|
|
2039
|
+
if (result.incidents.length === 0) {
|
|
2040
|
+
console.log(chalk.gray('No staging incidents found'));
|
|
2041
|
+
} else {
|
|
2042
|
+
const table = new Table({
|
|
2043
|
+
head: ['ID', 'State', 'Attempts', 'Title', 'Updated'].map((item) => chalk.cyan(item)),
|
|
2044
|
+
colWidths: [20, 12, 10, 56, 24]
|
|
2045
|
+
});
|
|
2046
|
+
result.incidents.forEach((incident) => {
|
|
2047
|
+
table.push([
|
|
2048
|
+
incident.id,
|
|
2049
|
+
incident.state,
|
|
2050
|
+
Number(incident.attempt_count || 0),
|
|
2051
|
+
normalizeText(incident.title).length > 52
|
|
2052
|
+
? `${normalizeText(incident.title).slice(0, 52)}...`
|
|
2053
|
+
: normalizeText(incident.title),
|
|
2054
|
+
incident.updated_at || ''
|
|
2055
|
+
]);
|
|
2056
|
+
});
|
|
2057
|
+
console.log(table.toString());
|
|
2058
|
+
console.log(chalk.gray(`Total: ${result.total_results} (stored: ${result.total_incidents})`));
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
return result;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async function runErrorbookIncidentShowCommand(options = {}, dependencies = {}) {
|
|
2066
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
2067
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
2068
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
2069
|
+
const index = await readIncidentIndex(paths, fileSystem);
|
|
2070
|
+
|
|
2071
|
+
const id = normalizeText(options.id || options.incidentId);
|
|
2072
|
+
if (!id) {
|
|
2073
|
+
throw new Error('incident id is required');
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const summary = findIncidentSummaryById(index, id);
|
|
2077
|
+
if (!summary) {
|
|
2078
|
+
throw new Error(`staging incident not found: ${id}`);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
const incident = await readIncident(paths, summary.id, fileSystem);
|
|
2082
|
+
if (!incident) {
|
|
2083
|
+
throw new Error(`staging incident file not found: ${summary.id}`);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const result = {
|
|
2087
|
+
mode: 'errorbook-incident-show',
|
|
2088
|
+
incident
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
if (options.json) {
|
|
2092
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2093
|
+
} else if (!options.silent) {
|
|
2094
|
+
console.log(chalk.cyan.bold(incident.title || summary.title || summary.id));
|
|
2095
|
+
console.log(chalk.gray(`id: ${incident.id}`));
|
|
2096
|
+
console.log(chalk.gray(`state: ${incident.state}`));
|
|
2097
|
+
console.log(chalk.gray(`attempts: ${Number(incident.attempt_count || 0)}`));
|
|
2098
|
+
console.log(chalk.gray(`fingerprint: ${incident.fingerprint}`));
|
|
2099
|
+
console.log(chalk.gray(`updated_at: ${incident.updated_at}`));
|
|
2100
|
+
if (incident.state === 'resolved') {
|
|
2101
|
+
console.log(chalk.gray(`resolved_at: ${incident.resolved_at || '(none)'}`));
|
|
2102
|
+
if (incident.resolution && incident.resolution.entry_id) {
|
|
2103
|
+
console.log(chalk.gray(`linked_entry: ${incident.resolution.entry_id}`));
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
return result;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1738
2111
|
async function runErrorbookListCommand(options = {}, dependencies = {}) {
|
|
1739
2112
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
1740
2113
|
const fileSystem = dependencies.fileSystem || fs;
|
|
@@ -2299,6 +2672,36 @@ function registerErrorbookCommands(program) {
|
|
|
2299
2672
|
}
|
|
2300
2673
|
});
|
|
2301
2674
|
|
|
2675
|
+
const incident = errorbook
|
|
2676
|
+
.command('incident')
|
|
2677
|
+
.description('Inspect temporary trial-and-error incident loop before final curation');
|
|
2678
|
+
|
|
2679
|
+
incident
|
|
2680
|
+
.command('list')
|
|
2681
|
+
.description('List staging incidents')
|
|
2682
|
+
.option('--state <state>', 'Filter incident state (open|resolved)')
|
|
2683
|
+
.option('--limit <n>', 'Maximum incidents returned', parseInt, 20)
|
|
2684
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2685
|
+
.action(async (options) => {
|
|
2686
|
+
try {
|
|
2687
|
+
await runErrorbookIncidentListCommand(options);
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
emitCommandError(error, options.json);
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
incident
|
|
2694
|
+
.command('show <id>')
|
|
2695
|
+
.description('Show a staging incident with all attempts')
|
|
2696
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2697
|
+
.action(async (id, options) => {
|
|
2698
|
+
try {
|
|
2699
|
+
await runErrorbookIncidentShowCommand({ ...options, id });
|
|
2700
|
+
} catch (error) {
|
|
2701
|
+
emitCommandError(error, options.json);
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2302
2705
|
errorbook
|
|
2303
2706
|
.command('list')
|
|
2304
2707
|
.description('List curated errorbook entries')
|
|
@@ -2470,6 +2873,8 @@ module.exports = {
|
|
|
2470
2873
|
HIGH_RISK_SIGNAL_TAGS,
|
|
2471
2874
|
DEBUG_EVIDENCE_TAGS,
|
|
2472
2875
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
2876
|
+
ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
2877
|
+
ERRORBOOK_INCIDENT_API_VERSION,
|
|
2473
2878
|
resolveErrorbookPaths,
|
|
2474
2879
|
resolveErrorbookRegistryPaths,
|
|
2475
2880
|
normalizeOntologyTags,
|
|
@@ -2481,6 +2886,8 @@ module.exports = {
|
|
|
2481
2886
|
runErrorbookExportCommand,
|
|
2482
2887
|
runErrorbookSyncRegistryCommand,
|
|
2483
2888
|
runErrorbookRegistryHealthCommand,
|
|
2889
|
+
runErrorbookIncidentListCommand,
|
|
2890
|
+
runErrorbookIncidentShowCommand,
|
|
2484
2891
|
runErrorbookListCommand,
|
|
2485
2892
|
runErrorbookShowCommand,
|
|
2486
2893
|
runErrorbookFindCommand,
|
|
@@ -3,7 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const {
|
|
5
5
|
ensureSpecDomainArtifacts,
|
|
6
|
-
validateSpecDomainArtifacts
|
|
6
|
+
validateSpecDomainArtifacts,
|
|
7
|
+
analyzeSpecDomainCoverage
|
|
7
8
|
} = require('../spec/domain-modeling');
|
|
8
9
|
|
|
9
10
|
function normalizeText(value) {
|
|
@@ -109,18 +110,29 @@ async function runSpecDomainValidateCommand(options = {}, dependencies = {}) {
|
|
|
109
110
|
await assertSpecExists(projectPath, specId, fileSystem);
|
|
110
111
|
|
|
111
112
|
const validation = await validateSpecDomainArtifacts(projectPath, specId, fileSystem);
|
|
113
|
+
const coverage = await analyzeSpecDomainCoverage(projectPath, specId, fileSystem);
|
|
112
114
|
const payload = {
|
|
113
115
|
mode: 'spec-domain-validate',
|
|
114
116
|
spec_id: specId,
|
|
115
117
|
passed: validation.passed,
|
|
116
118
|
ratio: validation.ratio,
|
|
117
119
|
details: validation.details,
|
|
118
|
-
warnings: validation.warnings
|
|
120
|
+
warnings: validation.warnings,
|
|
121
|
+
coverage: {
|
|
122
|
+
passed: coverage.passed,
|
|
123
|
+
coverage_ratio: coverage.coverage_ratio,
|
|
124
|
+
covered_count: coverage.covered_count,
|
|
125
|
+
total_count: coverage.total_count,
|
|
126
|
+
uncovered: coverage.uncovered
|
|
127
|
+
}
|
|
119
128
|
};
|
|
120
129
|
|
|
121
130
|
if (options.failOnError && !validation.passed) {
|
|
122
131
|
throw new Error(`spec domain validation failed: ${validation.warnings.join('; ')}`);
|
|
123
132
|
}
|
|
133
|
+
if (options.failOnGap && !coverage.passed) {
|
|
134
|
+
throw new Error(`spec domain coverage has gaps: ${coverage.uncovered.join(', ')}`);
|
|
135
|
+
}
|
|
124
136
|
|
|
125
137
|
if (options.json) {
|
|
126
138
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -138,6 +150,49 @@ async function runSpecDomainValidateCommand(options = {}, dependencies = {}) {
|
|
|
138
150
|
return payload;
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
async function runSpecDomainCoverageCommand(options = {}, dependencies = {}) {
|
|
154
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
155
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
156
|
+
const specId = resolveSpecId(options);
|
|
157
|
+
if (!specId) {
|
|
158
|
+
throw new Error('--spec is required');
|
|
159
|
+
}
|
|
160
|
+
await assertSpecExists(projectPath, specId, fileSystem);
|
|
161
|
+
|
|
162
|
+
const coverage = await analyzeSpecDomainCoverage(projectPath, specId, fileSystem);
|
|
163
|
+
const payload = {
|
|
164
|
+
mode: 'spec-domain-coverage',
|
|
165
|
+
spec_id: specId,
|
|
166
|
+
passed: coverage.passed,
|
|
167
|
+
coverage_ratio: coverage.coverage_ratio,
|
|
168
|
+
covered_count: coverage.covered_count,
|
|
169
|
+
total_count: coverage.total_count,
|
|
170
|
+
uncovered: coverage.uncovered,
|
|
171
|
+
items: coverage.items
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (options.failOnGap && !coverage.passed) {
|
|
175
|
+
throw new Error(`spec domain coverage has gaps: ${coverage.uncovered.join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.json) {
|
|
179
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
180
|
+
} else if (!options.silent) {
|
|
181
|
+
if (coverage.passed) {
|
|
182
|
+
console.log(chalk.green('✓ Spec domain closed-loop coverage passed'));
|
|
183
|
+
} else {
|
|
184
|
+
console.log(chalk.red('✗ Spec domain closed-loop coverage has gaps'));
|
|
185
|
+
coverage.items
|
|
186
|
+
.filter((item) => !item.covered)
|
|
187
|
+
.forEach((item) => {
|
|
188
|
+
console.log(chalk.gray(` - ${item.id}: ${item.label}`));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return payload;
|
|
194
|
+
}
|
|
195
|
+
|
|
141
196
|
function registerSpecDomainCommand(program) {
|
|
142
197
|
const specDomain = program
|
|
143
198
|
.command('spec-domain')
|
|
@@ -194,6 +249,7 @@ function registerSpecDomainCommand(program) {
|
|
|
194
249
|
.description('Validate problem-domain artifacts for a Spec')
|
|
195
250
|
.requiredOption('--spec <name>', 'Spec identifier')
|
|
196
251
|
.option('--fail-on-error', 'Exit non-zero when validation fails')
|
|
252
|
+
.option('--fail-on-gap', 'Exit non-zero when closed-loop coverage is incomplete')
|
|
197
253
|
.option('--json', 'Output machine-readable JSON')
|
|
198
254
|
.action(async (options) => {
|
|
199
255
|
try {
|
|
@@ -207,11 +263,31 @@ function registerSpecDomainCommand(program) {
|
|
|
207
263
|
process.exit(1);
|
|
208
264
|
}
|
|
209
265
|
});
|
|
266
|
+
|
|
267
|
+
specDomain
|
|
268
|
+
.command('coverage')
|
|
269
|
+
.description('Analyze scene-closed-loop research coverage for a Spec')
|
|
270
|
+
.requiredOption('--spec <name>', 'Spec identifier')
|
|
271
|
+
.option('--fail-on-gap', 'Exit non-zero when closed-loop coverage is incomplete')
|
|
272
|
+
.option('--json', 'Output machine-readable JSON')
|
|
273
|
+
.action(async (options) => {
|
|
274
|
+
try {
|
|
275
|
+
await runSpecDomainCoverageCommand(options);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (options.json) {
|
|
278
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
279
|
+
} else {
|
|
280
|
+
console.error(chalk.red('❌ spec-domain coverage failed:'), error.message);
|
|
281
|
+
}
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
210
285
|
}
|
|
211
286
|
|
|
212
287
|
module.exports = {
|
|
213
288
|
runSpecDomainInitCommand,
|
|
214
289
|
runSpecDomainRefreshCommand,
|
|
215
290
|
runSpecDomainValidateCommand,
|
|
291
|
+
runSpecDomainCoverageCommand,
|
|
216
292
|
registerSpecDomainCommand
|
|
217
293
|
};
|