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.
@@ -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',
@@ -316,6 +316,8 @@ class BackupManager {
316
316
  'steering/CORE_PRINCIPLES.md',
317
317
  'steering/ENVIRONMENT.md',
318
318
  'config/studio-security.json',
319
+ 'config/orchestrator.json',
320
+ 'config/errorbook-registry.json',
319
321
  'version.json',
320
322
  'adoption-config.json'
321
323
  ];
@@ -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
@@ -283,6 +283,8 @@ class SmartOrchestrator {
283
283
  'steering/RULES_GUIDE.md',
284
284
  'tools/ultrawork_enhancer.py',
285
285
  'config/studio-security.json',
286
+ 'config/orchestrator.json',
287
+ 'config/errorbook-registry.json',
286
288
  'README.md'
287
289
  ];
288
290
 
@@ -110,6 +110,8 @@ class StrategySelector {
110
110
  'steering/RULES_GUIDE.md',
111
111
  'tools/ultrawork_enhancer.py',
112
112
  'config/studio-security.json',
113
+ 'config/orchestrator.json',
114
+ 'config/errorbook-registry.json',
113
115
  'README.md'
114
116
  ];
115
117
 
@@ -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.toLowerCase().split(/[\s,;|]+/).map((item) => item.trim()).filter(Boolean);
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
- candidates.sort((left, right) => {
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
- total_results: candidates.length,
1114
- entries: candidates.slice(0, limit)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.3.22",
3
+ "version": "3.3.23",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
+ }