scene-capability-engine 3.5.2 → 3.6.2
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 +28 -0
- package/README.md +14 -4
- package/README.zh.md +14 -4
- package/bin/scene-capability-engine.js +2 -0
- package/docs/command-reference.md +23 -4
- package/docs/release-checklist.md +3 -3
- package/docs/zh/release-checklist.md +3 -3
- package/lib/commands/studio.js +246 -59
- package/lib/commands/task.js +630 -68
- package/lib/state/sce-state-store.js +594 -0
- package/lib/task/task-ref-registry.js +139 -0
- package/package.json +3 -2
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BACKEND = 'sqlite';
|
|
5
|
+
const DEFAULT_DB_RELATIVE_PATH = path.join('.sce', 'state', 'sce-state.sqlite');
|
|
6
|
+
const SUPPORTED_BACKENDS = new Set(['sqlite']);
|
|
7
|
+
|
|
8
|
+
function normalizeString(value) {
|
|
9
|
+
if (typeof value !== 'string') {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeInteger(value, fallback = 0) {
|
|
16
|
+
const parsed = Number.parseInt(`${value}`, 10);
|
|
17
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseJsonSafe(value, fallback) {
|
|
24
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(value);
|
|
29
|
+
} catch (_error) {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatSegment(value) {
|
|
35
|
+
const normalized = normalizeInteger(value, 0);
|
|
36
|
+
if (normalized <= 0) {
|
|
37
|
+
return '00';
|
|
38
|
+
}
|
|
39
|
+
return `${normalized}`.padStart(2, '0');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildTaskRef(sceneNo, specNo, taskNo) {
|
|
43
|
+
return `${formatSegment(sceneNo)}.${formatSegment(specNo)}.${formatSegment(taskNo)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveBackend(explicitBackend = '', env = process.env) {
|
|
47
|
+
const backendFromEnv = normalizeString(env && env.SCE_STATE_BACKEND);
|
|
48
|
+
const normalized = normalizeString(explicitBackend || backendFromEnv || DEFAULT_BACKEND).toLowerCase();
|
|
49
|
+
if (!SUPPORTED_BACKENDS.has(normalized)) {
|
|
50
|
+
return DEFAULT_BACKEND;
|
|
51
|
+
}
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadNodeSqlite(sqliteModule) {
|
|
56
|
+
if (sqliteModule) {
|
|
57
|
+
return sqliteModule;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return require('node:sqlite');
|
|
61
|
+
} catch (_error) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class SceStateStore {
|
|
67
|
+
constructor(projectPath = process.cwd(), options = {}) {
|
|
68
|
+
this.projectPath = projectPath;
|
|
69
|
+
this.fileSystem = options.fileSystem || fs;
|
|
70
|
+
this.env = options.env || process.env;
|
|
71
|
+
this.backend = resolveBackend(options.backend, this.env);
|
|
72
|
+
this.dbPath = options.dbPath || path.join(projectPath, DEFAULT_DB_RELATIVE_PATH);
|
|
73
|
+
this.now = typeof options.now === 'function'
|
|
74
|
+
? options.now
|
|
75
|
+
: () => new Date().toISOString();
|
|
76
|
+
|
|
77
|
+
const sqlite = loadNodeSqlite(options.sqliteModule);
|
|
78
|
+
this.DatabaseSync = sqlite && sqlite.DatabaseSync ? sqlite.DatabaseSync : null;
|
|
79
|
+
this._db = null;
|
|
80
|
+
this._ready = false;
|
|
81
|
+
this._memory = {
|
|
82
|
+
scenes: {},
|
|
83
|
+
specs: {},
|
|
84
|
+
tasks: {},
|
|
85
|
+
refs: {},
|
|
86
|
+
sequences: {
|
|
87
|
+
scene_next: 1,
|
|
88
|
+
spec_next_by_scene: {},
|
|
89
|
+
task_next_by_scene_spec: {}
|
|
90
|
+
},
|
|
91
|
+
events_by_job: {}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isSqliteConfigured() {
|
|
96
|
+
return this.backend === 'sqlite';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isSqliteAvailable() {
|
|
100
|
+
return this.isSqliteConfigured() && Boolean(this.DatabaseSync);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getStoreRelativePath() {
|
|
104
|
+
if (!this.isSqliteConfigured()) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return path.relative(this.projectPath, this.dbPath).replace(/\\/g, '/');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async ensureReady() {
|
|
111
|
+
if (!this.isSqliteAvailable()) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (this._ready && this._db) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await this.fileSystem.ensureDir(path.dirname(this.dbPath));
|
|
119
|
+
this._db = new this.DatabaseSync(this.dbPath);
|
|
120
|
+
this._initializeSchema();
|
|
121
|
+
this._ready = true;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_useMemoryBackend() {
|
|
126
|
+
if (this.isSqliteAvailable()) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const memoryFallbackFlag = normalizeString(this.env && this.env.SCE_STATE_ALLOW_MEMORY_FALLBACK) === '1';
|
|
130
|
+
const isTestEnv = normalizeString(this.env && this.env.NODE_ENV).toLowerCase() === 'test';
|
|
131
|
+
return memoryFallbackFlag || isTestEnv;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_initializeSchema() {
|
|
135
|
+
this._db.exec('PRAGMA journal_mode = WAL;');
|
|
136
|
+
this._db.exec('PRAGMA foreign_keys = ON;');
|
|
137
|
+
this._db.exec(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS scene_registry (
|
|
139
|
+
scene_id TEXT PRIMARY KEY,
|
|
140
|
+
scene_no INTEGER NOT NULL UNIQUE,
|
|
141
|
+
created_at TEXT NOT NULL,
|
|
142
|
+
updated_at TEXT NOT NULL
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE TABLE IF NOT EXISTS spec_registry (
|
|
146
|
+
scene_id TEXT NOT NULL,
|
|
147
|
+
spec_id TEXT NOT NULL,
|
|
148
|
+
spec_no INTEGER NOT NULL,
|
|
149
|
+
created_at TEXT NOT NULL,
|
|
150
|
+
updated_at TEXT NOT NULL,
|
|
151
|
+
PRIMARY KEY (scene_id, spec_id),
|
|
152
|
+
UNIQUE (scene_id, spec_no),
|
|
153
|
+
FOREIGN KEY (scene_id) REFERENCES scene_registry(scene_id) ON DELETE CASCADE
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
CREATE TABLE IF NOT EXISTS task_ref_registry (
|
|
157
|
+
task_ref TEXT PRIMARY KEY,
|
|
158
|
+
scene_id TEXT NOT NULL,
|
|
159
|
+
spec_id TEXT NOT NULL,
|
|
160
|
+
task_key TEXT NOT NULL,
|
|
161
|
+
task_no INTEGER NOT NULL,
|
|
162
|
+
source TEXT NOT NULL,
|
|
163
|
+
metadata_json TEXT,
|
|
164
|
+
created_at TEXT NOT NULL,
|
|
165
|
+
updated_at TEXT NOT NULL,
|
|
166
|
+
UNIQUE (scene_id, spec_id, task_key),
|
|
167
|
+
UNIQUE (scene_id, spec_id, task_no),
|
|
168
|
+
FOREIGN KEY (scene_id, spec_id) REFERENCES spec_registry(scene_id, spec_id) ON DELETE CASCADE
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE IF NOT EXISTS studio_event_stream (
|
|
172
|
+
event_id TEXT PRIMARY KEY,
|
|
173
|
+
job_id TEXT NOT NULL,
|
|
174
|
+
event_type TEXT NOT NULL,
|
|
175
|
+
event_timestamp TEXT NOT NULL,
|
|
176
|
+
scene_id TEXT,
|
|
177
|
+
spec_id TEXT,
|
|
178
|
+
created_at TEXT NOT NULL,
|
|
179
|
+
raw_json TEXT NOT NULL
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_studio_event_stream_job_ts
|
|
183
|
+
ON studio_event_stream(job_id, event_timestamp);
|
|
184
|
+
`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_withTransaction(callback) {
|
|
188
|
+
this._db.exec('BEGIN IMMEDIATE');
|
|
189
|
+
try {
|
|
190
|
+
const result = callback();
|
|
191
|
+
this._db.exec('COMMIT');
|
|
192
|
+
return result;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
try {
|
|
195
|
+
this._db.exec('ROLLBACK');
|
|
196
|
+
} catch (_rollbackError) {
|
|
197
|
+
// Ignore rollback failure.
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_ensureSceneRow(sceneId, nowIso) {
|
|
204
|
+
const existing = this._db
|
|
205
|
+
.prepare('SELECT scene_no FROM scene_registry WHERE scene_id = ?')
|
|
206
|
+
.get(sceneId);
|
|
207
|
+
if (existing && Number.isFinite(existing.scene_no)) {
|
|
208
|
+
this._db
|
|
209
|
+
.prepare('UPDATE scene_registry SET updated_at = ? WHERE scene_id = ?')
|
|
210
|
+
.run(nowIso, sceneId);
|
|
211
|
+
return Number(existing.scene_no);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const next = this._db
|
|
215
|
+
.prepare('SELECT COALESCE(MAX(scene_no), 0) + 1 AS next_no FROM scene_registry')
|
|
216
|
+
.get();
|
|
217
|
+
const sceneNo = normalizeInteger(next && next.next_no, 1);
|
|
218
|
+
this._db
|
|
219
|
+
.prepare('INSERT INTO scene_registry(scene_id, scene_no, created_at, updated_at) VALUES(?, ?, ?, ?)')
|
|
220
|
+
.run(sceneId, sceneNo, nowIso, nowIso);
|
|
221
|
+
return sceneNo;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_ensureSpecRow(sceneId, specId, nowIso) {
|
|
225
|
+
const existing = this._db
|
|
226
|
+
.prepare('SELECT spec_no FROM spec_registry WHERE scene_id = ? AND spec_id = ?')
|
|
227
|
+
.get(sceneId, specId);
|
|
228
|
+
if (existing && Number.isFinite(existing.spec_no)) {
|
|
229
|
+
this._db
|
|
230
|
+
.prepare('UPDATE spec_registry SET updated_at = ? WHERE scene_id = ? AND spec_id = ?')
|
|
231
|
+
.run(nowIso, sceneId, specId);
|
|
232
|
+
return Number(existing.spec_no);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const next = this._db
|
|
236
|
+
.prepare('SELECT COALESCE(MAX(spec_no), 0) + 1 AS next_no FROM spec_registry WHERE scene_id = ?')
|
|
237
|
+
.get(sceneId);
|
|
238
|
+
const specNo = normalizeInteger(next && next.next_no, 1);
|
|
239
|
+
this._db
|
|
240
|
+
.prepare('INSERT INTO spec_registry(scene_id, spec_id, spec_no, created_at, updated_at) VALUES(?, ?, ?, ?, ?)')
|
|
241
|
+
.run(sceneId, specId, specNo, nowIso, nowIso);
|
|
242
|
+
return specNo;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_mapTaskRefRow(row) {
|
|
246
|
+
if (!row) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const sceneNo = normalizeInteger(row.scene_no, 0);
|
|
251
|
+
const specNo = normalizeInteger(row.spec_no, 0);
|
|
252
|
+
const taskNo = normalizeInteger(row.task_no, 0);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
task_ref: normalizeString(row.task_ref),
|
|
256
|
+
scene_id: normalizeString(row.scene_id),
|
|
257
|
+
spec_id: normalizeString(row.spec_id),
|
|
258
|
+
task_key: normalizeString(row.task_key),
|
|
259
|
+
scene_no: sceneNo,
|
|
260
|
+
spec_no: specNo,
|
|
261
|
+
task_no: taskNo,
|
|
262
|
+
source: normalizeString(row.source) || 'unknown',
|
|
263
|
+
metadata: parseJsonSafe(row.metadata_json, {}) || {}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async resolveOrCreateTaskRef(options = {}) {
|
|
268
|
+
const sceneId = normalizeString(options.sceneId);
|
|
269
|
+
const specId = normalizeString(options.specId);
|
|
270
|
+
const taskKey = normalizeString(options.taskKey);
|
|
271
|
+
if (!sceneId || !specId || !taskKey) {
|
|
272
|
+
throw new Error('sceneId/specId/taskKey are required for sqlite task ref assignment');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const source = normalizeString(options.source) || 'unknown';
|
|
276
|
+
const metadata = options.metadata && typeof options.metadata === 'object'
|
|
277
|
+
? options.metadata
|
|
278
|
+
: {};
|
|
279
|
+
|
|
280
|
+
if (this._useMemoryBackend()) {
|
|
281
|
+
return this._resolveOrCreateTaskRefInMemory({
|
|
282
|
+
sceneId,
|
|
283
|
+
specId,
|
|
284
|
+
taskKey,
|
|
285
|
+
source,
|
|
286
|
+
metadata
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!await this.ensureReady()) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = this._withTransaction(() => {
|
|
295
|
+
const existing = this._db
|
|
296
|
+
.prepare(`
|
|
297
|
+
SELECT t.task_ref, t.scene_id, t.spec_id, t.task_key, t.task_no, t.source, t.metadata_json,
|
|
298
|
+
s.scene_no, p.spec_no
|
|
299
|
+
FROM task_ref_registry t
|
|
300
|
+
INNER JOIN scene_registry s ON s.scene_id = t.scene_id
|
|
301
|
+
INNER JOIN spec_registry p ON p.scene_id = t.scene_id AND p.spec_id = t.spec_id
|
|
302
|
+
WHERE t.scene_id = ? AND t.spec_id = ? AND t.task_key = ?
|
|
303
|
+
`)
|
|
304
|
+
.get(sceneId, specId, taskKey);
|
|
305
|
+
|
|
306
|
+
if (existing) {
|
|
307
|
+
const nowIso = this.now();
|
|
308
|
+
const mergedMetadata = {
|
|
309
|
+
...(parseJsonSafe(existing.metadata_json, {}) || {}),
|
|
310
|
+
...metadata
|
|
311
|
+
};
|
|
312
|
+
this._db
|
|
313
|
+
.prepare('UPDATE task_ref_registry SET source = ?, metadata_json = ?, updated_at = ? WHERE task_ref = ?')
|
|
314
|
+
.run(source, JSON.stringify(mergedMetadata), nowIso, existing.task_ref);
|
|
315
|
+
|
|
316
|
+
return this._db
|
|
317
|
+
.prepare(`
|
|
318
|
+
SELECT t.task_ref, t.scene_id, t.spec_id, t.task_key, t.task_no, t.source, t.metadata_json,
|
|
319
|
+
s.scene_no, p.spec_no
|
|
320
|
+
FROM task_ref_registry t
|
|
321
|
+
INNER JOIN scene_registry s ON s.scene_id = t.scene_id
|
|
322
|
+
INNER JOIN spec_registry p ON p.scene_id = t.scene_id AND p.spec_id = t.spec_id
|
|
323
|
+
WHERE t.task_ref = ?
|
|
324
|
+
`)
|
|
325
|
+
.get(existing.task_ref);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const nowIso = this.now();
|
|
329
|
+
const sceneNo = this._ensureSceneRow(sceneId, nowIso);
|
|
330
|
+
const specNo = this._ensureSpecRow(sceneId, specId, nowIso);
|
|
331
|
+
|
|
332
|
+
const nextTask = this._db
|
|
333
|
+
.prepare('SELECT COALESCE(MAX(task_no), 0) + 1 AS next_no FROM task_ref_registry WHERE scene_id = ? AND spec_id = ?')
|
|
334
|
+
.get(sceneId, specId);
|
|
335
|
+
const taskNo = normalizeInteger(nextTask && nextTask.next_no, 1);
|
|
336
|
+
const taskRef = buildTaskRef(sceneNo, specNo, taskNo);
|
|
337
|
+
|
|
338
|
+
this._db
|
|
339
|
+
.prepare(`
|
|
340
|
+
INSERT INTO task_ref_registry(task_ref, scene_id, spec_id, task_key, task_no, source, metadata_json, created_at, updated_at)
|
|
341
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
342
|
+
`)
|
|
343
|
+
.run(taskRef, sceneId, specId, taskKey, taskNo, source, JSON.stringify(metadata), nowIso, nowIso);
|
|
344
|
+
|
|
345
|
+
return this._db
|
|
346
|
+
.prepare(`
|
|
347
|
+
SELECT t.task_ref, t.scene_id, t.spec_id, t.task_key, t.task_no, t.source, t.metadata_json,
|
|
348
|
+
s.scene_no, p.spec_no
|
|
349
|
+
FROM task_ref_registry t
|
|
350
|
+
INNER JOIN scene_registry s ON s.scene_id = t.scene_id
|
|
351
|
+
INNER JOIN spec_registry p ON p.scene_id = t.scene_id AND p.spec_id = t.spec_id
|
|
352
|
+
WHERE t.task_ref = ?
|
|
353
|
+
`)
|
|
354
|
+
.get(taskRef);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return this._mapTaskRefRow(result);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async lookupTaskRef(taskRef) {
|
|
361
|
+
const normalizedTaskRef = normalizeString(taskRef);
|
|
362
|
+
if (!normalizedTaskRef) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (this._useMemoryBackend()) {
|
|
367
|
+
const row = this._memory.refs[normalizedTaskRef];
|
|
368
|
+
return row ? { ...row, metadata: { ...(row.metadata || {}) } } : null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!await this.ensureReady()) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const row = this._db
|
|
376
|
+
.prepare(`
|
|
377
|
+
SELECT t.task_ref, t.scene_id, t.spec_id, t.task_key, t.task_no, t.source, t.metadata_json,
|
|
378
|
+
s.scene_no, p.spec_no
|
|
379
|
+
FROM task_ref_registry t
|
|
380
|
+
INNER JOIN scene_registry s ON s.scene_id = t.scene_id
|
|
381
|
+
INNER JOIN spec_registry p ON p.scene_id = t.scene_id AND p.spec_id = t.spec_id
|
|
382
|
+
WHERE t.task_ref = ?
|
|
383
|
+
`)
|
|
384
|
+
.get(normalizedTaskRef);
|
|
385
|
+
|
|
386
|
+
return this._mapTaskRefRow(row);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async lookupTaskTuple(options = {}) {
|
|
390
|
+
const sceneId = normalizeString(options.sceneId);
|
|
391
|
+
const specId = normalizeString(options.specId);
|
|
392
|
+
const taskKey = normalizeString(options.taskKey);
|
|
393
|
+
if (!sceneId || !specId || !taskKey) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (this._useMemoryBackend()) {
|
|
398
|
+
const tupleKey = `${sceneId}::${specId}::${taskKey}`;
|
|
399
|
+
const row = this._memory.tasks[tupleKey];
|
|
400
|
+
return row ? { ...row, metadata: { ...(row.metadata || {}) } } : null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!await this.ensureReady()) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const row = this._db
|
|
408
|
+
.prepare(`
|
|
409
|
+
SELECT t.task_ref, t.scene_id, t.spec_id, t.task_key, t.task_no, t.source, t.metadata_json,
|
|
410
|
+
s.scene_no, p.spec_no
|
|
411
|
+
FROM task_ref_registry t
|
|
412
|
+
INNER JOIN scene_registry s ON s.scene_id = t.scene_id
|
|
413
|
+
INNER JOIN spec_registry p ON p.scene_id = t.scene_id AND p.spec_id = t.spec_id
|
|
414
|
+
WHERE t.scene_id = ? AND t.spec_id = ? AND t.task_key = ?
|
|
415
|
+
`)
|
|
416
|
+
.get(sceneId, specId, taskKey);
|
|
417
|
+
|
|
418
|
+
return this._mapTaskRefRow(row);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async appendStudioEvent(event = {}) {
|
|
422
|
+
const eventId = normalizeString(event.event_id);
|
|
423
|
+
const jobId = normalizeString(event.job_id);
|
|
424
|
+
const eventType = normalizeString(event.event_type);
|
|
425
|
+
const timestamp = normalizeString(event.timestamp) || this.now();
|
|
426
|
+
if (!eventId || !jobId || !eventType) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (this._useMemoryBackend()) {
|
|
431
|
+
if (!this._memory.events_by_job[jobId]) {
|
|
432
|
+
this._memory.events_by_job[jobId] = [];
|
|
433
|
+
}
|
|
434
|
+
const existingIndex = this._memory.events_by_job[jobId]
|
|
435
|
+
.findIndex((item) => normalizeString(item.event_id) === eventId);
|
|
436
|
+
const normalized = {
|
|
437
|
+
...event,
|
|
438
|
+
event_id: eventId,
|
|
439
|
+
job_id: jobId,
|
|
440
|
+
event_type: eventType,
|
|
441
|
+
timestamp: timestamp
|
|
442
|
+
};
|
|
443
|
+
if (existingIndex >= 0) {
|
|
444
|
+
this._memory.events_by_job[jobId][existingIndex] = normalized;
|
|
445
|
+
} else {
|
|
446
|
+
this._memory.events_by_job[jobId].push(normalized);
|
|
447
|
+
}
|
|
448
|
+
this._memory.events_by_job[jobId].sort((left, right) => {
|
|
449
|
+
const l = Date.parse(left.timestamp || '') || 0;
|
|
450
|
+
const r = Date.parse(right.timestamp || '') || 0;
|
|
451
|
+
return l - r;
|
|
452
|
+
});
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!await this.ensureReady()) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const sceneId = normalizeString(event.scene_id) || null;
|
|
461
|
+
const specId = normalizeString(event.spec_id) || null;
|
|
462
|
+
const rawJson = JSON.stringify(event);
|
|
463
|
+
|
|
464
|
+
this._db
|
|
465
|
+
.prepare(`
|
|
466
|
+
INSERT OR REPLACE INTO studio_event_stream(event_id, job_id, event_type, event_timestamp, scene_id, spec_id, created_at, raw_json)
|
|
467
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
|
468
|
+
`)
|
|
469
|
+
.run(eventId, jobId, eventType, timestamp, sceneId, specId, this.now(), rawJson);
|
|
470
|
+
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async listStudioEvents(jobId, options = {}) {
|
|
475
|
+
const normalizedJobId = normalizeString(jobId);
|
|
476
|
+
if (!normalizedJobId) {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (this._useMemoryBackend()) {
|
|
481
|
+
const events = Array.isArray(this._memory.events_by_job[normalizedJobId])
|
|
482
|
+
? [...this._memory.events_by_job[normalizedJobId]]
|
|
483
|
+
: [];
|
|
484
|
+
const limit = normalizeInteger(options.limit, 50);
|
|
485
|
+
if (limit <= 0) {
|
|
486
|
+
return events;
|
|
487
|
+
}
|
|
488
|
+
return events.slice(-limit);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!await this.ensureReady()) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const limit = normalizeInteger(options.limit, 50);
|
|
496
|
+
const query = limit > 0
|
|
497
|
+
? 'SELECT raw_json FROM studio_event_stream WHERE job_id = ? ORDER BY event_timestamp DESC LIMIT ?'
|
|
498
|
+
: 'SELECT raw_json FROM studio_event_stream WHERE job_id = ? ORDER BY event_timestamp DESC';
|
|
499
|
+
|
|
500
|
+
const statement = this._db.prepare(query);
|
|
501
|
+
const rows = limit > 0
|
|
502
|
+
? statement.all(normalizedJobId, limit)
|
|
503
|
+
: statement.all(normalizedJobId);
|
|
504
|
+
|
|
505
|
+
const events = rows
|
|
506
|
+
.map((row) => parseJsonSafe(row.raw_json, null))
|
|
507
|
+
.filter(Boolean)
|
|
508
|
+
.reverse();
|
|
509
|
+
|
|
510
|
+
return events;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
_resolveOrCreateTaskRefInMemory(options = {}) {
|
|
514
|
+
const sceneId = normalizeString(options.sceneId);
|
|
515
|
+
const specId = normalizeString(options.specId);
|
|
516
|
+
const taskKey = normalizeString(options.taskKey);
|
|
517
|
+
const source = normalizeString(options.source) || 'unknown';
|
|
518
|
+
const metadata = options.metadata && typeof options.metadata === 'object'
|
|
519
|
+
? options.metadata
|
|
520
|
+
: {};
|
|
521
|
+
const nowIso = this.now();
|
|
522
|
+
|
|
523
|
+
if (!this._memory.scenes[sceneId]) {
|
|
524
|
+
this._memory.scenes[sceneId] = normalizeInteger(this._memory.sequences.scene_next, 1);
|
|
525
|
+
this._memory.sequences.scene_next = this._memory.scenes[sceneId] + 1;
|
|
526
|
+
}
|
|
527
|
+
const sceneNo = this._memory.scenes[sceneId];
|
|
528
|
+
|
|
529
|
+
const sceneSpecKey = `${sceneId}::${specId}`;
|
|
530
|
+
if (!this._memory.specs[sceneSpecKey]) {
|
|
531
|
+
const nextSpec = normalizeInteger(this._memory.sequences.spec_next_by_scene[sceneId], 1);
|
|
532
|
+
this._memory.specs[sceneSpecKey] = nextSpec;
|
|
533
|
+
this._memory.sequences.spec_next_by_scene[sceneId] = nextSpec + 1;
|
|
534
|
+
}
|
|
535
|
+
const specNo = this._memory.specs[sceneSpecKey];
|
|
536
|
+
|
|
537
|
+
const tupleKey = `${sceneId}::${specId}::${taskKey}`;
|
|
538
|
+
if (this._memory.tasks[tupleKey]) {
|
|
539
|
+
const existing = this._memory.tasks[tupleKey];
|
|
540
|
+
existing.source = source;
|
|
541
|
+
existing.metadata = { ...(existing.metadata || {}), ...metadata };
|
|
542
|
+
existing.updated_at = nowIso;
|
|
543
|
+
this._memory.refs[existing.task_ref] = existing;
|
|
544
|
+
return { ...existing, metadata: { ...(existing.metadata || {}) } };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const nextTask = normalizeInteger(this._memory.sequences.task_next_by_scene_spec[sceneSpecKey], 1);
|
|
548
|
+
const taskNo = nextTask;
|
|
549
|
+
this._memory.sequences.task_next_by_scene_spec[sceneSpecKey] = nextTask + 1;
|
|
550
|
+
const taskRef = buildTaskRef(sceneNo, specNo, taskNo);
|
|
551
|
+
|
|
552
|
+
const row = {
|
|
553
|
+
task_ref: taskRef,
|
|
554
|
+
scene_id: sceneId,
|
|
555
|
+
spec_id: specId,
|
|
556
|
+
task_key: taskKey,
|
|
557
|
+
scene_no: sceneNo,
|
|
558
|
+
spec_no: specNo,
|
|
559
|
+
task_no: taskNo,
|
|
560
|
+
source,
|
|
561
|
+
metadata: { ...metadata },
|
|
562
|
+
created_at: nowIso,
|
|
563
|
+
updated_at: nowIso
|
|
564
|
+
};
|
|
565
|
+
this._memory.tasks[tupleKey] = row;
|
|
566
|
+
this._memory.refs[taskRef] = row;
|
|
567
|
+
return { ...row, metadata: { ...(row.metadata || {}) } };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const STORE_CACHE = new Map();
|
|
572
|
+
|
|
573
|
+
function getSceStateStore(projectPath = process.cwd(), options = {}) {
|
|
574
|
+
const normalizedRoot = path.resolve(projectPath);
|
|
575
|
+
if (options.noCache === true) {
|
|
576
|
+
return new SceStateStore(normalizedRoot, options);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!STORE_CACHE.has(normalizedRoot)) {
|
|
580
|
+
STORE_CACHE.set(normalizedRoot, new SceStateStore(normalizedRoot, options));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return STORE_CACHE.get(normalizedRoot);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
module.exports = {
|
|
587
|
+
DEFAULT_BACKEND,
|
|
588
|
+
DEFAULT_DB_RELATIVE_PATH,
|
|
589
|
+
SceStateStore,
|
|
590
|
+
getSceStateStore,
|
|
591
|
+
resolveBackend,
|
|
592
|
+
buildTaskRef,
|
|
593
|
+
formatSegment
|
|
594
|
+
};
|