ultra-builder-pro-cli 0.24.2 → 0.25.1
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 +51 -0
- package/adapters/claude.js +104 -0
- package/adapters/codex.js +363 -40
- package/adapters/grok.js +1254 -58
- package/adapters/kimi.js +682 -13
- package/adapters/opencode.js +132 -11
- package/docs/AGENT-CONTEXT.md +14 -0
- package/docs/ARTIFACT-AUTHORITY.md +13 -0
- package/docs/COMMIT-HASH-BACKFILL.md +25 -18
- package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
- package/mcp-server/lib/artifact-registry.cjs +181 -51
- package/mcp-server/lib/baseline-workflow.cjs +160 -23
- package/mcp-server/lib/change-workflow.cjs +21 -16
- package/mcp-server/lib/context-envelope.cjs +124 -51
- package/mcp-server/lib/decision-records.cjs +164 -78
- package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
- package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
- package/mcp-server/lib/managed-file-write.cjs +56 -0
- package/mcp-server/lib/prd-parser.cjs +2 -2
- package/mcp-server/lib/runtime-paths.cjs +43 -16
- package/mcp-server/lib/stage-checkpoints.cjs +163 -9
- package/mcp-server/lib/state-db.cjs +186 -8
- package/mcp-server/lib/state-ops.cjs +214 -31
- package/mcp-server/lib/task-contract.cjs +404 -0
- package/mcp-server/lib/task-expander.cjs +2 -2
- package/mcp-server/lib/task-ledger.cjs +175 -62
- package/mcp-server/lib/ultra-facade.cjs +1786 -169
- package/mcp-server/lib/worker-packet.cjs +2 -2
- package/mcp-server/server.cjs +23 -4
- package/orchestrator/parallel-orchestrator.cjs +114 -27
- package/orchestrator/session-runner.cjs +25 -136
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +6 -1
- package/skills/ultra-dev/SKILL.md +9 -4
- package/skills/ultra-plan/SKILL.md +13 -4
- package/skills/ultra-plan/references/semantic-preflight.md +6 -4
- package/spec/mcp-tools.yaml +114 -9
- package/spec/schemas/context-file.v4.5.schema.json +2 -2
- package/spec/schemas/state-db.sql +10 -6
- package/spec/schemas/task-ledger.v2.schema.json +23 -8
- package/spec/schemas/tasks.v4.5.schema.json +34 -12
|
@@ -9,7 +9,7 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
|
9
9
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
10
10
|
: path.resolve(__dirname, '..', '..');
|
|
11
11
|
const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
|
|
12
|
-
const EXPECTED_VERSION = '
|
|
12
|
+
const EXPECTED_VERSION = '23.0';
|
|
13
13
|
const KIMI_SCHEMA_VERSION = '9.1';
|
|
14
14
|
const CONTEXT_SCHEMA_VERSION = '10.0';
|
|
15
15
|
const BASELINE_SCHEMA_VERSION = '11.0';
|
|
@@ -24,6 +24,7 @@ const DECISION_COMPLETION_SCHEMA_VERSION = '19.0';
|
|
|
24
24
|
const ARTIFACT_REGISTRY_SCHEMA_VERSION = '20.0';
|
|
25
25
|
const KERNEL_SCHEMA_VERSION = '21.0';
|
|
26
26
|
const CHECKPOINT_NATIVE_SCHEMA_VERSION = '22.0';
|
|
27
|
+
const SEMANTIC_KERNEL_SCHEMA_VERSION = '23.0';
|
|
27
28
|
|
|
28
29
|
const MIGRATED_GAPS = Object.freeze([{
|
|
29
30
|
id: 'legacy-rebaseline-required',
|
|
@@ -306,9 +307,154 @@ function applyChangeColumns(db) {
|
|
|
306
307
|
db, 'changes', columns, 'alignment_thread_id',
|
|
307
308
|
'TEXT REFERENCES decision_threads(id) ON DELETE SET NULL',
|
|
308
309
|
) || changed;
|
|
310
|
+
changed = addColumnIfMissing(
|
|
311
|
+
db, 'changes', columns, 'supersedes_id',
|
|
312
|
+
'TEXT REFERENCES changes(id) ON DELETE SET NULL',
|
|
313
|
+
) || changed;
|
|
309
314
|
return changed;
|
|
310
315
|
}
|
|
311
316
|
|
|
317
|
+
function upgradeSemanticKernelConstraints(db) {
|
|
318
|
+
const tables = new Set(tableNames(db));
|
|
319
|
+
if (!tables.has('changes') || !tables.has('tasks')) return false;
|
|
320
|
+
const taskColumns = columnNames(db, 'tasks');
|
|
321
|
+
const preservesLegacyComplexityHint = taskColumns.has('complexity_hint');
|
|
322
|
+
const changesSql = db.prepare(
|
|
323
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'changes'",
|
|
324
|
+
).get()?.sql || '';
|
|
325
|
+
const tasksSql = db.prepare(
|
|
326
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'tasks'",
|
|
327
|
+
).get()?.sql || '';
|
|
328
|
+
const current = /length\s*\(\s*trim\s*\(\s*kind\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
|
|
329
|
+
.test(changesSql)
|
|
330
|
+
&& /length\s*\(\s*trim\s*\(\s*type\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
|
|
331
|
+
.test(tasksSql)
|
|
332
|
+
&& /length\s*\(\s*trim\s*\(\s*priority\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
|
|
333
|
+
.test(tasksSql)
|
|
334
|
+
&& /length\s*\(\s*trim\s*\(\s*slice_kind\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
|
|
335
|
+
.test(tasksSql)
|
|
336
|
+
&& columnNames(db, 'changes').has('supersedes_id');
|
|
337
|
+
if (current) return false;
|
|
338
|
+
|
|
339
|
+
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
340
|
+
db.pragma('foreign_keys = OFF');
|
|
341
|
+
try {
|
|
342
|
+
db.transaction(() => {
|
|
343
|
+
db.exec(`
|
|
344
|
+
DROP TABLE IF EXISTS changes_semantic_kernel_upgrade;
|
|
345
|
+
CREATE TABLE changes_semantic_kernel_upgrade (
|
|
346
|
+
id TEXT PRIMARY KEY,
|
|
347
|
+
title TEXT NOT NULL,
|
|
348
|
+
kind TEXT NOT NULL CHECK (length(trim(kind)) BETWEEN 1 AND 80),
|
|
349
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
350
|
+
CHECK (status IN ('active', 'blocked', 'ready', 'archived', 'cancelled')),
|
|
351
|
+
intent TEXT NOT NULL,
|
|
352
|
+
docs_impact_json TEXT NOT NULL DEFAULT '{"status":"unknown","files":[],"rationale":null}',
|
|
353
|
+
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
354
|
+
baseline_bypass_json TEXT,
|
|
355
|
+
contract_json TEXT NOT NULL DEFAULT '{}',
|
|
356
|
+
classification_json TEXT NOT NULL DEFAULT '{}',
|
|
357
|
+
research_disposition_json TEXT NOT NULL DEFAULT '{}',
|
|
358
|
+
alignment_thread_id TEXT REFERENCES decision_threads(id) ON DELETE SET NULL,
|
|
359
|
+
base_commit TEXT,
|
|
360
|
+
supersedes_id TEXT REFERENCES changes_semantic_kernel_upgrade(id) ON DELETE SET NULL,
|
|
361
|
+
artifact_root TEXT NOT NULL,
|
|
362
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
363
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
364
|
+
closed_at TEXT
|
|
365
|
+
);
|
|
366
|
+
INSERT INTO changes_semantic_kernel_upgrade (
|
|
367
|
+
id, title, kind, status, intent, docs_impact_json, provider_refs_json,
|
|
368
|
+
baseline_bypass_json, contract_json, classification_json,
|
|
369
|
+
research_disposition_json, alignment_thread_id, base_commit,
|
|
370
|
+
supersedes_id, artifact_root, created_at, updated_at, closed_at
|
|
371
|
+
)
|
|
372
|
+
SELECT
|
|
373
|
+
id, title, kind, status, intent, docs_impact_json, provider_refs_json,
|
|
374
|
+
baseline_bypass_json, contract_json, classification_json,
|
|
375
|
+
research_disposition_json, alignment_thread_id, base_commit,
|
|
376
|
+
supersedes_id, artifact_root, created_at, updated_at, closed_at
|
|
377
|
+
FROM changes;
|
|
378
|
+
|
|
379
|
+
DROP TABLE IF EXISTS tasks_semantic_kernel_upgrade;
|
|
380
|
+
CREATE TABLE tasks_semantic_kernel_upgrade (
|
|
381
|
+
id TEXT PRIMARY KEY,
|
|
382
|
+
title TEXT NOT NULL,
|
|
383
|
+
type TEXT NOT NULL CHECK (length(trim(type)) BETWEEN 1 AND 80),
|
|
384
|
+
priority TEXT NOT NULL CHECK (length(trim(priority)) BETWEEN 1 AND 80),
|
|
385
|
+
complexity INTEGER CHECK (complexity BETWEEN 1 AND 10),
|
|
386
|
+
estimated_days REAL CHECK (estimated_days IS NULL OR estimated_days > 0),
|
|
387
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
388
|
+
CHECK (status IN ('pending', 'in_progress', 'completed', 'blocked', 'expanded')),
|
|
389
|
+
deps TEXT,
|
|
390
|
+
files_modified TEXT,
|
|
391
|
+
session_id TEXT,
|
|
392
|
+
stale INTEGER NOT NULL DEFAULT 0 CHECK (stale IN (0, 1)),
|
|
393
|
+
${preservesLegacyComplexityHint ? 'complexity_hint TEXT,' : ''}
|
|
394
|
+
tag TEXT,
|
|
395
|
+
trace_to TEXT,
|
|
396
|
+
outcome TEXT,
|
|
397
|
+
slice_kind TEXT CHECK (
|
|
398
|
+
slice_kind IS NULL OR length(trim(slice_kind)) BETWEEN 1 AND 80
|
|
399
|
+
),
|
|
400
|
+
public_seam TEXT,
|
|
401
|
+
verification_command TEXT,
|
|
402
|
+
acceptance_json TEXT NOT NULL DEFAULT '[]',
|
|
403
|
+
context_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
404
|
+
docs_impact_json TEXT NOT NULL DEFAULT '{"status":"unknown","files":[],"rationale":null}',
|
|
405
|
+
ownership_json TEXT NOT NULL DEFAULT '{}',
|
|
406
|
+
context_file TEXT,
|
|
407
|
+
completion_commit TEXT,
|
|
408
|
+
change_id TEXT REFERENCES changes_semantic_kernel_upgrade(id) ON DELETE SET NULL,
|
|
409
|
+
parent_id TEXT REFERENCES tasks_semantic_kernel_upgrade(id) ON DELETE SET NULL,
|
|
410
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
411
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
412
|
+
);
|
|
413
|
+
INSERT INTO tasks_semantic_kernel_upgrade (
|
|
414
|
+
id, title, type, priority, complexity, estimated_days, status, deps,
|
|
415
|
+
files_modified, session_id, stale,
|
|
416
|
+
${preservesLegacyComplexityHint ? 'complexity_hint,' : ''}
|
|
417
|
+
tag, trace_to, outcome, slice_kind,
|
|
418
|
+
public_seam, verification_command, acceptance_json, context_refs_json,
|
|
419
|
+
docs_impact_json, ownership_json, context_file, completion_commit,
|
|
420
|
+
change_id, parent_id, created_at, updated_at
|
|
421
|
+
)
|
|
422
|
+
SELECT
|
|
423
|
+
id, title, type, priority, complexity, estimated_days, status, deps,
|
|
424
|
+
files_modified, session_id, stale,
|
|
425
|
+
${preservesLegacyComplexityHint ? 'complexity_hint,' : ''}
|
|
426
|
+
tag, trace_to, outcome, slice_kind,
|
|
427
|
+
public_seam, verification_command, acceptance_json, context_refs_json,
|
|
428
|
+
docs_impact_json, ownership_json, context_file, completion_commit,
|
|
429
|
+
change_id, parent_id, created_at, updated_at
|
|
430
|
+
FROM tasks;
|
|
431
|
+
|
|
432
|
+
DROP TABLE tasks;
|
|
433
|
+
DROP TABLE changes;
|
|
434
|
+
ALTER TABLE changes_semantic_kernel_upgrade RENAME TO changes;
|
|
435
|
+
ALTER TABLE tasks_semantic_kernel_upgrade RENAME TO tasks;
|
|
436
|
+
CREATE INDEX changes_status ON changes(status, created_at);
|
|
437
|
+
CREATE INDEX changes_kind ON changes(kind, created_at);
|
|
438
|
+
CREATE INDEX tasks_status ON tasks(status);
|
|
439
|
+
CREATE INDEX tasks_tag ON tasks(tag);
|
|
440
|
+
CREATE INDEX tasks_session ON tasks(session_id) WHERE session_id IS NOT NULL;
|
|
441
|
+
CREATE INDEX tasks_stale ON tasks(stale) WHERE stale = 1;
|
|
442
|
+
CREATE INDEX tasks_parent ON tasks(parent_id) WHERE parent_id IS NOT NULL;
|
|
443
|
+
CREATE INDEX tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL;
|
|
444
|
+
`);
|
|
445
|
+
const violations = db.pragma('foreign_key_check');
|
|
446
|
+
if (violations.length > 0) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`semantic kernel migration produced ${violations.length} foreign key violation(s)`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
})();
|
|
452
|
+
} finally {
|
|
453
|
+
db.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`);
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
312
458
|
function applySemanticAuthorityColumns(db) {
|
|
313
459
|
let changed = false;
|
|
314
460
|
if (tableNames(db).includes('workflow_steps')) {
|
|
@@ -416,7 +562,7 @@ function applyCompatibleColumns(db) {
|
|
|
416
562
|
}
|
|
417
563
|
const taskAdditions = [
|
|
418
564
|
['outcome', 'TEXT'],
|
|
419
|
-
['slice_kind',
|
|
565
|
+
['slice_kind', 'TEXT CHECK (slice_kind IS NULL OR length(trim(slice_kind)) BETWEEN 1 AND 80)'],
|
|
420
566
|
['public_seam', 'TEXT'],
|
|
421
567
|
['verification_command', 'TEXT'],
|
|
422
568
|
['acceptance_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
@@ -486,7 +632,7 @@ function migrateAdaptiveWorkflowRuns(db, fromVersion = latestSchemaVersion(db))
|
|
|
486
632
|
if ([
|
|
487
633
|
ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
488
634
|
ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
|
|
489
|
-
CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
635
|
+
CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
490
636
|
].includes(fromVersion) || existing) {
|
|
491
637
|
return false;
|
|
492
638
|
}
|
|
@@ -1007,6 +1153,11 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1007
1153
|
}
|
|
1008
1154
|
const gitStateChangedFrom = upgradeBaselineWorktreeConstraint(db, fromVersion);
|
|
1009
1155
|
const decisionCompletionChanged = upgradeDecisionThreadLifecycle(db);
|
|
1156
|
+
db.transaction(() => {
|
|
1157
|
+
applyCompatibleColumns(db);
|
|
1158
|
+
applyChangeColumns(db);
|
|
1159
|
+
})();
|
|
1160
|
+
const semanticKernelChanged = upgradeSemanticKernelConstraints(db);
|
|
1010
1161
|
db.transaction(() => {
|
|
1011
1162
|
applyCompatibleColumns(db);
|
|
1012
1163
|
const contextChanged = applyContextSpineUpgrade(db);
|
|
@@ -1020,6 +1171,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1020
1171
|
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
1021
1172
|
DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
|
|
1022
1173
|
KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1174
|
+
SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1023
1175
|
].includes(fromVersion)
|
|
1024
1176
|
&& !contextMigration,
|
|
1025
1177
|
);
|
|
@@ -1049,6 +1201,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1049
1201
|
GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
1050
1202
|
DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
|
|
1051
1203
|
KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1204
|
+
SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1052
1205
|
].includes(fromVersion)
|
|
1053
1206
|
&& !baselineMigration) {
|
|
1054
1207
|
db.prepare(
|
|
@@ -1123,6 +1276,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1123
1276
|
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
1124
1277
|
DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
|
|
1125
1278
|
KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1279
|
+
SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1126
1280
|
].includes(fromVersion) && !workflowMigration) {
|
|
1127
1281
|
db.prepare(
|
|
1128
1282
|
`INSERT INTO migration_history
|
|
@@ -1141,7 +1295,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1141
1295
|
AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
|
|
1142
1296
|
GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
1143
1297
|
ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
|
|
1144
|
-
CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1298
|
+
CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1145
1299
|
].includes(fromVersion) && !authorityMigration) {
|
|
1146
1300
|
db.prepare(
|
|
1147
1301
|
`INSERT INTO migration_history
|
|
@@ -1160,7 +1314,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1160
1314
|
SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION,
|
|
1161
1315
|
ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
1162
1316
|
ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
|
|
1163
|
-
CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1317
|
+
CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1164
1318
|
].includes(fromVersion) && !semanticMigration) {
|
|
1165
1319
|
db.prepare(
|
|
1166
1320
|
`INSERT INTO migration_history
|
|
@@ -1179,6 +1333,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1179
1333
|
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
1180
1334
|
DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
|
|
1181
1335
|
KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1336
|
+
SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1182
1337
|
].includes(fromVersion)
|
|
1183
1338
|
&& !dialogueMigration) {
|
|
1184
1339
|
db.prepare(
|
|
@@ -1197,7 +1352,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1197
1352
|
if (fromVersion && ![
|
|
1198
1353
|
GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
1199
1354
|
ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
|
|
1200
|
-
CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1355
|
+
CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1201
1356
|
].includes(fromVersion)
|
|
1202
1357
|
&& !gitStateMigration) {
|
|
1203
1358
|
db.prepare(
|
|
@@ -1217,7 +1372,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1217
1372
|
if (fromVersion && ![
|
|
1218
1373
|
ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
1219
1374
|
ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
|
|
1220
|
-
CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1375
|
+
CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1221
1376
|
].includes(fromVersion) && !adaptiveMigration) {
|
|
1222
1377
|
db.prepare(
|
|
1223
1378
|
`INSERT INTO migration_history
|
|
@@ -1237,6 +1392,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1237
1392
|
if (fromVersion && ![
|
|
1238
1393
|
DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
|
|
1239
1394
|
KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1395
|
+
SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1240
1396
|
].includes(fromVersion) && !completionMigration) {
|
|
1241
1397
|
db.prepare(
|
|
1242
1398
|
`INSERT INTO migration_history
|
|
@@ -1256,6 +1412,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1256
1412
|
if (fromVersion && fromVersion !== ARTIFACT_REGISTRY_SCHEMA_VERSION
|
|
1257
1413
|
&& fromVersion !== KERNEL_SCHEMA_VERSION
|
|
1258
1414
|
&& fromVersion !== CHECKPOINT_NATIVE_SCHEMA_VERSION
|
|
1415
|
+
&& fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
|
|
1259
1416
|
&& !artifactRegistryMigration) {
|
|
1260
1417
|
db.prepare(
|
|
1261
1418
|
`INSERT INTO migration_history
|
|
@@ -1275,6 +1432,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1275
1432
|
if (fromVersion
|
|
1276
1433
|
&& fromVersion !== KERNEL_SCHEMA_VERSION
|
|
1277
1434
|
&& fromVersion !== CHECKPOINT_NATIVE_SCHEMA_VERSION
|
|
1435
|
+
&& fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
|
|
1278
1436
|
&& !kernelMigration) {
|
|
1279
1437
|
db.prepare(
|
|
1280
1438
|
`INSERT INTO migration_history
|
|
@@ -1291,6 +1449,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1291
1449
|
).get();
|
|
1292
1450
|
if (fromVersion
|
|
1293
1451
|
&& fromVersion !== CHECKPOINT_NATIVE_SCHEMA_VERSION
|
|
1452
|
+
&& fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
|
|
1294
1453
|
&& !checkpointNativeMigration) {
|
|
1295
1454
|
db.prepare(
|
|
1296
1455
|
`INSERT INTO migration_history
|
|
@@ -1302,6 +1461,24 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1302
1461
|
'Bind Baseline research to Stage Checkpoints and add byte digests for Context Envelopes and Worker Packets',
|
|
1303
1462
|
);
|
|
1304
1463
|
}
|
|
1464
|
+
const semanticKernelMigration = db.prepare(
|
|
1465
|
+
"SELECT 1 FROM migration_history WHERE to_version = '23.0' AND status = 'success' LIMIT 1",
|
|
1466
|
+
).get();
|
|
1467
|
+
if (fromVersion
|
|
1468
|
+
&& fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
|
|
1469
|
+
&& !semanticKernelMigration) {
|
|
1470
|
+
db.prepare(
|
|
1471
|
+
`INSERT INTO migration_history
|
|
1472
|
+
(from_version, to_version, direction, status, notes)
|
|
1473
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
1474
|
+
).run(
|
|
1475
|
+
CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1476
|
+
SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1477
|
+
semanticKernelChanged
|
|
1478
|
+
? 'Move Task and Change business vocabulary out of SQLite enums and add immutable Change successor linkage'
|
|
1479
|
+
: 'Record semantic-kernel vocabulary and Change successor authority',
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1305
1482
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
1306
1483
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
1307
1484
|
ensureActiveSessionLeaseIndex(db);
|
|
@@ -1309,7 +1486,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
1309
1486
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
1310
1487
|
).run(
|
|
1311
1488
|
EXPECTED_VERSION,
|
|
1312
|
-
'
|
|
1489
|
+
'Persistence and safety kernel with open semantic vocabulary and immutable Change successors',
|
|
1313
1490
|
);
|
|
1314
1491
|
})();
|
|
1315
1492
|
}
|
|
@@ -1343,6 +1520,7 @@ function initStateDb(dbPath) {
|
|
|
1343
1520
|
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
1344
1521
|
DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
|
|
1345
1522
|
KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
|
|
1523
|
+
SEMANTIC_KERNEL_SCHEMA_VERSION,
|
|
1346
1524
|
].includes(fromVersion);
|
|
1347
1525
|
backupPath = migrationBackup(db, dbPath, fromVersion, existing);
|
|
1348
1526
|
// This must precede every ALTER, table rebuild, or index creation. Runtime
|
|
@@ -13,8 +13,14 @@ const fs = require('node:fs');
|
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
const { spawnSync } = require('node:child_process');
|
|
15
15
|
|
|
16
|
+
const canonical = require('./canonical-json.cjs');
|
|
16
17
|
const { openStateDb } = require('./state-db.cjs');
|
|
18
|
+
const {
|
|
19
|
+
TASK_CONTRACT_DEFINE_FIELDS,
|
|
20
|
+
TASK_CONTRACT_PATCH_FIELDS,
|
|
21
|
+
} = require('./task-contract.cjs');
|
|
17
22
|
const { isSupportedRuntime } = require('../../adapters/_shared/runtime-assets.cjs');
|
|
23
|
+
const closeJournal = require('../../orchestrator/session-close-journal.cjs');
|
|
18
24
|
|
|
19
25
|
const STATUS_TRANSITIONS = Object.freeze({
|
|
20
26
|
pending: new Set(['in_progress', 'blocked', 'expanded']),
|
|
@@ -35,10 +41,8 @@ const TASK_FIELDS = Object.freeze([
|
|
|
35
41
|
]);
|
|
36
42
|
|
|
37
43
|
const PATCHABLE_FIELDS = Object.freeze([
|
|
38
|
-
|
|
39
|
-
'session_id', 'stale', 'tag',
|
|
40
|
-
'outcome', 'slice_kind', 'public_seam', 'verification_command',
|
|
41
|
-
'acceptance', 'context_refs', 'docs_impact', 'ownership',
|
|
44
|
+
...TASK_CONTRACT_PATCH_FIELDS,
|
|
45
|
+
'session_id', 'stale', 'tag',
|
|
42
46
|
'context_file', 'completion_commit',
|
|
43
47
|
'change_id',
|
|
44
48
|
]);
|
|
@@ -48,11 +52,7 @@ const STALE_RECONCILIATION_FIELDS = Object.freeze([
|
|
|
48
52
|
'verification_command', 'acceptance', 'context_refs', 'docs_impact', 'ownership',
|
|
49
53
|
]);
|
|
50
54
|
|
|
51
|
-
const
|
|
52
|
-
'priority', 'complexity', 'estimated_days', 'deps', 'files_modified', 'tag',
|
|
53
|
-
'trace_to', 'outcome', 'slice_kind', 'public_seam', 'verification_command',
|
|
54
|
-
'acceptance', 'context_refs', 'docs_impact', 'ownership',
|
|
55
|
-
]);
|
|
55
|
+
const TASK_CONTRACT_PATCH_FIELD_SET = new Set(TASK_CONTRACT_PATCH_FIELDS);
|
|
56
56
|
|
|
57
57
|
const SESSION_PATCHABLE = Object.freeze([
|
|
58
58
|
'pid', 'status', 'lease_expires_at', 'heartbeat_at', 'worktree_path', 'artifact_dir',
|
|
@@ -142,7 +142,7 @@ function rowToTask(row) {
|
|
|
142
142
|
delete out[column];
|
|
143
143
|
}
|
|
144
144
|
if (out.stale !== undefined && out.stale !== null) {
|
|
145
|
-
out.stale = Boolean(out.stale);
|
|
145
|
+
if (out.stale === 0 || out.stale === 1) out.stale = Boolean(out.stale);
|
|
146
146
|
}
|
|
147
147
|
return out;
|
|
148
148
|
}
|
|
@@ -197,11 +197,17 @@ function normalizeContextRefs(value) {
|
|
|
197
197
|
`context_refs[${index}].freshness_policy must be digest, existence, or advisory`,
|
|
198
198
|
);
|
|
199
199
|
}
|
|
200
|
+
if (item.required !== undefined && typeof item.required !== 'boolean') {
|
|
201
|
+
throw new StateOpsError(
|
|
202
|
+
'VALIDATION_ERROR',
|
|
203
|
+
`context_refs[${index}].required must be a boolean`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
200
206
|
const normalized = {
|
|
201
207
|
ref,
|
|
202
208
|
reason,
|
|
203
209
|
kind,
|
|
204
|
-
required: item.required
|
|
210
|
+
required: item.required === undefined ? true : item.required,
|
|
205
211
|
freshness_policy: freshnessPolicy,
|
|
206
212
|
};
|
|
207
213
|
if (expectedDigest !== null) normalized.expected_digest = String(expectedDigest);
|
|
@@ -254,7 +260,7 @@ function normalizeOwnership(value) {
|
|
|
254
260
|
function taskContractBlockers(task) {
|
|
255
261
|
const blockers = [];
|
|
256
262
|
if (!String(task?.outcome || '').trim()) blockers.push('TASK_OUTCOME_MISSING');
|
|
257
|
-
if (!
|
|
263
|
+
if (!String(task?.slice_kind || '').trim()) {
|
|
258
264
|
blockers.push('TASK_SLICE_KIND_MISSING');
|
|
259
265
|
}
|
|
260
266
|
if (!String(task?.public_seam || '').trim()) blockers.push('TASK_PUBLIC_SEAM_MISSING');
|
|
@@ -548,8 +554,6 @@ function patchTask(db, id, patch = {}) {
|
|
|
548
554
|
params.push(id);
|
|
549
555
|
db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
550
556
|
const updated = readTask(db, id);
|
|
551
|
-
if (clearingStale) assertTaskExecutionContract(updated);
|
|
552
|
-
|
|
553
557
|
if (nextStatus && nextStatus !== current.status) {
|
|
554
558
|
appendEventInTx(db, {
|
|
555
559
|
type: current.status === 'completed' && nextStatus === 'in_progress'
|
|
@@ -566,7 +570,9 @@ function patchTask(db, id, patch = {}) {
|
|
|
566
570
|
},
|
|
567
571
|
});
|
|
568
572
|
}
|
|
569
|
-
const contractFields = Object.keys(patch).filter(
|
|
573
|
+
const contractFields = Object.keys(patch).filter(
|
|
574
|
+
(field) => TASK_CONTRACT_PATCH_FIELD_SET.has(field),
|
|
575
|
+
);
|
|
570
576
|
if (contractFields.length > 0) {
|
|
571
577
|
appendEventInTx(db, {
|
|
572
578
|
type: clearingStale ? 'task_contract_reconciled' : 'task_contract_updated',
|
|
@@ -597,37 +603,214 @@ function updateTaskStatus(db, id, nextStatus) {
|
|
|
597
603
|
return patchTask(db, id, { status: nextStatus });
|
|
598
604
|
}
|
|
599
605
|
|
|
600
|
-
function deleteTask(db, id, { force = false } = {}) {
|
|
606
|
+
function deleteTask(db, id, { force = false, rootDir = null } = {}) {
|
|
601
607
|
return tx(db, () => {
|
|
602
|
-
const
|
|
603
|
-
if (!
|
|
604
|
-
|
|
605
|
-
|
|
608
|
+
const task = readTask(db, id);
|
|
609
|
+
if (!task) throw new StateOpsError('TASK_NOT_FOUND', `no task ${id}`);
|
|
610
|
+
const lifecycleEvents = db.prepare(
|
|
611
|
+
'SELECT type FROM events WHERE task_id = ? ORDER BY id',
|
|
612
|
+
).all(id).map((row) => row.type).filter((type) => (
|
|
613
|
+
[
|
|
614
|
+
'task_started',
|
|
615
|
+
'task_completed',
|
|
616
|
+
'task_blocked',
|
|
617
|
+
'task_reopened',
|
|
618
|
+
'task_stale_marked',
|
|
619
|
+
'task_expanded',
|
|
620
|
+
].includes(type)
|
|
621
|
+
|| type.startsWith('session_')
|
|
622
|
+
|| type.startsWith('worker_packet_')
|
|
623
|
+
|| type.startsWith('task_failure')
|
|
624
|
+
|| type.startsWith('task_circuit')
|
|
625
|
+
|| type.includes('checkpoint')
|
|
626
|
+
|| type.startsWith('workflow_')
|
|
627
|
+
));
|
|
628
|
+
const references = [
|
|
629
|
+
['sessions', db.prepare(
|
|
630
|
+
'SELECT COUNT(*) AS count FROM sessions WHERE task_id = ?',
|
|
631
|
+
).get(id).count],
|
|
632
|
+
['children', db.prepare(
|
|
633
|
+
'SELECT COUNT(*) AS count FROM tasks WHERE parent_id = ?',
|
|
634
|
+
).get(id).count],
|
|
635
|
+
['dependencies', db.prepare(
|
|
636
|
+
`SELECT COUNT(*) AS count FROM tasks
|
|
637
|
+
WHERE deps IS NOT NULL AND json_valid(deps)
|
|
638
|
+
AND EXISTS (SELECT 1 FROM json_each(tasks.deps) WHERE value = ?)`,
|
|
639
|
+
).get(id).count],
|
|
640
|
+
['worker_packets', db.prepare(
|
|
641
|
+
`SELECT COUNT(*) AS count FROM worker_packets
|
|
642
|
+
WHERE scope_type = 'task' AND scope_id = ?`,
|
|
643
|
+
).get(id).count],
|
|
644
|
+
['artifacts', db.prepare(
|
|
645
|
+
`SELECT COUNT(*) AS count FROM artifacts
|
|
646
|
+
WHERE task_id = ? OR (owner_type = 'task' AND owner_id = ?)`,
|
|
647
|
+
).get(id, id).count],
|
|
648
|
+
['artifact_edges', db.prepare(
|
|
649
|
+
`SELECT COUNT(*) AS count FROM artifact_edges
|
|
650
|
+
WHERE (source_type = 'task' AND source_id = ?)
|
|
651
|
+
OR (target_type = 'task' AND target_id = ?)`,
|
|
652
|
+
).get(id, id).count],
|
|
653
|
+
['stage_checkpoints', db.prepare(
|
|
654
|
+
`SELECT COUNT(*) AS count FROM stage_checkpoints
|
|
655
|
+
WHERE scope_type = 'task' AND scope_id = ?`,
|
|
656
|
+
).get(id).count],
|
|
657
|
+
['context_envelopes', db.prepare(
|
|
658
|
+
`SELECT COUNT(*) AS count FROM context_envelopes
|
|
659
|
+
WHERE scope_type = 'task' AND scope_id = ?`,
|
|
660
|
+
).get(id).count],
|
|
661
|
+
['workflow_runs', db.prepare(
|
|
662
|
+
'SELECT COUNT(*) AS count FROM workflow_runs WHERE task_id = ?',
|
|
663
|
+
).get(id).count],
|
|
664
|
+
['context_snapshots', db.prepare(
|
|
665
|
+
'SELECT COUNT(*) AS count FROM context_snapshots WHERE task_id = ?',
|
|
666
|
+
).get(id).count],
|
|
667
|
+
['incidents', db.prepare(
|
|
668
|
+
'SELECT COUNT(*) AS count FROM incidents WHERE task_id = ?',
|
|
669
|
+
).get(id).count],
|
|
670
|
+
['spec_learning_candidates', db.prepare(
|
|
671
|
+
'SELECT COUNT(*) AS count FROM spec_learning_candidates WHERE task_id = ?',
|
|
672
|
+
).get(id).count],
|
|
673
|
+
['trace_links', db.prepare(
|
|
674
|
+
'SELECT COUNT(*) AS count FROM trace_links WHERE task_id = ?',
|
|
675
|
+
).get(id).count],
|
|
676
|
+
['circuit_breaker', db.prepare(
|
|
677
|
+
'SELECT COUNT(*) AS count FROM circuit_breaker WHERE task_id = ?',
|
|
678
|
+
).get(id).count],
|
|
679
|
+
['session_close_journal', rootDir && closeJournal.findForTask(rootDir, id) ? 1 : 0],
|
|
680
|
+
].filter(([, count]) => count > 0)
|
|
681
|
+
.map(([kind, count]) => ({ kind, count }));
|
|
682
|
+
if (references.length > 0) {
|
|
683
|
+
throw new StateOpsError(
|
|
684
|
+
'TASK_DELETE_REFERENCED',
|
|
685
|
+
`task ${id} cannot be deleted because durable authority still references it`,
|
|
686
|
+
{ details: { task_id: id, references } },
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (!force && (
|
|
690
|
+
task.status !== 'pending' || task.session_id || task.completion_commit
|
|
691
|
+
|| lifecycleEvents.length > 0
|
|
692
|
+
)) {
|
|
693
|
+
throw new StateOpsError(
|
|
694
|
+
'TASK_DELETE_NOT_DRAFT',
|
|
695
|
+
`task ${id} is not an unowned pending draft`,
|
|
696
|
+
{
|
|
697
|
+
details: {
|
|
698
|
+
task_id: id,
|
|
699
|
+
status: task.status,
|
|
700
|
+
session_id: task.session_id,
|
|
701
|
+
completion_commit: task.completion_commit,
|
|
702
|
+
lifecycle_events: [...new Set(lifecycleEvents)],
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
);
|
|
606
706
|
}
|
|
607
707
|
db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
608
|
-
|
|
708
|
+
const contract = Object.fromEntries(
|
|
709
|
+
TASK_CONTRACT_DEFINE_FIELDS
|
|
710
|
+
.filter((field) => task[field] !== undefined && task[field] !== null)
|
|
711
|
+
.map((field) => [field, task[field]]),
|
|
712
|
+
);
|
|
713
|
+
const tombstone = appendEventInTx(db, {
|
|
714
|
+
type: 'task_deleted',
|
|
715
|
+
task_id: id,
|
|
716
|
+
change_id: task.change_id,
|
|
717
|
+
payload: {
|
|
718
|
+
id: task.id,
|
|
719
|
+
title: task.title,
|
|
720
|
+
type: task.type,
|
|
721
|
+
priority: task.priority,
|
|
722
|
+
reason: 'task_contract_remove',
|
|
723
|
+
contract_digest: canonical.digest(contract),
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
return { ok: true, tombstone };
|
|
609
727
|
});
|
|
610
728
|
}
|
|
611
729
|
|
|
612
730
|
// ─── events ──────────────────────────────────────────────────────────────
|
|
613
731
|
|
|
732
|
+
const EVENT_FIELDS = new Set([
|
|
733
|
+
'type', 'task_id', 'change_id', 'session_id', 'runtime', 'payload',
|
|
734
|
+
]);
|
|
735
|
+
|
|
736
|
+
function eventText(value, field, { nullable = false } = {}) {
|
|
737
|
+
if (value === null && nullable) return null;
|
|
738
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
739
|
+
throw new StateOpsError(
|
|
740
|
+
'VALIDATION_ERROR',
|
|
741
|
+
`${field} must be a non-empty string${nullable ? ' or null' : ''}`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
return value.trim();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function assertJsonValue(value, field, seen = new Set()) {
|
|
748
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
|
|
749
|
+
if (typeof value === 'number') {
|
|
750
|
+
if (Number.isFinite(value)) return;
|
|
751
|
+
throw new StateOpsError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
|
|
752
|
+
}
|
|
753
|
+
if (typeof value !== 'object') {
|
|
754
|
+
throw new StateOpsError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
|
|
755
|
+
}
|
|
756
|
+
if (seen.has(value)) {
|
|
757
|
+
throw new StateOpsError('VALIDATION_ERROR', `${field} must not contain circular references`);
|
|
758
|
+
}
|
|
759
|
+
if (!Array.isArray(value)) {
|
|
760
|
+
const prototype = Object.getPrototypeOf(value);
|
|
761
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
762
|
+
throw new StateOpsError('VALIDATION_ERROR', `${field} must contain only JSON objects`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
seen.add(value);
|
|
766
|
+
if (Array.isArray(value)) {
|
|
767
|
+
value.forEach((item, index) => assertJsonValue(item, `${field}[${index}]`, seen));
|
|
768
|
+
} else {
|
|
769
|
+
Object.entries(value).forEach(([key, item]) => (
|
|
770
|
+
assertJsonValue(item, `${field}.${key}`, seen)
|
|
771
|
+
));
|
|
772
|
+
}
|
|
773
|
+
seen.delete(value);
|
|
774
|
+
}
|
|
775
|
+
|
|
614
776
|
function appendEventInTx(db, event) {
|
|
615
|
-
if (!event ||
|
|
616
|
-
throw new StateOpsError('VALIDATION_ERROR', 'event
|
|
777
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
778
|
+
throw new StateOpsError('VALIDATION_ERROR', 'event must be an object');
|
|
779
|
+
}
|
|
780
|
+
const unknown = Object.keys(event).filter((field) => !EVENT_FIELDS.has(field));
|
|
781
|
+
if (unknown.length > 0) {
|
|
782
|
+
throw new StateOpsError('VALIDATION_ERROR', `event.${unknown[0]} is not allowed`);
|
|
617
783
|
}
|
|
618
|
-
|
|
619
|
-
|
|
784
|
+
const type = eventText(event.type, 'event.type');
|
|
785
|
+
const taskId = event.task_id === undefined
|
|
786
|
+
? null : eventText(event.task_id, 'event.task_id', { nullable: true });
|
|
787
|
+
const changeId = event.change_id === undefined
|
|
788
|
+
? null : eventText(event.change_id, 'event.change_id', { nullable: true });
|
|
789
|
+
const sessionId = event.session_id === undefined
|
|
790
|
+
? null : eventText(event.session_id, 'event.session_id', { nullable: true });
|
|
791
|
+
const runtime = event.runtime === undefined
|
|
792
|
+
? null : eventText(event.runtime, 'event.runtime', { nullable: true });
|
|
793
|
+
if (runtime !== null && !isSupportedRuntime(runtime)) {
|
|
794
|
+
throw new StateOpsError('VALIDATION_ERROR', `unsupported runtime: ${runtime}`);
|
|
795
|
+
}
|
|
796
|
+
let payloadJson = null;
|
|
797
|
+
if (event.payload !== undefined) {
|
|
798
|
+
if (!event.payload || typeof event.payload !== 'object' || Array.isArray(event.payload)) {
|
|
799
|
+
throw new StateOpsError('VALIDATION_ERROR', 'event.payload must be a JSON object');
|
|
800
|
+
}
|
|
801
|
+
assertJsonValue(event.payload, 'event.payload');
|
|
802
|
+
payloadJson = JSON.stringify(event.payload);
|
|
620
803
|
}
|
|
621
804
|
const result = db.prepare(
|
|
622
805
|
`INSERT INTO events (type, task_id, change_id, session_id, runtime, payload_json)
|
|
623
806
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
624
807
|
).run(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
808
|
+
type,
|
|
809
|
+
taskId,
|
|
810
|
+
changeId,
|
|
811
|
+
sessionId,
|
|
812
|
+
runtime,
|
|
813
|
+
payloadJson,
|
|
631
814
|
);
|
|
632
815
|
const row = db.prepare('SELECT id, ts FROM events WHERE id = ?').get(result.lastInsertRowid);
|
|
633
816
|
return { event_id: Number(row.id), ts: row.ts };
|