scene-capability-engine 3.6.38 → 3.6.44

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/bin/scene-capability-engine.js +42 -2
  3. package/docs/command-reference.md +27 -0
  4. package/docs/developer-guide.md +1 -1
  5. package/docs/document-governance.md +22 -2
  6. package/docs/releases/README.md +6 -0
  7. package/docs/releases/v3.6.39.md +24 -0
  8. package/docs/releases/v3.6.40.md +19 -0
  9. package/docs/releases/v3.6.41.md +20 -0
  10. package/docs/releases/v3.6.42.md +19 -0
  11. package/docs/releases/v3.6.43.md +17 -0
  12. package/docs/releases/v3.6.44.md +17 -0
  13. package/docs/spec-collaboration-guide.md +1 -1
  14. package/docs/state-migration-reconciliation-runbook.md +76 -0
  15. package/docs/state-storage-tiering.md +104 -0
  16. package/docs/zh/releases/README.md +6 -0
  17. package/docs/zh/releases/v3.6.39.md +24 -0
  18. package/docs/zh/releases/v3.6.40.md +19 -0
  19. package/docs/zh/releases/v3.6.41.md +20 -0
  20. package/docs/zh/releases/v3.6.42.md +19 -0
  21. package/docs/zh/releases/v3.6.43.md +17 -0
  22. package/docs/zh/releases/v3.6.44.md +17 -0
  23. package/lib/adoption/adoption-logger.js +1 -1
  24. package/lib/adoption/adoption-strategy.js +29 -29
  25. package/lib/adoption/detection-engine.js +16 -13
  26. package/lib/adoption/smart-orchestrator.js +3 -3
  27. package/lib/adoption/strategy-selector.js +19 -15
  28. package/lib/adoption/template-sync.js +3 -3
  29. package/lib/auto/autonomous-engine.js +5 -5
  30. package/lib/auto/handoff-release-gate-history-loaders-service.js +24 -4
  31. package/lib/auto/handoff-run-service.js +37 -0
  32. package/lib/backup/backup-system.js +10 -10
  33. package/lib/collab/collab-manager.js +8 -5
  34. package/lib/collab/dependency-manager.js +1 -1
  35. package/lib/commands/adopt.js +2 -2
  36. package/lib/commands/auto.js +239 -97
  37. package/lib/commands/collab.js +10 -4
  38. package/lib/commands/docs.js +8 -2
  39. package/lib/commands/scene.js +78 -18
  40. package/lib/commands/status.js +3 -3
  41. package/lib/commands/studio.js +8 -0
  42. package/lib/commands/watch.js +10 -1
  43. package/lib/governance/config-manager.js +16 -0
  44. package/lib/governance/diagnostic-engine.js +2 -1
  45. package/lib/governance/validation-engine.js +3 -2
  46. package/lib/repo/config-manager.js +2 -2
  47. package/lib/runtime/session-store.js +8 -0
  48. package/lib/spec/bootstrap/context-collector.js +5 -4
  49. package/lib/spec-gate/rules/default-rules.js +8 -8
  50. package/lib/state/sce-state-store.js +265 -0
  51. package/lib/state/state-migration-manager.js +27 -2
  52. package/lib/state/state-storage-policy.js +179 -0
  53. package/lib/upgrade/migration-engine.js +5 -5
  54. package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +3 -3
  55. package/lib/utils/tool-detector.js +4 -4
  56. package/lib/utils/validation.js +6 -6
  57. package/lib/watch/action-executor.js +10 -1
  58. package/lib/watch/event-debouncer.js +3 -0
  59. package/lib/watch/file-watcher.js +51 -10
  60. package/lib/watch/watch-manager.js +10 -1
  61. package/lib/workspace/multi/workspace-context-resolver.js +3 -3
  62. package/lib/workspace/multi/workspace-registry.js +3 -3
  63. package/lib/workspace/multi/workspace-state-manager.js +3 -3
  64. package/lib/workspace/spec-delivery-audit.js +553 -0
  65. package/lib/workspace/takeover-baseline.js +11 -0
  66. package/package.json +5 -1
  67. package/template/.sce/config/state-storage-policy.json +165 -0
