scene-capability-engine 3.6.37 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/docs/command-reference.md +27 -0
  3. package/docs/document-governance.md +22 -2
  4. package/docs/releases/README.md +2 -0
  5. package/docs/releases/v3.6.38.md +19 -0
  6. package/docs/releases/v3.6.39.md +24 -0
  7. package/docs/state-migration-reconciliation-runbook.md +76 -0
  8. package/docs/state-storage-tiering.md +104 -0
  9. package/docs/steering-governance.md +112 -0
  10. package/docs/zh/releases/README.md +2 -0
  11. package/docs/zh/releases/v3.6.38.md +19 -0
  12. package/docs/zh/releases/v3.6.39.md +24 -0
  13. package/lib/commands/docs.js +8 -2
  14. package/lib/commands/scene.js +78 -18
  15. package/lib/commands/watch.js +10 -1
  16. package/lib/governance/config-manager.js +16 -0
  17. package/lib/governance/diagnostic-engine.js +2 -1
  18. package/lib/governance/validation-engine.js +3 -2
  19. package/lib/runtime/session-store.js +8 -0
  20. package/lib/state/sce-state-store.js +265 -0
  21. package/lib/state/state-migration-manager.js +27 -2
  22. package/lib/state/state-storage-policy.js +179 -0
  23. package/lib/watch/action-executor.js +10 -1
  24. package/lib/watch/event-debouncer.js +3 -0
  25. package/lib/watch/file-watcher.js +51 -10
  26. package/lib/watch/watch-manager.js +10 -1
  27. package/lib/workspace/takeover-baseline.js +11 -0
  28. package/package.json +8 -2
  29. package/template/.sce/config/state-storage-policy.json +165 -0
  30. package/template/.sce/steering/CORE_PRINCIPLES.md +21 -211
  31. package/template/.sce/steering/CURRENT_CONTEXT.md +9 -27
  32. package/template/.sce/steering/ENVIRONMENT.md +18 -32
  33. package/template/.sce/steering/RULES_GUIDE.md +16 -44
  34. package/template/.sce/steering/manifest.yaml +50 -0
