scene-capability-engine 3.3.26 → 3.4.6
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 +82 -0
- package/README.md +100 -711
- package/README.zh.md +103 -577
- package/bin/scene-capability-engine.js +103 -0
- package/docs/README.md +47 -249
- package/docs/command-reference.md +84 -9
- package/docs/images/wechat-qr.png +0 -0
- package/docs/spec-workflow.md +35 -4
- package/docs/zh/README.md +44 -331
- package/lib/adoption/adoption-strategy.js +5 -0
- package/lib/adoption/detection-engine.js +5 -0
- package/lib/adoption/file-classifier.js +6 -1
- package/lib/adoption/smart-orchestrator.js +5 -0
- package/lib/commands/adopt.js +32 -0
- package/lib/commands/errorbook.js +551 -4
- package/lib/commands/spec-domain.js +78 -2
- package/lib/commands/studio.js +550 -9
- package/lib/commands/upgrade.js +16 -0
- package/lib/problem/problem-evaluator.js +1035 -0
- package/lib/spec/domain-modeling.js +266 -5
- package/lib/workspace/takeover-baseline.js +514 -0
- package/package.json +1 -1
- package/template/.sce/config/problem-closure-policy.json +18 -0
- package/template/.sce/config/problem-eval-policy.json +78 -0
- package/template/.sce/config/session-governance.json +8 -0
- package/template/.sce/config/spec-domain-policy.json +14 -0
- package/template/.sce/config/takeover-baseline.json +33 -0
- package/template/.sce/steering/CORE_PRINCIPLES.md +4 -2
|
@@ -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';
|
|
@@ -46,6 +48,7 @@ const ONTOLOGY_TAG_ALIASES = Object.freeze({
|
|
|
46
48
|
action_chain: 'execution_flow'
|
|
47
49
|
});
|
|
48
50
|
const DEFAULT_PROMOTE_MIN_QUALITY = 75;
|
|
51
|
+
const DEFAULT_RELEASE_GATE_MIN_QUALITY = 70;
|
|
49
52
|
const ERRORBOOK_RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);
|
|
50
53
|
const DEBUG_EVIDENCE_TAGS = Object.freeze([
|
|
51
54
|
'debug-evidence',
|
|
@@ -65,11 +68,16 @@ const HIGH_RISK_SIGNAL_TAGS = Object.freeze([
|
|
|
65
68
|
|
|
66
69
|
function resolveErrorbookPaths(projectPath = process.cwd()) {
|
|
67
70
|
const baseDir = path.join(projectPath, '.sce', 'errorbook');
|
|
71
|
+
const stagingDir = path.join(baseDir, 'staging');
|
|
68
72
|
return {
|
|
69
73
|
projectPath,
|
|
70
74
|
baseDir,
|
|
71
75
|
entriesDir: path.join(baseDir, 'entries'),
|
|
72
|
-
indexFile: path.join(baseDir, 'index.json')
|
|
76
|
+
indexFile: path.join(baseDir, 'index.json'),
|
|
77
|
+
stagingDir,
|
|
78
|
+
incidentsDir: path.join(stagingDir, 'incidents'),
|
|
79
|
+
resolvedDir: path.join(stagingDir, 'resolved'),
|
|
80
|
+
incidentIndexFile: path.join(stagingDir, 'index.json')
|
|
73
81
|
};
|
|
74
82
|
}
|
|
75
83
|
|
|
@@ -417,6 +425,266 @@ async function writeErrorbookEntry(paths, entry, fileSystem = fs) {
|
|
|
417
425
|
return entryPath;
|
|
418
426
|
}
|
|
419
427
|
|
|
428
|
+
function buildDefaultIncidentIndex() {
|
|
429
|
+
return {
|
|
430
|
+
api_version: ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
431
|
+
updated_at: nowIso(),
|
|
432
|
+
total_incidents: 0,
|
|
433
|
+
incidents: []
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeIncidentState(value, fallback = 'open') {
|
|
438
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
439
|
+
if (!normalized) {
|
|
440
|
+
return fallback;
|
|
441
|
+
}
|
|
442
|
+
if (normalized === 'open' || normalized === 'resolved') {
|
|
443
|
+
return normalized;
|
|
444
|
+
}
|
|
445
|
+
return fallback;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function shouldResolveIncidentByStatus(status = '') {
|
|
449
|
+
const normalized = normalizeStatus(status, 'candidate');
|
|
450
|
+
return normalized === 'verified' || normalized === 'promoted' || normalized === 'deprecated';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function createIncidentId() {
|
|
454
|
+
return `ebi-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function createIncidentAttemptId() {
|
|
458
|
+
return `attempt-${Date.now()}-${crypto.randomBytes(2).toString('hex')}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function buildIncidentFilePath(paths, incidentId) {
|
|
462
|
+
return path.join(paths.incidentsDir, `${incidentId}.json`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function buildIncidentResolvedSnapshotPath(paths, incidentId) {
|
|
466
|
+
return path.join(paths.resolvedDir, `${incidentId}.json`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function ensureIncidentStorage(paths, fileSystem = fs) {
|
|
470
|
+
await fileSystem.ensureDir(paths.incidentsDir);
|
|
471
|
+
await fileSystem.ensureDir(paths.resolvedDir);
|
|
472
|
+
if (!await fileSystem.pathExists(paths.incidentIndexFile)) {
|
|
473
|
+
await fileSystem.writeJson(paths.incidentIndexFile, buildDefaultIncidentIndex(), { spaces: 2 });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function readIncidentIndex(paths, fileSystem = fs) {
|
|
478
|
+
await ensureIncidentStorage(paths, fileSystem);
|
|
479
|
+
const payload = await fileSystem.readJson(paths.incidentIndexFile).catch(() => null);
|
|
480
|
+
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.incidents)) {
|
|
481
|
+
return buildDefaultIncidentIndex();
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
api_version: payload.api_version || ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
485
|
+
updated_at: normalizeText(payload.updated_at) || nowIso(),
|
|
486
|
+
total_incidents: Number.isInteger(payload.total_incidents) ? payload.total_incidents : payload.incidents.length,
|
|
487
|
+
incidents: payload.incidents
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function writeIncidentIndex(paths, index, fileSystem = fs) {
|
|
492
|
+
const payload = {
|
|
493
|
+
api_version: ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
494
|
+
updated_at: nowIso(),
|
|
495
|
+
incidents: Array.isArray(index.incidents) ? index.incidents : []
|
|
496
|
+
};
|
|
497
|
+
payload.total_incidents = payload.incidents.length;
|
|
498
|
+
await fileSystem.ensureDir(path.dirname(paths.incidentIndexFile));
|
|
499
|
+
await fileSystem.writeJson(paths.incidentIndexFile, payload, { spaces: 2 });
|
|
500
|
+
return payload;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function readIncident(paths, incidentId, fileSystem = fs) {
|
|
504
|
+
const filePath = buildIncidentFilePath(paths, incidentId);
|
|
505
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return fileSystem.readJson(filePath);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function writeIncident(paths, incident, fileSystem = fs) {
|
|
512
|
+
const filePath = buildIncidentFilePath(paths, incident.id);
|
|
513
|
+
await fileSystem.ensureDir(path.dirname(filePath));
|
|
514
|
+
await fileSystem.writeJson(filePath, incident, { spaces: 2 });
|
|
515
|
+
return filePath;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function createIncidentAttemptSignature(payload = {}) {
|
|
519
|
+
const attemptContract = payload && payload.attempt_contract && typeof payload.attempt_contract === 'object'
|
|
520
|
+
? payload.attempt_contract
|
|
521
|
+
: {};
|
|
522
|
+
const basis = JSON.stringify({
|
|
523
|
+
root_cause: normalizeText(payload.root_cause),
|
|
524
|
+
fix_actions: normalizeStringList(payload.fix_actions),
|
|
525
|
+
verification_evidence: normalizeStringList(payload.verification_evidence),
|
|
526
|
+
notes: normalizeText(payload.notes),
|
|
527
|
+
attempt_contract: {
|
|
528
|
+
hypothesis: normalizeText(attemptContract.hypothesis),
|
|
529
|
+
change_points: normalizeStringList(attemptContract.change_points),
|
|
530
|
+
verification_result: normalizeText(attemptContract.verification_result),
|
|
531
|
+
rollback_point: normalizeText(attemptContract.rollback_point),
|
|
532
|
+
conclusion: normalizeText(attemptContract.conclusion)
|
|
533
|
+
},
|
|
534
|
+
source: {
|
|
535
|
+
spec: normalizeText(payload?.source?.spec),
|
|
536
|
+
files: normalizeStringList(payload?.source?.files),
|
|
537
|
+
tests: normalizeStringList(payload?.source?.tests)
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
return crypto.createHash('sha1').update(basis).digest('hex').slice(0, 16);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function createIncidentSummaryFromIncident(incident = {}) {
|
|
544
|
+
return {
|
|
545
|
+
id: incident.id,
|
|
546
|
+
fingerprint: normalizeText(incident.fingerprint),
|
|
547
|
+
title: normalizeText(incident.title),
|
|
548
|
+
symptom: normalizeText(incident.symptom),
|
|
549
|
+
state: normalizeIncidentState(incident.state, 'open'),
|
|
550
|
+
attempt_count: Number(incident.attempt_count || 0),
|
|
551
|
+
created_at: normalizeText(incident.created_at),
|
|
552
|
+
updated_at: normalizeText(incident.updated_at),
|
|
553
|
+
last_attempt_at: normalizeText(incident.last_attempt_at),
|
|
554
|
+
resolved_at: normalizeText(incident.resolved_at),
|
|
555
|
+
linked_entry_id: normalizeText(incident?.resolution?.entry_id || '')
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function syncIncidentLoopForRecord(paths, payload = {}, entry = {}, options = {}, fileSystem = fs) {
|
|
560
|
+
await ensureIncidentStorage(paths, fileSystem);
|
|
561
|
+
const incidentIndex = await readIncidentIndex(paths, fileSystem);
|
|
562
|
+
const fingerprint = normalizeText(payload.fingerprint || entry.fingerprint);
|
|
563
|
+
const title = normalizeText(payload.title || entry.title);
|
|
564
|
+
const symptom = normalizeText(payload.symptom || entry.symptom);
|
|
565
|
+
const currentTime = normalizeText(options.nowIso) || nowIso();
|
|
566
|
+
|
|
567
|
+
let incidentSummary = incidentIndex.incidents.find((item) => item.fingerprint === fingerprint) || null;
|
|
568
|
+
let incident = incidentSummary ? await readIncident(paths, incidentSummary.id, fileSystem) : null;
|
|
569
|
+
|
|
570
|
+
if (!incident) {
|
|
571
|
+
const incidentId = incidentSummary ? incidentSummary.id : createIncidentId();
|
|
572
|
+
incident = {
|
|
573
|
+
api_version: ERRORBOOK_INCIDENT_API_VERSION,
|
|
574
|
+
id: incidentId,
|
|
575
|
+
fingerprint,
|
|
576
|
+
title,
|
|
577
|
+
symptom,
|
|
578
|
+
state: 'open',
|
|
579
|
+
created_at: currentTime,
|
|
580
|
+
updated_at: currentTime,
|
|
581
|
+
last_attempt_at: '',
|
|
582
|
+
resolved_at: '',
|
|
583
|
+
attempt_count: 0,
|
|
584
|
+
attempts: [],
|
|
585
|
+
resolution: {
|
|
586
|
+
entry_id: '',
|
|
587
|
+
status: '',
|
|
588
|
+
quality_score: 0,
|
|
589
|
+
resolved_at: ''
|
|
590
|
+
},
|
|
591
|
+
tags: [],
|
|
592
|
+
ontology_tags: []
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const existingAttempts = Array.isArray(incident.attempts) ? incident.attempts : [];
|
|
597
|
+
const attemptNo = existingAttempts.length + 1;
|
|
598
|
+
const attemptSignature = createIncidentAttemptSignature(payload);
|
|
599
|
+
const duplicateOf = existingAttempts.find((item) => normalizeText(item.signature) === attemptSignature);
|
|
600
|
+
const attempt = {
|
|
601
|
+
id: createIncidentAttemptId(),
|
|
602
|
+
attempt_no: attemptNo,
|
|
603
|
+
recorded_at: currentTime,
|
|
604
|
+
signature: attemptSignature,
|
|
605
|
+
duplicate_of_attempt_no: duplicateOf ? Number(duplicateOf.attempt_no || 0) : 0,
|
|
606
|
+
entry_status: normalizeStatus(entry.status, 'candidate'),
|
|
607
|
+
quality_score: Number(entry.quality_score || scoreQuality(entry)),
|
|
608
|
+
root_cause: normalizeText(payload.root_cause || entry.root_cause),
|
|
609
|
+
fix_actions: normalizeStringList(payload.fix_actions || entry.fix_actions),
|
|
610
|
+
verification_evidence: normalizeStringList(payload.verification_evidence || entry.verification_evidence),
|
|
611
|
+
tags: normalizeStringList(payload.tags || entry.tags),
|
|
612
|
+
ontology_tags: normalizeOntologyTags(payload.ontology_tags || entry.ontology_tags),
|
|
613
|
+
notes: normalizeText(payload.notes || entry.notes),
|
|
614
|
+
attempt_contract: {
|
|
615
|
+
hypothesis: normalizeText(payload?.attempt_contract?.hypothesis || entry?.attempt_contract?.hypothesis),
|
|
616
|
+
change_points: normalizeStringList(payload?.attempt_contract?.change_points || entry?.attempt_contract?.change_points),
|
|
617
|
+
verification_result: normalizeText(
|
|
618
|
+
payload?.attempt_contract?.verification_result || entry?.attempt_contract?.verification_result
|
|
619
|
+
),
|
|
620
|
+
rollback_point: normalizeText(payload?.attempt_contract?.rollback_point || entry?.attempt_contract?.rollback_point),
|
|
621
|
+
conclusion: normalizeText(payload?.attempt_contract?.conclusion || entry?.attempt_contract?.conclusion)
|
|
622
|
+
},
|
|
623
|
+
source: {
|
|
624
|
+
spec: normalizeText(payload?.source?.spec || entry?.source?.spec),
|
|
625
|
+
files: normalizeStringList(payload?.source?.files || entry?.source?.files),
|
|
626
|
+
tests: normalizeStringList(payload?.source?.tests || entry?.source?.tests)
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
incident.attempts = [...existingAttempts, attempt];
|
|
631
|
+
incident.attempt_count = incident.attempts.length;
|
|
632
|
+
incident.title = title || incident.title;
|
|
633
|
+
incident.symptom = symptom || incident.symptom;
|
|
634
|
+
incident.tags = normalizeStringList(incident.tags, attempt.tags);
|
|
635
|
+
incident.ontology_tags = normalizeOntologyTags(incident.ontology_tags, attempt.ontology_tags);
|
|
636
|
+
incident.last_attempt_at = currentTime;
|
|
637
|
+
incident.updated_at = currentTime;
|
|
638
|
+
|
|
639
|
+
const resolveIncident = shouldResolveIncidentByStatus(entry.status);
|
|
640
|
+
if (resolveIncident) {
|
|
641
|
+
incident.state = 'resolved';
|
|
642
|
+
incident.resolved_at = currentTime;
|
|
643
|
+
incident.resolution = {
|
|
644
|
+
entry_id: normalizeText(entry.id),
|
|
645
|
+
status: normalizeStatus(entry.status, 'candidate'),
|
|
646
|
+
quality_score: Number(entry.quality_score || scoreQuality(entry)),
|
|
647
|
+
resolved_at: currentTime
|
|
648
|
+
};
|
|
649
|
+
} else {
|
|
650
|
+
incident.state = 'open';
|
|
651
|
+
incident.resolved_at = '';
|
|
652
|
+
incident.resolution = {
|
|
653
|
+
entry_id: '',
|
|
654
|
+
status: '',
|
|
655
|
+
quality_score: 0,
|
|
656
|
+
resolved_at: ''
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
await writeIncident(paths, incident, fileSystem);
|
|
661
|
+
if (incident.state === 'resolved') {
|
|
662
|
+
const resolvedSnapshotPath = buildIncidentResolvedSnapshotPath(paths, incident.id);
|
|
663
|
+
await fileSystem.ensureDir(path.dirname(resolvedSnapshotPath));
|
|
664
|
+
await fileSystem.writeJson(resolvedSnapshotPath, incident, { spaces: 2 });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const summary = createIncidentSummaryFromIncident(incident);
|
|
668
|
+
const summaryIndex = incidentIndex.incidents.findIndex((item) => item.id === summary.id);
|
|
669
|
+
if (summaryIndex >= 0) {
|
|
670
|
+
incidentIndex.incidents[summaryIndex] = summary;
|
|
671
|
+
} else {
|
|
672
|
+
incidentIndex.incidents.push(summary);
|
|
673
|
+
}
|
|
674
|
+
incidentIndex.incidents.sort((left, right) => `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`));
|
|
675
|
+
await writeIncidentIndex(paths, incidentIndex, fileSystem);
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
incident: summary,
|
|
679
|
+
latest_attempt: {
|
|
680
|
+
id: attempt.id,
|
|
681
|
+
attempt_no: attempt.attempt_no,
|
|
682
|
+
duplicate_of_attempt_no: attempt.duplicate_of_attempt_no,
|
|
683
|
+
signature: attempt.signature
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
420
688
|
function scoreQuality(entry = {}) {
|
|
421
689
|
let score = 0;
|
|
422
690
|
|
|
@@ -447,6 +715,19 @@ function scoreQuality(entry = {}) {
|
|
|
447
715
|
if (normalizeText(entry.symptom).length >= 24 && normalizeText(entry.root_cause).length >= 24) {
|
|
448
716
|
score += 2;
|
|
449
717
|
}
|
|
718
|
+
const attemptContract = entry && typeof entry.attempt_contract === 'object'
|
|
719
|
+
? entry.attempt_contract
|
|
720
|
+
: {};
|
|
721
|
+
const attemptContractComplete = Boolean(
|
|
722
|
+
normalizeText(attemptContract.hypothesis)
|
|
723
|
+
&& normalizeStringList(attemptContract.change_points).length > 0
|
|
724
|
+
&& normalizeText(attemptContract.verification_result)
|
|
725
|
+
&& normalizeText(attemptContract.rollback_point)
|
|
726
|
+
&& normalizeText(attemptContract.conclusion)
|
|
727
|
+
);
|
|
728
|
+
if (attemptContractComplete) {
|
|
729
|
+
score += 5;
|
|
730
|
+
}
|
|
450
731
|
|
|
451
732
|
return Math.max(0, Math.min(100, score));
|
|
452
733
|
}
|
|
@@ -464,6 +745,24 @@ function validateRecordPayload(payload) {
|
|
|
464
745
|
if (!Array.isArray(payload.fix_actions) || payload.fix_actions.length === 0) {
|
|
465
746
|
throw new Error('at least one --fix-action is required');
|
|
466
747
|
}
|
|
748
|
+
const attemptContract = payload.attempt_contract && typeof payload.attempt_contract === 'object'
|
|
749
|
+
? payload.attempt_contract
|
|
750
|
+
: {};
|
|
751
|
+
if (!normalizeText(attemptContract.hypothesis)) {
|
|
752
|
+
throw new Error('attempt contract requires hypothesis');
|
|
753
|
+
}
|
|
754
|
+
if (!Array.isArray(attemptContract.change_points) || attemptContract.change_points.length === 0) {
|
|
755
|
+
throw new Error('attempt contract requires change_points');
|
|
756
|
+
}
|
|
757
|
+
if (!normalizeText(attemptContract.verification_result)) {
|
|
758
|
+
throw new Error('attempt contract requires verification_result');
|
|
759
|
+
}
|
|
760
|
+
if (!normalizeText(attemptContract.rollback_point)) {
|
|
761
|
+
throw new Error('attempt contract requires rollback_point');
|
|
762
|
+
}
|
|
763
|
+
if (!normalizeText(attemptContract.conclusion)) {
|
|
764
|
+
throw new Error('attempt contract requires conclusion');
|
|
765
|
+
}
|
|
467
766
|
|
|
468
767
|
const status = normalizeStatus(payload.status, 'candidate');
|
|
469
768
|
if (status === 'promoted') {
|
|
@@ -559,6 +858,42 @@ function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
|
559
858
|
})
|
|
560
859
|
};
|
|
561
860
|
|
|
861
|
+
const attemptContractFromFile = fromFilePayload && typeof fromFilePayload.attempt_contract === 'object'
|
|
862
|
+
? fromFilePayload.attempt_contract
|
|
863
|
+
: {};
|
|
864
|
+
payload.attempt_contract = {
|
|
865
|
+
hypothesis: normalizeText(
|
|
866
|
+
options.attemptHypothesis
|
|
867
|
+
|| attemptContractFromFile.hypothesis
|
|
868
|
+
|| payload.root_cause
|
|
869
|
+
),
|
|
870
|
+
change_points: normalizeStringList(
|
|
871
|
+
options.attemptChangePoints,
|
|
872
|
+
attemptContractFromFile.change_points,
|
|
873
|
+
attemptContractFromFile.changePoints,
|
|
874
|
+
payload.fix_actions
|
|
875
|
+
),
|
|
876
|
+
verification_result: normalizeText(
|
|
877
|
+
options.attemptVerificationResult
|
|
878
|
+
|| attemptContractFromFile.verification_result
|
|
879
|
+
|| attemptContractFromFile.verificationResult
|
|
880
|
+
|| (payload.verification_evidence[0] || '')
|
|
881
|
+
|| 'pending-verification'
|
|
882
|
+
),
|
|
883
|
+
rollback_point: normalizeText(
|
|
884
|
+
options.attemptRollbackPoint
|
|
885
|
+
|| attemptContractFromFile.rollback_point
|
|
886
|
+
|| attemptContractFromFile.rollbackPoint
|
|
887
|
+
|| 'not-required'
|
|
888
|
+
),
|
|
889
|
+
conclusion: normalizeText(
|
|
890
|
+
options.attemptConclusion
|
|
891
|
+
|| attemptContractFromFile.conclusion
|
|
892
|
+
|| payload.notes
|
|
893
|
+
|| payload.root_cause
|
|
894
|
+
)
|
|
895
|
+
};
|
|
896
|
+
|
|
562
897
|
return payload;
|
|
563
898
|
}
|
|
564
899
|
|
|
@@ -624,6 +959,30 @@ function mergeEntry(existingEntry, incomingPayload) {
|
|
|
624
959
|
ontology_tags: normalizeOntologyTags(existingEntry.ontology_tags, incomingPayload.ontology_tags),
|
|
625
960
|
status: selectStatus(existingEntry.status, incomingPayload.status),
|
|
626
961
|
notes: normalizeText(incomingPayload.notes) || existingEntry.notes || '',
|
|
962
|
+
attempt_contract: {
|
|
963
|
+
hypothesis: normalizeText(incomingPayload?.attempt_contract?.hypothesis)
|
|
964
|
+
|| normalizeText(existingEntry?.attempt_contract?.hypothesis)
|
|
965
|
+
|| normalizeText(incomingPayload.root_cause)
|
|
966
|
+
|| normalizeText(existingEntry.root_cause),
|
|
967
|
+
change_points: normalizeStringList(
|
|
968
|
+
existingEntry?.attempt_contract?.change_points,
|
|
969
|
+
incomingPayload?.attempt_contract?.change_points,
|
|
970
|
+
incomingPayload.fix_actions
|
|
971
|
+
),
|
|
972
|
+
verification_result: normalizeText(incomingPayload?.attempt_contract?.verification_result)
|
|
973
|
+
|| normalizeText(existingEntry?.attempt_contract?.verification_result)
|
|
974
|
+
|| normalizeStringList(incomingPayload.verification_evidence, existingEntry.verification_evidence)[0]
|
|
975
|
+
|| '',
|
|
976
|
+
rollback_point: normalizeText(incomingPayload?.attempt_contract?.rollback_point)
|
|
977
|
+
|| normalizeText(existingEntry?.attempt_contract?.rollback_point)
|
|
978
|
+
|| 'not-required',
|
|
979
|
+
conclusion: normalizeText(incomingPayload?.attempt_contract?.conclusion)
|
|
980
|
+
|| normalizeText(existingEntry?.attempt_contract?.conclusion)
|
|
981
|
+
|| normalizeText(incomingPayload.notes)
|
|
982
|
+
|| normalizeText(existingEntry.notes)
|
|
983
|
+
|| normalizeText(incomingPayload.root_cause)
|
|
984
|
+
|| normalizeText(existingEntry.root_cause)
|
|
985
|
+
},
|
|
627
986
|
source: {
|
|
628
987
|
spec: normalizeText(incomingPayload?.source?.spec) || normalizeText(existingEntry?.source?.spec),
|
|
629
988
|
files: normalizeStringList(existingEntry?.source?.files, incomingPayload?.source?.files),
|
|
@@ -1147,7 +1506,7 @@ function evaluateEntryRisk(entry = {}) {
|
|
|
1147
1506
|
if (hasHighRiskTag) {
|
|
1148
1507
|
return 'high';
|
|
1149
1508
|
}
|
|
1150
|
-
if (status === 'candidate' && qualityScore
|
|
1509
|
+
if (status === 'candidate' && qualityScore > 85) {
|
|
1151
1510
|
return 'high';
|
|
1152
1511
|
}
|
|
1153
1512
|
if (status === 'candidate' && qualityScore >= 75 && ontologyTags.includes('decision_policy')) {
|
|
@@ -1156,7 +1515,7 @@ function evaluateEntryRisk(entry = {}) {
|
|
|
1156
1515
|
if (status === 'candidate') {
|
|
1157
1516
|
return 'medium';
|
|
1158
1517
|
}
|
|
1159
|
-
if (qualityScore
|
|
1518
|
+
if (qualityScore > 85 && ontologyTags.includes('decision_policy')) {
|
|
1160
1519
|
return 'high';
|
|
1161
1520
|
}
|
|
1162
1521
|
return 'medium';
|
|
@@ -1169,6 +1528,9 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
1169
1528
|
const index = await readErrorbookIndex(paths, fileSystem);
|
|
1170
1529
|
const minRisk = normalizeRiskLevel(options.minRisk || options.min_risk || 'high', 'high');
|
|
1171
1530
|
const includeVerified = options.includeVerified === true;
|
|
1531
|
+
const minQuality = Number.isFinite(Number(options.minQuality || options.min_quality))
|
|
1532
|
+
? Math.max(0, Math.min(100, Number(options.minQuality || options.min_quality)))
|
|
1533
|
+
: DEFAULT_RELEASE_GATE_MIN_QUALITY;
|
|
1172
1534
|
|
|
1173
1535
|
const inspected = [];
|
|
1174
1536
|
const mitigationInspected = [];
|
|
@@ -1226,6 +1588,14 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
1226
1588
|
block_reasons: ['risk_threshold']
|
|
1227
1589
|
}));
|
|
1228
1590
|
|
|
1591
|
+
const curationBlocked = inspected
|
|
1592
|
+
.filter((item) => item.status === 'verified' && Number(item.quality_score || 0) < minQuality)
|
|
1593
|
+
.map((item) => ({
|
|
1594
|
+
...item,
|
|
1595
|
+
block_reasons: ['curation_quality'],
|
|
1596
|
+
policy_violations: [`quality_score<${minQuality}`]
|
|
1597
|
+
}));
|
|
1598
|
+
|
|
1229
1599
|
const blockedById = new Map();
|
|
1230
1600
|
for (const item of riskBlocked) {
|
|
1231
1601
|
blockedById.set(item.id, {
|
|
@@ -1249,6 +1619,19 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
1249
1619
|
}
|
|
1250
1620
|
blockedById.set(existing.id, existing);
|
|
1251
1621
|
}
|
|
1622
|
+
for (const item of curationBlocked) {
|
|
1623
|
+
const existing = blockedById.get(item.id);
|
|
1624
|
+
if (!existing) {
|
|
1625
|
+
blockedById.set(item.id, {
|
|
1626
|
+
...item,
|
|
1627
|
+
policy_violations: normalizeStringList(item.policy_violations)
|
|
1628
|
+
});
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
existing.block_reasons = normalizeStringList(existing.block_reasons, item.block_reasons);
|
|
1632
|
+
existing.policy_violations = normalizeStringList(existing.policy_violations, item.policy_violations);
|
|
1633
|
+
blockedById.set(existing.id, existing);
|
|
1634
|
+
}
|
|
1252
1635
|
|
|
1253
1636
|
const blocked = Array.from(blockedById.values())
|
|
1254
1637
|
.sort((left, right) => {
|
|
@@ -1271,12 +1654,14 @@ async function evaluateErrorbookReleaseGate(options = {}, dependencies = {}) {
|
|
|
1271
1654
|
mode: 'errorbook-release-gate',
|
|
1272
1655
|
gate: {
|
|
1273
1656
|
min_risk: minRisk,
|
|
1657
|
+
min_quality: minQuality,
|
|
1274
1658
|
include_verified: includeVerified,
|
|
1275
1659
|
mitigation_policy_enforced: true
|
|
1276
1660
|
},
|
|
1277
1661
|
passed: blocked.length === 0,
|
|
1278
1662
|
inspected_count: inspected.length,
|
|
1279
1663
|
risk_blocked_count: riskBlocked.length,
|
|
1664
|
+
curation_blocked_count: curationBlocked.length,
|
|
1280
1665
|
mitigation_inspected_count: mitigationInspected.length,
|
|
1281
1666
|
mitigation_blocked_count: mitigationBlocked.length,
|
|
1282
1667
|
blocked_count: blocked.length,
|
|
@@ -1361,6 +1746,7 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
1361
1746
|
source: normalized.source,
|
|
1362
1747
|
temporary_mitigation: mitigationPayload,
|
|
1363
1748
|
notes: normalized.notes || '',
|
|
1749
|
+
attempt_contract: normalized.attempt_contract,
|
|
1364
1750
|
occurrences: 1
|
|
1365
1751
|
};
|
|
1366
1752
|
entry.quality_score = scoreQuality(entry);
|
|
@@ -1380,12 +1766,16 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
1380
1766
|
}
|
|
1381
1767
|
index.entries.sort((left, right) => `${right.updated_at}`.localeCompare(`${left.updated_at}`));
|
|
1382
1768
|
await writeErrorbookIndex(paths, index, fileSystem);
|
|
1769
|
+
const incidentLoop = await syncIncidentLoopForRecord(paths, normalized, entry, {
|
|
1770
|
+
nowIso: entry.updated_at
|
|
1771
|
+
}, fileSystem);
|
|
1383
1772
|
|
|
1384
1773
|
const result = {
|
|
1385
1774
|
mode: 'errorbook-record',
|
|
1386
1775
|
created,
|
|
1387
1776
|
deduplicated,
|
|
1388
|
-
entry
|
|
1777
|
+
entry,
|
|
1778
|
+
incident_loop: incidentLoop
|
|
1389
1779
|
};
|
|
1390
1780
|
|
|
1391
1781
|
if (options.json) {
|
|
@@ -1735,6 +2125,127 @@ async function runErrorbookRegistryHealthCommand(options = {}, dependencies = {}
|
|
|
1735
2125
|
return result;
|
|
1736
2126
|
}
|
|
1737
2127
|
|
|
2128
|
+
function findIncidentSummaryById(index, id) {
|
|
2129
|
+
const normalized = normalizeText(id);
|
|
2130
|
+
if (!normalized) {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
const exact = index.incidents.find((item) => item.id === normalized);
|
|
2134
|
+
if (exact) {
|
|
2135
|
+
return exact;
|
|
2136
|
+
}
|
|
2137
|
+
const startsWith = index.incidents.filter((item) => item.id.startsWith(normalized));
|
|
2138
|
+
if (startsWith.length === 1) {
|
|
2139
|
+
return startsWith[0];
|
|
2140
|
+
}
|
|
2141
|
+
if (startsWith.length > 1) {
|
|
2142
|
+
throw new Error(`incident id prefix "${normalized}" is ambiguous (${startsWith.length} matches)`);
|
|
2143
|
+
}
|
|
2144
|
+
return null;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
async function runErrorbookIncidentListCommand(options = {}, dependencies = {}) {
|
|
2148
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
2149
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
2150
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
2151
|
+
const index = await readIncidentIndex(paths, fileSystem);
|
|
2152
|
+
|
|
2153
|
+
const stateFilter = normalizeText(options.state).toLowerCase();
|
|
2154
|
+
if (stateFilter && !['open', 'resolved'].includes(stateFilter)) {
|
|
2155
|
+
throw new Error('incident state must be one of: open, resolved');
|
|
2156
|
+
}
|
|
2157
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
2158
|
+
? Number(options.limit)
|
|
2159
|
+
: 20;
|
|
2160
|
+
|
|
2161
|
+
let incidents = [...index.incidents];
|
|
2162
|
+
if (stateFilter) {
|
|
2163
|
+
incidents = incidents.filter((item) => normalizeIncidentState(item.state) === stateFilter);
|
|
2164
|
+
}
|
|
2165
|
+
incidents.sort((left, right) => `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`));
|
|
2166
|
+
|
|
2167
|
+
const result = {
|
|
2168
|
+
mode: 'errorbook-incident-list',
|
|
2169
|
+
total_incidents: index.incidents.length,
|
|
2170
|
+
total_results: incidents.length,
|
|
2171
|
+
incidents: incidents.slice(0, limit)
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
if (options.json) {
|
|
2175
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2176
|
+
} else if (!options.silent) {
|
|
2177
|
+
if (result.incidents.length === 0) {
|
|
2178
|
+
console.log(chalk.gray('No staging incidents found'));
|
|
2179
|
+
} else {
|
|
2180
|
+
const table = new Table({
|
|
2181
|
+
head: ['ID', 'State', 'Attempts', 'Title', 'Updated'].map((item) => chalk.cyan(item)),
|
|
2182
|
+
colWidths: [20, 12, 10, 56, 24]
|
|
2183
|
+
});
|
|
2184
|
+
result.incidents.forEach((incident) => {
|
|
2185
|
+
table.push([
|
|
2186
|
+
incident.id,
|
|
2187
|
+
incident.state,
|
|
2188
|
+
Number(incident.attempt_count || 0),
|
|
2189
|
+
normalizeText(incident.title).length > 52
|
|
2190
|
+
? `${normalizeText(incident.title).slice(0, 52)}...`
|
|
2191
|
+
: normalizeText(incident.title),
|
|
2192
|
+
incident.updated_at || ''
|
|
2193
|
+
]);
|
|
2194
|
+
});
|
|
2195
|
+
console.log(table.toString());
|
|
2196
|
+
console.log(chalk.gray(`Total: ${result.total_results} (stored: ${result.total_incidents})`));
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
return result;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
async function runErrorbookIncidentShowCommand(options = {}, dependencies = {}) {
|
|
2204
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
2205
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
2206
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
2207
|
+
const index = await readIncidentIndex(paths, fileSystem);
|
|
2208
|
+
|
|
2209
|
+
const id = normalizeText(options.id || options.incidentId);
|
|
2210
|
+
if (!id) {
|
|
2211
|
+
throw new Error('incident id is required');
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
const summary = findIncidentSummaryById(index, id);
|
|
2215
|
+
if (!summary) {
|
|
2216
|
+
throw new Error(`staging incident not found: ${id}`);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
const incident = await readIncident(paths, summary.id, fileSystem);
|
|
2220
|
+
if (!incident) {
|
|
2221
|
+
throw new Error(`staging incident file not found: ${summary.id}`);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
const result = {
|
|
2225
|
+
mode: 'errorbook-incident-show',
|
|
2226
|
+
incident
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
if (options.json) {
|
|
2230
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2231
|
+
} else if (!options.silent) {
|
|
2232
|
+
console.log(chalk.cyan.bold(incident.title || summary.title || summary.id));
|
|
2233
|
+
console.log(chalk.gray(`id: ${incident.id}`));
|
|
2234
|
+
console.log(chalk.gray(`state: ${incident.state}`));
|
|
2235
|
+
console.log(chalk.gray(`attempts: ${Number(incident.attempt_count || 0)}`));
|
|
2236
|
+
console.log(chalk.gray(`fingerprint: ${incident.fingerprint}`));
|
|
2237
|
+
console.log(chalk.gray(`updated_at: ${incident.updated_at}`));
|
|
2238
|
+
if (incident.state === 'resolved') {
|
|
2239
|
+
console.log(chalk.gray(`resolved_at: ${incident.resolved_at || '(none)'}`));
|
|
2240
|
+
if (incident.resolution && incident.resolution.entry_id) {
|
|
2241
|
+
console.log(chalk.gray(`linked_entry: ${incident.resolution.entry_id}`));
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
return result;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
1738
2249
|
async function runErrorbookListCommand(options = {}, dependencies = {}) {
|
|
1739
2250
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
1740
2251
|
const fileSystem = dependencies.fileSystem || fs;
|
|
@@ -2299,6 +2810,36 @@ function registerErrorbookCommands(program) {
|
|
|
2299
2810
|
}
|
|
2300
2811
|
});
|
|
2301
2812
|
|
|
2813
|
+
const incident = errorbook
|
|
2814
|
+
.command('incident')
|
|
2815
|
+
.description('Inspect temporary trial-and-error incident loop before final curation');
|
|
2816
|
+
|
|
2817
|
+
incident
|
|
2818
|
+
.command('list')
|
|
2819
|
+
.description('List staging incidents')
|
|
2820
|
+
.option('--state <state>', 'Filter incident state (open|resolved)')
|
|
2821
|
+
.option('--limit <n>', 'Maximum incidents returned', parseInt, 20)
|
|
2822
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2823
|
+
.action(async (options) => {
|
|
2824
|
+
try {
|
|
2825
|
+
await runErrorbookIncidentListCommand(options);
|
|
2826
|
+
} catch (error) {
|
|
2827
|
+
emitCommandError(error, options.json);
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
incident
|
|
2832
|
+
.command('show <id>')
|
|
2833
|
+
.description('Show a staging incident with all attempts')
|
|
2834
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2835
|
+
.action(async (id, options) => {
|
|
2836
|
+
try {
|
|
2837
|
+
await runErrorbookIncidentShowCommand({ ...options, id });
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
emitCommandError(error, options.json);
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2302
2843
|
errorbook
|
|
2303
2844
|
.command('list')
|
|
2304
2845
|
.description('List curated errorbook entries')
|
|
@@ -2420,6 +2961,7 @@ function registerErrorbookCommands(program) {
|
|
|
2420
2961
|
.command('release-gate')
|
|
2421
2962
|
.description('Block release on unresolved high-risk entries and temporary-mitigation policy violations')
|
|
2422
2963
|
.option('--min-risk <level>', 'Risk threshold (low|medium|high)', 'high')
|
|
2964
|
+
.option('--min-quality <n>', `Minimum quality for unresolved entries (default: ${DEFAULT_RELEASE_GATE_MIN_QUALITY})`, parseInt)
|
|
2423
2965
|
.option('--include-verified', 'Also inspect verified (non-promoted) entries')
|
|
2424
2966
|
.option('--fail-on-block', 'Exit with error when gate is blocked')
|
|
2425
2967
|
.option('--json', 'Emit machine-readable JSON')
|
|
@@ -2470,6 +3012,9 @@ module.exports = {
|
|
|
2470
3012
|
HIGH_RISK_SIGNAL_TAGS,
|
|
2471
3013
|
DEBUG_EVIDENCE_TAGS,
|
|
2472
3014
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
3015
|
+
DEFAULT_RELEASE_GATE_MIN_QUALITY,
|
|
3016
|
+
ERRORBOOK_INCIDENT_INDEX_API_VERSION,
|
|
3017
|
+
ERRORBOOK_INCIDENT_API_VERSION,
|
|
2473
3018
|
resolveErrorbookPaths,
|
|
2474
3019
|
resolveErrorbookRegistryPaths,
|
|
2475
3020
|
normalizeOntologyTags,
|
|
@@ -2481,6 +3026,8 @@ module.exports = {
|
|
|
2481
3026
|
runErrorbookExportCommand,
|
|
2482
3027
|
runErrorbookSyncRegistryCommand,
|
|
2483
3028
|
runErrorbookRegistryHealthCommand,
|
|
3029
|
+
runErrorbookIncidentListCommand,
|
|
3030
|
+
runErrorbookIncidentShowCommand,
|
|
2484
3031
|
runErrorbookListCommand,
|
|
2485
3032
|
runErrorbookShowCommand,
|
|
2486
3033
|
runErrorbookFindCommand,
|