scene-capability-engine 3.6.38 → 3.6.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/docs/command-reference.md +27 -0
- package/docs/document-governance.md +22 -2
- package/docs/releases/README.md +1 -0
- package/docs/releases/v3.6.39.md +24 -0
- package/docs/state-migration-reconciliation-runbook.md +76 -0
- package/docs/state-storage-tiering.md +104 -0
- package/docs/zh/releases/README.md +1 -0
- package/docs/zh/releases/v3.6.39.md +24 -0
- package/lib/commands/docs.js +8 -2
- package/lib/commands/scene.js +78 -18
- 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/runtime/session-store.js +8 -0
- 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/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/takeover-baseline.js +11 -0
- package/package.json +5 -1
- package/template/.sce/config/state-storage-policy.json +165 -0
|
@@ -84,6 +84,8 @@ class ValidationEngine {
|
|
|
84
84
|
*/
|
|
85
85
|
async validateSpec(specName) {
|
|
86
86
|
const specPath = this.scanner.getSpecDirectory(specName);
|
|
87
|
+
const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
|
|
88
|
+
const allowedRootFiles = this.config.specAllowedRootFiles || requiredFiles;
|
|
87
89
|
|
|
88
90
|
// Check if Spec directory exists
|
|
89
91
|
if (!await this.scanner.exists(specPath)) {
|
|
@@ -97,7 +99,6 @@ class ValidationEngine {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
// Check required files
|
|
100
|
-
const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
|
|
101
102
|
for (const file of requiredFiles) {
|
|
102
103
|
const filePath = path.join(specPath, file);
|
|
103
104
|
if (!await this.scanner.exists(filePath)) {
|
|
@@ -118,7 +119,7 @@ class ValidationEngine {
|
|
|
118
119
|
const basename = path.basename(filePath);
|
|
119
120
|
|
|
120
121
|
// Skip required files
|
|
121
|
-
if (
|
|
122
|
+
if (allowedRootFiles.includes(basename)) {
|
|
122
123
|
continue;
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -568,26 +568,34 @@ class SessionStore {
|
|
|
568
568
|
|
|
569
569
|
const fileCount = fileRecords.length;
|
|
570
570
|
const sqliteCount = Array.isArray(sqliteRecords) ? sqliteRecords.length : null;
|
|
571
|
+
let readSource = 'file';
|
|
571
572
|
|
|
572
573
|
let status = 'file-only';
|
|
573
574
|
if (sqliteCount === null) {
|
|
574
575
|
status = 'sqlite-unavailable';
|
|
575
576
|
} else if (fileCount === 0 && sqliteCount === 0) {
|
|
576
577
|
status = 'empty';
|
|
578
|
+
readSource = 'empty';
|
|
577
579
|
} else if (fileCount === 0 && sqliteCount > 0) {
|
|
578
580
|
status = 'sqlite-only';
|
|
581
|
+
readSource = 'sqlite';
|
|
579
582
|
} else if (fileCount > 0 && sqliteCount === 0) {
|
|
580
583
|
status = 'file-only';
|
|
584
|
+
readSource = 'file';
|
|
581
585
|
} else if (fileCount === sqliteCount) {
|
|
582
586
|
status = 'aligned';
|
|
587
|
+
readSource = this._preferSqliteSceneReads ? 'sqlite' : 'file';
|
|
583
588
|
} else if (sqliteCount < fileCount) {
|
|
584
589
|
status = 'pending-sync';
|
|
590
|
+
readSource = 'file';
|
|
585
591
|
} else if (sqliteCount > fileCount) {
|
|
586
592
|
status = 'sqlite-ahead';
|
|
593
|
+
readSource = 'sqlite';
|
|
587
594
|
}
|
|
588
595
|
|
|
589
596
|
return {
|
|
590
597
|
read_preference: this._preferSqliteSceneReads ? 'sqlite' : 'file',
|
|
598
|
+
read_source: readSource,
|
|
591
599
|
file_scene_count: fileCount,
|
|
592
600
|
sqlite_scene_count: sqliteCount,
|
|
593
601
|
status
|
|
@@ -162,6 +162,7 @@ class SceStateStore {
|
|
|
162
162
|
migration_records: {},
|
|
163
163
|
auth_leases: {},
|
|
164
164
|
auth_events: [],
|
|
165
|
+
interactive_approval_events: {},
|
|
165
166
|
sequences: {
|
|
166
167
|
scene_next: 1,
|
|
167
168
|
spec_next_by_scene: {},
|
|
@@ -294,6 +295,34 @@ class SceStateStore {
|
|
|
294
295
|
CREATE INDEX IF NOT EXISTS idx_auth_event_stream_ts
|
|
295
296
|
ON auth_event_stream(event_timestamp);
|
|
296
297
|
|
|
298
|
+
CREATE TABLE IF NOT EXISTS interactive_approval_event_projection (
|
|
299
|
+
event_id TEXT PRIMARY KEY,
|
|
300
|
+
workflow_id TEXT,
|
|
301
|
+
event_timestamp TEXT NOT NULL,
|
|
302
|
+
event_type TEXT NOT NULL,
|
|
303
|
+
action TEXT,
|
|
304
|
+
actor TEXT,
|
|
305
|
+
actor_role TEXT,
|
|
306
|
+
from_status TEXT,
|
|
307
|
+
to_status TEXT,
|
|
308
|
+
blocked INTEGER,
|
|
309
|
+
reason TEXT,
|
|
310
|
+
audit_file TEXT,
|
|
311
|
+
line_no INTEGER,
|
|
312
|
+
raw_json TEXT NOT NULL,
|
|
313
|
+
source TEXT,
|
|
314
|
+
indexed_at TEXT NOT NULL
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
CREATE INDEX IF NOT EXISTS idx_interactive_approval_event_projection_workflow_ts
|
|
318
|
+
ON interactive_approval_event_projection(workflow_id, event_timestamp DESC);
|
|
319
|
+
|
|
320
|
+
CREATE INDEX IF NOT EXISTS idx_interactive_approval_event_projection_actor_action_ts
|
|
321
|
+
ON interactive_approval_event_projection(actor, action, event_timestamp DESC);
|
|
322
|
+
|
|
323
|
+
CREATE INDEX IF NOT EXISTS idx_interactive_approval_event_projection_blocked_ts
|
|
324
|
+
ON interactive_approval_event_projection(blocked, event_timestamp DESC);
|
|
325
|
+
|
|
297
326
|
CREATE TABLE IF NOT EXISTS timeline_snapshot_registry (
|
|
298
327
|
snapshot_id TEXT PRIMARY KEY,
|
|
299
328
|
created_at TEXT NOT NULL,
|
|
@@ -1007,6 +1036,30 @@ class SceStateStore {
|
|
|
1007
1036
|
};
|
|
1008
1037
|
}
|
|
1009
1038
|
|
|
1039
|
+
_mapInteractiveApprovalEventProjectionRow(row) {
|
|
1040
|
+
if (!row) {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
event_id: normalizeString(row.event_id),
|
|
1045
|
+
workflow_id: normalizeString(row.workflow_id) || null,
|
|
1046
|
+
event_timestamp: normalizeIsoTimestamp(row.event_timestamp) || null,
|
|
1047
|
+
event_type: normalizeString(row.event_type),
|
|
1048
|
+
action: normalizeString(row.action) || null,
|
|
1049
|
+
actor: normalizeString(row.actor) || null,
|
|
1050
|
+
actor_role: normalizeString(row.actor_role) || null,
|
|
1051
|
+
from_status: normalizeString(row.from_status) || null,
|
|
1052
|
+
to_status: normalizeString(row.to_status) || null,
|
|
1053
|
+
blocked: normalizeBooleanValue(row.blocked, false),
|
|
1054
|
+
reason: normalizeString(row.reason) || null,
|
|
1055
|
+
audit_file: normalizeString(row.audit_file) || null,
|
|
1056
|
+
line_no: normalizeNonNegativeInteger(row.line_no, 0),
|
|
1057
|
+
raw: parseJsonSafe(row.raw_json, null),
|
|
1058
|
+
source: normalizeString(row.source) || null,
|
|
1059
|
+
indexed_at: normalizeIsoTimestamp(row.indexed_at) || null
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1010
1063
|
_mapTimelineSnapshotRow(row) {
|
|
1011
1064
|
if (!row) {
|
|
1012
1065
|
return null;
|
|
@@ -2151,6 +2204,218 @@ class SceStateStore {
|
|
|
2151
2204
|
.filter(Boolean);
|
|
2152
2205
|
}
|
|
2153
2206
|
|
|
2207
|
+
async clearInteractiveApprovalEventProjection(options = {}) {
|
|
2208
|
+
const auditFileFilter = normalizeString(options.auditFile || options.audit_file);
|
|
2209
|
+
|
|
2210
|
+
if (this._useMemoryBackend()) {
|
|
2211
|
+
if (!auditFileFilter) {
|
|
2212
|
+
this._memory.interactive_approval_events = {};
|
|
2213
|
+
return { success: true, removed: 0 };
|
|
2214
|
+
}
|
|
2215
|
+
let removed = 0;
|
|
2216
|
+
for (const [eventId, item] of Object.entries(this._memory.interactive_approval_events || {})) {
|
|
2217
|
+
if (normalizeString(item.audit_file) === auditFileFilter) {
|
|
2218
|
+
delete this._memory.interactive_approval_events[eventId];
|
|
2219
|
+
removed += 1;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return { success: true, removed };
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
if (!await this.ensureReady()) {
|
|
2226
|
+
return null;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
if (auditFileFilter) {
|
|
2230
|
+
const info = this._db
|
|
2231
|
+
.prepare('DELETE FROM interactive_approval_event_projection WHERE audit_file = ?')
|
|
2232
|
+
.run(auditFileFilter);
|
|
2233
|
+
return {
|
|
2234
|
+
success: true,
|
|
2235
|
+
removed: normalizeNonNegativeInteger(info && info.changes, 0)
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
const info = this._db
|
|
2240
|
+
.prepare('DELETE FROM interactive_approval_event_projection')
|
|
2241
|
+
.run();
|
|
2242
|
+
return {
|
|
2243
|
+
success: true,
|
|
2244
|
+
removed: normalizeNonNegativeInteger(info && info.changes, 0)
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
async upsertInteractiveApprovalEventProjection(records = [], options = {}) {
|
|
2249
|
+
const source = normalizeString(options.source) || 'jsonl.interactive-approval-events';
|
|
2250
|
+
const auditFile = normalizeString(options.auditFile || options.audit_file) || null;
|
|
2251
|
+
const nowIso = this.now();
|
|
2252
|
+
const normalizedRecords = Array.isArray(records)
|
|
2253
|
+
? records.map((item, index) => ({
|
|
2254
|
+
event_id: normalizeString(item && item.event_id),
|
|
2255
|
+
workflow_id: normalizeString(item && item.workflow_id) || null,
|
|
2256
|
+
event_timestamp: normalizeIsoTimestamp(item && (item.event_timestamp || item.timestamp), nowIso) || nowIso,
|
|
2257
|
+
event_type: normalizeString(item && item.event_type),
|
|
2258
|
+
action: normalizeString(item && item.action) || null,
|
|
2259
|
+
actor: normalizeString(item && item.actor) || null,
|
|
2260
|
+
actor_role: normalizeString(item && item.actor_role) || null,
|
|
2261
|
+
from_status: normalizeString(item && item.from_status) || null,
|
|
2262
|
+
to_status: normalizeString(item && item.to_status) || null,
|
|
2263
|
+
blocked: normalizeBooleanValue(item && item.blocked, false),
|
|
2264
|
+
reason: normalizeString(item && item.reason) || null,
|
|
2265
|
+
audit_file: normalizeString(item && (item.audit_file || item.auditFile)) || auditFile,
|
|
2266
|
+
line_no: normalizeNonNegativeInteger(item && (item.line_no || item.lineNo), index + 1),
|
|
2267
|
+
raw_json: JSON.stringify(item && typeof item === 'object' ? item : {}),
|
|
2268
|
+
source,
|
|
2269
|
+
indexed_at: nowIso
|
|
2270
|
+
}))
|
|
2271
|
+
.filter((item) => item.event_id && item.event_type)
|
|
2272
|
+
: [];
|
|
2273
|
+
|
|
2274
|
+
if (this._useMemoryBackend()) {
|
|
2275
|
+
for (const item of normalizedRecords) {
|
|
2276
|
+
this._memory.interactive_approval_events[item.event_id] = { ...item };
|
|
2277
|
+
}
|
|
2278
|
+
return {
|
|
2279
|
+
success: true,
|
|
2280
|
+
written: normalizedRecords.length,
|
|
2281
|
+
total: Object.keys(this._memory.interactive_approval_events || {}).length
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
if (!await this.ensureReady()) {
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const statement = this._db.prepare(`
|
|
2290
|
+
INSERT OR REPLACE INTO interactive_approval_event_projection(
|
|
2291
|
+
event_id, workflow_id, event_timestamp, event_type, action, actor, actor_role,
|
|
2292
|
+
from_status, to_status, blocked, reason, audit_file, line_no, raw_json, source, indexed_at
|
|
2293
|
+
)
|
|
2294
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2295
|
+
`);
|
|
2296
|
+
|
|
2297
|
+
this._withTransaction(() => {
|
|
2298
|
+
for (const item of normalizedRecords) {
|
|
2299
|
+
statement.run(
|
|
2300
|
+
item.event_id,
|
|
2301
|
+
item.workflow_id,
|
|
2302
|
+
item.event_timestamp,
|
|
2303
|
+
item.event_type,
|
|
2304
|
+
item.action,
|
|
2305
|
+
item.actor,
|
|
2306
|
+
item.actor_role,
|
|
2307
|
+
item.from_status,
|
|
2308
|
+
item.to_status,
|
|
2309
|
+
item.blocked ? 1 : 0,
|
|
2310
|
+
item.reason,
|
|
2311
|
+
item.audit_file,
|
|
2312
|
+
item.line_no,
|
|
2313
|
+
item.raw_json,
|
|
2314
|
+
item.source,
|
|
2315
|
+
item.indexed_at
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
const totalRow = this._db
|
|
2321
|
+
.prepare('SELECT COUNT(*) AS total FROM interactive_approval_event_projection')
|
|
2322
|
+
.get();
|
|
2323
|
+
|
|
2324
|
+
return {
|
|
2325
|
+
success: true,
|
|
2326
|
+
written: normalizedRecords.length,
|
|
2327
|
+
total: normalizeNonNegativeInteger(totalRow && totalRow.total, 0)
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
async listInteractiveApprovalEventProjection(options = {}) {
|
|
2332
|
+
const limit = normalizeInteger(options.limit, 100);
|
|
2333
|
+
const workflowId = normalizeString(options.workflowId || options.workflow_id);
|
|
2334
|
+
const actor = normalizeString(options.actor);
|
|
2335
|
+
const action = normalizeString(options.action);
|
|
2336
|
+
const eventType = normalizeString(options.eventType || options.event_type);
|
|
2337
|
+
const auditFile = normalizeString(options.auditFile || options.audit_file);
|
|
2338
|
+
const blockedFilter = options.blocked === undefined || options.blocked === null
|
|
2339
|
+
? null
|
|
2340
|
+
: normalizeBooleanValue(options.blocked, false);
|
|
2341
|
+
|
|
2342
|
+
if (this._useMemoryBackend()) {
|
|
2343
|
+
let rows = Object.values(this._memory.interactive_approval_events || {}).map((item) => ({ ...item }));
|
|
2344
|
+
if (workflowId) {
|
|
2345
|
+
rows = rows.filter((item) => normalizeString(item.workflow_id) === workflowId);
|
|
2346
|
+
}
|
|
2347
|
+
if (actor) {
|
|
2348
|
+
rows = rows.filter((item) => normalizeString(item.actor) === actor);
|
|
2349
|
+
}
|
|
2350
|
+
if (action) {
|
|
2351
|
+
rows = rows.filter((item) => normalizeString(item.action) === action);
|
|
2352
|
+
}
|
|
2353
|
+
if (eventType) {
|
|
2354
|
+
rows = rows.filter((item) => normalizeString(item.event_type) === eventType);
|
|
2355
|
+
}
|
|
2356
|
+
if (auditFile) {
|
|
2357
|
+
rows = rows.filter((item) => normalizeString(item.audit_file) === auditFile);
|
|
2358
|
+
}
|
|
2359
|
+
if (blockedFilter !== null) {
|
|
2360
|
+
rows = rows.filter((item) => normalizeBooleanValue(item.blocked, false) === blockedFilter);
|
|
2361
|
+
}
|
|
2362
|
+
rows.sort((left, right) => (Date.parse(right.event_timestamp || '') || 0) - (Date.parse(left.event_timestamp || '') || 0));
|
|
2363
|
+
if (limit > 0) {
|
|
2364
|
+
rows = rows.slice(0, limit);
|
|
2365
|
+
}
|
|
2366
|
+
return rows.map((row) => this._mapInteractiveApprovalEventProjectionRow(row)).filter(Boolean);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
if (!await this.ensureReady()) {
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
let query = `
|
|
2374
|
+
SELECT event_id, workflow_id, event_timestamp, event_type, action, actor, actor_role,
|
|
2375
|
+
from_status, to_status, blocked, reason, audit_file, line_no, raw_json, source, indexed_at
|
|
2376
|
+
FROM interactive_approval_event_projection
|
|
2377
|
+
`;
|
|
2378
|
+
const clauses = [];
|
|
2379
|
+
const params = [];
|
|
2380
|
+
if (workflowId) {
|
|
2381
|
+
clauses.push('workflow_id = ?');
|
|
2382
|
+
params.push(workflowId);
|
|
2383
|
+
}
|
|
2384
|
+
if (actor) {
|
|
2385
|
+
clauses.push('actor = ?');
|
|
2386
|
+
params.push(actor);
|
|
2387
|
+
}
|
|
2388
|
+
if (action) {
|
|
2389
|
+
clauses.push('action = ?');
|
|
2390
|
+
params.push(action);
|
|
2391
|
+
}
|
|
2392
|
+
if (eventType) {
|
|
2393
|
+
clauses.push('event_type = ?');
|
|
2394
|
+
params.push(eventType);
|
|
2395
|
+
}
|
|
2396
|
+
if (auditFile) {
|
|
2397
|
+
clauses.push('audit_file = ?');
|
|
2398
|
+
params.push(auditFile);
|
|
2399
|
+
}
|
|
2400
|
+
if (blockedFilter !== null) {
|
|
2401
|
+
clauses.push('blocked = ?');
|
|
2402
|
+
params.push(blockedFilter ? 1 : 0);
|
|
2403
|
+
}
|
|
2404
|
+
if (clauses.length > 0) {
|
|
2405
|
+
query += ` WHERE ${clauses.join(' AND ')}`;
|
|
2406
|
+
}
|
|
2407
|
+
query += ' ORDER BY event_timestamp DESC, line_no DESC';
|
|
2408
|
+
if (limit > 0) {
|
|
2409
|
+
query += ' LIMIT ?';
|
|
2410
|
+
params.push(limit);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
const rows = this._db.prepare(query).all(...params);
|
|
2414
|
+
return rows
|
|
2415
|
+
.map((row) => this._mapInteractiveApprovalEventProjectionRow(row))
|
|
2416
|
+
.filter(Boolean);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2154
2419
|
async upsertTimelineSnapshotIndex(records = [], options = {}) {
|
|
2155
2420
|
const source = normalizeString(options.source) || 'file.timeline.index';
|
|
2156
2421
|
const nowIso = this.now();
|
|
@@ -723,8 +723,12 @@ async function runStateDoctor(options = {}, dependencies = {}) {
|
|
|
723
723
|
syncStatus = 'source-parse-error';
|
|
724
724
|
} else if (sourceCount === 0 && targetCount === 0) {
|
|
725
725
|
syncStatus = 'empty';
|
|
726
|
+
} else if (sourceCount === 0 && targetCount > 0) {
|
|
727
|
+
syncStatus = 'sqlite-only';
|
|
726
728
|
} else if (targetCount < sourceCount) {
|
|
727
729
|
syncStatus = 'pending-migration';
|
|
730
|
+
} else if (targetCount > sourceCount) {
|
|
731
|
+
syncStatus = 'sqlite-ahead';
|
|
728
732
|
}
|
|
729
733
|
return {
|
|
730
734
|
id: component.id,
|
|
@@ -750,22 +754,37 @@ async function runStateDoctor(options = {}, dependencies = {}) {
|
|
|
750
754
|
if (checks.some((item) => item.sync_status === 'source-parse-error')) {
|
|
751
755
|
blocking.push('source-parse-error');
|
|
752
756
|
}
|
|
757
|
+
if (checks.some((item) => item.sync_status === 'sqlite-ahead')) {
|
|
758
|
+
blocking.push('sqlite-ahead');
|
|
759
|
+
}
|
|
760
|
+
if (checks.some((item) => item.sync_status === 'sqlite-only')) {
|
|
761
|
+
blocking.push('sqlite-only');
|
|
762
|
+
}
|
|
753
763
|
|
|
754
764
|
const alerts = checks
|
|
755
765
|
.filter((item) => item.sync_status === 'pending-migration')
|
|
756
766
|
.map((item) => `pending migration: ${item.id}`);
|
|
767
|
+
alerts.push(...checks
|
|
768
|
+
.filter((item) => item.sync_status === 'missing-source')
|
|
769
|
+
.map((item) => `missing source: ${item.id}`));
|
|
757
770
|
|
|
758
771
|
if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'pending-sync') {
|
|
759
772
|
alerts.push('runtime timeline index pending-sync');
|
|
760
773
|
}
|
|
761
774
|
if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'sqlite-ahead') {
|
|
762
|
-
|
|
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
|
+
};
|
|
@@ -4,6 +4,15 @@ const { promisify } = require('util');
|
|
|
4
4
|
|
|
5
5
|
const execAsync = promisify(exec);
|
|
6
6
|
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise(resolve => {
|
|
9
|
+
const timer = setTimeout(resolve, ms);
|
|
10
|
+
if (typeof timer.unref === 'function') {
|
|
11
|
+
timer.unref();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
/**
|
|
8
17
|
* ActionExecutor - 动作执行器
|
|
9
18
|
*
|
|
@@ -203,7 +212,7 @@ class ActionExecutor extends EventEmitter {
|
|
|
203
212
|
});
|
|
204
213
|
|
|
205
214
|
// 等待
|
|
206
|
-
await
|
|
215
|
+
await sleep(delay);
|
|
207
216
|
|
|
208
217
|
try {
|
|
209
218
|
// 重新执行
|