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.
- package/CHANGELOG.md +63 -0
- package/bin/scene-capability-engine.js +42 -2
- package/docs/command-reference.md +27 -0
- package/docs/developer-guide.md +1 -1
- package/docs/document-governance.md +22 -2
- package/docs/releases/README.md +6 -0
- package/docs/releases/v3.6.39.md +24 -0
- package/docs/releases/v3.6.40.md +19 -0
- package/docs/releases/v3.6.41.md +20 -0
- package/docs/releases/v3.6.42.md +19 -0
- package/docs/releases/v3.6.43.md +17 -0
- package/docs/releases/v3.6.44.md +17 -0
- package/docs/spec-collaboration-guide.md +1 -1
- package/docs/state-migration-reconciliation-runbook.md +76 -0
- package/docs/state-storage-tiering.md +104 -0
- package/docs/zh/releases/README.md +6 -0
- package/docs/zh/releases/v3.6.39.md +24 -0
- package/docs/zh/releases/v3.6.40.md +19 -0
- package/docs/zh/releases/v3.6.41.md +20 -0
- package/docs/zh/releases/v3.6.42.md +19 -0
- package/docs/zh/releases/v3.6.43.md +17 -0
- package/docs/zh/releases/v3.6.44.md +17 -0
- package/lib/adoption/adoption-logger.js +1 -1
- package/lib/adoption/adoption-strategy.js +29 -29
- package/lib/adoption/detection-engine.js +16 -13
- package/lib/adoption/smart-orchestrator.js +3 -3
- package/lib/adoption/strategy-selector.js +19 -15
- package/lib/adoption/template-sync.js +3 -3
- package/lib/auto/autonomous-engine.js +5 -5
- package/lib/auto/handoff-release-gate-history-loaders-service.js +24 -4
- package/lib/auto/handoff-run-service.js +37 -0
- package/lib/backup/backup-system.js +10 -10
- package/lib/collab/collab-manager.js +8 -5
- package/lib/collab/dependency-manager.js +1 -1
- package/lib/commands/adopt.js +2 -2
- package/lib/commands/auto.js +239 -97
- package/lib/commands/collab.js +10 -4
- package/lib/commands/docs.js +8 -2
- package/lib/commands/scene.js +78 -18
- package/lib/commands/status.js +3 -3
- package/lib/commands/studio.js +8 -0
- package/lib/commands/watch.js +10 -1
- package/lib/governance/config-manager.js +16 -0
- package/lib/governance/diagnostic-engine.js +2 -1
- package/lib/governance/validation-engine.js +3 -2
- package/lib/repo/config-manager.js +2 -2
- package/lib/runtime/session-store.js +8 -0
- package/lib/spec/bootstrap/context-collector.js +5 -4
- package/lib/spec-gate/rules/default-rules.js +8 -8
- package/lib/state/sce-state-store.js +265 -0
- package/lib/state/state-migration-manager.js +27 -2
- package/lib/state/state-storage-policy.js +179 -0
- package/lib/upgrade/migration-engine.js +5 -5
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +3 -3
- package/lib/utils/tool-detector.js +4 -4
- package/lib/utils/validation.js +6 -6
- package/lib/watch/action-executor.js +10 -1
- package/lib/watch/event-debouncer.js +3 -0
- package/lib/watch/file-watcher.js +51 -10
- package/lib/watch/watch-manager.js +10 -1
- package/lib/workspace/multi/workspace-context-resolver.js +3 -3
- package/lib/workspace/multi/workspace-registry.js +3 -3
- package/lib/workspace/multi/workspace-state-manager.js +3 -3
- package/lib/workspace/spec-delivery-audit.js +553 -0
- package/lib/workspace/takeover-baseline.js +11 -0
- package/package.json +5 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
304
|
-
const
|
|
303
|
+
const scePath = path.join(projectPath, '.sce');
|
|
304
|
+
const sceExists = await pathExists(scePath);
|
|
305
305
|
|
|
306
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
|
29
|
+
const scePath = path.join(projectPath, '.sce');
|
|
30
30
|
|
|
31
31
|
// 1. Ensure backups/ directory exists
|
|
32
|
-
const backupsPath = path.join(
|
|
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(
|
|
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
|
|
68
|
-
if (await fs.pathExists(
|
|
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
|
|
75
|
+
const sceFiles = [
|
|
76
76
|
'.sce/steering',
|
|
77
77
|
'.sce/specs',
|
|
78
78
|
'.sce/tools'
|
|
79
79
|
];
|
|
80
80
|
|
|
81
|
-
for (const file of
|
|
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`);
|
package/lib/utils/validation.js
CHANGED
|
@@ -21,11 +21,11 @@ async function validateProjectStructure(projectPath) {
|
|
|
21
21
|
const warnings = [];
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
|
-
const
|
|
24
|
+
const scePath = path.join(projectPath, '.sce');
|
|
25
25
|
|
|
26
26
|
// Check if .sce/ directory exists
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
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(
|
|
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(
|
|
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(
|
|
81
|
+
const filePath = path.join(scePath, file.path);
|
|
82
82
|
const exists = await pathExists(filePath);
|
|
83
83
|
|
|
84
84
|
if (!exists) {
|