scene-capability-engine 3.6.38 → 3.6.39

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.
@@ -84,6 +84,8 @@ class ValidationEngine {
84
84
  */
85
85
  async validateSpec(specName) {
86
86
  const specPath = this.scanner.getSpecDirectory(specName);
87
+ const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
88
+ const allowedRootFiles = this.config.specAllowedRootFiles || requiredFiles;
87
89
 
88
90
  // Check if Spec directory exists
89
91
  if (!await this.scanner.exists(specPath)) {
@@ -97,7 +99,6 @@ class ValidationEngine {
97
99
  }
98
100
 
99
101
  // Check required files
100
- const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
101
102
  for (const file of requiredFiles) {
102
103
  const filePath = path.join(specPath, file);
103
104
  if (!await this.scanner.exists(filePath)) {
@@ -118,7 +119,7 @@ class ValidationEngine {
118
119
  const basename = path.basename(filePath);
119
120
 
120
121
  // Skip required files
121
- if (requiredFiles.includes(basename)) {
122
+ if (allowedRootFiles.includes(basename)) {
122
123
  continue;
123
124
  }
124
125
 
@@ -568,26 +568,34 @@ class SessionStore {
568
568
 
569
569
  const fileCount = fileRecords.length;
570
570
  const sqliteCount = Array.isArray(sqliteRecords) ? sqliteRecords.length : null;
571
+ let readSource = 'file';
571
572
 
572
573
  let status = 'file-only';
573
574
  if (sqliteCount === null) {
574
575
  status = 'sqlite-unavailable';
575
576
  } else if (fileCount === 0 && sqliteCount === 0) {
576
577
  status = 'empty';
578
+ readSource = 'empty';
577
579
  } else if (fileCount === 0 && sqliteCount > 0) {
578
580
  status = 'sqlite-only';
581
+ readSource = 'sqlite';
579
582
  } else if (fileCount > 0 && sqliteCount === 0) {
580
583
  status = 'file-only';
584
+ readSource = 'file';
581
585
  } else if (fileCount === sqliteCount) {
582
586
  status = 'aligned';
587
+ readSource = this._preferSqliteSceneReads ? 'sqlite' : 'file';
583
588
  } else if (sqliteCount < fileCount) {
584
589
  status = 'pending-sync';
590
+ readSource = 'file';
585
591
  } else if (sqliteCount > fileCount) {
586
592
  status = 'sqlite-ahead';
593
+ readSource = 'sqlite';
587
594
  }
588
595
 
589
596
  return {
590
597
  read_preference: this._preferSqliteSceneReads ? 'sqlite' : 'file',
598
+ read_source: readSource,
591
599
  file_scene_count: fileCount,
592
600
  sqlite_scene_count: sqliteCount,
593
601
  status
@@ -162,6 +162,7 @@ class SceStateStore {
162
162
  migration_records: {},
163
163
  auth_leases: {},
164
164
  auth_events: [],
165
+ interactive_approval_events: {},
165
166
  sequences: {
166
167
  scene_next: 1,
167
168
  spec_next_by_scene: {},
@@ -294,6 +295,34 @@ class SceStateStore {
294
295
  CREATE INDEX IF NOT EXISTS idx_auth_event_stream_ts
295
296
  ON auth_event_stream(event_timestamp);
296
297
 
298
+ CREATE TABLE IF NOT EXISTS interactive_approval_event_projection (
299
+ event_id TEXT PRIMARY KEY,
300
+ workflow_id TEXT,
301
+ event_timestamp TEXT NOT NULL,
302
+ event_type TEXT NOT NULL,
303
+ action TEXT,
304
+ actor TEXT,
305
+ actor_role TEXT,
306
+ from_status TEXT,
307
+ to_status TEXT,
308
+ blocked INTEGER,
309
+ reason TEXT,
310
+ audit_file TEXT,
311
+ line_no INTEGER,
312
+ raw_json TEXT NOT NULL,
313
+ source TEXT,
314
+ indexed_at TEXT NOT NULL
315
+ );
316
+
317
+ CREATE INDEX IF NOT EXISTS idx_interactive_approval_event_projection_workflow_ts
318
+ ON interactive_approval_event_projection(workflow_id, event_timestamp DESC);
319
+
320
+ CREATE INDEX IF NOT EXISTS idx_interactive_approval_event_projection_actor_action_ts
321
+ ON interactive_approval_event_projection(actor, action, event_timestamp DESC);
322
+
323
+ CREATE INDEX IF NOT EXISTS idx_interactive_approval_event_projection_blocked_ts
324
+ ON interactive_approval_event_projection(blocked, event_timestamp DESC);
325
+
297
326
  CREATE TABLE IF NOT EXISTS timeline_snapshot_registry (
298
327
  snapshot_id TEXT PRIMARY KEY,
299
328
  created_at TEXT NOT NULL,
@@ -1007,6 +1036,30 @@ class SceStateStore {
1007
1036
  };
1008
1037
  }
1009
1038
 
1039
+ _mapInteractiveApprovalEventProjectionRow(row) {
1040
+ if (!row) {
1041
+ return null;
1042
+ }
1043
+ return {
1044
+ event_id: normalizeString(row.event_id),
1045
+ workflow_id: normalizeString(row.workflow_id) || null,
1046
+ event_timestamp: normalizeIsoTimestamp(row.event_timestamp) || null,
1047
+ event_type: normalizeString(row.event_type),
1048
+ action: normalizeString(row.action) || null,
1049
+ actor: normalizeString(row.actor) || null,
1050
+ actor_role: normalizeString(row.actor_role) || null,
1051
+ from_status: normalizeString(row.from_status) || null,
1052
+ to_status: normalizeString(row.to_status) || null,
1053
+ blocked: normalizeBooleanValue(row.blocked, false),
1054
+ reason: normalizeString(row.reason) || null,
1055
+ audit_file: normalizeString(row.audit_file) || null,
1056
+ line_no: normalizeNonNegativeInteger(row.line_no, 0),
1057
+ raw: parseJsonSafe(row.raw_json, null),
1058
+ source: normalizeString(row.source) || null,
1059
+ indexed_at: normalizeIsoTimestamp(row.indexed_at) || null
1060
+ };
1061
+ }
1062
+
1010
1063
  _mapTimelineSnapshotRow(row) {
1011
1064
  if (!row) {
1012
1065
  return null;
@@ -2151,6 +2204,218 @@ class SceStateStore {
2151
2204
  .filter(Boolean);
2152
2205
  }
2153
2206
 
2207
+ async clearInteractiveApprovalEventProjection(options = {}) {
2208
+ const auditFileFilter = normalizeString(options.auditFile || options.audit_file);
2209
+
2210
+ if (this._useMemoryBackend()) {
2211
+ if (!auditFileFilter) {
2212
+ this._memory.interactive_approval_events = {};
2213
+ return { success: true, removed: 0 };
2214
+ }
2215
+ let removed = 0;
2216
+ for (const [eventId, item] of Object.entries(this._memory.interactive_approval_events || {})) {
2217
+ if (normalizeString(item.audit_file) === auditFileFilter) {
2218
+ delete this._memory.interactive_approval_events[eventId];
2219
+ removed += 1;
2220
+ }
2221
+ }
2222
+ return { success: true, removed };
2223
+ }
2224
+
2225
+ if (!await this.ensureReady()) {
2226
+ return null;
2227
+ }
2228
+
2229
+ if (auditFileFilter) {
2230
+ const info = this._db
2231
+ .prepare('DELETE FROM interactive_approval_event_projection WHERE audit_file = ?')
2232
+ .run(auditFileFilter);
2233
+ return {
2234
+ success: true,
2235
+ removed: normalizeNonNegativeInteger(info && info.changes, 0)
2236
+ };
2237
+ }
2238
+
2239
+ const info = this._db
2240
+ .prepare('DELETE FROM interactive_approval_event_projection')
2241
+ .run();
2242
+ return {
2243
+ success: true,
2244
+ removed: normalizeNonNegativeInteger(info && info.changes, 0)
2245
+ };
2246
+ }
2247
+
2248
+ async upsertInteractiveApprovalEventProjection(records = [], options = {}) {
2249
+ const source = normalizeString(options.source) || 'jsonl.interactive-approval-events';
2250
+ const auditFile = normalizeString(options.auditFile || options.audit_file) || null;
2251
+ const nowIso = this.now();
2252
+ const normalizedRecords = Array.isArray(records)
2253
+ ? records.map((item, index) => ({
2254
+ event_id: normalizeString(item && item.event_id),
2255
+ workflow_id: normalizeString(item && item.workflow_id) || null,
2256
+ event_timestamp: normalizeIsoTimestamp(item && (item.event_timestamp || item.timestamp), nowIso) || nowIso,
2257
+ event_type: normalizeString(item && item.event_type),
2258
+ action: normalizeString(item && item.action) || null,
2259
+ actor: normalizeString(item && item.actor) || null,
2260
+ actor_role: normalizeString(item && item.actor_role) || null,
2261
+ from_status: normalizeString(item && item.from_status) || null,
2262
+ to_status: normalizeString(item && item.to_status) || null,
2263
+ blocked: normalizeBooleanValue(item && item.blocked, false),
2264
+ reason: normalizeString(item && item.reason) || null,
2265
+ audit_file: normalizeString(item && (item.audit_file || item.auditFile)) || auditFile,
2266
+ line_no: normalizeNonNegativeInteger(item && (item.line_no || item.lineNo), index + 1),
2267
+ raw_json: JSON.stringify(item && typeof item === 'object' ? item : {}),
2268
+ source,
2269
+ indexed_at: nowIso
2270
+ }))
2271
+ .filter((item) => item.event_id && item.event_type)
2272
+ : [];
2273
+
2274
+ if (this._useMemoryBackend()) {
2275
+ for (const item of normalizedRecords) {
2276
+ this._memory.interactive_approval_events[item.event_id] = { ...item };
2277
+ }
2278
+ return {
2279
+ success: true,
2280
+ written: normalizedRecords.length,
2281
+ total: Object.keys(this._memory.interactive_approval_events || {}).length
2282
+ };
2283
+ }
2284
+
2285
+ if (!await this.ensureReady()) {
2286
+ return null;
2287
+ }
2288
+
2289
+ const statement = this._db.prepare(`
2290
+ INSERT OR REPLACE INTO interactive_approval_event_projection(
2291
+ event_id, workflow_id, event_timestamp, event_type, action, actor, actor_role,
2292
+ from_status, to_status, blocked, reason, audit_file, line_no, raw_json, source, indexed_at
2293
+ )
2294
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2295
+ `);
2296
+
2297
+ this._withTransaction(() => {
2298
+ for (const item of normalizedRecords) {
2299
+ statement.run(
2300
+ item.event_id,
2301
+ item.workflow_id,
2302
+ item.event_timestamp,
2303
+ item.event_type,
2304
+ item.action,
2305
+ item.actor,
2306
+ item.actor_role,
2307
+ item.from_status,
2308
+ item.to_status,
2309
+ item.blocked ? 1 : 0,
2310
+ item.reason,
2311
+ item.audit_file,
2312
+ item.line_no,
2313
+ item.raw_json,
2314
+ item.source,
2315
+ item.indexed_at
2316
+ );
2317
+ }
2318
+ });
2319
+
2320
+ const totalRow = this._db
2321
+ .prepare('SELECT COUNT(*) AS total FROM interactive_approval_event_projection')
2322
+ .get();
2323
+
2324
+ return {
2325
+ success: true,
2326
+ written: normalizedRecords.length,
2327
+ total: normalizeNonNegativeInteger(totalRow && totalRow.total, 0)
2328
+ };
2329
+ }
2330
+
2331
+ async listInteractiveApprovalEventProjection(options = {}) {
2332
+ const limit = normalizeInteger(options.limit, 100);
2333
+ const workflowId = normalizeString(options.workflowId || options.workflow_id);
2334
+ const actor = normalizeString(options.actor);
2335
+ const action = normalizeString(options.action);
2336
+ const eventType = normalizeString(options.eventType || options.event_type);
2337
+ const auditFile = normalizeString(options.auditFile || options.audit_file);
2338
+ const blockedFilter = options.blocked === undefined || options.blocked === null
2339
+ ? null
2340
+ : normalizeBooleanValue(options.blocked, false);
2341
+
2342
+ if (this._useMemoryBackend()) {
2343
+ let rows = Object.values(this._memory.interactive_approval_events || {}).map((item) => ({ ...item }));
2344
+ if (workflowId) {
2345
+ rows = rows.filter((item) => normalizeString(item.workflow_id) === workflowId);
2346
+ }
2347
+ if (actor) {
2348
+ rows = rows.filter((item) => normalizeString(item.actor) === actor);
2349
+ }
2350
+ if (action) {
2351
+ rows = rows.filter((item) => normalizeString(item.action) === action);
2352
+ }
2353
+ if (eventType) {
2354
+ rows = rows.filter((item) => normalizeString(item.event_type) === eventType);
2355
+ }
2356
+ if (auditFile) {
2357
+ rows = rows.filter((item) => normalizeString(item.audit_file) === auditFile);
2358
+ }
2359
+ if (blockedFilter !== null) {
2360
+ rows = rows.filter((item) => normalizeBooleanValue(item.blocked, false) === blockedFilter);
2361
+ }
2362
+ rows.sort((left, right) => (Date.parse(right.event_timestamp || '') || 0) - (Date.parse(left.event_timestamp || '') || 0));
2363
+ if (limit > 0) {
2364
+ rows = rows.slice(0, limit);
2365
+ }
2366
+ return rows.map((row) => this._mapInteractiveApprovalEventProjectionRow(row)).filter(Boolean);
2367
+ }
2368
+
2369
+ if (!await this.ensureReady()) {
2370
+ return null;
2371
+ }
2372
+
2373
+ let query = `
2374
+ SELECT event_id, workflow_id, event_timestamp, event_type, action, actor, actor_role,
2375
+ from_status, to_status, blocked, reason, audit_file, line_no, raw_json, source, indexed_at
2376
+ FROM interactive_approval_event_projection
2377
+ `;
2378
+ const clauses = [];
2379
+ const params = [];
2380
+ if (workflowId) {
2381
+ clauses.push('workflow_id = ?');
2382
+ params.push(workflowId);
2383
+ }
2384
+ if (actor) {
2385
+ clauses.push('actor = ?');
2386
+ params.push(actor);
2387
+ }
2388
+ if (action) {
2389
+ clauses.push('action = ?');
2390
+ params.push(action);
2391
+ }
2392
+ if (eventType) {
2393
+ clauses.push('event_type = ?');
2394
+ params.push(eventType);
2395
+ }
2396
+ if (auditFile) {
2397
+ clauses.push('audit_file = ?');
2398
+ params.push(auditFile);
2399
+ }
2400
+ if (blockedFilter !== null) {
2401
+ clauses.push('blocked = ?');
2402
+ params.push(blockedFilter ? 1 : 0);
2403
+ }
2404
+ if (clauses.length > 0) {
2405
+ query += ` WHERE ${clauses.join(' AND ')}`;
2406
+ }
2407
+ query += ' ORDER BY event_timestamp DESC, line_no DESC';
2408
+ if (limit > 0) {
2409
+ query += ' LIMIT ?';
2410
+ params.push(limit);
2411
+ }
2412
+
2413
+ const rows = this._db.prepare(query).all(...params);
2414
+ return rows
2415
+ .map((row) => this._mapInteractiveApprovalEventProjectionRow(row))
2416
+ .filter(Boolean);
2417
+ }
2418
+
2154
2419
  async upsertTimelineSnapshotIndex(records = [], options = {}) {
2155
2420
  const source = normalizeString(options.source) || 'file.timeline.index';
2156
2421
  const nowIso = this.now();
@@ -723,8 +723,12 @@ async function runStateDoctor(options = {}, dependencies = {}) {
723
723
  syncStatus = 'source-parse-error';
724
724
  } else if (sourceCount === 0 && targetCount === 0) {
725
725
  syncStatus = 'empty';
726
+ } else if (sourceCount === 0 && targetCount > 0) {
727
+ syncStatus = 'sqlite-only';
726
728
  } else if (targetCount < sourceCount) {
727
729
  syncStatus = 'pending-migration';
730
+ } else if (targetCount > sourceCount) {
731
+ syncStatus = 'sqlite-ahead';
728
732
  }
729
733
  return {
730
734
  id: component.id,
@@ -750,22 +754,37 @@ async function runStateDoctor(options = {}, dependencies = {}) {
750
754
  if (checks.some((item) => item.sync_status === 'source-parse-error')) {
751
755
  blocking.push('source-parse-error');
752
756
  }
757
+ if (checks.some((item) => item.sync_status === 'sqlite-ahead')) {
758
+ blocking.push('sqlite-ahead');
759
+ }
760
+ if (checks.some((item) => item.sync_status === 'sqlite-only')) {
761
+ blocking.push('sqlite-only');
762
+ }
753
763
 
754
764
  const alerts = checks
755
765
  .filter((item) => item.sync_status === 'pending-migration')
756
766
  .map((item) => `pending migration: ${item.id}`);
767
+ alerts.push(...checks
768
+ .filter((item) => item.sync_status === 'missing-source')
769
+ .map((item) => `missing source: ${item.id}`));
757
770
 
758
771
  if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'pending-sync') {
759
772
  alerts.push('runtime timeline index pending-sync');
760
773
  }
761
774
  if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'sqlite-ahead') {
762
- alerts.push('runtime timeline index sqlite-ahead');
775
+ blocking.push('runtime timeline index sqlite-ahead');
776
+ }
777
+ if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'sqlite-only') {
778
+ blocking.push('runtime timeline index sqlite-only');
763
779
  }
764
780
  if (runtime.scene_session && runtime.scene_session.consistency && runtime.scene_session.consistency.status === 'pending-sync') {
765
781
  alerts.push('runtime scene-session index pending-sync');
766
782
  }
767
783
  if (runtime.scene_session && runtime.scene_session.consistency && runtime.scene_session.consistency.status === 'sqlite-ahead') {
768
- alerts.push('runtime scene-session index sqlite-ahead');
784
+ blocking.push('runtime scene-session index sqlite-ahead');
785
+ }
786
+ if (runtime.scene_session && runtime.scene_session.consistency && runtime.scene_session.consistency.status === 'sqlite-only') {
787
+ blocking.push('runtime scene-session index sqlite-only');
769
788
  }
770
789
 
771
790
  const summary = summarizeDoctorChecks(checks, alerts, blocking);
@@ -792,7 +811,9 @@ function summarizeDoctorChecks(checks = [], alerts = [], blocking = []) {
792
811
  const pendingComponents = normalizedChecks.filter((item) => item.sync_status === 'pending-migration').length;
793
812
  const syncedComponents = normalizedChecks.filter((item) => item.sync_status === 'synced').length;
794
813
  const sqliteOnlyComponents = normalizedChecks.filter((item) => item.sync_status === 'sqlite-only').length;
814
+ const sqliteAheadComponents = normalizedChecks.filter((item) => item.sync_status === 'sqlite-ahead').length;
795
815
  const missingSourceComponents = normalizedChecks.filter((item) => item.sync_status === 'missing-source').length;
816
+ const parseErrorComponents = normalizedChecks.filter((item) => item.sync_status === 'source-parse-error').length;
796
817
  const driftRecords = normalizedChecks.reduce((sum, item) => {
797
818
  const source = normalizeCount(item.source_record_count);
798
819
  const target = normalizeCount(item.sqlite_record_count);
@@ -804,7 +825,9 @@ function summarizeDoctorChecks(checks = [], alerts = [], blocking = []) {
804
825
  synced_components: syncedComponents,
805
826
  pending_components: pendingComponents,
806
827
  sqlite_only_components: sqliteOnlyComponents,
828
+ sqlite_ahead_components: sqliteAheadComponents,
807
829
  missing_source_components: missingSourceComponents,
830
+ source_parse_error_components: parseErrorComponents,
808
831
  total_source_records: sourceRecords,
809
832
  total_sqlite_records: sqliteRecords,
810
833
  total_record_drift: driftRecords,
@@ -878,6 +901,7 @@ async function collectRuntimeDiagnostics(dependencies = {}) {
878
901
  const sceneIndex = await sessionStore.getSceneIndexDiagnostics();
879
902
  runtime.scene_session = {
880
903
  read_preference: normalizeString(sceneIndex.read_preference) || 'file',
904
+ read_source: normalizeString(sceneIndex.read_source) || 'file',
881
905
  consistency: {
882
906
  status: normalizeString(sceneIndex.status) || 'unknown',
883
907
  file_index_count: normalizeCount(sceneIndex.file_scene_count),
@@ -889,6 +913,7 @@ async function collectRuntimeDiagnostics(dependencies = {}) {
889
913
  } catch (_error) {
890
914
  runtime.scene_session = {
891
915
  read_preference: 'file',
916
+ read_source: 'unavailable',
892
917
  consistency: {
893
918
  status: 'unavailable',
894
919
  file_index_count: 0,
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_STATE_STORAGE_POLICY = Object.freeze({
4
+ schema_version: '1.0',
5
+ strategy: 'selective-sqlite-advancement',
6
+ tiers: {
7
+ 'file-source': {
8
+ description: 'Canonical file-backed storage for low-cardinality config, raw evidence, audit streams, and recovery-oriented payloads.'
9
+ },
10
+ 'sqlite-index': {
11
+ description: 'SQLite index/registry layer for file-backed resources that need high-frequency filtering, sorting, and cross-run aggregation.'
12
+ },
13
+ 'derived-sqlite-projection': {
14
+ description: 'Disposable SQLite projection rebuilt from canonical files for append-only streams with query pressure.'
15
+ }
16
+ },
17
+ admission: {
18
+ required_signals: [
19
+ 'cross-run or cross-session query pressure is proven',
20
+ 'file scans are materially weaker than indexed filtering/sorting',
21
+ 'sqlite content remains rebuildable from a canonical source',
22
+ 'operator diagnostics and reconcile path are defined before rollout'
23
+ ],
24
+ deny_if_any: [
25
+ 'resource is the only copy of raw audit or evidence payload',
26
+ 'resource is low-cardinality personal workspace or preference state',
27
+ 'human-readable diff and manual recovery are more valuable than query speed',
28
+ 'migration would introduce silent source-of-truth cutover'
29
+ ],
30
+ future_candidate_checklist: [
31
+ 'identify canonical source path or stream',
32
+ 'document expected query patterns and consumers',
33
+ 'define rebuild and reconcile semantics',
34
+ 'define release gate or audit behavior for drift states',
35
+ 'document why existing file-only storage is insufficient'
36
+ ]
37
+ },
38
+ component_scope: [
39
+ {
40
+ component_id: 'collab.agent-registry',
41
+ tier: 'sqlite-index',
42
+ canonical_source: 'file',
43
+ source_path: '.sce/config/agent-registry.json',
44
+ sqlite_tables: ['agent_runtime_registry'],
45
+ rationale: 'Registry-style lookup with repeated status and capability queries.'
46
+ },
47
+ {
48
+ component_id: 'runtime.timeline-index',
49
+ tier: 'sqlite-index',
50
+ canonical_source: 'file',
51
+ source_path: '.sce/timeline/index.json',
52
+ sqlite_tables: ['timeline_snapshot_registry'],
53
+ rationale: 'Timeline index benefits from filtered and cross-session reads while file snapshots remain recoverable source artifacts.'
54
+ },
55
+ {
56
+ component_id: 'runtime.scene-session-index',
57
+ tier: 'sqlite-index',
58
+ canonical_source: 'file',
59
+ source_path: '.sce/session-governance/scene-index.json',
60
+ sqlite_tables: ['scene_session_cycle_registry'],
61
+ rationale: 'Scene/session lookups have query pressure and consistency checks but still rely on file session payloads.'
62
+ },
63
+ {
64
+ component_id: 'errorbook.entry-index',
65
+ tier: 'sqlite-index',
66
+ canonical_source: 'file',
67
+ source_path: '.sce/errorbook/index.json',
68
+ sqlite_tables: ['errorbook_entry_index_registry'],
69
+ rationale: 'Promoted errorbook registry queries benefit from indexed status and quality filtering.'
70
+ },
71
+ {
72
+ component_id: 'errorbook.incident-index',
73
+ tier: 'sqlite-index',
74
+ canonical_source: 'file',
75
+ source_path: '.sce/errorbook/staging/index.json',
76
+ sqlite_tables: ['errorbook_incident_index_registry'],
77
+ rationale: 'Incident staging state requires queryable triage views without replacing raw incident artifacts.'
78
+ },
79
+ {
80
+ component_id: 'governance.spec-scene-overrides',
81
+ tier: 'sqlite-index',
82
+ canonical_source: 'file',
83
+ source_path: '.sce/spec-governance/spec-scene-overrides.json',
84
+ sqlite_tables: ['governance_spec_scene_override_registry'],
85
+ rationale: 'Override lookups are registry-like and join naturally with other governance indexes.'
86
+ },
87
+ {
88
+ component_id: 'governance.scene-index',
89
+ tier: 'sqlite-index',
90
+ canonical_source: 'file',
91
+ source_path: '.sce/spec-governance/scene-index.json',
92
+ sqlite_tables: ['governance_scene_index_registry'],
93
+ rationale: 'Scene governance summaries are better served by indexed counts and status filters.'
94
+ },
95
+ {
96
+ component_id: 'release.evidence-runs-index',
97
+ tier: 'sqlite-index',
98
+ canonical_source: 'file',
99
+ source_path: '.sce/reports/release-evidence/handoff-runs.json',
100
+ sqlite_tables: ['release_evidence_run_registry'],
101
+ rationale: 'Release evidence run summaries need fast historical querying while release assets remain file-backed.'
102
+ },
103
+ {
104
+ component_id: 'release.gate-history-index',
105
+ tier: 'sqlite-index',
106
+ canonical_source: 'file',
107
+ source_path: '.sce/reports/release-evidence/release-gate-history.json',
108
+ sqlite_tables: ['release_gate_history_registry'],
109
+ rationale: 'Gate history is registry-shaped and queried by tag, pass/fail, and drift metrics.'
110
+ }
111
+ ],
112
+ resource_rules: [
113
+ {
114
+ rule_id: 'workspace-personal-state',
115
+ tier: 'file-source',
116
+ explicit_paths: ['~/.sce/workspace-state.json'],
117
+ derived_projection_allowed: false,
118
+ source_replacement_allowed: false,
119
+ rationale: 'Personal workspace selection and preferences are low-cardinality, atomic, and not worth migrating into SQLite.'
120
+ },
121
+ {
122
+ rule_id: 'append-only-report-streams',
123
+ tier: 'file-source',
124
+ path_patterns: ['.sce/reports/**/*.jsonl'],
125
+ derived_projection_allowed: true,
126
+ source_replacement_allowed: false,
127
+ rationale: 'Raw governance and evidence streams must stay append-only files; projection is allowed only for query acceleration.'
128
+ },
129
+ {
130
+ rule_id: 'append-only-audit-streams',
131
+ tier: 'file-source',
132
+ path_patterns: ['.sce/audit/**/*.jsonl'],
133
+ derived_projection_allowed: true,
134
+ source_replacement_allowed: false,
135
+ rationale: 'Audit streams remain canonical evidence and should never become SQLite-only write paths.'
136
+ },
137
+ {
138
+ rule_id: 'timeline-snapshot-payloads',
139
+ tier: 'file-source',
140
+ path_patterns: ['.sce/timeline/snapshots/**'],
141
+ derived_projection_allowed: false,
142
+ source_replacement_allowed: false,
143
+ rationale: 'Timeline snapshots are recovery-oriented payload artifacts, not registry data.'
144
+ },
145
+ {
146
+ rule_id: 'session-payload-artifacts',
147
+ tier: 'file-source',
148
+ path_patterns: ['.sce/session-governance/sessions/**'],
149
+ derived_projection_allowed: false,
150
+ source_replacement_allowed: false,
151
+ rationale: 'Session payloads must stay file-backed for recovery, archive, and manual debugging.'
152
+ },
153
+ {
154
+ rule_id: 'release-evidence-assets',
155
+ tier: 'file-source',
156
+ path_patterns: [
157
+ '.sce/reports/release-evidence/**/*.json',
158
+ '.sce/reports/release-evidence/**/*.md',
159
+ '.sce/reports/release-evidence/**/*.jsonl',
160
+ '.sce/reports/release-evidence/**/*.lines'
161
+ ],
162
+ derived_projection_allowed: true,
163
+ source_replacement_allowed: false,
164
+ rationale: 'Release evidence assets remain portable files even when selected summary indexes are mirrored into SQLite.'
165
+ }
166
+ ]
167
+ });
168
+
169
+ const REQUIRED_COMPONENT_IDS = Object.freeze(DEFAULT_STATE_STORAGE_POLICY.component_scope.map((item) => item.component_id));
170
+
171
+ function cloneStateStoragePolicyDefaults() {
172
+ return JSON.parse(JSON.stringify(DEFAULT_STATE_STORAGE_POLICY));
173
+ }
174
+
175
+ module.exports = {
176
+ DEFAULT_STATE_STORAGE_POLICY,
177
+ REQUIRED_COMPONENT_IDS,
178
+ cloneStateStoragePolicyDefaults
179
+ };
@@ -4,6 +4,15 @@ const { promisify } = require('util');
4
4
 
5
5
  const execAsync = promisify(exec);
6
6
 
7
+ function sleep(ms) {
8
+ return new Promise(resolve => {
9
+ const timer = setTimeout(resolve, ms);
10
+ if (typeof timer.unref === 'function') {
11
+ timer.unref();
12
+ }
13
+ });
14
+ }
15
+
7
16
  /**
8
17
  * ActionExecutor - 动作执行器
9
18
  *
@@ -203,7 +212,7 @@ class ActionExecutor extends EventEmitter {
203
212
  });
204
213
 
205
214
  // 等待
206
- await new Promise(resolve => setTimeout(resolve, delay));
215
+ await sleep(delay);
207
216
 
208
217
  try {
209
218
  // 重新执行
@@ -71,6 +71,9 @@ class EventDebouncer extends EventEmitter {
71
71
  this.emit('error', { error, key, type: 'debounce' });
72
72
  }
73
73
  }, actualDelay);
74
+ if (typeof timer.unref === 'function') {
75
+ timer.unref();
76
+ }
74
77
 
75
78
  this.debounceTimers.set(key, timer);
76
79
  }