@@ -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
+ };
@@ -300,10 +300,10 @@ class MigrationEngine {
300
300
 
301
301
  try {
302
302
  // Check if .sce/ directory exists
303
- const kiroPath = path.join(projectPath, '.sce');
304
- const kiroExists = await pathExists(kiroPath);
303
+ const scePath = path.join(projectPath, '.sce');
304
+ const sceExists = await pathExists(scePath);
305
305
 
306
- if (!kiroExists) {
306
+ if (!sceExists) {
307
307
  errors.push('.sce/ directory not found');
308
308
  return { success: false, errors, warnings };
309
309
  }
@@ -320,7 +320,7 @@ class MigrationEngine {
320
320
  const requiredDirs = ['specs', 'steering', 'tools', 'backups'];
321
321
 
322
322
  for (const dir of requiredDirs) {
323
- const dirPath = path.join(kiroPath, dir);
323
+ const dirPath = path.join(scePath, dir);
324
324
  const exists = await pathExists(dirPath);
325
325
 
326
326
  if (!exists) {
@@ -337,7 +337,7 @@ class MigrationEngine {
337
337
  ];
338
338
 
339
339
  for (const file of requiredSteeringFiles) {
340
- const filePath = path.join(kiroPath, file);
340
+ const filePath = path.join(scePath, file);
341
341
  const exists = await pathExists(filePath);
342
342
 
343
343
  if (!exists) {
@@ -26,10 +26,10 @@ module.exports = {
26
26
  const changes = [];
27
27
 
28
28
  try {
29
- const kiroPath = path.join(projectPath, '.sce');
29
+ const scePath = path.join(projectPath, '.sce');
30
30
 
31
31
  // 1. Ensure backups/ directory exists
32
- const backupsPath = path.join(kiroPath, 'backups');
32
+ const backupsPath = path.join(scePath, 'backups');
33
33
  const backupsExists = await pathExists(backupsPath);
34
34
 
35
35
  if (!backupsExists) {
@@ -39,7 +39,7 @@ module.exports = {
39
39
 
40
40
  // 2. Ensure version.json has correct structure
41
41
  // (This is handled by VersionManager, but we verify it here)
42
- const versionPath = path.join(kiroPath, 'version.json');
42
+ const versionPath = path.join(scePath, 'version.json');
43
43
  const versionExists = await pathExists(versionPath);
44
44
 
45
45
  if (versionExists) {
@@ -64,21 +64,21 @@ async function detectKiroIDE(projectPath) {
64
64
  let confidence = 'low';
65
65
 
66
66
  // Check for .sce directory
67
- const kiroDir = path.join(projectPath, '.sce');
68
- if (await fs.pathExists(kiroDir)) {
67
+ const sceDir = path.join(projectPath, '.sce');
68
+ if (await fs.pathExists(sceDir)) {
69
69
  indicators.push('.sce directory exists');
70
70
  detected = true;
71
71
  confidence = 'medium';
72
72
  }
73
73
 
74
74
  // Check for SCE-specific files
75
- const kiroFiles = [
75
+ const sceFiles = [
76
76
  '.sce/steering',
77
77
  '.sce/specs',
78
78
  '.sce/tools'
79
79
  ];
80
80
 
81
- for (const file of kiroFiles) {
81
+ for (const file of sceFiles) {
82
82
  const filePath = path.join(projectPath, file);
83
83
  if (await fs.pathExists(filePath)) {
84
84
  indicators.push(`${file} exists`);
@@ -21,11 +21,11 @@ async function validateProjectStructure(projectPath) {
21
21
  const warnings = [];
22
22
 
23
23
  try {
24
- const kiroPath = path.join(projectPath, '.sce');
24
+ const scePath = path.join(projectPath, '.sce');
25
25
 
26
26
  // Check if .sce/ directory exists
27
- const kiroExists = await pathExists(kiroPath);
28
- if (!kiroExists) {
27
+ const sceExists = await pathExists(scePath);
28
+ if (!sceExists) {
29
29
  errors.push('.sce/ directory not found');
30
30
  return { success: false, errors, warnings };
31
31
  }
@@ -39,7 +39,7 @@ async function validateProjectStructure(projectPath) {
39
39
  ];
40
40
 
41
41
  for (const dir of requiredDirs) {
42
- const dirPath = path.join(kiroPath, dir.path);
42
+ const dirPath = path.join(scePath, dir.path);
43
43
  const exists = await pathExists(dirPath);
44
44
 
45
45
  if (!exists) {
@@ -60,7 +60,7 @@ async function validateProjectStructure(projectPath) {
60
60
  ];
61
61
 
62
62
  for (const file of requiredSteeringFiles) {
63
- const filePath = path.join(kiroPath, file.path);
63
+ const filePath = path.join(scePath, file.path);
64
64
  const exists = await pathExists(filePath);
65
65
 
66
66
  if (!exists) {
@@ -78,7 +78,7 @@ async function validateProjectStructure(projectPath) {
78
78
  ];
79
79
 
80
80
  for (const file of requiredToolFiles) {
81
- const filePath = path.join(kiroPath, file.path);
81
+ const filePath = path.join(scePath, file.path);
82
82
  const exists = await pathExists(filePath);
83
83
 
84
84
  if (!exists) {