@@ -158,6 +158,45 @@ function createDoctorTraceId() {
158
158
  return `doctor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
159
159
  }
160
160
 
161
+ function stringifyJsonForCli(value, indentSize = 2, currentIndent = 0) {
162
+ if (value === null) {
163
+ return 'null';
164
+ }
165
+
166
+ if (typeof value === 'number') {
167
+ return Object.is(value, -0) ? '-0' : JSON.stringify(value);
168
+ }
169
+
170
+ if (typeof value === 'string' || typeof value === 'boolean') {
171
+ return JSON.stringify(value);
172
+ }
173
+
174
+ if (Array.isArray(value)) {
175
+ if (value.length === 0) {
176
+ return '[]';
177
+ }
178
+
179
+ const nextIndent = currentIndent + indentSize;
180
+ const indent = ' '.repeat(currentIndent);
181
+ const childIndent = ' '.repeat(nextIndent);
182
+ const items = value.map((entry) => `${childIndent}${stringifyJsonForCli(entry, indentSize, nextIndent)}`);
183
+ return `[\n${items.join(',\n')}\n${indent}]`;
184
+ }
185
+
186
+ const keys = Object.keys(value);
187
+ if (keys.length === 0) {
188
+ return '{}';
189
+ }
190
+
191
+ const nextIndent = currentIndent + indentSize;
192
+ const indent = ' '.repeat(currentIndent);
193
+ const childIndent = ' '.repeat(nextIndent);
194
+ const entries = keys.map((key) => (
195
+ `${childIndent}${JSON.stringify(key)}: ${stringifyJsonForCli(value[key], indentSize, nextIndent)}`
196
+ ));
197
+ return `{\n${entries.join(',\n')}\n${indent}}`;
198
+ }
199
+
161
200
  function registerSceneCommands(program) {
162
201
  const sceneCmd = program
163
202
  .command('scene')
@@ -4795,7 +4834,7 @@ function createSceneEvalConfigTemplateByProfile(profile = 'default') {
4795
4834
  }
4796
4835
 
4797
4836
  function normalizeRelativePath(targetPath = '') {
4798
- return String(targetPath || '').replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '');
4837
+ return String(targetPath || '').trim().replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '');
4799
4838
  }
4800
4839
 
4801
4840
  function collectManifestDiscoveryCandidates(preferredPath = 'custom/scene.yaml') {
@@ -13639,6 +13678,35 @@ async function runSceneVersionCommand(rawOptions = {}, dependencies = {}) {
13639
13678
  }
13640
13679
  }
13641
13680
 
13681
+ function isBinaryContent(buffer) {
13682
+ if (!Buffer.isBuffer(buffer)) {
13683
+ return false;
13684
+ }
13685
+
13686
+ for (let index = 0; index < buffer.length; index++) {
13687
+ if (buffer[index] === 0) {
13688
+ return true;
13689
+ }
13690
+ }
13691
+
13692
+ return false;
13693
+ }
13694
+
13695
+ function countChangedLines(fromContent, toContent) {
13696
+ const oldLines = fromContent.toString('utf8').split('\n');
13697
+ const newLines = toContent.toString('utf8').split('\n');
13698
+ const maxLen = Math.max(oldLines.length, newLines.length);
13699
+ let changedLines = 0;
13700
+
13701
+ for (let index = 0; index < maxLen; index++) {
13702
+ if ((oldLines[index] || '') !== (newLines[index] || '')) {
13703
+ changedLines++;
13704
+ }
13705
+ }
13706
+
13707
+ return changedLines;
13708
+ }
13709
+
13642
13710
  function buildPackageDiff(fromFiles, toFiles) {
13643
13711
  const fromMap = new Map();
13644
13712
  for (const f of (fromFiles || [])) {
@@ -13662,19 +13730,9 @@ function buildPackageDiff(fromFiles, toFiles) {
13662
13730
  if (Buffer.compare(content, toContent) === 0) {
13663
13731
  unchanged.push(filePath);
13664
13732
  } else {
13665
- let changedLines = 0;
13666
- try {
13667
- const oldLines = content.toString('utf8').split('\n');
13668
- const newLines = toContent.toString('utf8').split('\n');
13669
- const maxLen = Math.max(oldLines.length, newLines.length);
13670
- for (let i = 0; i < maxLen; i++) {
13671
- if ((oldLines[i] || '') !== (newLines[i] || '')) {
13672
- changedLines++;
13673
- }
13674
- }
13675
- } catch (_e) {
13676
- changedLines = -1;
13677
- }
13733
+ const changedLines = isBinaryContent(content) || isBinaryContent(toContent)
13734
+ ? -1
13735
+ : countChangedLines(content, toContent);
13678
13736
  modified.push({ path: filePath, changedLines });
13679
13737
  }
13680
13738
  }
@@ -13734,7 +13792,9 @@ function printSceneDiffSummary(options, payload, projectRoot = process.cwd()) {
13734
13792
  console.log(chalk.red(` - ${f}`));
13735
13793
  }
13736
13794
  for (const f of payload.files.modified) {
13737
- const detail = f.changedLines >= 0 ? ` (${f.changedLines} lines changed)` : ' (binary content differs)';
13795
+ const detail = options.stat
13796
+ ? ''
13797
+ : (f.changedLines >= 0 ? ` (${f.changedLines} lines changed)` : ' (binary content differs)');
13738
13798
  console.log(chalk.yellow(` ~ ${f.path}${detail}`));
13739
13799
  }
13740
13800
  }
@@ -15248,7 +15308,7 @@ async function runSceneLintCommand(rawOptions = {}, dependencies = {}) {
15248
15308
 
15249
15309
  function printSceneLintSummary(options, payload, projectRoot = process.cwd()) {
15250
15310
  if (options.json) {
15251
- console.log(JSON.stringify(payload, null, 2));
15311
+ console.log(stringifyJsonForCli(payload));
15252
15312
  return;
15253
15313
  }
15254
15314
 
@@ -15344,7 +15404,7 @@ async function runSceneScoreCommand(rawOptions = {}, dependencies = {}) {
15344
15404
 
15345
15405
  function printSceneScoreSummary(options, payload, projectRoot = process.cwd()) {
15346
15406
  if (options.json) {
15347
- console.log(JSON.stringify(payload, null, 2));
15407
+ console.log(stringifyJsonForCli(payload));
15348
15408
  return;
15349
15409
  }
15350
15410
 
@@ -15524,7 +15584,7 @@ async function runSceneContributeCommand(rawOptions = {}, dependencies = {}) {
15524
15584
 
15525
15585
  function printSceneContributeSummary(options, payload, projectRoot = process.cwd()) {
15526
15586
  if (options.json) {
15527
- console.log(JSON.stringify(payload, null, 2));
15587
+ console.log(stringifyJsonForCli(payload));
15528
15588
  return;
15529
15589
  }
15530
15590
 
@@ -11,6 +11,15 @@ const inquirer = require('inquirer');
11
11
  const WatchManager = require('../watch/watch-manager');
12
12
  const { listPresets, getPreset, mergePreset, validatePreset } = require('../watch/presets');
13
13
 
14
+ function sleep(ms) {
15
+ return new Promise(resolve => {
16
+ const timer = setTimeout(resolve, ms);
17
+ if (typeof timer.unref === 'function') {
18
+ timer.unref();
19
+ }
20
+ });
21
+ }
22
+
14
23
  /**
15
24
  * Start watch mode
16
25
  *
@@ -329,7 +338,7 @@ async function followLogStream(logPath, options = {}) {
329
338
  break;
330
339
  }
331
340
 
332
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
341
+ await sleep(pollIntervalMs);
333
342
  }
334
343
  } finally {
335
344
  process.removeListener('SIGINT', onSigInt);
@@ -52,6 +52,12 @@ class ConfigManager {
52
52
  'CHANGELOG.md',
53
53
  'CONTRIBUTING.md'
54
54
  ],
55
+ specAllowedRootFiles: [
56
+ 'requirements.md',
57
+ 'design.md',
58
+ 'tasks.md',
59
+ 'collaboration.json'
60
+ ],
55
61
  specSubdirs: [
56
62
  'reports',
57
63
  'scripts',
@@ -152,6 +158,10 @@ class ConfigManager {
152
158
  if (!config.specSubdirs || !Array.isArray(config.specSubdirs)) {
153
159
  errors.push('specSubdirs must be an array');
154
160
  }
161
+
162
+ if (!config.specAllowedRootFiles || !Array.isArray(config.specAllowedRootFiles)) {
163
+ errors.push('specAllowedRootFiles must be an array');
164
+ }
155
165
 
156
166
  if (!config.temporaryPatterns || !Array.isArray(config.temporaryPatterns)) {
157
167
  errors.push('temporaryPatterns must be an array');
@@ -169,6 +179,12 @@ class ConfigManager {
169
179
  errors.push('specSubdirs must contain only strings');
170
180
  }
171
181
  }
182
+
183
+ if (config.specAllowedRootFiles && Array.isArray(config.specAllowedRootFiles)) {
184
+ if (config.specAllowedRootFiles.some(f => typeof f !== 'string')) {
185
+ errors.push('specAllowedRootFiles must contain only strings');
186
+ }
187
+ }
172
188
 
173
189
  if (config.temporaryPatterns && Array.isArray(config.temporaryPatterns)) {
174
190
  if (config.temporaryPatterns.some(p => typeof p !== 'string')) {
@@ -81,6 +81,7 @@ class DiagnosticEngine {
81
81
  async scanSpecDirectory(specPath) {
82
82
  const specName = path.basename(specPath);
83
83
  const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
84
+ const allowedRootFiles = this.config.specAllowedRootFiles || requiredFiles;
84
85
 
85
86
  // Check for missing required files
86
87
  for (const requiredFile of requiredFiles) {
@@ -124,7 +125,7 @@ class DiagnosticEngine {
124
125
  const basename = path.basename(filePath);
125
126
 
126
127
  // Skip required files
127
- if (requiredFiles.includes(basename)) {
128
+ if (allowedRootFiles.includes(basename)) {
128
129
  continue;
129
130
  }
130
131
 
@@ -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,