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.
@@ -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.toLowerCase().split(/[\s,;|]+/).map((item) => item.trim()).filter(Boolean);
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
- candidates.sort((left, right) => {
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
- total_results: candidates.length,
1114
- entries: candidates.slice(0, limit)
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,