scene-capability-engine 3.3.22 → 3.3.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/command-reference.md +15 -0
- package/docs/errorbook-registry.md +116 -0
- package/lib/adoption/adoption-strategy.js +2 -0
- package/lib/adoption/backup-manager.js +2 -0
- package/lib/adoption/detection-engine.js +2 -0
- package/lib/adoption/file-classifier.js +3 -1
- package/lib/adoption/smart-orchestrator.js +2 -0
- package/lib/adoption/strategy-selector.js +2 -0
- package/lib/adoption/template-sync.js +2 -0
- package/lib/commands/errorbook.js +710 -4
- package/package.json +1 -1
- package/template/.sce/config/errorbook-registry.json +13 -0
|
@@ -351,6 +351,16 @@ sce errorbook record \
|
|
|
351
351
|
sce errorbook list --status promoted --min-quality 75 --json
|
|
352
352
|
sce errorbook show <entry-id> --json
|
|
353
353
|
sce errorbook find --query "approve order timeout" --limit 10 --json
|
|
354
|
+
sce errorbook find --query "approve order timeout" --include-registry --json
|
|
355
|
+
# Prefer remote indexed search for large registry
|
|
356
|
+
sce errorbook find --query "approve order timeout" --include-registry --registry-mode remote --json
|
|
357
|
+
sce errorbook find --query "approve order timeout" --include-registry --registry-mode hybrid --json
|
|
358
|
+
|
|
359
|
+
# Export curated local entries for central registry publication
|
|
360
|
+
sce errorbook export --status promoted --min-quality 75 --out .sce/errorbook/exports/registry.json --json
|
|
361
|
+
|
|
362
|
+
# Sync central registry (GitHub raw URL or local file) to local cache
|
|
363
|
+
sce errorbook sync-registry --source https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/errorbook-registry.json --json
|
|
354
364
|
|
|
355
365
|
# Promote only after strict gate checks pass
|
|
356
366
|
sce errorbook promote <entry-id> --json
|
|
@@ -400,6 +410,10 @@ Curated quality policy (`宁缺毋滥,优胜略汰`) defaults:
|
|
|
400
410
|
- `release-gate` also blocks when temporary mitigation policy is violated:
|
|
401
411
|
- missing exit/cleanup/deadline metadata
|
|
402
412
|
- expired mitigation deadline
|
|
413
|
+
- `export` outputs a machine-readable registry bundle from curated local entries (recommended default: `promoted`, `quality>=75`).
|
|
414
|
+
- `sync-registry` pulls external registry JSON into local cache (`.sce/errorbook/registry-cache.json`) for unified `find` retrieval.
|
|
415
|
+
- `find --include-registry --registry-mode remote` supports direct remote query for large registries (no full local sync required).
|
|
416
|
+
- Recommended for large registries: maintain a remote index file (`registry/errorbook-registry.index.json`) and shard files, then provide `index_url` in registry config.
|
|
403
417
|
- `git-managed-gate` blocks release when:
|
|
404
418
|
- worktree has uncommitted changes
|
|
405
419
|
- branch has no upstream
|
|
@@ -2036,6 +2050,7 @@ Overall Health: 2 healthy, 1 unhealthy
|
|
|
2036
2050
|
- [Cross-Tool Guide](./cross-tool-guide.md)
|
|
2037
2051
|
- [Adoption Guide](./adoption-guide.md)
|
|
2038
2052
|
- [Developer Guide](./developer-guide.md)
|
|
2053
|
+
- [Errorbook Registry Guide](./errorbook-registry.md)
|
|
2039
2054
|
|
|
2040
2055
|
---
|
|
2041
2056
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Errorbook Registry Guide
|
|
2
|
+
|
|
3
|
+
This guide defines how to run a shared, cross-project `errorbook` registry as a dedicated GitHub repository.
|
|
4
|
+
|
|
5
|
+
## 1) Repository Scope
|
|
6
|
+
|
|
7
|
+
- Repository role: shared curated failure/remediation knowledge.
|
|
8
|
+
- Recommended repo name: `sce-errorbook-registry`.
|
|
9
|
+
- Keep this repository independent from scene/spec template repositories.
|
|
10
|
+
|
|
11
|
+
## 2) Recommended Repository Structure
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
sce-errorbook-registry/
|
|
15
|
+
registry/
|
|
16
|
+
errorbook-registry.json
|
|
17
|
+
README.md
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`registry/errorbook-registry.json` should follow:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"api_version": "sce.errorbook.registry/v0.1",
|
|
25
|
+
"generated_at": "2026-02-27T00:00:00.000Z",
|
|
26
|
+
"source": {
|
|
27
|
+
"project": "curation-pipeline",
|
|
28
|
+
"statuses": ["promoted"],
|
|
29
|
+
"min_quality": 75
|
|
30
|
+
},
|
|
31
|
+
"total_entries": 0,
|
|
32
|
+
"entries": []
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For large registries, add an index + shard layout:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
registry/
|
|
40
|
+
errorbook-registry.index.json
|
|
41
|
+
shards/
|
|
42
|
+
order.json
|
|
43
|
+
payment.json
|
|
44
|
+
auth.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Example `registry/errorbook-registry.index.json`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"api_version": "sce.errorbook.registry-index/v0.1",
|
|
52
|
+
"generated_at": "2026-02-27T00:00:00.000Z",
|
|
53
|
+
"min_token_length": 2,
|
|
54
|
+
"token_to_bucket": {
|
|
55
|
+
"order": "order",
|
|
56
|
+
"approve": "order",
|
|
57
|
+
"payment": "payment"
|
|
58
|
+
},
|
|
59
|
+
"buckets": {
|
|
60
|
+
"order": "https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/shards/order.json",
|
|
61
|
+
"payment": "https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/shards/payment.json"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 3) Project-Side Configuration
|
|
67
|
+
|
|
68
|
+
Create `.sce/config/errorbook-registry.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"enabled": true,
|
|
73
|
+
"search_mode": "remote",
|
|
74
|
+
"cache_file": ".sce/errorbook/registry-cache.json",
|
|
75
|
+
"sources": [
|
|
76
|
+
{
|
|
77
|
+
"name": "central",
|
|
78
|
+
"enabled": true,
|
|
79
|
+
"url": "https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/errorbook-registry.json",
|
|
80
|
+
"index_url": "https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/errorbook-registry.index.json"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Notes:
|
|
87
|
+
- `url` must be a raw JSON URL (`raw.githubusercontent.com`) or use a local file path.
|
|
88
|
+
- `search_mode` supports `cache|remote|hybrid` (recommended: `remote` for very large registries).
|
|
89
|
+
- Local cache file is used by cache/hybrid mode.
|
|
90
|
+
|
|
91
|
+
## 4) Daily Workflow
|
|
92
|
+
|
|
93
|
+
1. Export curated local entries:
|
|
94
|
+
```bash
|
|
95
|
+
sce errorbook export --status promoted --min-quality 75 --out .sce/errorbook/exports/registry.json --json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
2. Merge approved entries into central repo `registry/errorbook-registry.json`.
|
|
99
|
+
|
|
100
|
+
3. Sync central registry into local cache:
|
|
101
|
+
```bash
|
|
102
|
+
sce errorbook sync-registry --source https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/errorbook-registry.json --json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
4. Search local + shared entries:
|
|
106
|
+
```bash
|
|
107
|
+
sce errorbook find --query "approve order timeout" --include-registry --json
|
|
108
|
+
sce errorbook find --query "approve order timeout" --include-registry --registry-mode remote --json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 5) Governance Rules
|
|
112
|
+
|
|
113
|
+
- Publish to central registry only curated entries (recommended: `status=promoted` and `quality>=75`).
|
|
114
|
+
- Do not publish sensitive tenant/customer data.
|
|
115
|
+
- Temporary mitigation entries must remain bounded and governed (exit criteria, cleanup task, deadline).
|
|
116
|
+
- Keep central registry append-only by PR review; deprecate low-value entries through normal curation.
|
|
@@ -113,6 +113,8 @@ class AdoptionStrategy {
|
|
|
113
113
|
'steering/CURRENT_CONTEXT.md',
|
|
114
114
|
'steering/RULES_GUIDE.md',
|
|
115
115
|
'config/studio-security.json',
|
|
116
|
+
'config/orchestrator.json',
|
|
117
|
+
'config/errorbook-registry.json',
|
|
116
118
|
'specs/SPEC_WORKFLOW_GUIDE.md',
|
|
117
119
|
'hooks/sync-tasks-on-edit.sce.hook',
|
|
118
120
|
'hooks/check-spec-on-create.sce.hook',
|
|
@@ -162,6 +162,8 @@ class DetectionEngine {
|
|
|
162
162
|
'steering/RULES_GUIDE.md',
|
|
163
163
|
'tools/ultrawork_enhancer.py',
|
|
164
164
|
'config/studio-security.json',
|
|
165
|
+
'config/orchestrator.json',
|
|
166
|
+
'config/errorbook-registry.json',
|
|
165
167
|
'README.md',
|
|
166
168
|
'ultrawork-application-guide.md',
|
|
167
169
|
'ultrawork-integration-summary.md',
|
|
@@ -63,7 +63,9 @@ class FileClassifier {
|
|
|
63
63
|
this.configPatterns = [
|
|
64
64
|
'version.json',
|
|
65
65
|
'adoption-config.json',
|
|
66
|
-
'config/studio-security.json'
|
|
66
|
+
'config/studio-security.json',
|
|
67
|
+
'config/orchestrator.json',
|
|
68
|
+
'config/errorbook-registry.json'
|
|
67
69
|
];
|
|
68
70
|
|
|
69
71
|
// Generated directory patterns
|
|
@@ -29,6 +29,8 @@ class TemplateSync {
|
|
|
29
29
|
'steering/RULES_GUIDE.md',
|
|
30
30
|
'tools/ultrawork_enhancer.py',
|
|
31
31
|
'config/studio-security.json',
|
|
32
|
+
'config/orchestrator.json',
|
|
33
|
+
'config/errorbook-registry.json',
|
|
32
34
|
'README.md',
|
|
33
35
|
'ultrawork-application-guide.md',
|
|
34
36
|
'ultrawork-integration-summary.md',
|
|
@@ -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,
|
|
@@ -60,6 +68,28 @@ function resolveErrorbookPaths(projectPath = process.cwd()) {
|
|
|
60
68
|
};
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
function resolveProjectPath(projectPath, maybeRelativePath, fallbackRelativePath) {
|
|
72
|
+
const normalized = normalizeText(maybeRelativePath || fallbackRelativePath || '');
|
|
73
|
+
if (!normalized) {
|
|
74
|
+
return path.resolve(projectPath, fallbackRelativePath || '');
|
|
75
|
+
}
|
|
76
|
+
return path.isAbsolute(normalized)
|
|
77
|
+
? normalized
|
|
78
|
+
: path.resolve(projectPath, normalized);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveErrorbookRegistryPaths(projectPath = process.cwd(), overrides = {}) {
|
|
82
|
+
const configFile = resolveProjectPath(projectPath, overrides.configPath, DEFAULT_ERRORBOOK_REGISTRY_CONFIG);
|
|
83
|
+
const cacheFile = resolveProjectPath(projectPath, overrides.cachePath, DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
84
|
+
const exportFile = resolveProjectPath(projectPath, overrides.exportPath, DEFAULT_ERRORBOOK_REGISTRY_EXPORT);
|
|
85
|
+
return {
|
|
86
|
+
projectPath,
|
|
87
|
+
configFile,
|
|
88
|
+
cacheFile,
|
|
89
|
+
exportFile
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
function nowIso() {
|
|
64
94
|
return new Date().toISOString();
|
|
65
95
|
}
|
|
@@ -589,6 +619,382 @@ async function loadRecordPayloadFromFile(projectPath, sourcePath, fileSystem = f
|
|
|
589
619
|
}
|
|
590
620
|
}
|
|
591
621
|
|
|
622
|
+
function normalizeStatusList(values = [], fallback = ['promoted']) {
|
|
623
|
+
const raw = Array.isArray(values) ? values : normalizeStringList(values);
|
|
624
|
+
const list = raw.length > 0 ? raw : fallback;
|
|
625
|
+
const normalized = list
|
|
626
|
+
.map((item) => normalizeText(item).toLowerCase())
|
|
627
|
+
.filter(Boolean);
|
|
628
|
+
const unique = Array.from(new Set(normalized));
|
|
629
|
+
for (const status of unique) {
|
|
630
|
+
if (!ERRORBOOK_STATUSES.includes(status)) {
|
|
631
|
+
throw new Error(`invalid status in list: ${status}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return unique;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizeRegistrySource(input = {}) {
|
|
638
|
+
const candidate = input || {};
|
|
639
|
+
const name = normalizeText(candidate.name) || 'default';
|
|
640
|
+
const url = normalizeText(candidate.url || candidate.source);
|
|
641
|
+
const file = normalizeText(candidate.file || candidate.path);
|
|
642
|
+
const source = url || file;
|
|
643
|
+
const indexUrl = normalizeText(candidate.index_url || candidate.indexUrl || candidate.registry_index || candidate.registryIndex);
|
|
644
|
+
return {
|
|
645
|
+
name,
|
|
646
|
+
source,
|
|
647
|
+
index_url: indexUrl,
|
|
648
|
+
enabled: candidate.enabled !== false
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function normalizeRegistryMode(value, fallback = 'cache') {
|
|
653
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
654
|
+
if (!normalized) {
|
|
655
|
+
return fallback;
|
|
656
|
+
}
|
|
657
|
+
if (['cache', 'remote', 'hybrid'].includes(normalized)) {
|
|
658
|
+
return normalized;
|
|
659
|
+
}
|
|
660
|
+
throw new Error('registry mode must be one of: cache, remote, hybrid');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function readErrorbookRegistryConfig(paths, fileSystem = fs) {
|
|
664
|
+
const fallback = {
|
|
665
|
+
enabled: false,
|
|
666
|
+
search_mode: 'cache',
|
|
667
|
+
cache_file: DEFAULT_ERRORBOOK_REGISTRY_CACHE,
|
|
668
|
+
sources: []
|
|
669
|
+
};
|
|
670
|
+
if (!await fileSystem.pathExists(paths.configFile)) {
|
|
671
|
+
return fallback;
|
|
672
|
+
}
|
|
673
|
+
const payload = await fileSystem.readJson(paths.configFile).catch(() => null);
|
|
674
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
675
|
+
return fallback;
|
|
676
|
+
}
|
|
677
|
+
const sources = Array.isArray(payload.sources)
|
|
678
|
+
? payload.sources.map((item) => normalizeRegistrySource(item)).filter((item) => item.enabled && item.source)
|
|
679
|
+
: [];
|
|
680
|
+
return {
|
|
681
|
+
enabled: normalizeBoolean(payload.enabled, true),
|
|
682
|
+
search_mode: normalizeRegistryMode(payload.search_mode || payload.searchMode, 'cache'),
|
|
683
|
+
cache_file: normalizeText(payload.cache_file || payload.cacheFile || DEFAULT_ERRORBOOK_REGISTRY_CACHE),
|
|
684
|
+
sources
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isHttpSource(source = '') {
|
|
689
|
+
return /^https?:\/\//i.test(normalizeText(source));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function fetchJsonFromHttp(source, timeoutMs = 15000) {
|
|
693
|
+
const normalized = normalizeText(source);
|
|
694
|
+
if (!normalized) {
|
|
695
|
+
return Promise.reject(new Error('registry source is required'));
|
|
696
|
+
}
|
|
697
|
+
const client = normalized.startsWith('https://') ? https : http;
|
|
698
|
+
return new Promise((resolve, reject) => {
|
|
699
|
+
const request = client.get(normalized, {
|
|
700
|
+
timeout: timeoutMs,
|
|
701
|
+
headers: {
|
|
702
|
+
Accept: 'application/json'
|
|
703
|
+
}
|
|
704
|
+
}, (response) => {
|
|
705
|
+
const chunks = [];
|
|
706
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
707
|
+
response.on('end', () => {
|
|
708
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
709
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
710
|
+
reject(new Error(`registry source responded ${response.statusCode}`));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
resolve(JSON.parse(body));
|
|
715
|
+
} catch (error) {
|
|
716
|
+
reject(new Error(`registry source returned invalid JSON: ${error.message}`));
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
request.on('timeout', () => {
|
|
721
|
+
request.destroy(new Error('registry source request timed out'));
|
|
722
|
+
});
|
|
723
|
+
request.on('error', reject);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function loadRegistryPayload(projectPath, source, fileSystem = fs) {
|
|
728
|
+
const normalized = normalizeText(source);
|
|
729
|
+
if (!normalized) {
|
|
730
|
+
throw new Error('registry source is required');
|
|
731
|
+
}
|
|
732
|
+
if (isHttpSource(normalized)) {
|
|
733
|
+
return fetchJsonFromHttp(normalized);
|
|
734
|
+
}
|
|
735
|
+
const absolutePath = path.isAbsolute(normalized)
|
|
736
|
+
? normalized
|
|
737
|
+
: path.resolve(projectPath, normalized);
|
|
738
|
+
if (!await fileSystem.pathExists(absolutePath)) {
|
|
739
|
+
throw new Error(`registry source file not found: ${source}`);
|
|
740
|
+
}
|
|
741
|
+
return fileSystem.readJson(absolutePath);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function normalizeRegistryEntry(entry = {}, sourceName = 'registry') {
|
|
745
|
+
const title = normalizeText(entry.title || entry.name);
|
|
746
|
+
const symptom = normalizeText(entry.symptom);
|
|
747
|
+
const rootCause = normalizeText(entry.root_cause || entry.rootCause);
|
|
748
|
+
const fingerprint = createFingerprint({
|
|
749
|
+
fingerprint: normalizeText(entry.fingerprint),
|
|
750
|
+
title,
|
|
751
|
+
symptom,
|
|
752
|
+
root_cause: rootCause
|
|
753
|
+
});
|
|
754
|
+
const statusRaw = normalizeText(entry.status || 'candidate').toLowerCase();
|
|
755
|
+
const status = ERRORBOOK_STATUSES.includes(statusRaw) ? statusRaw : 'candidate';
|
|
756
|
+
const mitigation = normalizeExistingTemporaryMitigation(entry.temporary_mitigation);
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
id: normalizeText(entry.id) || `registry-${fingerprint}`,
|
|
760
|
+
fingerprint,
|
|
761
|
+
title,
|
|
762
|
+
symptom,
|
|
763
|
+
root_cause: rootCause,
|
|
764
|
+
fix_actions: normalizeStringList(entry.fix_actions, entry.fixActions),
|
|
765
|
+
verification_evidence: normalizeStringList(entry.verification_evidence, entry.verificationEvidence),
|
|
766
|
+
tags: normalizeStringList(entry.tags, mitigation.enabled ? TEMPORARY_MITIGATION_TAG : []),
|
|
767
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
768
|
+
status,
|
|
769
|
+
quality_score: Number.isFinite(Number(entry.quality_score)) ? Number(entry.quality_score) : scoreQuality(entry),
|
|
770
|
+
updated_at: normalizeIsoTimestamp(entry.updated_at || entry.updatedAt, 'registry.updated_at') || nowIso(),
|
|
771
|
+
source: {
|
|
772
|
+
...entry.source,
|
|
773
|
+
registry: sourceName
|
|
774
|
+
},
|
|
775
|
+
temporary_mitigation: mitigation,
|
|
776
|
+
entry_source: 'registry'
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function extractRegistryEntries(payload = {}, sourceName = 'registry') {
|
|
781
|
+
const rawEntries = Array.isArray(payload)
|
|
782
|
+
? payload
|
|
783
|
+
: Array.isArray(payload.entries)
|
|
784
|
+
? payload.entries
|
|
785
|
+
: [];
|
|
786
|
+
const normalized = [];
|
|
787
|
+
for (const item of rawEntries) {
|
|
788
|
+
if (!item || typeof item !== 'object') {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const entry = normalizeRegistryEntry(item, sourceName);
|
|
792
|
+
if (!entry.title || !entry.fingerprint) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
normalized.push(entry);
|
|
796
|
+
}
|
|
797
|
+
const deduped = new Map();
|
|
798
|
+
for (const entry of normalized) {
|
|
799
|
+
const key = entry.fingerprint;
|
|
800
|
+
const existing = deduped.get(key);
|
|
801
|
+
if (!existing) {
|
|
802
|
+
deduped.set(key, entry);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if ((Number(entry.quality_score) || 0) >= (Number(existing.quality_score) || 0)) {
|
|
806
|
+
deduped.set(key, entry);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return Array.from(deduped.values());
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function loadRegistryCache(projectPath, cachePathInput = '', fileSystem = fs) {
|
|
813
|
+
const cachePath = resolveProjectPath(projectPath, cachePathInput, DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
814
|
+
if (!await fileSystem.pathExists(cachePath)) {
|
|
815
|
+
return {
|
|
816
|
+
cache_path: cachePath,
|
|
817
|
+
entries: []
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const payload = await fileSystem.readJson(cachePath).catch(() => null);
|
|
821
|
+
const entries = extractRegistryEntries(payload || {}, 'registry-cache');
|
|
822
|
+
return {
|
|
823
|
+
cache_path: cachePath,
|
|
824
|
+
entries
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function tokenizeQueryText(query = '') {
|
|
829
|
+
return normalizeText(query)
|
|
830
|
+
.toLowerCase()
|
|
831
|
+
.split(/[^a-z0-9_]+/i)
|
|
832
|
+
.map((item) => item.trim())
|
|
833
|
+
.filter((item) => item.length >= 2);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function normalizeRegistryIndex(payload = {}, sourceName = '') {
|
|
837
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
api_version: normalizeText(payload.api_version || payload.version || ERRORBOOK_REGISTRY_INDEX_API_VERSION),
|
|
842
|
+
source_name: sourceName || normalizeText(payload.source_name || payload.sourceName),
|
|
843
|
+
min_token_length: Number.isFinite(Number(payload.min_token_length))
|
|
844
|
+
? Number(payload.min_token_length)
|
|
845
|
+
: 2,
|
|
846
|
+
token_to_source: payload.token_to_source && typeof payload.token_to_source === 'object'
|
|
847
|
+
? payload.token_to_source
|
|
848
|
+
: {},
|
|
849
|
+
token_to_bucket: payload.token_to_bucket && typeof payload.token_to_bucket === 'object'
|
|
850
|
+
? payload.token_to_bucket
|
|
851
|
+
: {},
|
|
852
|
+
buckets: payload.buckets && typeof payload.buckets === 'object'
|
|
853
|
+
? payload.buckets
|
|
854
|
+
: {},
|
|
855
|
+
default_source: normalizeText(payload.default_source || payload.fallback_source || '')
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function collectRegistryShardSources(indexPayload, queryTokens = [], maxShards = 8) {
|
|
860
|
+
const index = normalizeRegistryIndex(indexPayload);
|
|
861
|
+
if (!index) {
|
|
862
|
+
return [];
|
|
863
|
+
}
|
|
864
|
+
const sources = [];
|
|
865
|
+
const minTokenLength = Number.isFinite(index.min_token_length) ? index.min_token_length : 2;
|
|
866
|
+
for (const token of queryTokens) {
|
|
867
|
+
const normalizedToken = normalizeText(token).toLowerCase();
|
|
868
|
+
if (!normalizedToken || normalizedToken.length < minTokenLength) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const direct = index.token_to_source[normalizedToken];
|
|
872
|
+
if (direct) {
|
|
873
|
+
const items = Array.isArray(direct) ? direct : [direct];
|
|
874
|
+
for (const item of items) {
|
|
875
|
+
sources.push(normalizeText(item));
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const bucket = normalizeText(index.token_to_bucket[normalizedToken]);
|
|
880
|
+
if (!bucket) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const bucketSource = normalizeText(index.buckets[bucket] || index.buckets[normalizedToken]);
|
|
884
|
+
if (bucketSource) {
|
|
885
|
+
sources.push(bucketSource);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const deduped = Array.from(new Set(sources.filter(Boolean)));
|
|
890
|
+
if (deduped.length > 0) {
|
|
891
|
+
return Number.isFinite(Number(maxShards)) && Number(maxShards) > 0
|
|
892
|
+
? deduped.slice(0, Number(maxShards))
|
|
893
|
+
: deduped;
|
|
894
|
+
}
|
|
895
|
+
if (index.default_source) {
|
|
896
|
+
return [index.default_source];
|
|
897
|
+
}
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function searchRegistryRemote(options = {}, dependencies = {}) {
|
|
902
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
903
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
904
|
+
const source = normalizeRegistrySource(options.source || {});
|
|
905
|
+
const query = normalizeText(options.query);
|
|
906
|
+
const queryTokens = Array.isArray(options.queryTokens) ? options.queryTokens : tokenizeQueryText(query);
|
|
907
|
+
const requestedStatus = options.requestedStatus || null;
|
|
908
|
+
const maxShards = Number.isFinite(Number(options.maxShards)) ? Number(options.maxShards) : 8;
|
|
909
|
+
const allowRemoteFullscan = options.allowRemoteFullscan === true;
|
|
910
|
+
|
|
911
|
+
if (!source.source) {
|
|
912
|
+
return {
|
|
913
|
+
source_name: source.name || 'registry',
|
|
914
|
+
shard_sources: [],
|
|
915
|
+
matched_count: 0,
|
|
916
|
+
candidates: [],
|
|
917
|
+
warnings: ['registry source is empty']
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const warnings = [];
|
|
922
|
+
let shardSources = [];
|
|
923
|
+
if (source.index_url) {
|
|
924
|
+
try {
|
|
925
|
+
const indexPayload = await loadRegistryPayload(projectPath, source.index_url, fileSystem);
|
|
926
|
+
shardSources = collectRegistryShardSources(indexPayload, queryTokens, maxShards);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
warnings.push(`failed to load registry index (${source.index_url}): ${error.message}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (shardSources.length === 0) {
|
|
933
|
+
if (allowRemoteFullscan) {
|
|
934
|
+
shardSources = [source.source];
|
|
935
|
+
warnings.push('remote index unavailable; fallback to full-source scan');
|
|
936
|
+
} else {
|
|
937
|
+
warnings.push('remote index unavailable and full-source scan disabled');
|
|
938
|
+
return {
|
|
939
|
+
source_name: source.name || 'registry',
|
|
940
|
+
shard_sources: [],
|
|
941
|
+
matched_count: 0,
|
|
942
|
+
candidates: [],
|
|
943
|
+
warnings
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const candidates = [];
|
|
949
|
+
for (const shardSource of shardSources) {
|
|
950
|
+
try {
|
|
951
|
+
const payload = await loadRegistryPayload(projectPath, shardSource, fileSystem);
|
|
952
|
+
const entries = extractRegistryEntries(payload, source.name || 'registry');
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
if (requestedStatus && normalizeStatus(entry.status, 'candidate') !== requestedStatus) {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const matchScore = scoreSearchMatch(entry, queryTokens);
|
|
958
|
+
if (matchScore <= 0) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
candidates.push({
|
|
962
|
+
id: entry.id,
|
|
963
|
+
entry_source: 'registry-remote',
|
|
964
|
+
registry_source: source.name || 'registry',
|
|
965
|
+
status: entry.status,
|
|
966
|
+
quality_score: entry.quality_score,
|
|
967
|
+
title: entry.title,
|
|
968
|
+
fingerprint: entry.fingerprint,
|
|
969
|
+
tags: normalizeStringList(entry.tags),
|
|
970
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
971
|
+
match_score: matchScore,
|
|
972
|
+
updated_at: entry.updated_at
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
} catch (error) {
|
|
976
|
+
warnings.push(`failed to load registry shard (${shardSource}): ${error.message}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const deduped = new Map();
|
|
981
|
+
for (const item of candidates) {
|
|
982
|
+
const key = normalizeText(item.fingerprint || item.id);
|
|
983
|
+
const existing = deduped.get(key);
|
|
984
|
+
if (!existing || Number(item.match_score || 0) >= Number(existing.match_score || 0)) {
|
|
985
|
+
deduped.set(key, item);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return {
|
|
990
|
+
source_name: source.name || 'registry',
|
|
991
|
+
shard_sources: shardSources,
|
|
992
|
+
matched_count: deduped.size,
|
|
993
|
+
candidates: Array.from(deduped.values()),
|
|
994
|
+
warnings
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
592
998
|
function printRecordSummary(result) {
|
|
593
999
|
const action = result.created ? 'Recorded new entry' : 'Updated duplicate fingerprint';
|
|
594
1000
|
console.log(chalk.green(`✓ ${action}`));
|
|
@@ -945,6 +1351,154 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
945
1351
|
return result;
|
|
946
1352
|
}
|
|
947
1353
|
|
|
1354
|
+
async function runErrorbookExportCommand(options = {}, dependencies = {}) {
|
|
1355
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1356
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1357
|
+
const paths = resolveErrorbookPaths(projectPath);
|
|
1358
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1359
|
+
exportPath: options.out
|
|
1360
|
+
});
|
|
1361
|
+
const index = await readErrorbookIndex(paths, fileSystem);
|
|
1362
|
+
|
|
1363
|
+
const statuses = normalizeStatusList(options.statuses || options.status || 'promoted', ['promoted']);
|
|
1364
|
+
const minQuality = Number.isFinite(Number(options.minQuality))
|
|
1365
|
+
? Number(options.minQuality)
|
|
1366
|
+
: 75;
|
|
1367
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
1368
|
+
? Number(options.limit)
|
|
1369
|
+
: 0;
|
|
1370
|
+
|
|
1371
|
+
const selected = [];
|
|
1372
|
+
for (const summary of index.entries) {
|
|
1373
|
+
const entry = await readErrorbookEntry(paths, summary.id, fileSystem);
|
|
1374
|
+
if (!entry) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const status = normalizeStatus(entry.status, 'candidate');
|
|
1378
|
+
if (!statuses.includes(status)) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
if (Number(entry.quality_score || 0) < minQuality) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
selected.push({
|
|
1385
|
+
id: entry.id,
|
|
1386
|
+
fingerprint: entry.fingerprint,
|
|
1387
|
+
title: entry.title,
|
|
1388
|
+
symptom: entry.symptom,
|
|
1389
|
+
root_cause: entry.root_cause,
|
|
1390
|
+
fix_actions: normalizeStringList(entry.fix_actions),
|
|
1391
|
+
verification_evidence: normalizeStringList(entry.verification_evidence),
|
|
1392
|
+
tags: normalizeStringList(entry.tags),
|
|
1393
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
1394
|
+
status,
|
|
1395
|
+
quality_score: Number(entry.quality_score || 0),
|
|
1396
|
+
updated_at: entry.updated_at,
|
|
1397
|
+
source: entry.source || {},
|
|
1398
|
+
temporary_mitigation: normalizeExistingTemporaryMitigation(entry.temporary_mitigation)
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
selected.sort((left, right) => {
|
|
1403
|
+
const qualityDiff = Number(right.quality_score || 0) - Number(left.quality_score || 0);
|
|
1404
|
+
if (qualityDiff !== 0) {
|
|
1405
|
+
return qualityDiff;
|
|
1406
|
+
}
|
|
1407
|
+
return `${right.updated_at || ''}`.localeCompare(`${left.updated_at || ''}`);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
const entries = limit > 0 ? selected.slice(0, limit) : selected;
|
|
1411
|
+
const payload = {
|
|
1412
|
+
api_version: ERRORBOOK_REGISTRY_API_VERSION,
|
|
1413
|
+
generated_at: nowIso(),
|
|
1414
|
+
source: {
|
|
1415
|
+
project: path.basename(projectPath),
|
|
1416
|
+
statuses,
|
|
1417
|
+
min_quality: minQuality
|
|
1418
|
+
},
|
|
1419
|
+
total_entries: entries.length,
|
|
1420
|
+
entries
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
await fileSystem.ensureDir(path.dirname(registryPaths.exportFile));
|
|
1424
|
+
await fileSystem.writeJson(registryPaths.exportFile, payload, { spaces: 2 });
|
|
1425
|
+
|
|
1426
|
+
const result = {
|
|
1427
|
+
mode: 'errorbook-export',
|
|
1428
|
+
out_file: registryPaths.exportFile,
|
|
1429
|
+
statuses,
|
|
1430
|
+
min_quality: minQuality,
|
|
1431
|
+
total_entries: entries.length
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
if (options.json) {
|
|
1435
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1436
|
+
} else if (!options.silent) {
|
|
1437
|
+
console.log(chalk.green('✓ Exported curated errorbook entries'));
|
|
1438
|
+
console.log(chalk.gray(` out: ${registryPaths.exportFile}`));
|
|
1439
|
+
console.log(chalk.gray(` total: ${entries.length}`));
|
|
1440
|
+
console.log(chalk.gray(` statuses: ${statuses.join(', ')}`));
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return result;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function runErrorbookSyncRegistryCommand(options = {}, dependencies = {}) {
|
|
1447
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1448
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1449
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1450
|
+
configPath: options.config,
|
|
1451
|
+
cachePath: options.cache
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
const config = await readErrorbookRegistryConfig(registryPaths, fileSystem);
|
|
1455
|
+
const sourceOption = normalizeText(options.source);
|
|
1456
|
+
const configuredSource = config.sources.find((item) => item.enabled && item.source);
|
|
1457
|
+
const source = sourceOption || (configuredSource ? configuredSource.source : '');
|
|
1458
|
+
if (!source) {
|
|
1459
|
+
throw new Error('registry source is required (use --source or configure .sce/config/errorbook-registry.json)');
|
|
1460
|
+
}
|
|
1461
|
+
const sourceName = normalizeText(options.sourceName)
|
|
1462
|
+
|| (configuredSource ? configuredSource.name : '')
|
|
1463
|
+
|| 'registry';
|
|
1464
|
+
|
|
1465
|
+
const payload = await loadRegistryPayload(projectPath, source, fileSystem);
|
|
1466
|
+
const entries = extractRegistryEntries(payload, sourceName);
|
|
1467
|
+
const cachePath = resolveProjectPath(projectPath, options.cache, config.cache_file || DEFAULT_ERRORBOOK_REGISTRY_CACHE);
|
|
1468
|
+
const cachePayload = {
|
|
1469
|
+
api_version: ERRORBOOK_REGISTRY_CACHE_API_VERSION,
|
|
1470
|
+
synced_at: nowIso(),
|
|
1471
|
+
source: {
|
|
1472
|
+
name: sourceName,
|
|
1473
|
+
uri: source
|
|
1474
|
+
},
|
|
1475
|
+
total_entries: entries.length,
|
|
1476
|
+
entries
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
await fileSystem.ensureDir(path.dirname(cachePath));
|
|
1480
|
+
await fileSystem.writeJson(cachePath, cachePayload, { spaces: 2 });
|
|
1481
|
+
|
|
1482
|
+
const result = {
|
|
1483
|
+
mode: 'errorbook-sync-registry',
|
|
1484
|
+
source,
|
|
1485
|
+
source_name: sourceName,
|
|
1486
|
+
cache_file: cachePath,
|
|
1487
|
+
total_entries: entries.length
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
if (options.json) {
|
|
1491
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1492
|
+
} else if (!options.silent) {
|
|
1493
|
+
console.log(chalk.green('✓ Synced external errorbook registry'));
|
|
1494
|
+
console.log(chalk.gray(` source: ${source}`));
|
|
1495
|
+
console.log(chalk.gray(` cache: ${cachePath}`));
|
|
1496
|
+
console.log(chalk.gray(` entries: ${entries.length}`));
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
return result;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
948
1502
|
async function runErrorbookListCommand(options = {}, dependencies = {}) {
|
|
949
1503
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
950
1504
|
const fileSystem = dependencies.fileSystem || fs;
|
|
@@ -1071,9 +1625,15 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1071
1625
|
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
1072
1626
|
? Number(options.limit)
|
|
1073
1627
|
: 10;
|
|
1074
|
-
const tokens = query
|
|
1628
|
+
const tokens = tokenizeQueryText(query);
|
|
1629
|
+
const includeRegistry = options.includeRegistry === true;
|
|
1075
1630
|
|
|
1076
1631
|
const candidates = [];
|
|
1632
|
+
let localMatched = 0;
|
|
1633
|
+
let registryMatched = 0;
|
|
1634
|
+
let registryCacheMatched = 0;
|
|
1635
|
+
let registryRemoteMatched = 0;
|
|
1636
|
+
const registryWarnings = [];
|
|
1077
1637
|
for (const summary of index.entries) {
|
|
1078
1638
|
if (requestedStatus && summary.status !== requestedStatus) {
|
|
1079
1639
|
continue;
|
|
@@ -1086,8 +1646,10 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1086
1646
|
if (matchScore <= 0) {
|
|
1087
1647
|
continue;
|
|
1088
1648
|
}
|
|
1649
|
+
localMatched += 1;
|
|
1089
1650
|
candidates.push({
|
|
1090
1651
|
id: entry.id,
|
|
1652
|
+
entry_source: 'local',
|
|
1091
1653
|
status: entry.status,
|
|
1092
1654
|
quality_score: entry.quality_score,
|
|
1093
1655
|
title: entry.title,
|
|
@@ -1099,7 +1661,96 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1099
1661
|
});
|
|
1100
1662
|
}
|
|
1101
1663
|
|
|
1102
|
-
|
|
1664
|
+
if (includeRegistry) {
|
|
1665
|
+
const configPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1666
|
+
configPath: options.config,
|
|
1667
|
+
cachePath: options.registryCache
|
|
1668
|
+
});
|
|
1669
|
+
const registryConfig = await readErrorbookRegistryConfig(configPaths, fileSystem);
|
|
1670
|
+
const registryMode = normalizeRegistryMode(options.registryMode, registryConfig.search_mode || 'cache');
|
|
1671
|
+
const useCache = registryMode === 'cache' || registryMode === 'hybrid';
|
|
1672
|
+
const useRemote = registryMode === 'remote' || registryMode === 'hybrid';
|
|
1673
|
+
|
|
1674
|
+
if (useRemote) {
|
|
1675
|
+
const configuredSources = Array.isArray(registryConfig.sources)
|
|
1676
|
+
? registryConfig.sources.filter((item) => item.enabled && item.source)
|
|
1677
|
+
: [];
|
|
1678
|
+
const overrideSource = normalizeText(options.registrySource);
|
|
1679
|
+
const remoteSources = overrideSource
|
|
1680
|
+
? [normalizeRegistrySource({
|
|
1681
|
+
name: normalizeText(options.registrySourceName) || 'override',
|
|
1682
|
+
source: overrideSource,
|
|
1683
|
+
index_url: normalizeText(options.registryIndex)
|
|
1684
|
+
})]
|
|
1685
|
+
: configuredSources;
|
|
1686
|
+
|
|
1687
|
+
for (const source of remoteSources) {
|
|
1688
|
+
const remoteResult = await searchRegistryRemote({
|
|
1689
|
+
source,
|
|
1690
|
+
query,
|
|
1691
|
+
queryTokens: tokens,
|
|
1692
|
+
requestedStatus,
|
|
1693
|
+
maxShards: options.registryMaxShards,
|
|
1694
|
+
allowRemoteFullscan: options.allowRemoteFullscan === true
|
|
1695
|
+
}, {
|
|
1696
|
+
projectPath,
|
|
1697
|
+
fileSystem
|
|
1698
|
+
});
|
|
1699
|
+
registryRemoteMatched += Number(remoteResult.matched_count || 0);
|
|
1700
|
+
registryMatched += Number(remoteResult.matched_count || 0);
|
|
1701
|
+
if (Array.isArray(remoteResult.warnings)) {
|
|
1702
|
+
registryWarnings.push(...remoteResult.warnings);
|
|
1703
|
+
}
|
|
1704
|
+
if (Array.isArray(remoteResult.candidates)) {
|
|
1705
|
+
candidates.push(...remoteResult.candidates);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
if (useCache) {
|
|
1711
|
+
const cachePath = resolveProjectPath(
|
|
1712
|
+
projectPath,
|
|
1713
|
+
options.registryCache,
|
|
1714
|
+
registryConfig.cache_file || DEFAULT_ERRORBOOK_REGISTRY_CACHE
|
|
1715
|
+
);
|
|
1716
|
+
const registryCache = await loadRegistryCache(projectPath, cachePath, fileSystem);
|
|
1717
|
+
for (const entry of registryCache.entries) {
|
|
1718
|
+
if (requestedStatus && normalizeStatus(entry.status, 'candidate') !== requestedStatus) {
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
const matchScore = scoreSearchMatch(entry, tokens);
|
|
1722
|
+
if (matchScore <= 0) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
registryMatched += 1;
|
|
1726
|
+
registryCacheMatched += 1;
|
|
1727
|
+
candidates.push({
|
|
1728
|
+
id: entry.id,
|
|
1729
|
+
entry_source: 'registry-cache',
|
|
1730
|
+
status: entry.status,
|
|
1731
|
+
quality_score: entry.quality_score,
|
|
1732
|
+
title: entry.title,
|
|
1733
|
+
fingerprint: entry.fingerprint,
|
|
1734
|
+
tags: normalizeStringList(entry.tags),
|
|
1735
|
+
ontology_tags: normalizeOntologyTags(entry.ontology_tags),
|
|
1736
|
+
match_score: matchScore,
|
|
1737
|
+
updated_at: entry.updated_at
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const dedupedCandidates = new Map();
|
|
1744
|
+
for (const item of candidates) {
|
|
1745
|
+
const key = normalizeText(item.fingerprint || item.id);
|
|
1746
|
+
const existing = dedupedCandidates.get(key);
|
|
1747
|
+
if (!existing || Number(item.match_score || 0) >= Number(existing.match_score || 0)) {
|
|
1748
|
+
dedupedCandidates.set(key, item);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const sortedCandidates = Array.from(dedupedCandidates.values());
|
|
1752
|
+
|
|
1753
|
+
sortedCandidates.sort((left, right) => {
|
|
1103
1754
|
const scoreDiff = Number(right.match_score) - Number(left.match_score);
|
|
1104
1755
|
if (scoreDiff !== 0) {
|
|
1105
1756
|
return scoreDiff;
|
|
@@ -1110,8 +1761,16 @@ async function runErrorbookFindCommand(options = {}, dependencies = {}) {
|
|
|
1110
1761
|
const result = {
|
|
1111
1762
|
mode: 'errorbook-find',
|
|
1112
1763
|
query,
|
|
1113
|
-
|
|
1114
|
-
|
|
1764
|
+
include_registry: includeRegistry,
|
|
1765
|
+
source_breakdown: {
|
|
1766
|
+
local_results: localMatched,
|
|
1767
|
+
registry_results: registryMatched,
|
|
1768
|
+
registry_cache_results: registryCacheMatched,
|
|
1769
|
+
registry_remote_results: registryRemoteMatched
|
|
1770
|
+
},
|
|
1771
|
+
warnings: normalizeStringList(registryWarnings),
|
|
1772
|
+
total_results: sortedCandidates.length,
|
|
1773
|
+
entries: sortedCandidates.slice(0, limit)
|
|
1115
1774
|
};
|
|
1116
1775
|
|
|
1117
1776
|
if (options.json) {
|
|
@@ -1439,6 +2098,15 @@ function registerErrorbookCommands(program) {
|
|
|
1439
2098
|
.requiredOption('--query <text>', 'Search query')
|
|
1440
2099
|
.option('--status <status>', `Filter by status (${ERRORBOOK_STATUSES.join(', ')})`)
|
|
1441
2100
|
.option('--limit <n>', 'Maximum entries returned', parseInt, 10)
|
|
2101
|
+
.option('--include-registry', 'Include external registry entries in search')
|
|
2102
|
+
.option('--registry-mode <mode>', 'Registry lookup mode (cache|remote|hybrid)')
|
|
2103
|
+
.option('--registry-source <url-or-path>', 'Override registry source (for remote mode)')
|
|
2104
|
+
.option('--registry-source-name <name>', 'Override registry source label')
|
|
2105
|
+
.option('--registry-index <url-or-path>', 'Override registry index source (for remote mode)')
|
|
2106
|
+
.option('--registry-max-shards <n>', 'Max remote shards to fetch per query', parseInt, 8)
|
|
2107
|
+
.option('--allow-remote-fullscan', 'Allow remote full-source fallback when index is unavailable')
|
|
2108
|
+
.option('--registry-cache <path>', `Registry cache path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2109
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
1442
2110
|
.option('--json', 'Emit machine-readable JSON')
|
|
1443
2111
|
.action(async (options) => {
|
|
1444
2112
|
try {
|
|
@@ -1448,6 +2116,38 @@ function registerErrorbookCommands(program) {
|
|
|
1448
2116
|
}
|
|
1449
2117
|
});
|
|
1450
2118
|
|
|
2119
|
+
errorbook
|
|
2120
|
+
.command('export')
|
|
2121
|
+
.description('Export curated local entries for external registry publication')
|
|
2122
|
+
.option('--status <csv>', 'Statuses to include (csv, default: promoted)', 'promoted')
|
|
2123
|
+
.option('--min-quality <n>', 'Minimum quality score (default: 75)', parseInt)
|
|
2124
|
+
.option('--limit <n>', 'Maximum entries exported', parseInt)
|
|
2125
|
+
.option('--out <path>', `Output file (default: ${DEFAULT_ERRORBOOK_REGISTRY_EXPORT})`)
|
|
2126
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2127
|
+
.action(async (options) => {
|
|
2128
|
+
try {
|
|
2129
|
+
await runErrorbookExportCommand(options);
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
emitCommandError(error, options.json);
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
errorbook
|
|
2136
|
+
.command('sync-registry')
|
|
2137
|
+
.description('Sync external errorbook registry to local cache')
|
|
2138
|
+
.option('--source <url-or-path>', 'Registry source JSON (https://... or local file)')
|
|
2139
|
+
.option('--source-name <name>', 'Registry source name label')
|
|
2140
|
+
.option('--cache <path>', `Registry cache output path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2141
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
2142
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2143
|
+
.action(async (options) => {
|
|
2144
|
+
try {
|
|
2145
|
+
await runErrorbookSyncRegistryCommand(options);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
emitCommandError(error, options.json);
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
|
|
1451
2151
|
errorbook
|
|
1452
2152
|
.command('promote <id>')
|
|
1453
2153
|
.description('Promote entry after strict quality gate')
|
|
@@ -1508,15 +2208,21 @@ module.exports = {
|
|
|
1508
2208
|
ERRORBOOK_ONTOLOGY_TAGS,
|
|
1509
2209
|
ERRORBOOK_RISK_LEVELS,
|
|
1510
2210
|
TEMPORARY_MITIGATION_TAG,
|
|
2211
|
+
DEFAULT_ERRORBOOK_REGISTRY_CONFIG,
|
|
2212
|
+
DEFAULT_ERRORBOOK_REGISTRY_CACHE,
|
|
2213
|
+
DEFAULT_ERRORBOOK_REGISTRY_EXPORT,
|
|
1511
2214
|
HIGH_RISK_SIGNAL_TAGS,
|
|
1512
2215
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
1513
2216
|
resolveErrorbookPaths,
|
|
2217
|
+
resolveErrorbookRegistryPaths,
|
|
1514
2218
|
normalizeOntologyTags,
|
|
1515
2219
|
normalizeRecordPayload,
|
|
1516
2220
|
scoreQuality,
|
|
1517
2221
|
evaluateEntryRisk,
|
|
1518
2222
|
evaluateErrorbookReleaseGate,
|
|
1519
2223
|
runErrorbookRecordCommand,
|
|
2224
|
+
runErrorbookExportCommand,
|
|
2225
|
+
runErrorbookSyncRegistryCommand,
|
|
1520
2226
|
runErrorbookListCommand,
|
|
1521
2227
|
runErrorbookShowCommand,
|
|
1522
2228
|
runErrorbookFindCommand,
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"enabled": true,
|
|
3
|
+
"search_mode": "remote",
|
|
4
|
+
"cache_file": ".sce/errorbook/registry-cache.json",
|
|
5
|
+
"sources": [
|
|
6
|
+
{
|
|
7
|
+
"name": "central",
|
|
8
|
+
"enabled": true,
|
|
9
|
+
"url": "https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/errorbook-registry.json",
|
|
10
|
+
"index_url": "https://raw.githubusercontent.com/heguangyong/sce-errorbook-registry/main/registry/errorbook-registry.index.json"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|