scene-capability-engine 3.6.3 → 3.6.4
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.
- package/CHANGELOG.md +33 -0
- package/README.md +8 -0
- package/README.zh.md +8 -0
- package/bin/scene-capability-engine.js +31 -1
- package/docs/command-reference.md +48 -0
- package/lib/collab/agent-registry.js +38 -1
- package/lib/commands/session.js +60 -2
- package/lib/commands/state.js +210 -0
- package/lib/runtime/project-timeline.js +202 -17
- package/lib/runtime/session-store.js +167 -14
- package/lib/state/sce-state-store.js +629 -0
- package/lib/state/state-migration-manager.js +659 -0
- package/lib/steering/compliance-error-reporter.js +6 -0
- package/lib/steering/steering-compliance-checker.js +43 -8
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.6.4] - 2026-03-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Gradual state migration command group:
|
|
14
|
+
- `sce state plan`
|
|
15
|
+
- `sce state doctor`
|
|
16
|
+
- `sce state migrate` (dry-run default, `--apply` to write)
|
|
17
|
+
- `sce state export`
|
|
18
|
+
- New migration manager module: `lib/state/state-migration-manager.js`.
|
|
19
|
+
- SQLite index tables for staged migration of file-based registries:
|
|
20
|
+
- `agent_runtime_registry`
|
|
21
|
+
- `timeline_snapshot_registry`
|
|
22
|
+
- `scene_session_cycle_registry`
|
|
23
|
+
- `state_migration_registry`
|
|
24
|
+
- Unit tests for state migration planning/execution/export:
|
|
25
|
+
- `tests/unit/state/state-migration-manager.test.js`
|
|
26
|
+
- `tests/unit/commands/state.test.js`
|
|
27
|
+
- New reconciliation gate script for migration parity checks:
|
|
28
|
+
- `scripts/state-migration-reconciliation-gate.js`
|
|
29
|
+
- npm script: `gate:state-migration-reconciliation`
|
|
30
|
+
- Session runtime diagnostics expansion:
|
|
31
|
+
- new command: `sce session list`
|
|
32
|
+
- `sce session show/list` JSON includes `session_source` and `scene_index` consistency metadata
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Timeline runtime reads now prefer SQLite index entries (`timeline_snapshot_registry`) when available.
|
|
36
|
+
- Scene session listing views now prefer SQLite cycle index entries (`scene_session_cycle_registry`) when available.
|
|
37
|
+
- Agent registry writes now mirror to SQLite agent runtime index (`agent_runtime_registry`) as a best-effort sync.
|
|
38
|
+
- `sce state doctor` now emits aggregate `summary` metrics and runtime read diagnostics (`runtime.timeline`, `runtime.scene_session`) to expose sqlite/file drift status for IDE and CI gates.
|
|
39
|
+
- Startup compliance behavior hardened to avoid runtime side effects on read-only commands:
|
|
40
|
+
- `sce --version/-v`, `sce --help/-h`, `sce help`, and `sce version-info` now skip steering cleanup checks.
|
|
41
|
+
- steering compliance allowlist now includes `.sce/steering/manifest.yaml`, `compiled/`, and runtime lock/pending files.
|
|
42
|
+
|
|
10
43
|
## [3.6.3] - 2026-03-05
|
|
11
44
|
|
|
12
45
|
### Added
|
package/README.md
CHANGED
|
@@ -145,6 +145,14 @@ Studio task-stream output contract (default):
|
|
|
145
145
|
- SQLite-only backend (`.sce/state/sce-state.sqlite`)
|
|
146
146
|
- In-memory fallback only in `NODE_ENV=test` or when `SCE_STATE_ALLOW_MEMORY_FALLBACK=1`
|
|
147
147
|
- Outside those conditions, unavailable SQLite support fails fast for task-ref/event persistence
|
|
148
|
+
- Gradual file-to-sqlite migration tooling:
|
|
149
|
+
- `sce state plan --json`
|
|
150
|
+
- `sce state doctor --json`
|
|
151
|
+
- `sce state migrate --all --apply --json`
|
|
152
|
+
- `sce state export --out .sce/reports/state-migration/state-export.latest.json --json`
|
|
153
|
+
- reconciliation gate: `npm run gate:state-migration-reconciliation`
|
|
154
|
+
- runtime reads now prefer sqlite indexes for timeline/scene-session views when indexed data exists
|
|
155
|
+
- `state doctor` now emits `summary` and runtime diagnostics (`runtime.timeline`, `runtime.scene_session`) with read-source and consistency status
|
|
148
156
|
- Write authorization lease model (SQLite-backed):
|
|
149
157
|
- policy file: `.sce/config/authorization-policy.json`
|
|
150
158
|
- grant lease: `sce auth grant --scope studio:* --reason "<reason>" --auth-password <password> --json`
|
package/README.zh.md
CHANGED
|
@@ -145,6 +145,14 @@ Studio 任务流输出契约(默认):
|
|
|
145
145
|
- 仅支持 SQLite 后端(`.sce/state/sce-state.sqlite`)
|
|
146
146
|
- 仅在 `NODE_ENV=test` 或 `SCE_STATE_ALLOW_MEMORY_FALLBACK=1` 时允许内存回退
|
|
147
147
|
- 在上述条件之外若 SQLite 不可用,任务引用/事件持久化会快速失败
|
|
148
|
+
- 渐进式文件到 SQLite 迁移工具:
|
|
149
|
+
- `sce state plan --json`
|
|
150
|
+
- `sce state doctor --json`
|
|
151
|
+
- `sce state migrate --all --apply --json`
|
|
152
|
+
- `sce state export --out .sce/reports/state-migration/state-export.latest.json --json`
|
|
153
|
+
- 对账门禁:`npm run gate:state-migration-reconciliation`
|
|
154
|
+
- 运行时读取在存在索引数据时优先使用 SQLite(timeline/scene-session 视图)
|
|
155
|
+
- `state doctor` 新增 `summary` 与运行时诊断(`runtime.timeline`、`runtime.scene_session`),可直接读取读源与一致性状态
|
|
148
156
|
- 写入授权租约模型(SQLite 持久化):
|
|
149
157
|
- 策略文件:`.sce/config/authorization-policy.json`
|
|
150
158
|
- 申请租约:`sce auth grant --scope studio:* --reason "<原因>" --auth-password <密码> --json`
|
|
@@ -25,6 +25,7 @@ const { registerTimelineCommands } = require('../lib/commands/timeline');
|
|
|
25
25
|
const { registerValueCommands } = require('../lib/commands/value');
|
|
26
26
|
const { registerTaskCommands } = require('../lib/commands/task');
|
|
27
27
|
const { registerAuthCommands } = require('../lib/commands/auth');
|
|
28
|
+
const { registerStateCommands } = require('../lib/commands/state');
|
|
28
29
|
const VersionChecker = require('../lib/version/version-checker');
|
|
29
30
|
const {
|
|
30
31
|
findLegacyKiroDirectories,
|
|
@@ -161,6 +162,9 @@ function isLegacyMigrationAllowlistedCommand(args) {
|
|
|
161
162
|
if (command === 'help') {
|
|
162
163
|
return true;
|
|
163
164
|
}
|
|
165
|
+
if (command === 'version-info') {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
164
168
|
|
|
165
169
|
if (command === 'workspace') {
|
|
166
170
|
const subcommand = args[commandIndex + 1];
|
|
@@ -195,6 +199,30 @@ function isTakeoverAutoApplySkippedCommand(args) {
|
|
|
195
199
|
return subcommand === 'takeover-audit';
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Commands/options that must be read-only and have no runtime side effects.
|
|
204
|
+
*
|
|
205
|
+
* @param {string[]} args
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
function isNoMutationCommand(args) {
|
|
209
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (args.includes('-v') || args.includes('--version') || args.includes('-h') || args.includes('--help')) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const commandIndex = findCommandIndex(args);
|
|
218
|
+
if (commandIndex < 0) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const command = args[commandIndex];
|
|
223
|
+
return command === 'help' || command === 'version-info';
|
|
224
|
+
}
|
|
225
|
+
|
|
198
226
|
// 版本和基本信息
|
|
199
227
|
program
|
|
200
228
|
.name(t('cli.name'))
|
|
@@ -947,6 +975,7 @@ registerOrchestrateCommands(program);
|
|
|
947
975
|
registerValueCommands(program);
|
|
948
976
|
registerTaskCommands(program);
|
|
949
977
|
registerAuthCommands(program);
|
|
978
|
+
registerStateCommands(program);
|
|
950
979
|
|
|
951
980
|
// Template management commands
|
|
952
981
|
const templatesCommand = require('../lib/commands/templates');
|
|
@@ -1074,7 +1103,8 @@ async function updateProjectConfig(projectName) {
|
|
|
1074
1103
|
const args = process.argv.slice(2);
|
|
1075
1104
|
const isLegacyAllowlistedCommand = isLegacyMigrationAllowlistedCommand(args);
|
|
1076
1105
|
const skipAutoTakeover = isTakeoverAutoApplySkippedCommand(args);
|
|
1077
|
-
const skipCheck = args
|
|
1106
|
+
const skipCheck = isNoMutationCommand(args) ||
|
|
1107
|
+
args.includes('--skip-steering-check') ||
|
|
1078
1108
|
process.env.KSE_SKIP_STEERING_CHECK === '1';
|
|
1079
1109
|
const forceCheck = args.includes('--force-steering-check');
|
|
1080
1110
|
|
|
@@ -232,12 +232,16 @@ sce session start "ship order workflow hardening" --tool codex --agent-version 1
|
|
|
232
232
|
sce session resume release-20260224 --status active
|
|
233
233
|
sce session snapshot release-20260224 --summary "post-gate checkpoint" --payload '{"tests_passed":42}' --json
|
|
234
234
|
sce session show release-20260224 --json
|
|
235
|
+
sce session list --limit 20 --json
|
|
235
236
|
```
|
|
236
237
|
|
|
237
238
|
Session governance defaults:
|
|
238
239
|
- `1 scene = 1 primary session` (managed by `studio plan --scene ...`)
|
|
239
240
|
- `spec` runs can bind as child sessions (`spec bootstrap|pipeline --scene <scene-id>`)
|
|
240
241
|
- successful `studio release` auto-archives current scene session and opens next cycle session
|
|
242
|
+
- `session show/list` JSON now includes runtime diagnostics:
|
|
243
|
+
- `session_source`
|
|
244
|
+
- `scene_index.status` (`aligned`, `pending-sync`, `sqlite-ahead`, etc.)
|
|
241
245
|
|
|
242
246
|
### Watch Mode
|
|
243
247
|
|
|
@@ -700,6 +704,50 @@ Default policy file (recommended to commit): `.sce/config/studio-security.json`
|
|
|
700
704
|
}
|
|
701
705
|
```
|
|
702
706
|
|
|
707
|
+
State migration commands (gradual file -> sqlite indexing):
|
|
708
|
+
|
|
709
|
+
```bash
|
|
710
|
+
# inspect migratable file-based registries
|
|
711
|
+
sce state plan --json
|
|
712
|
+
|
|
713
|
+
# diagnose sqlite readiness and file/sqlite index sync
|
|
714
|
+
sce state doctor --json
|
|
715
|
+
|
|
716
|
+
# dry-run migration (default no write)
|
|
717
|
+
sce state migrate --all --json
|
|
718
|
+
|
|
719
|
+
# apply migration writes into sqlite index tables
|
|
720
|
+
sce state migrate --all --apply --json
|
|
721
|
+
|
|
722
|
+
# migrate specific components
|
|
723
|
+
sce state migrate --component collab.agent-registry --component runtime.timeline-index --apply --json
|
|
724
|
+
|
|
725
|
+
# export sqlite migration tables for audit/debug
|
|
726
|
+
sce state export --out .sce/reports/state-migration/state-export.latest.json --json
|
|
727
|
+
|
|
728
|
+
# reconciliation gate (non-blocking by default; choose strict flags as needed)
|
|
729
|
+
npm run gate:state-migration-reconciliation
|
|
730
|
+
node scripts/state-migration-reconciliation-gate.js --fail-on-alert --fail-on-pending --json
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Current migratable components:
|
|
734
|
+
- `collab.agent-registry` (`.sce/config/agent-registry.json`)
|
|
735
|
+
- `runtime.timeline-index` (`.sce/timeline/index.json`)
|
|
736
|
+
- `runtime.scene-session-index` (`.sce/session-governance/scene-index.json`)
|
|
737
|
+
|
|
738
|
+
SQLite index tables introduced for gradual migration:
|
|
739
|
+
- `agent_runtime_registry`
|
|
740
|
+
- `timeline_snapshot_registry`
|
|
741
|
+
- `scene_session_cycle_registry`
|
|
742
|
+
- `state_migration_registry`
|
|
743
|
+
|
|
744
|
+
Runtime read preference:
|
|
745
|
+
- timeline/session runtime views now prefer SQLite indexes when indexed rows exist.
|
|
746
|
+
- file artifacts remain source-of-truth for content payload and recovery operations.
|
|
747
|
+
- `sce state doctor --json` now includes:
|
|
748
|
+
- `summary` aggregate (`pending_components`, `total_record_drift`, `blocking_count`, `alert_count`)
|
|
749
|
+
- runtime read diagnostics (`runtime.timeline`, `runtime.scene_session`) with read-source/read-preference and consistency status
|
|
750
|
+
|
|
703
751
|
Write lease model (optional, policy-driven, SQLite-backed):
|
|
704
752
|
- Policy file: `.sce/config/authorization-policy.json`
|
|
705
753
|
- Lease/event persistence: `.sce/state/sce-state.sqlite` (`auth_lease_registry`, `auth_event_stream`)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fsUtils = require('../utils/fs-utils');
|
|
13
13
|
const { MultiAgentConfig } = require('./multi-agent-config');
|
|
14
|
+
const { getSceStateStore } = require('../state/sce-state-store');
|
|
14
15
|
|
|
15
16
|
const REGISTRY_FILENAME = 'agent-registry.json';
|
|
16
17
|
const CONFIG_DIR = '.sce/config';
|
|
@@ -26,13 +27,16 @@ class AgentRegistry {
|
|
|
26
27
|
* @param {import('../lock/machine-identifier').MachineIdentifier} machineIdentifier
|
|
27
28
|
* @param {import('../lock/task-lock-manager').TaskLockManager|null} [taskLockManager=null] - Optional, injected to avoid circular deps
|
|
28
29
|
*/
|
|
29
|
-
constructor(workspaceRoot, machineIdentifier, taskLockManager = null) {
|
|
30
|
+
constructor(workspaceRoot, machineIdentifier, taskLockManager = null, options = {}) {
|
|
30
31
|
this._workspaceRoot = workspaceRoot;
|
|
31
32
|
this._machineIdentifier = machineIdentifier;
|
|
32
33
|
this._taskLockManager = taskLockManager;
|
|
33
34
|
this._registryPath = path.join(workspaceRoot, CONFIG_DIR, REGISTRY_FILENAME);
|
|
34
35
|
this._configDir = path.join(workspaceRoot, CONFIG_DIR);
|
|
35
36
|
this._multiAgentConfig = new MultiAgentConfig(workspaceRoot);
|
|
37
|
+
this._stateStore = options.stateStore || getSceStateStore(workspaceRoot, {
|
|
38
|
+
env: options.env || process.env
|
|
39
|
+
});
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
@@ -67,6 +71,39 @@ class AgentRegistry {
|
|
|
67
71
|
async _writeRegistry(registry) {
|
|
68
72
|
await fsUtils.ensureDirectory(this._configDir);
|
|
69
73
|
await fsUtils.writeJSON(this._registryPath, registry);
|
|
74
|
+
await this._syncRegistryToStateStore(registry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_mapRegistryRows(registry = {}) {
|
|
78
|
+
if (!registry || typeof registry !== 'object' || !registry.agents || typeof registry.agents !== 'object') {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return Object.values(registry.agents)
|
|
82
|
+
.map((agent) => ({
|
|
83
|
+
agent_id: `${agent && agent.agentId ? agent.agentId : ''}`.trim(),
|
|
84
|
+
machine_id: `${agent && agent.machineId ? agent.machineId : ''}`.trim(),
|
|
85
|
+
instance_index: Number.parseInt(`${agent && agent.instanceIndex}`, 10) || 0,
|
|
86
|
+
hostname: `${agent && agent.hostname ? agent.hostname : ''}`.trim() || null,
|
|
87
|
+
registered_at: `${agent && agent.registeredAt ? agent.registeredAt : ''}`.trim() || null,
|
|
88
|
+
last_heartbeat: `${agent && agent.lastHeartbeat ? agent.lastHeartbeat : ''}`.trim() || null,
|
|
89
|
+
status: `${agent && agent.status ? agent.status : ''}`.trim() || null,
|
|
90
|
+
current_task: agent && typeof agent.currentTask === 'object' ? agent.currentTask : null
|
|
91
|
+
}))
|
|
92
|
+
.filter((row) => row.agent_id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _syncRegistryToStateStore(registry = {}) {
|
|
96
|
+
if (!this._stateStore) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const rows = this._mapRegistryRows(registry);
|
|
101
|
+
await this._stateStore.upsertAgentRuntimeRecords(rows, {
|
|
102
|
+
source: 'collab.agent-registry'
|
|
103
|
+
});
|
|
104
|
+
} catch (_error) {
|
|
105
|
+
// best effort only, file registry remains source
|
|
106
|
+
}
|
|
70
107
|
}
|
|
71
108
|
|
|
72
109
|
/**
|
package/lib/commands/session.js
CHANGED
|
@@ -102,7 +102,32 @@ function registerSessionCommands(program) {
|
|
|
102
102
|
try {
|
|
103
103
|
const store = new SessionStore(process.cwd());
|
|
104
104
|
const current = await store.getSession(sessionRef || 'latest');
|
|
105
|
-
|
|
105
|
+
const sceneIndex = await store.getSceneIndexDiagnostics();
|
|
106
|
+
_printSessionResult('session_show', current, options.json, {
|
|
107
|
+
session_source: 'file',
|
|
108
|
+
scene_index: sceneIndex
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
_exitWithError(error, options.json);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
session
|
|
116
|
+
.command('list')
|
|
117
|
+
.description('List recent sessions')
|
|
118
|
+
.option('--limit <n>', 'Maximum sessions to return', '20')
|
|
119
|
+
.option('--json', 'Output as JSON')
|
|
120
|
+
.action(async (options) => {
|
|
121
|
+
try {
|
|
122
|
+
const store = new SessionStore(process.cwd());
|
|
123
|
+
const limitRaw = Number.parseInt(`${options.limit || '20'}`, 10);
|
|
124
|
+
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, 5000) : 20;
|
|
125
|
+
const sessions = await store.listSessions();
|
|
126
|
+
const sceneIndex = await store.getSceneIndexDiagnostics();
|
|
127
|
+
_printSessionListResult('session_list', sessions.slice(0, limit), options.json, {
|
|
128
|
+
session_source: 'file',
|
|
129
|
+
scene_index: sceneIndex
|
|
130
|
+
});
|
|
106
131
|
} catch (error) {
|
|
107
132
|
_exitWithError(error, options.json);
|
|
108
133
|
}
|
|
@@ -120,12 +145,13 @@ function _parsePayload(raw) {
|
|
|
120
145
|
}
|
|
121
146
|
}
|
|
122
147
|
|
|
123
|
-
function _printSessionResult(action, session, asJson = false) {
|
|
148
|
+
function _printSessionResult(action, session, asJson = false, metadata = {}) {
|
|
124
149
|
if (asJson) {
|
|
125
150
|
console.log(JSON.stringify({
|
|
126
151
|
success: true,
|
|
127
152
|
action,
|
|
128
153
|
session,
|
|
154
|
+
...metadata
|
|
129
155
|
}, null, 2));
|
|
130
156
|
return;
|
|
131
157
|
}
|
|
@@ -147,6 +173,38 @@ function _printSessionResult(action, session, asJson = false) {
|
|
|
147
173
|
if (session.objective) {
|
|
148
174
|
console.log(chalk.gray(`Objective: ${session.objective}`));
|
|
149
175
|
}
|
|
176
|
+
if (metadata.session_source) {
|
|
177
|
+
console.log(chalk.gray(`Session source: ${metadata.session_source}`));
|
|
178
|
+
}
|
|
179
|
+
if (metadata.scene_index && metadata.scene_index.status) {
|
|
180
|
+
console.log(chalk.gray(`Scene index consistency: ${metadata.scene_index.status}`));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _printSessionListResult(action, sessions, asJson = false, metadata = {}) {
|
|
185
|
+
if (asJson) {
|
|
186
|
+
console.log(JSON.stringify({
|
|
187
|
+
success: true,
|
|
188
|
+
action,
|
|
189
|
+
total: Array.isArray(sessions) ? sessions.length : 0,
|
|
190
|
+
sessions: Array.isArray(sessions) ? sessions : [],
|
|
191
|
+
...metadata
|
|
192
|
+
}, null, 2));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(chalk.green('✓ Sessions listed'));
|
|
197
|
+
const list = Array.isArray(sessions) ? sessions : [];
|
|
198
|
+
console.log(chalk.gray(`Total: ${list.length}`));
|
|
199
|
+
if (metadata.session_source) {
|
|
200
|
+
console.log(chalk.gray(`Session source: ${metadata.session_source}`));
|
|
201
|
+
}
|
|
202
|
+
if (metadata.scene_index && metadata.scene_index.status) {
|
|
203
|
+
console.log(chalk.gray(`Scene index consistency: ${metadata.scene_index.status}`));
|
|
204
|
+
}
|
|
205
|
+
for (const session of list) {
|
|
206
|
+
console.log(`- ${session.session_id} | ${session.status} | ${session.updated_at || session.started_at || 'n/a'}`);
|
|
207
|
+
}
|
|
150
208
|
}
|
|
151
209
|
|
|
152
210
|
function _exitWithError(error, asJson = false) {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const {
|
|
5
|
+
COMPONENT_DEFINITIONS,
|
|
6
|
+
buildStateMigrationPlan,
|
|
7
|
+
runStateMigration,
|
|
8
|
+
runStateDoctor,
|
|
9
|
+
runStateExport
|
|
10
|
+
} = require('../state/state-migration-manager');
|
|
11
|
+
|
|
12
|
+
function normalizeString(value) {
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
return value.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectOptionValue(value, previous = []) {
|
|
20
|
+
const normalized = normalizeString(value);
|
|
21
|
+
if (!normalized) {
|
|
22
|
+
return previous;
|
|
23
|
+
}
|
|
24
|
+
return [...previous, normalized];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeComponentInput(value = []) {
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const items = [];
|
|
32
|
+
for (const entry of value) {
|
|
33
|
+
const normalized = normalizeString(entry);
|
|
34
|
+
if (!normalized) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (const token of normalized.split(/[,\s]+/g)) {
|
|
38
|
+
const cleaned = normalizeString(token);
|
|
39
|
+
if (cleaned) {
|
|
40
|
+
items.push(cleaned);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Array.from(new Set(items));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printPayload(payload, options = {}, title = 'State') {
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(chalk.blue(title));
|
|
54
|
+
if (payload.mode) {
|
|
55
|
+
console.log(` Mode: ${payload.mode}`);
|
|
56
|
+
}
|
|
57
|
+
if (payload.store_path) {
|
|
58
|
+
console.log(` Store: ${payload.store_path}`);
|
|
59
|
+
}
|
|
60
|
+
if (payload.sqlite) {
|
|
61
|
+
console.log(` SQLite: configured=${payload.sqlite.configured ? 'yes' : 'no'} available=${payload.sqlite.available ? 'yes' : 'no'}`);
|
|
62
|
+
}
|
|
63
|
+
if (payload.summary && typeof payload.summary === 'object') {
|
|
64
|
+
for (const [key, value] of Object.entries(payload.summary)) {
|
|
65
|
+
console.log(` ${key}: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(payload.components)) {
|
|
69
|
+
for (const item of payload.components) {
|
|
70
|
+
console.log(` - ${item.id} | source=${item.source_record_count} | status=${item.status}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(payload.operations)) {
|
|
74
|
+
for (const item of payload.operations) {
|
|
75
|
+
console.log(` - ${item.component_id} | ${item.status} | source=${item.source_record_count}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (payload.out_file) {
|
|
79
|
+
console.log(` Export: ${payload.out_file}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runStatePlanCommand(options = {}, dependencies = {}) {
|
|
84
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
85
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
86
|
+
const env = dependencies.env || process.env;
|
|
87
|
+
const components = normalizeComponentInput(options.component);
|
|
88
|
+
|
|
89
|
+
const payload = await buildStateMigrationPlan({
|
|
90
|
+
componentIds: components
|
|
91
|
+
}, {
|
|
92
|
+
projectPath,
|
|
93
|
+
fileSystem,
|
|
94
|
+
env
|
|
95
|
+
});
|
|
96
|
+
printPayload(payload, options, 'State Plan');
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runStateDoctorCommand(options = {}, dependencies = {}) {
|
|
101
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
102
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
103
|
+
const env = dependencies.env || process.env;
|
|
104
|
+
|
|
105
|
+
const payload = await runStateDoctor({}, {
|
|
106
|
+
projectPath,
|
|
107
|
+
fileSystem,
|
|
108
|
+
env
|
|
109
|
+
});
|
|
110
|
+
printPayload(payload, options, 'State Doctor');
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runStateMigrateCommand(options = {}, dependencies = {}) {
|
|
115
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
116
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
117
|
+
const env = dependencies.env || process.env;
|
|
118
|
+
const components = normalizeComponentInput(options.component);
|
|
119
|
+
const componentIds = options.all === true ? [] : components;
|
|
120
|
+
|
|
121
|
+
const payload = await runStateMigration({
|
|
122
|
+
apply: options.apply === true,
|
|
123
|
+
all: options.all === true,
|
|
124
|
+
componentIds
|
|
125
|
+
}, {
|
|
126
|
+
projectPath,
|
|
127
|
+
fileSystem,
|
|
128
|
+
env
|
|
129
|
+
});
|
|
130
|
+
printPayload(payload, options, 'State Migrate');
|
|
131
|
+
return payload;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function runStateExportCommand(options = {}, dependencies = {}) {
|
|
135
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
136
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
137
|
+
const env = dependencies.env || process.env;
|
|
138
|
+
|
|
139
|
+
const payload = await runStateExport({
|
|
140
|
+
out: normalizeString(options.out)
|
|
141
|
+
}, {
|
|
142
|
+
projectPath,
|
|
143
|
+
fileSystem,
|
|
144
|
+
env
|
|
145
|
+
});
|
|
146
|
+
printPayload(payload, options, 'State Export');
|
|
147
|
+
return payload;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function safeRun(handler, options = {}, dependencies = {}, title = 'state command') {
|
|
151
|
+
try {
|
|
152
|
+
await handler(options, dependencies);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (options && options.json) {
|
|
155
|
+
console.log(JSON.stringify({
|
|
156
|
+
success: false,
|
|
157
|
+
mode: title.replace(/\s+/g, '-'),
|
|
158
|
+
error: error.message
|
|
159
|
+
}, null, 2));
|
|
160
|
+
} else {
|
|
161
|
+
console.error(chalk.red(`${title} failed:`), error.message);
|
|
162
|
+
}
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function registerStateCommands(program) {
|
|
168
|
+
const state = program
|
|
169
|
+
.command('state')
|
|
170
|
+
.description('Manage gradual migration from file registries to sqlite indexes');
|
|
171
|
+
|
|
172
|
+
const knownIds = COMPONENT_DEFINITIONS.map((item) => item.id).join(', ');
|
|
173
|
+
|
|
174
|
+
state
|
|
175
|
+
.command('plan')
|
|
176
|
+
.description('Inspect migratable file-based registries and produce migration plan')
|
|
177
|
+
.option('--component <id>', `Component id (repeatable): ${knownIds}`, collectOptionValue, [])
|
|
178
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
179
|
+
.action(async (options) => safeRun(runStatePlanCommand, options, {}, 'state plan'));
|
|
180
|
+
|
|
181
|
+
state
|
|
182
|
+
.command('doctor')
|
|
183
|
+
.description('Check sqlite readiness and file/sqlite index consistency')
|
|
184
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
185
|
+
.action(async (options) => safeRun(runStateDoctorCommand, options, {}, 'state doctor'));
|
|
186
|
+
|
|
187
|
+
state
|
|
188
|
+
.command('migrate')
|
|
189
|
+
.description('Migrate selected components to sqlite indexes (dry-run by default)')
|
|
190
|
+
.option('--component <id>', `Component id (repeatable): ${knownIds}`, collectOptionValue, [])
|
|
191
|
+
.option('--all', 'Migrate all known components')
|
|
192
|
+
.option('--apply', 'Apply migration writes (default is dry-run)')
|
|
193
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
194
|
+
.action(async (options) => safeRun(runStateMigrateCommand, options, {}, 'state migrate'));
|
|
195
|
+
|
|
196
|
+
state
|
|
197
|
+
.command('export')
|
|
198
|
+
.description('Export sqlite state migration tables as JSON snapshot')
|
|
199
|
+
.option('--out <path>', 'Output file path', '.sce/reports/state-migration/state-export.latest.json')
|
|
200
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
201
|
+
.action(async (options) => safeRun(runStateExportCommand, options, {}, 'state export'));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
runStatePlanCommand,
|
|
206
|
+
runStateDoctorCommand,
|
|
207
|
+
runStateMigrateCommand,
|
|
208
|
+
runStateExportCommand,
|
|
209
|
+
registerStateCommands
|
|
210
|
+
};
|