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