scene-capability-engine 3.6.3 → 3.6.5

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.
@@ -0,0 +1,850 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { getSceStateStore } = require('./sce-state-store');
4
+ const { ProjectTimelineStore } = require('../runtime/project-timeline');
5
+ const { SessionStore } = require('../runtime/session-store');
6
+
7
+ const COMPONENT_AGENT_REGISTRY = 'collab.agent-registry';
8
+ const COMPONENT_TIMELINE_INDEX = 'runtime.timeline-index';
9
+ const COMPONENT_SCENE_SESSION_INDEX = 'runtime.scene-session-index';
10
+ const COMPONENT_ERRORBOOK_ENTRY_INDEX = 'errorbook.entry-index';
11
+ const COMPONENT_ERRORBOOK_INCIDENT_INDEX = 'errorbook.incident-index';
12
+ const COMPONENT_SPEC_SCENE_OVERRIDES = 'governance.spec-scene-overrides';
13
+ const COMPONENT_SPEC_SCENE_INDEX = 'governance.scene-index';
14
+ const DEFAULT_STATE_EXPORT_PATH = '.sce/reports/state-migration/state-export.latest.json';
15
+
16
+ const COMPONENT_DEFINITIONS = Object.freeze([
17
+ {
18
+ id: COMPONENT_AGENT_REGISTRY,
19
+ source_path: '.sce/config/agent-registry.json'
20
+ },
21
+ {
22
+ id: COMPONENT_TIMELINE_INDEX,
23
+ source_path: '.sce/timeline/index.json'
24
+ },
25
+ {
26
+ id: COMPONENT_SCENE_SESSION_INDEX,
27
+ source_path: '.sce/session-governance/scene-index.json'
28
+ },
29
+ {
30
+ id: COMPONENT_ERRORBOOK_ENTRY_INDEX,
31
+ source_path: '.sce/errorbook/index.json'
32
+ },
33
+ {
34
+ id: COMPONENT_ERRORBOOK_INCIDENT_INDEX,
35
+ source_path: '.sce/errorbook/staging/index.json'
36
+ },
37
+ {
38
+ id: COMPONENT_SPEC_SCENE_OVERRIDES,
39
+ source_path: '.sce/spec-governance/spec-scene-overrides.json'
40
+ },
41
+ {
42
+ id: COMPONENT_SPEC_SCENE_INDEX,
43
+ source_path: '.sce/spec-governance/scene-index.json'
44
+ }
45
+ ]);
46
+
47
+ function normalizeString(value) {
48
+ if (typeof value !== 'string') {
49
+ return '';
50
+ }
51
+ return value.trim();
52
+ }
53
+
54
+ function normalizeBoolean(value, fallback = false) {
55
+ if (typeof value === 'boolean') {
56
+ return value;
57
+ }
58
+ const normalized = normalizeString(`${value}`).toLowerCase();
59
+ if (!normalized) {
60
+ return fallback;
61
+ }
62
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) {
63
+ return true;
64
+ }
65
+ if (['0', 'false', 'no', 'off'].includes(normalized)) {
66
+ return false;
67
+ }
68
+ return fallback;
69
+ }
70
+
71
+ function normalizeInteger(value, fallback = 0) {
72
+ const parsed = Number.parseInt(`${value}`, 10);
73
+ if (!Number.isFinite(parsed) || parsed < 0) {
74
+ return fallback;
75
+ }
76
+ return parsed;
77
+ }
78
+
79
+ function nowIso() {
80
+ return new Date().toISOString();
81
+ }
82
+
83
+ function normalizeCount(value) {
84
+ if (!Number.isFinite(Number(value))) {
85
+ return 0;
86
+ }
87
+ return Math.max(0, Number.parseInt(`${value}`, 10) || 0);
88
+ }
89
+
90
+ function parseComponentIds(value) {
91
+ if (Array.isArray(value)) {
92
+ return Array.from(new Set(value.map((item) => normalizeString(item)).filter(Boolean)));
93
+ }
94
+ const normalized = normalizeString(value);
95
+ if (!normalized) {
96
+ return [];
97
+ }
98
+ return Array.from(new Set(normalized.split(/[,\s]+/g).map((item) => normalizeString(item)).filter(Boolean)));
99
+ }
100
+
101
+ function resolveDefinitionsById(componentIds = []) {
102
+ if (!Array.isArray(componentIds) || componentIds.length === 0) {
103
+ return [...COMPONENT_DEFINITIONS];
104
+ }
105
+ const selected = new Set(componentIds);
106
+ return COMPONENT_DEFINITIONS.filter((item) => selected.has(item.id));
107
+ }
108
+
109
+ function mapAgentRegistryPayload(payload = {}) {
110
+ if (!payload || typeof payload !== 'object' || !payload.agents || typeof payload.agents !== 'object') {
111
+ return [];
112
+ }
113
+ return Object.values(payload.agents)
114
+ .map((item) => ({
115
+ agent_id: normalizeString(item && item.agentId),
116
+ machine_id: normalizeString(item && item.machineId),
117
+ instance_index: normalizeInteger(item && item.instanceIndex, 0),
118
+ hostname: normalizeString(item && item.hostname),
119
+ registered_at: normalizeString(item && item.registeredAt),
120
+ last_heartbeat: normalizeString(item && item.lastHeartbeat),
121
+ status: normalizeString(item && item.status),
122
+ current_task: item && item.currentTask && typeof item.currentTask === 'object'
123
+ ? item.currentTask
124
+ : null
125
+ }))
126
+ .filter((item) => item.agent_id);
127
+ }
128
+
129
+ function mapTimelineIndexPayload(payload = {}) {
130
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.snapshots)) {
131
+ return [];
132
+ }
133
+ return payload.snapshots
134
+ .map((item) => ({
135
+ snapshot_id: normalizeString(item && item.snapshot_id),
136
+ created_at: normalizeString(item && item.created_at),
137
+ trigger: normalizeString(item && item.trigger),
138
+ event: normalizeString(item && item.event),
139
+ summary: normalizeString(item && item.summary),
140
+ scene_id: normalizeString(item && item.scene_id),
141
+ session_id: normalizeString(item && item.session_id),
142
+ command: normalizeString(item && item.command),
143
+ file_count: normalizeInteger(item && item.file_count, 0),
144
+ total_bytes: normalizeInteger(item && item.total_bytes, 0),
145
+ snapshot_path: normalizeString(item && (item.path || item.snapshot_path)),
146
+ git: item && item.git && typeof item.git === 'object' ? item.git : {}
147
+ }))
148
+ .filter((item) => item.snapshot_id);
149
+ }
150
+
151
+ function mapSceneSessionIndexPayload(payload = {}) {
152
+ if (!payload || typeof payload !== 'object' || !payload.scenes || typeof payload.scenes !== 'object') {
153
+ return [];
154
+ }
155
+
156
+ const records = [];
157
+ for (const [sceneId, sceneRecord] of Object.entries(payload.scenes)) {
158
+ const normalizedSceneId = normalizeString(sceneId);
159
+ if (!normalizedSceneId) {
160
+ continue;
161
+ }
162
+ const cycles = Array.isArray(sceneRecord && sceneRecord.cycles) ? sceneRecord.cycles : [];
163
+ for (const cycle of cycles) {
164
+ const cycleNo = normalizeInteger(cycle && cycle.cycle, 0);
165
+ const sessionId = normalizeString(cycle && cycle.session_id);
166
+ if (!cycleNo || !sessionId) {
167
+ continue;
168
+ }
169
+ records.push({
170
+ scene_id: normalizedSceneId,
171
+ cycle: cycleNo,
172
+ session_id: sessionId,
173
+ status: normalizeString(cycle && cycle.status),
174
+ started_at: normalizeString(cycle && cycle.started_at),
175
+ completed_at: normalizeString(cycle && cycle.completed_at)
176
+ });
177
+ }
178
+ }
179
+ return records;
180
+ }
181
+
182
+ function mapErrorbookEntryIndexPayload(payload = {}) {
183
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.entries)) {
184
+ return [];
185
+ }
186
+ return payload.entries
187
+ .map((item) => ({
188
+ entry_id: normalizeString(item && (item.id || item.entry_id)),
189
+ fingerprint: normalizeString(item && item.fingerprint),
190
+ title: normalizeString(item && item.title),
191
+ status: normalizeString(item && item.status),
192
+ quality_score: normalizeInteger(item && item.quality_score, 0),
193
+ tags: Array.isArray(item && item.tags)
194
+ ? item.tags.map((token) => normalizeString(token)).filter(Boolean)
195
+ : [],
196
+ ontology_tags: Array.isArray(item && item.ontology_tags)
197
+ ? item.ontology_tags.map((token) => normalizeString(token)).filter(Boolean)
198
+ : [],
199
+ temporary_mitigation_active: item && item.temporary_mitigation_active === true,
200
+ temporary_mitigation_deadline_at: normalizeString(item && item.temporary_mitigation_deadline_at),
201
+ occurrences: normalizeInteger(item && item.occurrences, 0),
202
+ created_at: normalizeString(item && item.created_at),
203
+ updated_at: normalizeString(item && item.updated_at)
204
+ }))
205
+ .filter((item) => item.entry_id);
206
+ }
207
+
208
+ function mapErrorbookIncidentIndexPayload(payload = {}) {
209
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.incidents)) {
210
+ return [];
211
+ }
212
+ return payload.incidents
213
+ .map((item) => ({
214
+ incident_id: normalizeString(item && (item.id || item.incident_id)),
215
+ fingerprint: normalizeString(item && item.fingerprint),
216
+ title: normalizeString(item && item.title),
217
+ symptom: normalizeString(item && item.symptom),
218
+ state: normalizeString(item && item.state),
219
+ attempt_count: normalizeInteger(item && item.attempt_count, 0),
220
+ created_at: normalizeString(item && item.created_at),
221
+ updated_at: normalizeString(item && item.updated_at),
222
+ last_attempt_at: normalizeString(item && item.last_attempt_at),
223
+ resolved_at: normalizeString(item && item.resolved_at),
224
+ linked_entry_id: normalizeString(item && item.linked_entry_id)
225
+ }))
226
+ .filter((item) => item.incident_id);
227
+ }
228
+
229
+ function mapSpecSceneOverridesPayload(payload = {}) {
230
+ if (!payload || typeof payload !== 'object' || !payload.mappings || typeof payload.mappings !== 'object') {
231
+ return [];
232
+ }
233
+ return Object.entries(payload.mappings)
234
+ .map(([specId, mapping]) => {
235
+ if (typeof mapping === 'string') {
236
+ return {
237
+ spec_id: normalizeString(specId),
238
+ scene_id: normalizeString(mapping),
239
+ source: 'override',
240
+ rule_id: '',
241
+ updated_at: normalizeString(payload && payload.updated_at)
242
+ };
243
+ }
244
+ return {
245
+ spec_id: normalizeString(specId),
246
+ scene_id: normalizeString(mapping && mapping.scene_id),
247
+ source: normalizeString(mapping && mapping.source) || 'override',
248
+ rule_id: normalizeString(mapping && mapping.rule_id),
249
+ updated_at: normalizeString(mapping && mapping.updated_at) || normalizeString(payload && payload.updated_at)
250
+ };
251
+ })
252
+ .filter((item) => item.spec_id && item.scene_id);
253
+ }
254
+
255
+ function mapSpecSceneIndexPayload(payload = {}) {
256
+ if (!payload || typeof payload !== 'object' || !payload.scenes || typeof payload.scenes !== 'object') {
257
+ return [];
258
+ }
259
+ const generatedAt = normalizeString(payload.generated_at) || normalizeString(payload.updated_at);
260
+ const sceneFilter = normalizeString(payload.scene_filter);
261
+ return Object.entries(payload.scenes)
262
+ .map(([sceneId, scene]) => ({
263
+ scene_id: normalizeString(sceneId),
264
+ total_specs: normalizeInteger(scene && scene.total_specs, 0),
265
+ active_specs: normalizeInteger(scene && scene.active_specs, 0),
266
+ completed_specs: normalizeInteger(scene && scene.completed_specs, 0),
267
+ stale_specs: normalizeInteger(scene && scene.stale_specs, 0),
268
+ spec_ids: Array.isArray(scene && scene.spec_ids)
269
+ ? scene.spec_ids.map((token) => normalizeString(token)).filter(Boolean)
270
+ : [],
271
+ active_spec_ids: Array.isArray(scene && scene.active_spec_ids)
272
+ ? scene.active_spec_ids.map((token) => normalizeString(token)).filter(Boolean)
273
+ : [],
274
+ stale_spec_ids: Array.isArray(scene && scene.stale_spec_ids)
275
+ ? scene.stale_spec_ids.map((token) => normalizeString(token)).filter(Boolean)
276
+ : [],
277
+ generated_at: generatedAt,
278
+ scene_filter: sceneFilter
279
+ }))
280
+ .filter((item) => item.scene_id);
281
+ }
282
+
283
+ async function readJsonSource(absolutePath, fileSystem = fs) {
284
+ if (!await fileSystem.pathExists(absolutePath)) {
285
+ return {
286
+ exists: false,
287
+ payload: null,
288
+ parse_error: null
289
+ };
290
+ }
291
+
292
+ try {
293
+ const payload = await fileSystem.readJson(absolutePath);
294
+ return {
295
+ exists: true,
296
+ payload,
297
+ parse_error: null
298
+ };
299
+ } catch (error) {
300
+ return {
301
+ exists: true,
302
+ payload: null,
303
+ parse_error: error.message
304
+ };
305
+ }
306
+ }
307
+
308
+ async function readComponentSnapshot(component = {}, projectPath = process.cwd(), fileSystem = fs) {
309
+ const absolutePath = path.join(projectPath, component.source_path);
310
+ const source = await readJsonSource(absolutePath, fileSystem);
311
+
312
+ let records = [];
313
+ if (!source.parse_error && source.payload) {
314
+ if (component.id === COMPONENT_AGENT_REGISTRY) {
315
+ records = mapAgentRegistryPayload(source.payload);
316
+ } else if (component.id === COMPONENT_TIMELINE_INDEX) {
317
+ records = mapTimelineIndexPayload(source.payload);
318
+ } else if (component.id === COMPONENT_SCENE_SESSION_INDEX) {
319
+ records = mapSceneSessionIndexPayload(source.payload);
320
+ } else if (component.id === COMPONENT_ERRORBOOK_ENTRY_INDEX) {
321
+ records = mapErrorbookEntryIndexPayload(source.payload);
322
+ } else if (component.id === COMPONENT_ERRORBOOK_INCIDENT_INDEX) {
323
+ records = mapErrorbookIncidentIndexPayload(source.payload);
324
+ } else if (component.id === COMPONENT_SPEC_SCENE_OVERRIDES) {
325
+ records = mapSpecSceneOverridesPayload(source.payload);
326
+ } else if (component.id === COMPONENT_SPEC_SCENE_INDEX) {
327
+ records = mapSpecSceneIndexPayload(source.payload);
328
+ }
329
+ }
330
+
331
+ return {
332
+ id: component.id,
333
+ source_path: component.source_path,
334
+ exists: source.exists,
335
+ parse_error: source.parse_error,
336
+ source_record_count: records.length,
337
+ records
338
+ };
339
+ }
340
+
341
+ function buildComponentPlan(componentSnapshot = {}) {
342
+ const exists = componentSnapshot.exists === true;
343
+ const parseError = normalizeString(componentSnapshot.parse_error);
344
+ const sourceCount = normalizeInteger(componentSnapshot.source_record_count, 0);
345
+ let status = 'ready';
346
+ if (!exists) {
347
+ status = 'missing';
348
+ } else if (parseError) {
349
+ status = 'parse-error';
350
+ } else if (sourceCount <= 0) {
351
+ status = 'empty';
352
+ }
353
+
354
+ return {
355
+ id: componentSnapshot.id,
356
+ source_path: componentSnapshot.source_path,
357
+ exists,
358
+ parse_error: parseError || null,
359
+ source_record_count: sourceCount,
360
+ status
361
+ };
362
+ }
363
+
364
+ async function buildStateMigrationPlan(options = {}, dependencies = {}) {
365
+ const projectPath = dependencies.projectPath || process.cwd();
366
+ const fileSystem = dependencies.fileSystem || fs;
367
+ const env = dependencies.env || process.env;
368
+ const componentIds = parseComponentIds(options.components || options.component || options.componentIds);
369
+ const definitions = resolveDefinitionsById(componentIds);
370
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
371
+
372
+ const snapshots = [];
373
+ for (const definition of definitions) {
374
+ const snapshot = await readComponentSnapshot(definition, projectPath, fileSystem);
375
+ snapshots.push(snapshot);
376
+ }
377
+
378
+ const components = snapshots.map((snapshot) => buildComponentPlan(snapshot));
379
+ const totalSourceRecords = components.reduce((sum, item) => sum + normalizeInteger(item.source_record_count, 0), 0);
380
+ const readyComponents = components.filter((item) => item.status === 'ready').length;
381
+
382
+ return {
383
+ mode: 'state-plan',
384
+ success: true,
385
+ generated_at: nowIso(),
386
+ store_path: stateStore.getStoreRelativePath ? stateStore.getStoreRelativePath() : null,
387
+ sqlite: {
388
+ configured: stateStore.isSqliteConfigured ? stateStore.isSqliteConfigured() : false,
389
+ available: stateStore.isSqliteAvailable ? stateStore.isSqliteAvailable() : false
390
+ },
391
+ components,
392
+ summary: {
393
+ total_components: components.length,
394
+ ready_components: readyComponents,
395
+ total_source_records: totalSourceRecords
396
+ },
397
+ snapshots
398
+ };
399
+ }
400
+
401
+ async function writeComponentToStateStore(componentSnapshot = {}, stateStore, componentId = '') {
402
+ if (componentId === COMPONENT_AGENT_REGISTRY) {
403
+ return stateStore.upsertAgentRuntimeRecords(componentSnapshot.records, {
404
+ source: componentId
405
+ });
406
+ }
407
+ if (componentId === COMPONENT_TIMELINE_INDEX) {
408
+ return stateStore.upsertTimelineSnapshotIndex(componentSnapshot.records, {
409
+ source: componentId
410
+ });
411
+ }
412
+ if (componentId === COMPONENT_SCENE_SESSION_INDEX) {
413
+ return stateStore.upsertSceneSessionCycles(componentSnapshot.records, {
414
+ source: componentId
415
+ });
416
+ }
417
+ if (componentId === COMPONENT_ERRORBOOK_ENTRY_INDEX) {
418
+ return stateStore.upsertErrorbookEntryIndexRecords(componentSnapshot.records, {
419
+ source: componentId
420
+ });
421
+ }
422
+ if (componentId === COMPONENT_ERRORBOOK_INCIDENT_INDEX) {
423
+ return stateStore.upsertErrorbookIncidentIndexRecords(componentSnapshot.records, {
424
+ source: componentId
425
+ });
426
+ }
427
+ if (componentId === COMPONENT_SPEC_SCENE_OVERRIDES) {
428
+ return stateStore.upsertGovernanceSpecSceneOverrideRecords(componentSnapshot.records, {
429
+ source: componentId
430
+ });
431
+ }
432
+ if (componentId === COMPONENT_SPEC_SCENE_INDEX) {
433
+ return stateStore.upsertGovernanceSceneIndexRecords(componentSnapshot.records, {
434
+ source: componentId
435
+ });
436
+ }
437
+ return null;
438
+ }
439
+
440
+ async function runStateMigration(options = {}, dependencies = {}) {
441
+ const projectPath = dependencies.projectPath || process.cwd();
442
+ const fileSystem = dependencies.fileSystem || fs;
443
+ const env = dependencies.env || process.env;
444
+ const apply = normalizeBoolean(options.apply, false);
445
+ const requestedIds = parseComponentIds(options.components || options.component || options.componentIds);
446
+ const migrateAll = options.all === true || requestedIds.length === 0;
447
+ const selectedIds = migrateAll
448
+ ? COMPONENT_DEFINITIONS.map((item) => item.id)
449
+ : requestedIds;
450
+
451
+ const plan = await buildStateMigrationPlan({
452
+ componentIds: selectedIds
453
+ }, {
454
+ projectPath,
455
+ fileSystem,
456
+ env
457
+ });
458
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
459
+
460
+ const operations = [];
461
+ let migratedComponents = 0;
462
+ let migratedRecords = 0;
463
+
464
+ for (const snapshot of plan.snapshots) {
465
+ const componentPlan = buildComponentPlan(snapshot);
466
+ const op = {
467
+ component_id: snapshot.id,
468
+ source_path: snapshot.source_path,
469
+ source_record_count: snapshot.source_record_count,
470
+ status: componentPlan.status,
471
+ applied: false,
472
+ result: null
473
+ };
474
+
475
+ if (componentPlan.status !== 'ready') {
476
+ operations.push(op);
477
+ continue;
478
+ }
479
+
480
+ if (!apply) {
481
+ op.status = 'planned';
482
+ operations.push(op);
483
+ continue;
484
+ }
485
+
486
+ const startedAt = nowIso();
487
+ const writeResult = await writeComponentToStateStore(snapshot, stateStore, snapshot.id);
488
+ if (!writeResult) {
489
+ op.status = 'failed';
490
+ op.error = 'SQLite state backend unavailable for migration write';
491
+ operations.push(op);
492
+ await stateStore.appendStateMigrationRecord({
493
+ component_id: snapshot.id,
494
+ source_path: snapshot.source_path,
495
+ mode: 'apply',
496
+ status: 'failed',
497
+ metrics: {
498
+ source_record_count: snapshot.source_record_count
499
+ },
500
+ detail: {
501
+ error: op.error
502
+ },
503
+ started_at: startedAt,
504
+ completed_at: nowIso()
505
+ });
506
+ continue;
507
+ }
508
+
509
+ op.applied = true;
510
+ op.status = 'migrated';
511
+ op.result = writeResult;
512
+ operations.push(op);
513
+ migratedComponents += 1;
514
+ migratedRecords += normalizeInteger(writeResult.written, 0);
515
+
516
+ await stateStore.appendStateMigrationRecord({
517
+ component_id: snapshot.id,
518
+ source_path: snapshot.source_path,
519
+ mode: 'apply',
520
+ status: 'completed',
521
+ metrics: {
522
+ source_record_count: snapshot.source_record_count,
523
+ written: normalizeInteger(writeResult.written, 0),
524
+ target_total: normalizeInteger(writeResult.total, 0)
525
+ },
526
+ detail: {
527
+ component_id: snapshot.id
528
+ },
529
+ started_at: startedAt,
530
+ completed_at: nowIso()
531
+ });
532
+ }
533
+
534
+ return {
535
+ mode: 'state-migrate',
536
+ success: operations.every((item) => item.status !== 'failed'),
537
+ apply,
538
+ generated_at: nowIso(),
539
+ store_path: stateStore.getStoreRelativePath ? stateStore.getStoreRelativePath() : null,
540
+ operations,
541
+ summary: {
542
+ selected_components: selectedIds.length,
543
+ migrated_components: migratedComponents,
544
+ migrated_records: migratedRecords
545
+ }
546
+ };
547
+ }
548
+
549
+ async function runStateDoctor(options = {}, dependencies = {}) {
550
+ const projectPath = dependencies.projectPath || process.cwd();
551
+ const fileSystem = dependencies.fileSystem || fs;
552
+ const env = dependencies.env || process.env;
553
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
554
+ const plan = await buildStateMigrationPlan({}, { projectPath, fileSystem, env, stateStore });
555
+
556
+ const [
557
+ agentRows,
558
+ timelineRows,
559
+ sessionRows,
560
+ errorbookEntryRows,
561
+ errorbookIncidentRows,
562
+ governanceOverrideRows,
563
+ governanceSceneRows,
564
+ migrations
565
+ ] = await Promise.all([
566
+ stateStore.listAgentRuntimeRecords({ limit: 0 }),
567
+ stateStore.listTimelineSnapshotIndex({ limit: 0 }),
568
+ stateStore.listSceneSessionCycles({ limit: 0 }),
569
+ stateStore.listErrorbookEntryIndexRecords({ limit: 0 }),
570
+ stateStore.listErrorbookIncidentIndexRecords({ limit: 0 }),
571
+ stateStore.listGovernanceSpecSceneOverrideRecords({ limit: 0 }),
572
+ stateStore.listGovernanceSceneIndexRecords({ limit: 0 }),
573
+ stateStore.listStateMigrations({ limit: 20 })
574
+ ]);
575
+
576
+ const targetCounts = new Map([
577
+ [COMPONENT_AGENT_REGISTRY, Array.isArray(agentRows) ? agentRows.length : 0],
578
+ [COMPONENT_TIMELINE_INDEX, Array.isArray(timelineRows) ? timelineRows.length : 0],
579
+ [COMPONENT_SCENE_SESSION_INDEX, Array.isArray(sessionRows) ? sessionRows.length : 0],
580
+ [COMPONENT_ERRORBOOK_ENTRY_INDEX, Array.isArray(errorbookEntryRows) ? errorbookEntryRows.length : 0],
581
+ [COMPONENT_ERRORBOOK_INCIDENT_INDEX, Array.isArray(errorbookIncidentRows) ? errorbookIncidentRows.length : 0],
582
+ [COMPONENT_SPEC_SCENE_OVERRIDES, Array.isArray(governanceOverrideRows) ? governanceOverrideRows.length : 0],
583
+ [COMPONENT_SPEC_SCENE_INDEX, Array.isArray(governanceSceneRows) ? governanceSceneRows.length : 0]
584
+ ]);
585
+
586
+ const checks = plan.components.map((component) => {
587
+ const sourceCount = normalizeInteger(component.source_record_count, 0);
588
+ const targetCount = normalizeInteger(targetCounts.get(component.id), 0);
589
+ let syncStatus = 'synced';
590
+ if (component.status === 'missing') {
591
+ syncStatus = targetCount > 0 ? 'sqlite-only' : 'missing-source';
592
+ } else if (component.status === 'parse-error') {
593
+ syncStatus = 'source-parse-error';
594
+ } else if (sourceCount === 0 && targetCount === 0) {
595
+ syncStatus = 'empty';
596
+ } else if (targetCount < sourceCount) {
597
+ syncStatus = 'pending-migration';
598
+ }
599
+ return {
600
+ id: component.id,
601
+ source_path: component.source_path,
602
+ source_record_count: sourceCount,
603
+ sqlite_record_count: targetCount,
604
+ source_status: component.status,
605
+ sync_status: syncStatus
606
+ };
607
+ });
608
+
609
+ const runtime = await collectRuntimeDiagnostics({
610
+ projectPath,
611
+ fileSystem,
612
+ env,
613
+ stateStore
614
+ });
615
+
616
+ const blocking = [];
617
+ if (!plan.sqlite.available) {
618
+ blocking.push('sqlite-unavailable');
619
+ }
620
+ if (checks.some((item) => item.sync_status === 'source-parse-error')) {
621
+ blocking.push('source-parse-error');
622
+ }
623
+
624
+ const alerts = checks
625
+ .filter((item) => item.sync_status === 'pending-migration')
626
+ .map((item) => `pending migration: ${item.id}`);
627
+
628
+ if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'pending-sync') {
629
+ alerts.push('runtime timeline index pending-sync');
630
+ }
631
+ if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'sqlite-ahead') {
632
+ alerts.push('runtime timeline index sqlite-ahead');
633
+ }
634
+ if (runtime.scene_session && runtime.scene_session.consistency && runtime.scene_session.consistency.status === 'pending-sync') {
635
+ alerts.push('runtime scene-session index pending-sync');
636
+ }
637
+ if (runtime.scene_session && runtime.scene_session.consistency && runtime.scene_session.consistency.status === 'sqlite-ahead') {
638
+ alerts.push('runtime scene-session index sqlite-ahead');
639
+ }
640
+
641
+ const summary = summarizeDoctorChecks(checks, alerts, blocking);
642
+
643
+ return {
644
+ mode: 'state-doctor',
645
+ success: blocking.length === 0,
646
+ generated_at: nowIso(),
647
+ store_path: plan.store_path,
648
+ sqlite: plan.sqlite,
649
+ summary,
650
+ runtime,
651
+ checks,
652
+ migrations: Array.isArray(migrations) ? migrations : [],
653
+ blocking,
654
+ alerts
655
+ };
656
+ }
657
+
658
+ function summarizeDoctorChecks(checks = [], alerts = [], blocking = []) {
659
+ const normalizedChecks = Array.isArray(checks) ? checks : [];
660
+ const sourceRecords = normalizedChecks.reduce((sum, item) => sum + normalizeCount(item.source_record_count), 0);
661
+ const sqliteRecords = normalizedChecks.reduce((sum, item) => sum + normalizeCount(item.sqlite_record_count), 0);
662
+ const pendingComponents = normalizedChecks.filter((item) => item.sync_status === 'pending-migration').length;
663
+ const syncedComponents = normalizedChecks.filter((item) => item.sync_status === 'synced').length;
664
+ const sqliteOnlyComponents = normalizedChecks.filter((item) => item.sync_status === 'sqlite-only').length;
665
+ const missingSourceComponents = normalizedChecks.filter((item) => item.sync_status === 'missing-source').length;
666
+ const driftRecords = normalizedChecks.reduce((sum, item) => {
667
+ const source = normalizeCount(item.source_record_count);
668
+ const target = normalizeCount(item.sqlite_record_count);
669
+ return sum + Math.abs(source - target);
670
+ }, 0);
671
+
672
+ return {
673
+ total_components: normalizedChecks.length,
674
+ synced_components: syncedComponents,
675
+ pending_components: pendingComponents,
676
+ sqlite_only_components: sqliteOnlyComponents,
677
+ missing_source_components: missingSourceComponents,
678
+ total_source_records: sourceRecords,
679
+ total_sqlite_records: sqliteRecords,
680
+ total_record_drift: driftRecords,
681
+ blocking_count: Array.isArray(blocking) ? blocking.length : 0,
682
+ alert_count: Array.isArray(alerts) ? alerts.length : 0
683
+ };
684
+ }
685
+
686
+ async function collectRuntimeDiagnostics(dependencies = {}) {
687
+ const projectPath = dependencies.projectPath || process.cwd();
688
+ const fileSystem = dependencies.fileSystem || fs;
689
+ const env = dependencies.env || process.env;
690
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, {
691
+ fileSystem,
692
+ env
693
+ });
694
+
695
+ const runtime = {
696
+ timeline: {
697
+ read_source: 'unavailable',
698
+ consistency: {
699
+ status: 'unavailable',
700
+ file_index_count: 0,
701
+ sqlite_index_count: null
702
+ }
703
+ },
704
+ scene_session: {
705
+ read_preference: 'file',
706
+ consistency: {
707
+ status: 'unavailable',
708
+ file_index_count: 0,
709
+ sqlite_index_count: null
710
+ }
711
+ }
712
+ };
713
+
714
+ try {
715
+ const timelineStore = new ProjectTimelineStore(projectPath, fileSystem, {
716
+ env,
717
+ stateStore,
718
+ preferSqliteReads: true
719
+ });
720
+ const timelineView = await timelineStore.listSnapshots({ limit: 1 });
721
+ runtime.timeline = {
722
+ read_source: normalizeString(timelineView.read_source) || 'file',
723
+ consistency: {
724
+ status: normalizeString(timelineView && timelineView.consistency && timelineView.consistency.status) || 'unknown',
725
+ file_index_count: normalizeCount(timelineView && timelineView.consistency && timelineView.consistency.file_index_count),
726
+ sqlite_index_count: Number.isFinite(Number(timelineView && timelineView.consistency && timelineView.consistency.sqlite_index_count))
727
+ ? Number.parseInt(`${timelineView.consistency.sqlite_index_count}`, 10)
728
+ : null
729
+ }
730
+ };
731
+ } catch (_error) {
732
+ runtime.timeline = {
733
+ read_source: 'unavailable',
734
+ consistency: {
735
+ status: 'unavailable',
736
+ file_index_count: 0,
737
+ sqlite_index_count: null
738
+ }
739
+ };
740
+ }
741
+
742
+ try {
743
+ const sessionStore = new SessionStore(projectPath, fileSystem, {
744
+ env,
745
+ stateStore,
746
+ preferSqliteSceneReads: true
747
+ });
748
+ const sceneIndex = await sessionStore.getSceneIndexDiagnostics();
749
+ runtime.scene_session = {
750
+ read_preference: normalizeString(sceneIndex.read_preference) || 'file',
751
+ consistency: {
752
+ status: normalizeString(sceneIndex.status) || 'unknown',
753
+ file_index_count: normalizeCount(sceneIndex.file_scene_count),
754
+ sqlite_index_count: Number.isFinite(Number(sceneIndex.sqlite_scene_count))
755
+ ? Number.parseInt(`${sceneIndex.sqlite_scene_count}`, 10)
756
+ : null
757
+ }
758
+ };
759
+ } catch (_error) {
760
+ runtime.scene_session = {
761
+ read_preference: 'file',
762
+ consistency: {
763
+ status: 'unavailable',
764
+ file_index_count: 0,
765
+ sqlite_index_count: null
766
+ }
767
+ };
768
+ }
769
+
770
+ return runtime;
771
+ }
772
+
773
+ async function runStateExport(options = {}, dependencies = {}) {
774
+ const projectPath = dependencies.projectPath || process.cwd();
775
+ const fileSystem = dependencies.fileSystem || fs;
776
+ const env = dependencies.env || process.env;
777
+ const outPathRaw = normalizeString(options.out) || DEFAULT_STATE_EXPORT_PATH;
778
+ const outPath = path.isAbsolute(outPathRaw)
779
+ ? outPathRaw
780
+ : path.join(projectPath, outPathRaw);
781
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
782
+
783
+ const [
784
+ agentRows,
785
+ timelineRows,
786
+ sessionRows,
787
+ errorbookEntryRows,
788
+ errorbookIncidentRows,
789
+ governanceOverrideRows,
790
+ governanceSceneRows,
791
+ migrations
792
+ ] = await Promise.all([
793
+ stateStore.listAgentRuntimeRecords({ limit: 0 }),
794
+ stateStore.listTimelineSnapshotIndex({ limit: 0 }),
795
+ stateStore.listSceneSessionCycles({ limit: 0 }),
796
+ stateStore.listErrorbookEntryIndexRecords({ limit: 0 }),
797
+ stateStore.listErrorbookIncidentIndexRecords({ limit: 0 }),
798
+ stateStore.listGovernanceSpecSceneOverrideRecords({ limit: 0 }),
799
+ stateStore.listGovernanceSceneIndexRecords({ limit: 0 }),
800
+ stateStore.listStateMigrations({ limit: 0 })
801
+ ]);
802
+
803
+ const payload = {
804
+ mode: 'state-export',
805
+ success: true,
806
+ exported_at: nowIso(),
807
+ store_path: stateStore.getStoreRelativePath ? stateStore.getStoreRelativePath() : null,
808
+ tables: {
809
+ agent_runtime_registry: Array.isArray(agentRows) ? agentRows : [],
810
+ timeline_snapshot_registry: Array.isArray(timelineRows) ? timelineRows : [],
811
+ scene_session_cycle_registry: Array.isArray(sessionRows) ? sessionRows : [],
812
+ errorbook_entry_index_registry: Array.isArray(errorbookEntryRows) ? errorbookEntryRows : [],
813
+ errorbook_incident_index_registry: Array.isArray(errorbookIncidentRows) ? errorbookIncidentRows : [],
814
+ governance_spec_scene_override_registry: Array.isArray(governanceOverrideRows) ? governanceOverrideRows : [],
815
+ governance_scene_index_registry: Array.isArray(governanceSceneRows) ? governanceSceneRows : [],
816
+ state_migration_registry: Array.isArray(migrations) ? migrations : []
817
+ },
818
+ summary: {
819
+ agent_runtime_registry: Array.isArray(agentRows) ? agentRows.length : 0,
820
+ timeline_snapshot_registry: Array.isArray(timelineRows) ? timelineRows.length : 0,
821
+ scene_session_cycle_registry: Array.isArray(sessionRows) ? sessionRows.length : 0,
822
+ errorbook_entry_index_registry: Array.isArray(errorbookEntryRows) ? errorbookEntryRows.length : 0,
823
+ errorbook_incident_index_registry: Array.isArray(errorbookIncidentRows) ? errorbookIncidentRows.length : 0,
824
+ governance_spec_scene_override_registry: Array.isArray(governanceOverrideRows) ? governanceOverrideRows.length : 0,
825
+ governance_scene_index_registry: Array.isArray(governanceSceneRows) ? governanceSceneRows.length : 0,
826
+ state_migration_registry: Array.isArray(migrations) ? migrations.length : 0
827
+ },
828
+ out_file: path.relative(projectPath, outPath).replace(/\\/g, '/')
829
+ };
830
+
831
+ await fileSystem.ensureDir(path.dirname(outPath));
832
+ await fileSystem.writeJson(outPath, payload, { spaces: 2 });
833
+ return payload;
834
+ }
835
+
836
+ module.exports = {
837
+ COMPONENT_AGENT_REGISTRY,
838
+ COMPONENT_TIMELINE_INDEX,
839
+ COMPONENT_SCENE_SESSION_INDEX,
840
+ COMPONENT_ERRORBOOK_ENTRY_INDEX,
841
+ COMPONENT_ERRORBOOK_INCIDENT_INDEX,
842
+ COMPONENT_SPEC_SCENE_OVERRIDES,
843
+ COMPONENT_SPEC_SCENE_INDEX,
844
+ COMPONENT_DEFINITIONS,
845
+ DEFAULT_STATE_EXPORT_PATH,
846
+ buildStateMigrationPlan,
847
+ runStateMigration,
848
+ runStateDoctor,
849
+ runStateExport
850
+ };