scene-capability-engine 3.3.22 → 3.3.24
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 +54 -0
- package/bin/scene-capability-engine.js +10 -0
- package/docs/adoption-guide.md +8 -0
- package/docs/autonomous-control-guide.md +8 -8
- package/docs/command-reference.md +51 -2
- package/docs/errorbook-registry.md +128 -0
- package/lib/adoption/adoption-strategy.js +2 -0
- package/lib/adoption/backup-manager.js +2 -0
- package/lib/adoption/detection-engine.js +2 -0
- package/lib/adoption/file-classifier.js +3 -1
- package/lib/adoption/smart-orchestrator.js +2 -0
- package/lib/adoption/strategy-selector.js +2 -0
- package/lib/adoption/template-sync.js +2 -0
- package/lib/auto/config-schema.js +7 -7
- package/lib/commands/auto.js +2 -2
- package/lib/commands/errorbook.js +968 -4
- package/lib/commands/spec-bootstrap.js +17 -2
- package/lib/commands/spec-domain.js +217 -0
- package/lib/commands/studio.js +314 -9
- package/lib/spec/domain-modeling.js +439 -0
- package/lib/spec-gate/policy/default-policy.js +1 -0
- package/lib/spec-gate/rules/default-rules.js +8 -0
- package/package.json +3 -2
- package/template/.sce/config/errorbook-registry.json +13 -0
- package/template/.sce/steering/CORE_PRINCIPLES.md +30 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const https = require('https');
|
|
2
4
|
const path = require('path');
|
|
3
5
|
const fs = require('fs-extra');
|
|
4
6
|
const chalk = require('chalk');
|
|
@@ -6,8 +8,14 @@ const Table = require('cli-table3');
|
|
|
6
8
|
|
|
7
9
|
const ERRORBOOK_INDEX_API_VERSION = 'sce.errorbook.index/v0.1';
|
|
8
10
|
const ERRORBOOK_ENTRY_API_VERSION = 'sce.errorbook.entry/v0.1';
|
|
11
|
+
const ERRORBOOK_REGISTRY_API_VERSION = 'sce.errorbook.registry/v0.1';
|
|
12
|
+
const ERRORBOOK_REGISTRY_CACHE_API_VERSION = 'sce.errorbook.registry-cache/v0.1';
|
|
13
|
+
const ERRORBOOK_REGISTRY_INDEX_API_VERSION = 'sce.errorbook.registry-index/v0.1';
|
|
9
14
|
const ERRORBOOK_STATUSES = Object.freeze(['candidate', 'verified', 'promoted', 'deprecated']);
|
|
10
15
|
const TEMPORARY_MITIGATION_TAG = 'temporary-mitigation';
|
|
16
|
+
const DEFAULT_ERRORBOOK_REGISTRY_CONFIG = '.sce/config/errorbook-registry.json';
|
|
17
|
+
const DEFAULT_ERRORBOOK_REGISTRY_CACHE = '.sce/errorbook/registry-cache.json';
|
|
18
|
+
const DEFAULT_ERRORBOOK_REGISTRY_EXPORT = '.sce/errorbook/exports/errorbook-registry-export.json';
|
|
11
19
|
const STATUS_RANK = Object.freeze({
|
|
12
20
|
deprecated: 0,
|
|
13
21
|
candidate: 1,
|
|
@@ -39,6 +47,11 @@ const ONTOLOGY_TAG_ALIASES = Object.freeze({
|
|
|
39
47
|
});
|
|
40
48
|
const DEFAULT_PROMOTE_MIN_QUALITY = 75;
|
|
41
49
|
const ERRORBOOK_RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);
|
|
50
|
+
const DEBUG_EVIDENCE_TAGS = Object.freeze([
|
|
51
|
+
'debug-evidence',
|
|
52
|
+
'diagnostic-evidence',
|
|
53
|
+
'debug-log'
|
|
54
|
+
]);
|
|
42
55
|
const HIGH_RISK_SIGNAL_TAGS = Object.freeze([
|
|
43
56
|
'release-blocker',
|
|
44
57
|
'security',
|
|
@@ -60,6 +73,28 @@ function resolveErrorbookPaths(projectPath = process.cwd()) {
|
|
|
60
73
|
};
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
function resolveProjectPath(projectPath, maybeRelativePath, fallbackRelativePath) {
|
|
77
|
+
const normalized = normalizeText(maybeRelativePath || fallbackRelativePath || '');
|
|
78
|
+
if (!normalized) {
|
|
79
|
+
return path.resolve(projectPath, fallbackRelativePath || '');
|
|
80
|
+
}
|
|
81
|
+
return path.isAbsolute(normalized)
|
|
82
|
+
? normalized
|
|
83
|
+
: path.resolve(projectPath, normalized);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveErrorbookRegistryPaths(projectPath = process.cwd(), overrides = {}) {
|
|
87
|
+
const configFile = resolveProjectPath(projectPath, overrides.configPath, DEFAULT_ERRORBOOK_REGISTRY_CONFIG);
|
|
88
|
+
const cacheFile = resolveProjectPath(projectPath, overrides.cachePath, DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
89
|
+
const exportFile = resolveProjectPath(projectPath, overrides.exportPath, DEFAULT_ERRORBOOK_REGISTRY_EXPORT);
|
|
90
|
+
return {
|
|
91
|
+
projectPath,
|
|
92
|
+
configFile,
|
|
93
|
+
cacheFile,
|
|
94
|
+
exportFile
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
63
98
|
function nowIso() {
|
|
64
99
|
return new Date().toISOString();
|
|
65
100
|
}
|
|
@@ -451,6 +486,44 @@ function validateRecordPayload(payload) {
|
|
|
451
486
|
}
|
|
452
487
|
}
|
|
453
488
|
|
|
489
|
+
function hasDebugEvidenceSignals(entry = {}) {
|
|
490
|
+
const tags = normalizeStringList(entry.tags).map((item) => item.toLowerCase());
|
|
491
|
+
if (tags.some((tag) => DEBUG_EVIDENCE_TAGS.includes(tag))) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const verificationEvidence = normalizeStringList(entry.verification_evidence);
|
|
496
|
+
if (verificationEvidence.some((item) => /^debug:/i.test(item))) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const sourceFiles = normalizeStringList(entry?.source?.files);
|
|
501
|
+
if (sourceFiles.some((item) => /(^|[\\/._-])(debug|trace|diagnostic|observability|telemetry|stack)/i.test(item))) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const notes = normalizeText(entry.notes).toLowerCase();
|
|
506
|
+
if (notes && /(debug|trace|diagnostic|observability|telemetry|stack|日志|埋点|观测)/i.test(notes)) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function enforceDebugEvidenceAfterRepeatedFailures(entry = {}, options = {}) {
|
|
514
|
+
const attemptCount = Number(options.attemptCount || 0);
|
|
515
|
+
if (!Number.isFinite(attemptCount) || attemptCount < 3) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (hasDebugEvidenceSignals(entry)) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
throw new Error(
|
|
522
|
+
'two failed fix rounds detected (attempt #3+): debug evidence is required. '
|
|
523
|
+
+ 'Provide --verification "debug: ...", add tag debug-evidence, or include debug trace/log file references.'
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
454
527
|
function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
455
528
|
const temporaryMitigation = normalizeTemporaryMitigation(options, fromFilePayload);
|
|
456
529
|
const payload = {
|
|
@@ -589,6 +662,382 @@ async function loadRecordPayloadFromFile(projectPath, sourcePath, fileSystem = f
|
|
|
589
662
|
}
|
|
590
663
|
}
|
|
591
664
|
|
|
665
|
+
function normalizeStatusList(values = [], fallback = ['promoted']) {
|
|
666
|
+
const raw = Array.isArray(values) ? values : normalizeStringList(values);
|
|
667
|
+
const list = raw.length > 0 ? raw : fallback;
|
|
668
|
+
const normalized = list
|
|
669
|
+
.map((item) => normalizeText(item).toLowerCase())
|
|
670
|
+
.filter(Boolean);
|
|
671
|
+
const unique = Array.from(new Set(normalized));
|
|
672
|
+
for (const status of unique) {
|
|
673
|
+
if (!ERRORBOOK_STATUSES.includes(status)) {
|
|
674
|
+
throw new Error(`invalid status in list: ${status}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return unique;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function normalizeRegistrySource(input = {}) {
|
|
681
|
+
const candidate = input || {};
|
|
682
|
+
const name = normalizeText(candidate.name) || 'default';
|
|
683
|
+
const url = normalizeText(candidate.url || candidate.source);
|
|
684
|
+
const file = normalizeText(candidate.file || candidate.path);
|
|
685
|
+
const source = url || file;
|
|
686
|
+
const indexUrl = normalizeText(candidate.index_url || candidate.indexUrl || candidate.registry_index || candidate.registryIndex);
|
|
687
|
+
return {
|
|
688
|
+
name,
|
|
689
|
+
source,
|
|
690
|
+
index_url: indexUrl,
|
|
691
|
+
enabled: candidate.enabled !== false
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function normalizeRegistryMode(value, fallback = 'cache') {
|
|
696
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
697
|
+
if (!normalized) {
|
|
698
|
+
return fallback;
|
|
699
|
+
}
|
|
700
|
+
if (['cache', 'remote', 'hybrid'].includes(normalized)) {
|
|
701
|
+
return normalized;
|
|
702
|
+
}
|
|
703
|
+
throw new Error('registry mode must be one of: cache, remote, hybrid');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function readErrorbookRegistryConfig(paths, fileSystem = fs) {
|
|
707
|
+
const fallback = {
|
|
708
|
+
enabled: false,
|
|
709
|
+
search_mode: 'cache',
|
|
710
|
+
cache_file: DEFAULT_ERRORBOOK_REGISTRY_CACHE,
|
|
711
|
+
sources: []
|
|
712
|
+
};
|
|
713
|
+
if (!await fileSystem.pathExists(paths.configFile)) {
|
|
714
|
+
return fallback;
|
|
715
|
+
}
|
|
716
|
+
const payload = await fileSystem.readJson(paths.configFile).catch(() => null);
|
|
717
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
718
|
+
return fallback;
|
|
719
|
+
}
|
|
720
|
+
const sources = Array.isArray(payload.sources)
|
|
721
|
+
? payload.sources.map((item) => normalizeRegistrySource(item)).filter((item) => item.enabled && item.source)
|
|
722
|
+
: [];
|
|
723
|
+
return {
|
|
724
|
+
enabled: normalizeBoolean(payload.enabled, true),
|
|
725
|
+
search_mode: normalizeRegistryMode(payload.search_mode || payload.searchMode, 'cache'),
|
|
726
|
+
cache_file: normalizeText(payload.cache_file || payload.cacheFile || DEFAULT_ERRORBOOK_REGISTRY_CACHE),
|
|
727
|
+
sources
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function isHttpSource(source = '') {
|
|
732
|
+
return /^https?:\/\//i.test(normalizeText(source));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function fetchJsonFromHttp(source, timeoutMs = 15000) {
|
|
736
|
+
const normalized = normalizeText(source);
|
|
737
|
+
if (!normalized) {
|
|
738
|
+
return Promise.reject(new Error('registry source is required'));
|
|
739
|
+
}
|
|
740
|
+
const client = normalized.startsWith('https://') ? https : http;
|
|
741
|
+
return new Promise((resolve, reject) => {
|
|
742
|
+
const request = client.get(normalized, {
|
|
743
|
+
timeout: timeoutMs,
|
|
744
|
+
headers: {
|
|
745
|
+
Accept: 'application/json'
|
|
746
|
+
}
|
|
747
|
+
}, (response) => {
|
|
748
|
+
const chunks = [];
|
|
749
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
750
|
+
response.on('end', () => {
|
|
751
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
752
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
753
|
+
reject(new Error(`registry source responded ${response.statusCode}`));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
resolve(JSON.parse(body));
|
|
758
|
+
} catch (error) {
|
|
759
|
+
reject(new Error(`registry source returned invalid JSON: ${error.message}`));
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
request.on('timeout', () => {
|
|
764
|
+
request.destroy(new Error('registry source request timed out'));
|
|
765
|
+
});
|
|
766
|
+
request.on('error', reject);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function loadRegistryPayload(projectPath, source, fileSystem = fs) {
|
|
771
|
+
const normalized = normalizeText(source);
|
|
772
|
+
if (!normalized) {
|
|
773
|
+
throw new Error('registry source is required');
|
|
774
|
+
}
|
|
775
|
+
if (isHttpSource(normalized)) {
|
|
776
|
+
return fetchJsonFromHttp(normalized);
|
|
777
|
+
}
|
|
778
|
+
const absolutePath = path.isAbsolute(normalized)
|
|
779
|
+
? normalized
|
|
780
|
+
: path.resolve(projectPath, normalized);
|
|
781
|
+
if (!await fileSystem.pathExists(absolutePath)) {
|
|
782
|
+
throw new Error(`registry source file not found: ${source}`);
|
|
783
|
+
}
|
|
784
|
+
return fileSystem.readJson(absolutePath);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function normalizeRegistryEntry(entry = {}, sourceName = 'registry') {
|
|
788
|
+
const title = normalizeText(entry.title || entry.name);
|
|
789
|
+
const symptom = normalizeText(entry.symptom);
|
|
790
|
+
const rootCause = normalizeText(entry.root_cause || entry.rootCause);
|
|
791
|
+
const fingerprint = createFingerprint({
|
|
792
|
+
fingerprint: normalizeText(entry.fingerprint),
|
|
793
|
+
title,
|
|
794
|
+
symptom,
|
|
795
|
+
root_cause: rootCause
|
|
796
|
+
});
|
|
797
|
+
const statusRaw = normalizeText(entry.status || 'candidate').toLowerCase();
|
|
798
|
+
const status = ERRORBOOK_STATUSES.includes(statusRaw) ? statusRaw : 'candidate';
|
|
799
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
id: normalizeText(entry.id) || `registry-${fingerprint}`,
|
|
803
|
+
fingerprint,
|
|
804
|
+
title,
|
|
805
|
+
symptom,
|
|
806
|
+
root_cause: rootCause,
|
|
807
|
+
fix_actions: normalizeStringList(entry.fix_actions, entry.fixActions),
|
|
808
|
+
verification_evidence: normalizeStringList(entry.verification_evidence, entry.verificationEvidence),
|
|
809
|
+
tags: normalizeStringList(entry.tags, mitigation.enabled ? TEMPORARY_MITIGATION_TAG : []),
|
|
810
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
811
|
+
status,
|
|
812
|
+
quality_score: Number.isFinite(Number(entry.quality_score)) ? Number(entry.quality_score) : scoreQuality(entry),
|
|
813
|
+
updated_at: normalizeIsoTimestamp(entry.updated_at || entry.updatedAt, 'registry.updated_at') || nowIso(),
|
|
814
|
+
source: {
|
|
815
|
+
...entry.source,
|
|
816
|
+
registry: sourceName
|
|
817
|
+
},
|
|
818
|
+
temporary_mitigation: mitigation,
|
|
819
|
+
entry_source: 'registry'
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function extractRegistryEntries(payload = {}, sourceName = 'registry') {
|
|
824
|
+
const rawEntries = Array.isArray(payload)
|
|
825
|
+
? payload
|
|
826
|
+
: Array.isArray(payload.entries)
|
|
827
|
+
? payload.entries
|
|
828
|
+
: [];
|
|
829
|
+
const normalized = [];
|
|
830
|
+
for (const item of rawEntries) {
|
|
831
|
+
if (!item || typeof item !== 'object') {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const entry = normalizeRegistryEntry(item, sourceName);
|
|
835
|
+
if (!entry.title || !entry.fingerprint) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
normalized.push(entry);
|
|
839
|
+
}
|
|
840
|
+
const deduped = new Map();
|
|
841
|
+
for (const entry of normalized) {
|
|
842
|
+
const key = entry.fingerprint;
|
|
843
|
+
const existing = deduped.get(key);
|
|
844
|
+
if (!existing) {
|
|
845
|
+
deduped.set(key, entry);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if ((Number(entry.quality_score) || 0) >= (Number(existing.quality_score) || 0)) {
|
|
849
|
+
deduped.set(key, entry);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return Array.from(deduped.values());
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function loadRegistryCache(projectPath, cachePathInput = '', fileSystem = fs) {
|
|
856
|
+
const cachePath = resolveProjectPath(projectPath, cachePathInput, DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
857
|
+
if (!await fileSystem.pathExists(cachePath)) {
|
|
858
|
+
return {
|
|
859
|
+
cache_path: cachePath,
|
|
860
|
+
entries: []
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const payload = await fileSystem.readJson(cachePath).catch(() => null);
|
|
864
|
+
const entries = extractRegistryEntries(payload || {}, 'registry-cache');
|
|
865
|
+
return {
|
|
866
|
+
cache_path: cachePath,
|
|
867
|
+
entries
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function tokenizeQueryText(query = '') {
|
|
872
|
+
return normalizeText(query)
|
|
873
|
+
.toLowerCase()
|
|
874
|
+
.split(/[^a-z0-9_]+/i)
|
|
875
|
+
.map((item) => item.trim())
|
|
876
|
+
.filter((item) => item.length >= 2);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function normalizeRegistryIndex(payload = {}, sourceName = '') {
|
|
880
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
api_version: normalizeText(payload.api_version || payload.version || ERRORBOOK_REGISTRY_INDEX_API_VERSION),
|
|
885
|
+
source_name: sourceName || normalizeText(payload.source_name || payload.sourceName),
|
|
886
|
+
min_token_length: Number.isFinite(Number(payload.min_token_length))
|
|
887
|
+
? Number(payload.min_token_length)
|
|
888
|
+
: 2,
|
|
889
|
+
token_to_source: payload.token_to_source && typeof payload.token_to_source === 'object'
|
|
890
|
+
? payload.token_to_source
|
|
891
|
+
: {},
|
|
892
|
+
token_to_bucket: payload.token_to_bucket && typeof payload.token_to_bucket === 'object'
|
|
893
|
+
? payload.token_to_bucket
|
|
894
|
+
: {},
|
|
895
|
+
buckets: payload.buckets && typeof payload.buckets === 'object'
|
|
896
|
+
? payload.buckets
|
|
897
|
+
: {},
|
|
898
|
+
default_source: normalizeText(payload.default_source || payload.fallback_source || '')
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function collectRegistryShardSources(indexPayload, queryTokens = [], maxShards = 8) {
|
|
903
|
+
const index = normalizeRegistryIndex(indexPayload);
|
|
904
|
+
if (!index) {
|
|
905
|
+
return [];
|
|
906
|
+
}
|
|
907
|
+
const sources = [];
|
|
908
|
+
const minTokenLength = Number.isFinite(index.min_token_length) ? index.min_token_length : 2;
|
|
909
|
+
for (const token of queryTokens) {
|
|
910
|
+
const normalizedToken = normalizeText(token).toLowerCase();
|
|
911
|
+
if (!normalizedToken || normalizedToken.length < minTokenLength) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const direct = index.token_to_source[normalizedToken];
|
|
915
|
+
if (direct) {
|
|
916
|
+
const items = Array.isArray(direct) ? direct : [direct];
|
|
917
|
+
for (const item of items) {
|
|
918
|
+
sources.push(normalizeText(item));
|
|
919
|
+
}
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const bucket = normalizeText(index.token_to_bucket[normalizedToken]);
|
|
923
|
+
if (!bucket) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const bucketSource = normalizeText(index.buckets[bucket] || index.buckets[normalizedToken]);
|
|
927
|
+
if (bucketSource) {
|
|
928
|
+
sources.push(bucketSource);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const deduped = Array.from(new Set(sources.filter(Boolean)));
|
|
933
|
+
if (deduped.length > 0) {
|
|
934
|
+
return Number.isFinite(Number(maxShards)) && Number(maxShards) > 0
|
|
935
|
+
? deduped.slice(0, Number(maxShards))
|
|
936
|
+
: deduped;
|
|
937
|
+
}
|
|
938
|
+
if (index.default_source) {
|
|
939
|
+
return [index.default_source];
|
|
940
|
+
}
|
|
941
|
+
return [];
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function searchRegistryRemote(options = {}, dependencies = {}) {
|
|
945
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
946
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
947
|
+
const source = normalizeRegistrySource(options.source || {});
|
|
948
|
+
const query = normalizeText(options.query);
|
|
949
|
+
const queryTokens = Array.isArray(options.queryTokens) ? options.queryTokens : tokenizeQueryText(query);
|
|
950
|
+
const requestedStatus = options.requestedStatus || null;
|
|
951
|
+
const maxShards = Number.isFinite(Number(options.maxShards)) ? Number(options.maxShards) : 8;
|
|
952
|
+
const allowRemoteFullscan = options.allowRemoteFullscan === true;
|
|
953
|
+
|
|
954
|
+
if (!source.source) {
|
|
955
|
+
return {
|
|
956
|
+
source_name: source.name || 'registry',
|
|
957
|
+
shard_sources: [],
|
|
958
|
+
matched_count: 0,
|
|
959
|
+
candidates: [],
|
|
960
|
+
warnings: ['registry source is empty']
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const warnings = [];
|
|
965
|
+
let shardSources = [];
|
|
966
|
+
if (source.index_url) {
|
|
967
|
+
try {
|
|
968
|
+
const indexPayload = await loadRegistryPayload(projectPath, source.index_url, fileSystem);
|
|
969
|
+
shardSources = collectRegistryShardSources(indexPayload, queryTokens, maxShards);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
warnings.push(`failed to load registry index (${source.index_url}): ${error.message}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (shardSources.length === 0) {
|
|
976
|
+
if (allowRemoteFullscan) {
|
|
977
|
+
shardSources = [source.source];
|
|
978
|
+
warnings.push('remote index unavailable; fallback to full-source scan');
|
|
979
|
+
} else {
|
|
980
|
+
warnings.push('remote index unavailable and full-source scan disabled');
|
|
981
|
+
return {
|
|
982
|
+
source_name: source.name || 'registry',
|
|
983
|
+
shard_sources: [],
|
|
984
|
+
matched_count: 0,
|
|
985
|
+
candidates: [],
|
|
986
|
+
warnings
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const candidates = [];
|
|
992
|
+
for (const shardSource of shardSources) {
|
|
993
|
+
try {
|
|
994
|
+
const payload = await loadRegistryPayload(projectPath, shardSource, fileSystem);
|
|
995
|
+
const entries = extractRegistryEntries(payload, source.name || 'registry');
|
|
996
|
+
for (const entry of entries) {
|
|
997
|
+
if (requestedStatus && normalizeStatus(entry.status, 'candidate') !== requestedStatus) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const matchScore = scoreSearchMatch(entry, queryTokens);
|
|
1001
|
+
if (matchScore <= 0) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
candidates.push({
|
|
1005
|
+
id: entry.id,
|
|
1006
|
+
entry_source: 'registry-remote',
|
|
1007
|
+
registry_source: source.name || 'registry',
|
|
1008
|
+
status: entry.status,
|
|
1009
|
+
quality_score: entry.quality_score,
|
|
1010
|
+
title: entry.title,
|
|
1011
|
+
fingerprint: entry.fingerprint,
|
|
1012
|
+
tags: normalizeStringList(entry.tags),
|
|
1013
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
1014
|
+
match_score: matchScore,
|
|
1015
|
+
updated_at: entry.updated_at
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
warnings.push(`failed to load registry shard (${shardSource}): ${error.message}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const deduped = new Map();
|
|
1024
|
+
for (const item of candidates) {
|
|
1025
|
+
const key = normalizeText(item.fingerprint || item.id);
|
|
1026
|
+
const existing = deduped.get(key);
|
|
1027
|
+
if (!existing || Number(item.match_score || 0) >= Number(existing.match_score || 0)) {
|
|
1028
|
+
deduped.set(key, item);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
source_name: source.name || 'registry',
|
|
1034
|
+
shard_sources: shardSources,
|
|
1035
|
+
matched_count: deduped.size,
|
|
1036
|
+
candidates: Array.from(deduped.values()),
|
|
1037
|
+
warnings
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
592
1041
|
function printRecordSummary(result) {
|
|
593
1042
|
const action = result.created ? 'Recorded new entry' : 'Updated duplicate fingerprint';
|
|
594
1043
|
console.log(chalk.green(`✓ ${action}`));
|
|
@@ -881,6 +1330,9 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
881
1330
|
throw new Error(`errorbook index references missing entry: ${existingSummary.id}`);
|
|
882
1331
|
}
|
|
883
1332
|
entry = mergeEntry(existingEntry, normalized);
|
|
1333
|
+
enforceDebugEvidenceAfterRepeatedFailures(entry, {
|
|
1334
|
+
attemptCount: Number(entry.occurrences || 0)
|
|
1335
|
+
});
|
|
884
1336
|
deduplicated = true;
|
|
885
1337
|
} else {
|
|
886
1338
|
const temporaryMitigation = normalizeExistingTemporaryMitigation(normalized.temporary_mitigation);
|
|
@@ -945,6 +1397,344 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
945
1397
|
return result;
|
|
946
1398
|
}
|
|
947
1399
|
|
|
1400
|
+
async function runErrorbookExportCommand(options = {}, dependencies = {}) {
|
|
1401
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1402
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1403
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
1404
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1405
|
+
exportPath: options.out
|
|
1406
|
+
});
|
|
1407
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
1408
|
+
|
|
1409
|
+
const statuses = normalizeStatusList(options.statuses || options.status || 'promoted', ['promoted']);
|
|
1410
|
+
const minQuality = Number.isFinite(Number(options.minQuality))
|
|
1411
|
+
? Number(options.minQuality)
|
|
1412
|
+
: 75;
|
|
1413
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
1414
|
+
? Number(options.limit)
|
|
1415
|
+
: 0;
|
|
1416
|
+
|
|
1417
|
+
const selected = [];
|
|
1418
|
+
for (const summary of index.entries) {
|
|
1419
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
1420
|
+
if (!entry) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
const status = normalizeStatus(entry.status, 'candidate');
|
|
1424
|
+
if (!statuses.includes(status)) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
if (Number(entry.quality_score || 0) < minQuality) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
selected.push({
|
|
1431
|
+
id: entry.id,
|
|
1432
|
+
fingerprint: entry.fingerprint,
|
|
1433
|
+
title: entry.title,
|
|
1434
|
+
symptom: entry.symptom,
|
|
1435
|
+
root_cause: entry.root_cause,
|
|
1436
|
+
fix_actions: normalizeStringList(entry.fix_actions),
|
|
1437
|
+
verification_evidence: normalizeStringList(entry.verification_evidence),
|
|
1438
|
+
tags: normalizeStringList(entry.tags),
|
|
1439
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
1440
|
+
status,
|
|
1441
|
+
quality_score: Number(entry.quality_score || 0),
|
|
1442
|
+
updated_at: entry.updated_at,
|
|
1443
|
+
source: entry.source || {},
|
|
1444
|
+
temporary_mitigation: normalizeExistingTemporaryMitigation(entry.temporary_mitigation)
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
selected.sort((left, right) => {
|
|
1449
|
+
const qualityDiff = Number(right.quality_score || 0) - Number(left.quality_score || 0);
|
|
1450
|
+
if (qualityDiff !== 0) {
|
|
1451
|
+
return qualityDiff;
|
|
1452
|
+
}
|
|
1453
|
+
return `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
const entries = limit > 0 ? selected.slice(0, limit) : selected;
|
|
1457
|
+
const payload = {
|
|
1458
|
+
api_version: ERRORBOOK_REGISTRY_API_VERSION,
|
|
1459
|
+
generated_at: nowIso(),
|
|
1460
|
+
source: {
|
|
1461
|
+
project: path.basename(projectPath),
|
|
1462
|
+
statuses,
|
|
1463
|
+
min_quality: minQuality
|
|
1464
|
+
},
|
|
1465
|
+
total_entries: entries.length,
|
|
1466
|
+
entries
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
await fileSystem.ensureDir(path.dirname(registryPaths.exportFile));
|
|
1470
|
+
await fileSystem.writeJson(registryPaths.exportFile, payload, { spaces: 2 });
|
|
1471
|
+
|
|
1472
|
+
const result = {
|
|
1473
|
+
mode: 'errorbook-export',
|
|
1474
|
+
out_file: registryPaths.exportFile,
|
|
1475
|
+
statuses,
|
|
1476
|
+
min_quality: minQuality,
|
|
1477
|
+
total_entries: entries.length
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
if (options.json) {
|
|
1481
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1482
|
+
} else if (!options.silent) {
|
|
1483
|
+
console.log(chalk.green('✓ Exported curated errorbook entries'));
|
|
1484
|
+
console.log(chalk.gray(` out: ${registryPaths.exportFile}`));
|
|
1485
|
+
console.log(chalk.gray(` total: ${entries.length}`));
|
|
1486
|
+
console.log(chalk.gray(` statuses: ${statuses.join(', ')}`));
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
return result;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
async function runErrorbookSyncRegistryCommand(options = {}, dependencies = {}) {
|
|
1493
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1494
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1495
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1496
|
+
configPath: options.config,
|
|
1497
|
+
cachePath: options.cache
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
const config = await readErrorbookRegistryConfig(registryPaths, fileSystem);
|
|
1501
|
+
const sourceOption = normalizeText(options.source);
|
|
1502
|
+
const configuredSource = config.sources.find((item) => item.enabled && item.source);
|
|
1503
|
+
const source = sourceOption || (configuredSource ? configuredSource.source : '');
|
|
1504
|
+
if (!source) {
|
|
1505
|
+
throw new Error('registry source is required (use --source or configure .sce/config/errorbook-registry.json)');
|
|
1506
|
+
}
|
|
1507
|
+
const sourceName = normalizeText(options.sourceName)
|
|
1508
|
+
|| (configuredSource ? configuredSource.name : '')
|
|
1509
|
+
|| 'registry';
|
|
1510
|
+
|
|
1511
|
+
const payload = await loadRegistryPayload(projectPath, source, fileSystem);
|
|
1512
|
+
const entries = extractRegistryEntries(payload, sourceName);
|
|
1513
|
+
const cachePath = resolveProjectPath(projectPath, options.cache, config.cache_file || DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
1514
|
+
const cachePayload = {
|
|
1515
|
+
api_version: ERRORBOOK_REGISTRY_CACHE_API_VERSION,
|
|
1516
|
+
synced_at: nowIso(),
|
|
1517
|
+
source: {
|
|
1518
|
+
name: sourceName,
|
|
1519
|
+
uri: source
|
|
1520
|
+
},
|
|
1521
|
+
total_entries: entries.length,
|
|
1522
|
+
entries
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
await fileSystem.ensureDir(path.dirname(cachePath));
|
|
1526
|
+
await fileSystem.writeJson(cachePath, cachePayload, { spaces: 2 });
|
|
1527
|
+
|
|
1528
|
+
const result = {
|
|
1529
|
+
mode: 'errorbook-sync-registry',
|
|
1530
|
+
source,
|
|
1531
|
+
source_name: sourceName,
|
|
1532
|
+
cache_file: cachePath,
|
|
1533
|
+
total_entries: entries.length
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
if (options.json) {
|
|
1537
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1538
|
+
} else if (!options.silent) {
|
|
1539
|
+
console.log(chalk.green('✓ Synced external errorbook registry'));
|
|
1540
|
+
console.log(chalk.gray(` source: ${source}`));
|
|
1541
|
+
console.log(chalk.gray(` cache: ${cachePath}`));
|
|
1542
|
+
console.log(chalk.gray(` entries: ${entries.length}`));
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
return result;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function runErrorbookRegistryHealthCommand(options = {}, dependencies = {}) {
|
|
1549
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1550
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1551
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1552
|
+
configPath: options.config,
|
|
1553
|
+
cachePath: options.cache
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
const warnings = [];
|
|
1557
|
+
const errors = [];
|
|
1558
|
+
const overrideSource = normalizeText(options.source);
|
|
1559
|
+
const overrideIndex = normalizeText(options.index || options.registryIndex);
|
|
1560
|
+
const overrideSourceName = normalizeText(options.sourceName || options.registrySourceName) || 'override';
|
|
1561
|
+
|
|
1562
|
+
const configExists = await fileSystem.pathExists(registryPaths.configFile);
|
|
1563
|
+
if (configExists) {
|
|
1564
|
+
try {
|
|
1565
|
+
const rawConfig = await fileSystem.readJson(registryPaths.configFile);
|
|
1566
|
+
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
1567
|
+
errors.push(`registry config must be a JSON object: ${registryPaths.configFile}`);
|
|
1568
|
+
}
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
errors.push(`failed to parse registry config (${registryPaths.configFile}): ${error.message}`);
|
|
1571
|
+
}
|
|
1572
|
+
} else if (!overrideSource) {
|
|
1573
|
+
errors.push(`registry config file not found: ${registryPaths.configFile}`);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const config = await readErrorbookRegistryConfig(registryPaths, fileSystem);
|
|
1577
|
+
let registryEnabled = normalizeBoolean(config.enabled, true);
|
|
1578
|
+
let sources = Array.isArray(config.sources) ? config.sources : [];
|
|
1579
|
+
|
|
1580
|
+
if (overrideSource) {
|
|
1581
|
+
registryEnabled = true;
|
|
1582
|
+
sources = [normalizeRegistrySource({
|
|
1583
|
+
name: overrideSourceName,
|
|
1584
|
+
source: overrideSource,
|
|
1585
|
+
index_url: overrideIndex
|
|
1586
|
+
})];
|
|
1587
|
+
} else if (overrideIndex && sources.length > 0) {
|
|
1588
|
+
sources = sources.map((source, sourceIndex) => (
|
|
1589
|
+
sourceIndex === 0 ? { ...source, index_url: overrideIndex } : source
|
|
1590
|
+
));
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (!registryEnabled) {
|
|
1594
|
+
warnings.push('registry config is disabled');
|
|
1595
|
+
}
|
|
1596
|
+
if (registryEnabled && sources.length === 0) {
|
|
1597
|
+
errors.push('registry enabled but no sources configured');
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const maxShards = Number.isFinite(Number(options.maxShards)) && Number(options.maxShards) > 0
|
|
1601
|
+
? Number(options.maxShards)
|
|
1602
|
+
: 8;
|
|
1603
|
+
const shardSample = Number.isFinite(Number(options.shardSample)) && Number(options.shardSample) > 0
|
|
1604
|
+
? Number(options.shardSample)
|
|
1605
|
+
: 2;
|
|
1606
|
+
|
|
1607
|
+
const sourceResults = [];
|
|
1608
|
+
for (const source of sources) {
|
|
1609
|
+
const sourceName = normalizeText(source.name) || 'registry';
|
|
1610
|
+
const sourceReport = {
|
|
1611
|
+
source_name: sourceName,
|
|
1612
|
+
source: normalizeText(source.source),
|
|
1613
|
+
index_url: normalizeText(source.index_url),
|
|
1614
|
+
source_ok: false,
|
|
1615
|
+
index_ok: null,
|
|
1616
|
+
shard_sources_checked: 0,
|
|
1617
|
+
source_entries: 0,
|
|
1618
|
+
shard_entries: 0,
|
|
1619
|
+
warnings: [],
|
|
1620
|
+
errors: []
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
if (!sourceReport.source) {
|
|
1624
|
+
sourceReport.errors.push('source is empty');
|
|
1625
|
+
} else {
|
|
1626
|
+
try {
|
|
1627
|
+
const payload = await loadRegistryPayload(projectPath, sourceReport.source, fileSystem);
|
|
1628
|
+
const entries = extractRegistryEntries(payload, sourceName);
|
|
1629
|
+
sourceReport.source_ok = true;
|
|
1630
|
+
sourceReport.source_entries = entries.length;
|
|
1631
|
+
if (entries.length === 0) {
|
|
1632
|
+
sourceReport.warnings.push('source returned no valid entries');
|
|
1633
|
+
}
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
sourceReport.errors.push(`failed to load source (${sourceReport.source}): ${error.message}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
if (!sourceReport.index_url) {
|
|
1640
|
+
sourceReport.warnings.push('index_url not configured; remote indexed lookup health is partially validated');
|
|
1641
|
+
} else {
|
|
1642
|
+
try {
|
|
1643
|
+
const indexPayload = await loadRegistryPayload(projectPath, sourceReport.index_url, fileSystem);
|
|
1644
|
+
const index = normalizeRegistryIndex(indexPayload, sourceName);
|
|
1645
|
+
if (!index) {
|
|
1646
|
+
sourceReport.errors.push(`invalid index payload: ${sourceReport.index_url}`);
|
|
1647
|
+
} else {
|
|
1648
|
+
sourceReport.index_ok = true;
|
|
1649
|
+
const tokenToBucket = index.token_to_bucket || {};
|
|
1650
|
+
const unresolved = [];
|
|
1651
|
+
for (const [token, bucketRaw] of Object.entries(tokenToBucket)) {
|
|
1652
|
+
const bucket = normalizeText(bucketRaw);
|
|
1653
|
+
if (!bucket) {
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
const bucketSource = normalizeText(index.buckets[bucket] || index.buckets[token]);
|
|
1657
|
+
if (!bucketSource) {
|
|
1658
|
+
unresolved.push(`${token}->${bucket}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (unresolved.length > 0) {
|
|
1662
|
+
sourceReport.errors.push(`unresolved index bucket mappings: ${unresolved.slice(0, 10).join(', ')}`);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const sampleTokens = Object.keys(tokenToBucket).slice(0, 64);
|
|
1666
|
+
const shardSources = collectRegistryShardSources(index, sampleTokens, maxShards);
|
|
1667
|
+
sourceReport.shard_sources_checked = shardSources.length;
|
|
1668
|
+
if (shardSources.length === 0) {
|
|
1669
|
+
sourceReport.warnings.push('index resolved zero shard sources');
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
for (const shardSource of shardSources.slice(0, shardSample)) {
|
|
1673
|
+
try {
|
|
1674
|
+
const shardPayload = await loadRegistryPayload(projectPath, shardSource, fileSystem);
|
|
1675
|
+
const shardEntries = extractRegistryEntries(shardPayload, `${sourceName}-shard`);
|
|
1676
|
+
sourceReport.shard_entries += shardEntries.length;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
sourceReport.errors.push(`failed to load shard (${shardSource}): ${error.message}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
sourceReport.index_ok = false;
|
|
1684
|
+
sourceReport.errors.push(`failed to load index (${sourceReport.index_url}): ${error.message}`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
for (const message of sourceReport.warnings) {
|
|
1689
|
+
warnings.push(`[${sourceName}] ${message}`);
|
|
1690
|
+
}
|
|
1691
|
+
for (const message of sourceReport.errors) {
|
|
1692
|
+
errors.push(`[${sourceName}] ${message}`);
|
|
1693
|
+
}
|
|
1694
|
+
sourceResults.push(sourceReport);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const result = {
|
|
1698
|
+
mode: 'errorbook-health-registry',
|
|
1699
|
+
checked_at: nowIso(),
|
|
1700
|
+
passed: errors.length === 0,
|
|
1701
|
+
warning_count: warnings.length,
|
|
1702
|
+
error_count: errors.length,
|
|
1703
|
+
paths: {
|
|
1704
|
+
config_file: registryPaths.configFile,
|
|
1705
|
+
cache_file: registryPaths.cacheFile
|
|
1706
|
+
},
|
|
1707
|
+
config: {
|
|
1708
|
+
exists: configExists,
|
|
1709
|
+
enabled: registryEnabled,
|
|
1710
|
+
search_mode: config.search_mode || 'cache',
|
|
1711
|
+
source_count: sources.length
|
|
1712
|
+
},
|
|
1713
|
+
sources: sourceResults,
|
|
1714
|
+
warnings,
|
|
1715
|
+
errors
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
if (options.json) {
|
|
1719
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1720
|
+
} else if (!options.silent) {
|
|
1721
|
+
if (result.passed) {
|
|
1722
|
+
console.log(chalk.green('✓ Errorbook registry health check passed'));
|
|
1723
|
+
} else {
|
|
1724
|
+
console.log(chalk.red('✗ Errorbook registry health check failed'));
|
|
1725
|
+
}
|
|
1726
|
+
console.log(chalk.gray(` sources: ${result.config.source_count}`));
|
|
1727
|
+
console.log(chalk.gray(` warnings: ${result.warning_count}`));
|
|
1728
|
+
console.log(chalk.gray(` errors: ${result.error_count}`));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (options.failOnAlert && !result.passed) {
|
|
1732
|
+
throw new Error(`errorbook registry health failed: ${result.error_count} error(s)`);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
return result;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
948
1738
|
async function runErrorbookListCommand(options = {}, dependencies = {}) {
|
|
949
1739
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
950
1740
|
const fileSystem = dependencies.fileSystem || fs;
|
|
@@ -1071,9 +1861,15 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1071
1861
|
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
1072
1862
|
? Number(options.limit)
|
|
1073
1863
|
: 10;
|
|
1074
|
-
const tokens = query
|
|
1864
|
+
const tokens = tokenizeQueryText(query);
|
|
1865
|
+
const includeRegistry = options.includeRegistry === true;
|
|
1075
1866
|
|
|
1076
1867
|
const candidates = [];
|
|
1868
|
+
let localMatched = 0;
|
|
1869
|
+
let registryMatched = 0;
|
|
1870
|
+
let registryCacheMatched = 0;
|
|
1871
|
+
let registryRemoteMatched = 0;
|
|
1872
|
+
const registryWarnings = [];
|
|
1077
1873
|
for (const summary of index.entries) {
|
|
1078
1874
|
if (requestedStatus && summary.status !== requestedStatus) {
|
|
1079
1875
|
continue;
|
|
@@ -1086,8 +1882,10 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1086
1882
|
if (matchScore <= 0) {
|
|
1087
1883
|
continue;
|
|
1088
1884
|
}
|
|
1885
|
+
localMatched += 1;
|
|
1089
1886
|
candidates.push({
|
|
1090
1887
|
id: entry.id,
|
|
1888
|
+
entry_source: 'local',
|
|
1091
1889
|
status: entry.status,
|
|
1092
1890
|
quality_score: entry.quality_score,
|
|
1093
1891
|
title: entry.title,
|
|
@@ -1099,7 +1897,96 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1099
1897
|
});
|
|
1100
1898
|
}
|
|
1101
1899
|
|
|
1102
|
-
|
|
1900
|
+
if (includeRegistry) {
|
|
1901
|
+
const configPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1902
|
+
configPath: options.config,
|
|
1903
|
+
cachePath: options.registryCache
|
|
1904
|
+
});
|
|
1905
|
+
const registryConfig = await readErrorbookRegistryConfig(configPaths, fileSystem);
|
|
1906
|
+
const registryMode = normalizeRegistryMode(options.registryMode, registryConfig.search_mode || 'cache');
|
|
1907
|
+
const useCache = registryMode === 'cache' || registryMode === 'hybrid';
|
|
1908
|
+
const useRemote = registryMode === 'remote' || registryMode === 'hybrid';
|
|
1909
|
+
|
|
1910
|
+
if (useRemote) {
|
|
1911
|
+
const configuredSources = Array.isArray(registryConfig.sources)
|
|
1912
|
+
? registryConfig.sources.filter((item) => item.enabled && item.source)
|
|
1913
|
+
: [];
|
|
1914
|
+
const overrideSource = normalizeText(options.registrySource);
|
|
1915
|
+
const remoteSources = overrideSource
|
|
1916
|
+
? [normalizeRegistrySource({
|
|
1917
|
+
name: normalizeText(options.registrySourceName) || 'override',
|
|
1918
|
+
source: overrideSource,
|
|
1919
|
+
index_url: normalizeText(options.registryIndex)
|
|
1920
|
+
})]
|
|
1921
|
+
: configuredSources;
|
|
1922
|
+
|
|
1923
|
+
for (const source of remoteSources) {
|
|
1924
|
+
const remoteResult = await searchRegistryRemote({
|
|
1925
|
+
source,
|
|
1926
|
+
query,
|
|
1927
|
+
queryTokens: tokens,
|
|
1928
|
+
requestedStatus,
|
|
1929
|
+
maxShards: options.registryMaxShards,
|
|
1930
|
+
allowRemoteFullscan: options.allowRemoteFullscan === true
|
|
1931
|
+
}, {
|
|
1932
|
+
projectPath,
|
|
1933
|
+
fileSystem
|
|
1934
|
+
});
|
|
1935
|
+
registryRemoteMatched += Number(remoteResult.matched_count || 0);
|
|
1936
|
+
registryMatched += Number(remoteResult.matched_count || 0);
|
|
1937
|
+
if (Array.isArray(remoteResult.warnings)) {
|
|
1938
|
+
registryWarnings.push(...remoteResult.warnings);
|
|
1939
|
+
}
|
|
1940
|
+
if (Array.isArray(remoteResult.candidates)) {
|
|
1941
|
+
candidates.push(...remoteResult.candidates);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
if (useCache) {
|
|
1947
|
+
const cachePath = resolveProjectPath(
|
|
1948
|
+
projectPath,
|
|
1949
|
+
options.registryCache,
|
|
1950
|
+
registryConfig.cache_file || DEFAULT_ERRORBOOK_REGISTRY_CACHE
|
|
1951
|
+
);
|
|
1952
|
+
const registryCache = await loadRegistryCache(projectPath, cachePath, fileSystem);
|
|
1953
|
+
for (const entry of registryCache.entries) {
|
|
1954
|
+
if (requestedStatus && normalizeStatus(entry.status, 'candidate') !== requestedStatus) {
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
const matchScore = scoreSearchMatch(entry, tokens);
|
|
1958
|
+
if (matchScore <= 0) {
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
registryMatched += 1;
|
|
1962
|
+
registryCacheMatched += 1;
|
|
1963
|
+
candidates.push({
|
|
1964
|
+
id: entry.id,
|
|
1965
|
+
entry_source: 'registry-cache',
|
|
1966
|
+
status: entry.status,
|
|
1967
|
+
quality_score: entry.quality_score,
|
|
1968
|
+
title: entry.title,
|
|
1969
|
+
fingerprint: entry.fingerprint,
|
|
1970
|
+
tags: normalizeStringList(entry.tags),
|
|
1971
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
1972
|
+
match_score: matchScore,
|
|
1973
|
+
updated_at: entry.updated_at
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const dedupedCandidates = new Map();
|
|
1980
|
+
for (const item of candidates) {
|
|
1981
|
+
const key = normalizeText(item.fingerprint || item.id);
|
|
1982
|
+
const existing = dedupedCandidates.get(key);
|
|
1983
|
+
if (!existing || Number(item.match_score || 0) >= Number(existing.match_score || 0)) {
|
|
1984
|
+
dedupedCandidates.set(key, item);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
const sortedCandidates = Array.from(dedupedCandidates.values());
|
|
1988
|
+
|
|
1989
|
+
sortedCandidates.sort((left, right) => {
|
|
1103
1990
|
const scoreDiff = Number(right.match_score) - Number(left.match_score);
|
|
1104
1991
|
if (scoreDiff !== 0) {
|
|
1105
1992
|
return scoreDiff;
|
|
@@ -1110,8 +1997,16 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1110
1997
|
const result = {
|
|
1111
1998
|
mode: 'errorbook-find',
|
|
1112
1999
|
query,
|
|
1113
|
-
|
|
1114
|
-
|
|
2000
|
+
include_registry: includeRegistry,
|
|
2001
|
+
source_breakdown: {
|
|
2002
|
+
local_results: localMatched,
|
|
2003
|
+
registry_results: registryMatched,
|
|
2004
|
+
registry_cache_results: registryCacheMatched,
|
|
2005
|
+
registry_remote_results: registryRemoteMatched
|
|
2006
|
+
},
|
|
2007
|
+
warnings: normalizeStringList(registryWarnings),
|
|
2008
|
+
total_results: sortedCandidates.length,
|
|
2009
|
+
entries: sortedCandidates.slice(0, limit)
|
|
1115
2010
|
};
|
|
1116
2011
|
|
|
1117
2012
|
if (options.json) {
|
|
@@ -1439,6 +2334,15 @@ function registerErrorbookCommands(program) {
|
|
|
1439
2334
|
.requiredOption('--query <text>', 'Search query')
|
|
1440
2335
|
.option('--status <status>', `Filter by status (${ERRORBOOK_STATUSES.join(', ')})`)
|
|
1441
2336
|
.option('--limit <n>', 'Maximum entries returned', parseInt, 10)
|
|
2337
|
+
.option('--include-registry', 'Include external registry entries in search')
|
|
2338
|
+
.option('--registry-mode <mode>', 'Registry lookup mode (cache|remote|hybrid)')
|
|
2339
|
+
.option('--registry-source <url-or-path>', 'Override registry source (for remote mode)')
|
|
2340
|
+
.option('--registry-source-name <name>', 'Override registry source label')
|
|
2341
|
+
.option('--registry-index <url-or-path>', 'Override registry index source (for remote mode)')
|
|
2342
|
+
.option('--registry-max-shards <n>', 'Max remote shards to fetch per query', parseInt, 8)
|
|
2343
|
+
.option('--allow-remote-fullscan', 'Allow remote full-source fallback when index is unavailable')
|
|
2344
|
+
.option('--registry-cache <path>', `Registry cache path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2345
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
1442
2346
|
.option('--json', 'Emit machine-readable JSON')
|
|
1443
2347
|
.action(async (options) => {
|
|
1444
2348
|
try {
|
|
@@ -1448,6 +2352,58 @@ function registerErrorbookCommands(program) {
|
|
|
1448
2352
|
}
|
|
1449
2353
|
});
|
|
1450
2354
|
|
|
2355
|
+
errorbook
|
|
2356
|
+
.command('export')
|
|
2357
|
+
.description('Export curated local entries for external registry publication')
|
|
2358
|
+
.option('--status <csv>', 'Statuses to include (csv, default: promoted)', 'promoted')
|
|
2359
|
+
.option('--min-quality <n>', 'Minimum quality score (default: 75)', parseInt)
|
|
2360
|
+
.option('--limit <n>', 'Maximum entries exported', parseInt)
|
|
2361
|
+
.option('--out <path>', `Output file (default: ${DEFAULT_ERRORBOOK_REGISTRY_EXPORT})`)
|
|
2362
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2363
|
+
.action(async (options) => {
|
|
2364
|
+
try {
|
|
2365
|
+
await runErrorbookExportCommand(options);
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
emitCommandError(error, options.json);
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
errorbook
|
|
2372
|
+
.command('sync-registry')
|
|
2373
|
+
.description('Sync external errorbook registry to local cache')
|
|
2374
|
+
.option('--source <url-or-path>', 'Registry source JSON (https://... or local file)')
|
|
2375
|
+
.option('--source-name <name>', 'Registry source name label')
|
|
2376
|
+
.option('--cache <path>', `Registry cache output path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2377
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
2378
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2379
|
+
.action(async (options) => {
|
|
2380
|
+
try {
|
|
2381
|
+
await runErrorbookSyncRegistryCommand(options);
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
emitCommandError(error, options.json);
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
errorbook
|
|
2388
|
+
.command('health-registry')
|
|
2389
|
+
.description('Validate external registry config/source/index health')
|
|
2390
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
2391
|
+
.option('--cache <path>', `Registry cache path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2392
|
+
.option('--source <url-or-path>', 'Override registry source JSON (https://... or local file)')
|
|
2393
|
+
.option('--source-name <name>', 'Override source name label')
|
|
2394
|
+
.option('--index <url-or-path>', 'Override registry index source (https://... or local file)')
|
|
2395
|
+
.option('--max-shards <n>', 'Max index-resolved shards to validate', parseInt, 8)
|
|
2396
|
+
.option('--shard-sample <n>', 'Shard sample count to fetch and validate', parseInt, 2)
|
|
2397
|
+
.option('--fail-on-alert', 'Exit with error when health check finds errors')
|
|
2398
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2399
|
+
.action(async (options) => {
|
|
2400
|
+
try {
|
|
2401
|
+
await runErrorbookRegistryHealthCommand(options);
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
emitCommandError(error, options.json);
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
|
|
1451
2407
|
errorbook
|
|
1452
2408
|
.command('promote <id>')
|
|
1453
2409
|
.description('Promote entry after strict quality gate')
|
|
@@ -1508,15 +2464,23 @@ module.exports = {
|
|
|
1508
2464
|
ERRORBOOK_ONTOLOGY_TAGS,
|
|
1509
2465
|
ERRORBOOK_RISK_LEVELS,
|
|
1510
2466
|
TEMPORARY_MITIGATION_TAG,
|
|
2467
|
+
DEFAULT_ERRORBOOK_REGISTRY_CONFIG,
|
|
2468
|
+
DEFAULT_ERRORBOOK_REGISTRY_CACHE,
|
|
2469
|
+
DEFAULT_ERRORBOOK_REGISTRY_EXPORT,
|
|
1511
2470
|
HIGH_RISK_SIGNAL_TAGS,
|
|
2471
|
+
DEBUG_EVIDENCE_TAGS,
|
|
1512
2472
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
1513
2473
|
resolveErrorbookPaths,
|
|
2474
|
+
resolveErrorbookRegistryPaths,
|
|
1514
2475
|
normalizeOntologyTags,
|
|
1515
2476
|
normalizeRecordPayload,
|
|
1516
2477
|
scoreQuality,
|
|
1517
2478
|
evaluateEntryRisk,
|
|
1518
2479
|
evaluateErrorbookReleaseGate,
|
|
1519
2480
|
runErrorbookRecordCommand,
|
|
2481
|
+
runErrorbookExportCommand,
|
|
2482
|
+
runErrorbookSyncRegistryCommand,
|
|
2483
|
+
runErrorbookRegistryHealthCommand,
|
|
1520
2484
|
runErrorbookListCommand,
|
|
1521
2485
|
runErrorbookShowCommand,
|
|
1522
2486
|
runErrorbookFindCommand,
|