@opengsd/gsd-pi 1.10.0-dev.a5e42e99 → 1.11.0
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/README.md +10 -10
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/db/engine.js +8 -2
- package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
- package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
- package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +3 -3
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +1 -0
- package/dist/web/standalone/.next/static/chunks/app/{layout-4ae2d68984392bbf.js → layout-b35cbfff38aaf4cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{main-app-90d1d8d5e5d2dc6b.js → main-app-590a74400e35f685.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +1 -0
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +6 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/package.json +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/db/engine.ts +9 -1
- package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
- package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
- package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
- package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
- package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +0 -1
- package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +0 -1
- /package/dist/web/standalone/.next/static/{xwUaOqui04jIIFVsIBLiL → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{xwUaOqui04jIIFVsIBLiL → 8ub6oiEp2YnlOHQU20LJr}/_ssgManifest.js +0 -0
|
@@ -47,7 +47,9 @@ import {
|
|
|
47
47
|
applyMigrationV28MemoryLastHitAt,
|
|
48
48
|
applyMigrationV29RepositoryTargets,
|
|
49
49
|
applyMigrationV30ReworkBriefs,
|
|
50
|
+
applyMigrationV31CanonicalFoundation,
|
|
50
51
|
} from "../db-migration-steps.js";
|
|
52
|
+
import { createCanonicalFoundationSchemaV31 } from "../db-canonical-foundation-schema.js";
|
|
51
53
|
import {
|
|
52
54
|
isMemoriesFtsAvailableSchema,
|
|
53
55
|
rebuildMemoriesFtsSchemaOnce,
|
|
@@ -97,7 +99,7 @@ const providerLoader = createSqliteProviderLoader({
|
|
|
97
99
|
nodeVersion: process.versions.node,
|
|
98
100
|
writeStderr: (message: string) => process.stderr.write(message),
|
|
99
101
|
});
|
|
100
|
-
export const SCHEMA_VERSION =
|
|
102
|
+
export const SCHEMA_VERSION = 31;
|
|
101
103
|
function initSchema(db: DbAdapter, fileBacked: boolean, dbPath: string | null): void {
|
|
102
104
|
const conservativeFilePragmas = fileBacked && _isLikelyWslDrvFsPathForTest(dbPath);
|
|
103
105
|
if (fileBacked) db.exec(conservativeFilePragmas ? "PRAGMA journal_mode=DELETE" : "PRAGMA journal_mode=WAL");
|
|
@@ -135,6 +137,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean, dbPath: string | null):
|
|
|
135
137
|
} else {
|
|
136
138
|
createCoordinationTablesV24(db);
|
|
137
139
|
createRuntimeKvTableV25(db);
|
|
140
|
+
createCanonicalFoundationSchemaV31(db);
|
|
138
141
|
|
|
139
142
|
// Fresh install — all tables are created above with the full current schema,
|
|
140
143
|
// so it is safe to create all migration-specific indexes here. For existing
|
|
@@ -388,6 +391,11 @@ function migrateSchema(db: DbAdapter, dbPath: string | null): void {
|
|
|
388
391
|
recordSchemaVersion(db, 30);
|
|
389
392
|
}
|
|
390
393
|
|
|
394
|
+
if (currentVersion < 31) {
|
|
395
|
+
applyMigrationV31CanonicalFoundation(db);
|
|
396
|
+
recordSchemaVersion(db, 31);
|
|
397
|
+
}
|
|
398
|
+
|
|
391
399
|
if (_migrationFaultForTest) throw new Error("migration fault injected for test");
|
|
392
400
|
|
|
393
401
|
db.exec("COMMIT");
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Additive v31 authority, operation provenance, domain event, and outbox schema.
|
|
3
|
+
|
|
4
|
+
import type { DbAdapter } from "./db-adapter.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* v31 mapping:
|
|
8
|
+
* - schema_version remains DDL compatibility, not the domain revision.
|
|
9
|
+
* - audit_events remains optional operational telemetry, not domain history.
|
|
10
|
+
* - milestone_commit_attributions remains Git-specific, not operation provenance.
|
|
11
|
+
* - command_queue and runtime_kv remain coordination/cache surfaces, not an outbox.
|
|
12
|
+
*/
|
|
13
|
+
export function createCanonicalFoundationSchemaV31(db: DbAdapter): void {
|
|
14
|
+
db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS project_authority (
|
|
16
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
17
|
+
project_id TEXT NOT NULL UNIQUE,
|
|
18
|
+
project_root_realpath TEXT NOT NULL DEFAULT '',
|
|
19
|
+
revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
|
|
20
|
+
authority_epoch INTEGER NOT NULL DEFAULT 0 CHECK (authority_epoch >= 0),
|
|
21
|
+
created_at TEXT NOT NULL DEFAULT '',
|
|
22
|
+
updated_at TEXT NOT NULL DEFAULT ''
|
|
23
|
+
)
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
db.exec(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS workflow_operations (
|
|
28
|
+
operation_id TEXT PRIMARY KEY,
|
|
29
|
+
project_id TEXT NOT NULL,
|
|
30
|
+
operation_type TEXT NOT NULL,
|
|
31
|
+
idempotency_key TEXT NOT NULL,
|
|
32
|
+
expected_revision INTEGER NOT NULL CHECK (expected_revision >= 0),
|
|
33
|
+
resulting_revision INTEGER NOT NULL CHECK (resulting_revision = expected_revision + 1),
|
|
34
|
+
expected_authority_epoch INTEGER NOT NULL CHECK (expected_authority_epoch >= 0),
|
|
35
|
+
resulting_authority_epoch INTEGER NOT NULL CHECK (
|
|
36
|
+
resulting_authority_epoch = expected_authority_epoch OR
|
|
37
|
+
resulting_authority_epoch = expected_authority_epoch + 1
|
|
38
|
+
),
|
|
39
|
+
actor_type TEXT NOT NULL,
|
|
40
|
+
actor_id TEXT DEFAULT NULL,
|
|
41
|
+
source_transport TEXT NOT NULL,
|
|
42
|
+
trace_id TEXT DEFAULT NULL,
|
|
43
|
+
turn_id TEXT DEFAULT NULL,
|
|
44
|
+
request_hash TEXT NOT NULL,
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
UNIQUE (project_id, idempotency_key),
|
|
47
|
+
UNIQUE (project_id, resulting_revision),
|
|
48
|
+
UNIQUE (operation_id, project_id, resulting_revision, resulting_authority_epoch),
|
|
49
|
+
FOREIGN KEY (project_id) REFERENCES project_authority(project_id)
|
|
50
|
+
)
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS workflow_domain_events (
|
|
55
|
+
event_id TEXT PRIMARY KEY,
|
|
56
|
+
operation_id TEXT NOT NULL,
|
|
57
|
+
event_index INTEGER NOT NULL DEFAULT 0 CHECK (event_index >= 0),
|
|
58
|
+
project_id TEXT NOT NULL,
|
|
59
|
+
project_revision INTEGER NOT NULL CHECK (project_revision > 0),
|
|
60
|
+
authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 0),
|
|
61
|
+
event_type TEXT NOT NULL,
|
|
62
|
+
entity_type TEXT NOT NULL,
|
|
63
|
+
entity_id TEXT NOT NULL,
|
|
64
|
+
caused_by_event_id TEXT DEFAULT NULL,
|
|
65
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
66
|
+
created_at TEXT NOT NULL,
|
|
67
|
+
UNIQUE (operation_id, event_index),
|
|
68
|
+
FOREIGN KEY (operation_id, project_id, project_revision, authority_epoch)
|
|
69
|
+
REFERENCES workflow_operations(
|
|
70
|
+
operation_id, project_id, resulting_revision, resulting_authority_epoch
|
|
71
|
+
),
|
|
72
|
+
FOREIGN KEY (caused_by_event_id) REFERENCES workflow_domain_events(event_id)
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
db.exec(`
|
|
77
|
+
CREATE TRIGGER IF NOT EXISTS trg_workflow_domain_events_immutable_update
|
|
78
|
+
BEFORE UPDATE ON workflow_domain_events
|
|
79
|
+
BEGIN
|
|
80
|
+
SELECT RAISE(ABORT, 'workflow domain events are immutable');
|
|
81
|
+
END
|
|
82
|
+
`);
|
|
83
|
+
db.exec(`
|
|
84
|
+
CREATE TRIGGER IF NOT EXISTS trg_workflow_domain_events_immutable_delete
|
|
85
|
+
BEFORE DELETE ON workflow_domain_events
|
|
86
|
+
BEGIN
|
|
87
|
+
SELECT RAISE(ABORT, 'workflow domain events are immutable');
|
|
88
|
+
END
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
db.exec(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS workflow_outbox (
|
|
93
|
+
outbox_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
event_id TEXT NOT NULL,
|
|
95
|
+
destination TEXT NOT NULL,
|
|
96
|
+
available_at TEXT NOT NULL DEFAULT '',
|
|
97
|
+
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
|
98
|
+
claimed_by TEXT DEFAULT NULL,
|
|
99
|
+
claim_expires_at TEXT DEFAULT NULL,
|
|
100
|
+
delivered_at TEXT DEFAULT NULL,
|
|
101
|
+
last_error TEXT DEFAULT NULL,
|
|
102
|
+
UNIQUE (event_id, destination),
|
|
103
|
+
FOREIGN KEY (event_id) REFERENCES workflow_domain_events(event_id)
|
|
104
|
+
)
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
db.exec(`
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_operations_created
|
|
109
|
+
ON workflow_operations(project_id, created_at, operation_id)
|
|
110
|
+
`);
|
|
111
|
+
db.exec(`
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_domain_events_entity
|
|
113
|
+
ON workflow_domain_events(project_id, entity_type, entity_id, project_revision, event_index)
|
|
114
|
+
`);
|
|
115
|
+
db.exec(`
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_outbox_pending
|
|
117
|
+
ON workflow_outbox(delivered_at, available_at, outbox_id)
|
|
118
|
+
`);
|
|
119
|
+
|
|
120
|
+
db.exec(`
|
|
121
|
+
INSERT OR IGNORE INTO project_authority (
|
|
122
|
+
singleton, project_id, project_root_realpath,
|
|
123
|
+
revision, authority_epoch, created_at, updated_at
|
|
124
|
+
) VALUES (
|
|
125
|
+
1, lower(hex(randomblob(16))), '',
|
|
126
|
+
0, 0, '', ''
|
|
127
|
+
)
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// File Purpose: Schema migration DDL steps for the GSD database facade.
|
|
3
3
|
|
|
4
4
|
import type { DbAdapter } from "./db-adapter.js";
|
|
5
|
+
import { createCanonicalFoundationSchemaV31 } from "./db-canonical-foundation-schema.js";
|
|
5
6
|
import { ensureColumn } from "./db-schema-metadata.js";
|
|
6
7
|
|
|
7
8
|
export function applyMigrationV2Artifacts(db: DbAdapter): void {
|
|
@@ -494,3 +495,7 @@ export function applyMigrationV30ReworkBriefs(db: DbAdapter): void {
|
|
|
494
495
|
db.exec("CREATE INDEX IF NOT EXISTS idx_rework_briefs_task ON rework_briefs(milestone_id, slice_id, task_id)");
|
|
495
496
|
db.exec("CREATE INDEX IF NOT EXISTS idx_rework_findings_status ON rework_brief_findings(brief_id, severity, status)");
|
|
496
497
|
}
|
|
498
|
+
|
|
499
|
+
export function applyMigrationV31CanonicalFoundation(db: DbAdapter): void {
|
|
500
|
+
createCanonicalFoundationSchemaV31(db);
|
|
501
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Executable contract for the additive v31 canonical database foundation.
|
|
3
|
+
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { afterEach, test } from "node:test";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
SCHEMA_VERSION,
|
|
15
|
+
_getAdapter,
|
|
16
|
+
_setMigrationFaultForTest,
|
|
17
|
+
closeDatabase,
|
|
18
|
+
openDatabase,
|
|
19
|
+
} from "../gsd-db.ts";
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const tempDirs = new Set<string>();
|
|
23
|
+
|
|
24
|
+
interface RawDb {
|
|
25
|
+
exec(sql: string): void;
|
|
26
|
+
prepare(sql: string): {
|
|
27
|
+
get(...args: unknown[]): Record<string, unknown> | undefined;
|
|
28
|
+
all(...args: unknown[]): Array<Record<string, unknown>>;
|
|
29
|
+
};
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openRawDatabase(path: string): RawDb {
|
|
34
|
+
const sqlite = require("node:sqlite") as { DatabaseSync: new (path: string) => RawDb };
|
|
35
|
+
return new sqlite.DatabaseSync(path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createDatabasePath(): string {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-canonical-foundation-"));
|
|
40
|
+
tempDirs.add(dir);
|
|
41
|
+
return join(dir, "gsd.db");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tableExists(db: RawDb, table: string): boolean {
|
|
45
|
+
return Boolean(
|
|
46
|
+
db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?").get(table),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function maxSchemaVersion(db: RawDb): number {
|
|
51
|
+
return Number(db.prepare("SELECT MAX(version) AS version FROM schema_version").get()?.version);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rewindToV30(dbPath: string): void {
|
|
55
|
+
assert.equal(openDatabase(dbPath), true);
|
|
56
|
+
closeDatabase();
|
|
57
|
+
|
|
58
|
+
const raw = openRawDatabase(dbPath);
|
|
59
|
+
try {
|
|
60
|
+
raw.exec(`
|
|
61
|
+
DROP TABLE IF EXISTS workflow_outbox;
|
|
62
|
+
DROP TABLE IF EXISTS workflow_domain_events;
|
|
63
|
+
DROP TABLE IF EXISTS workflow_operations;
|
|
64
|
+
DROP TABLE IF EXISTS project_authority;
|
|
65
|
+
DELETE FROM schema_version;
|
|
66
|
+
INSERT INTO schema_version (version, applied_at) VALUES (30, '2026-07-12T00:00:00.000Z');
|
|
67
|
+
INSERT OR IGNORE INTO milestones (id, title, status, created_at)
|
|
68
|
+
VALUES ('M-LEGACY', 'Preserved legacy milestone', 'active', '2026-07-12T00:00:00.000Z');
|
|
69
|
+
INSERT OR IGNORE INTO audit_events
|
|
70
|
+
(event_id, trace_id, category, type, ts, payload_json)
|
|
71
|
+
VALUES
|
|
72
|
+
('audit-legacy', 'trace-legacy', 'workflow', 'legacy', '2026-07-12T00:00:00.000Z', '{"kept":true}');
|
|
73
|
+
`);
|
|
74
|
+
} finally {
|
|
75
|
+
raw.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function inspectFromFreshProcess(dbPath: string): Record<string, unknown> {
|
|
80
|
+
const moduleHref = pathToFileURL(
|
|
81
|
+
join(process.cwd(), "src/resources/extensions/gsd/gsd-db.ts"),
|
|
82
|
+
).href;
|
|
83
|
+
const script = [
|
|
84
|
+
`import { openDatabase, closeDatabase, _getAdapter } from ${JSON.stringify(moduleHref)};`,
|
|
85
|
+
"const path = process.argv[1];",
|
|
86
|
+
"if (!openDatabase(path)) throw new Error('database open failed');",
|
|
87
|
+
"const db = _getAdapter();",
|
|
88
|
+
"const version = db.prepare('SELECT MAX(version) AS value FROM schema_version').get().value;",
|
|
89
|
+
"const authority = db.prepare('SELECT project_id, revision, authority_epoch FROM project_authority').get();",
|
|
90
|
+
"const legacy = db.prepare(\"SELECT title FROM milestones WHERE id = 'M-LEGACY'\").get();",
|
|
91
|
+
"console.log(JSON.stringify({ version, authority, legacy }));",
|
|
92
|
+
"closeDatabase();",
|
|
93
|
+
].join("\n");
|
|
94
|
+
const env = { ...process.env };
|
|
95
|
+
delete env.NODE_TEST_CONTEXT;
|
|
96
|
+
const child = spawnSync(
|
|
97
|
+
process.execPath,
|
|
98
|
+
[
|
|
99
|
+
"--import",
|
|
100
|
+
"./src/resources/extensions/gsd/tests/resolve-ts.mjs",
|
|
101
|
+
"--experimental-strip-types",
|
|
102
|
+
"--input-type=module",
|
|
103
|
+
"-e",
|
|
104
|
+
script,
|
|
105
|
+
dbPath,
|
|
106
|
+
],
|
|
107
|
+
{ cwd: process.cwd(), encoding: "utf8", env, timeout: 30_000 },
|
|
108
|
+
);
|
|
109
|
+
assert.equal(child.status, 0, child.stderr || child.stdout);
|
|
110
|
+
return JSON.parse(child.stdout) as Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
_setMigrationFaultForTest(false);
|
|
115
|
+
closeDatabase();
|
|
116
|
+
for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true });
|
|
117
|
+
tempDirs.clear();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("fresh database creates the v31 authority root and linked operation journal", () => {
|
|
121
|
+
assert.equal(SCHEMA_VERSION, 31);
|
|
122
|
+
const dbPath = createDatabasePath();
|
|
123
|
+
assert.equal(openDatabase(dbPath), true);
|
|
124
|
+
const db = _getAdapter();
|
|
125
|
+
assert.ok(db);
|
|
126
|
+
|
|
127
|
+
const authority = db.prepare(`
|
|
128
|
+
SELECT singleton, project_id, project_root_realpath, revision, authority_epoch
|
|
129
|
+
FROM project_authority
|
|
130
|
+
`).get();
|
|
131
|
+
assert.deepEqual(
|
|
132
|
+
{
|
|
133
|
+
singleton: authority?.singleton,
|
|
134
|
+
projectIdLength: String(authority?.project_id ?? "").length,
|
|
135
|
+
root: authority?.project_root_realpath,
|
|
136
|
+
revision: authority?.revision,
|
|
137
|
+
epoch: authority?.authority_epoch,
|
|
138
|
+
},
|
|
139
|
+
{ singleton: 1, projectIdLength: 32, root: "", revision: 0, epoch: 0 },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
assert.throws(
|
|
143
|
+
() => db.exec("INSERT INTO project_authority (singleton, project_id) VALUES (2, 'second')"),
|
|
144
|
+
/CHECK constraint failed/i,
|
|
145
|
+
);
|
|
146
|
+
assert.throws(
|
|
147
|
+
() => db.exec("UPDATE project_authority SET revision = -1 WHERE singleton = 1"),
|
|
148
|
+
/CHECK constraint failed/i,
|
|
149
|
+
);
|
|
150
|
+
assert.throws(
|
|
151
|
+
() => db.exec("UPDATE project_authority SET authority_epoch = -1 WHERE singleton = 1"),
|
|
152
|
+
/CHECK constraint failed/i,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
db.exec(`
|
|
156
|
+
INSERT INTO workflow_operations (
|
|
157
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
158
|
+
expected_revision, resulting_revision,
|
|
159
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
160
|
+
actor_type, actor_id, source_transport, request_hash, created_at
|
|
161
|
+
)
|
|
162
|
+
SELECT
|
|
163
|
+
'op-1', project_id, 'test.operation', 'idem-1',
|
|
164
|
+
0, 1, 0, 0, 'agent', 'test-agent', 'test', 'request-1', '2026-07-12T00:00:01.000Z'
|
|
165
|
+
FROM project_authority WHERE singleton = 1;
|
|
166
|
+
|
|
167
|
+
INSERT INTO workflow_domain_events (
|
|
168
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
169
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
170
|
+
)
|
|
171
|
+
SELECT
|
|
172
|
+
'event-1', 'op-1', 0, project_id, 1,
|
|
173
|
+
0, 'test.started', 'project', project_id, '{"step":1}', '2026-07-12T00:00:02.000Z'
|
|
174
|
+
FROM project_authority WHERE singleton = 1;
|
|
175
|
+
|
|
176
|
+
INSERT INTO workflow_domain_events (
|
|
177
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
178
|
+
authority_epoch, event_type, entity_type, entity_id, caused_by_event_id,
|
|
179
|
+
payload_json, created_at
|
|
180
|
+
)
|
|
181
|
+
SELECT
|
|
182
|
+
'event-2', 'op-1', 1, project_id, 1,
|
|
183
|
+
0, 'test.finished', 'project', project_id, 'event-1',
|
|
184
|
+
'{"step":2}', '2026-07-12T00:00:03.000Z'
|
|
185
|
+
FROM project_authority WHERE singleton = 1;
|
|
186
|
+
|
|
187
|
+
INSERT INTO workflow_outbox (event_id, destination, available_at)
|
|
188
|
+
VALUES ('event-2', 'projection', '2026-07-12T00:00:04.000Z');
|
|
189
|
+
`);
|
|
190
|
+
|
|
191
|
+
const chain = db.prepare(`
|
|
192
|
+
SELECT
|
|
193
|
+
p.revision AS authority_revision,
|
|
194
|
+
o.expected_revision,
|
|
195
|
+
o.resulting_revision,
|
|
196
|
+
COUNT(DISTINCT e.event_id) AS event_count,
|
|
197
|
+
COUNT(DISTINCT x.outbox_id) AS outbox_count
|
|
198
|
+
FROM project_authority p
|
|
199
|
+
JOIN workflow_operations o ON o.project_id = p.project_id
|
|
200
|
+
JOIN workflow_domain_events e ON e.operation_id = o.operation_id
|
|
201
|
+
LEFT JOIN workflow_outbox x ON x.event_id = e.event_id
|
|
202
|
+
GROUP BY p.revision, o.expected_revision, o.resulting_revision
|
|
203
|
+
`).get();
|
|
204
|
+
assert.deepEqual(chain, {
|
|
205
|
+
authority_revision: 0,
|
|
206
|
+
expected_revision: 0,
|
|
207
|
+
resulting_revision: 1,
|
|
208
|
+
event_count: 2,
|
|
209
|
+
outbox_count: 1,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
assert.throws(
|
|
213
|
+
() => db.exec(`
|
|
214
|
+
INSERT INTO workflow_operations (
|
|
215
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
216
|
+
expected_revision, resulting_revision,
|
|
217
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
218
|
+
actor_type, source_transport, request_hash, created_at
|
|
219
|
+
)
|
|
220
|
+
SELECT 'op-duplicate', project_id, 'test.operation', 'idem-1', 1, 2, 0, 0,
|
|
221
|
+
'agent', 'test', 'request-2', '2026-07-12T00:00:05.000Z'
|
|
222
|
+
FROM project_authority WHERE singleton = 1
|
|
223
|
+
`),
|
|
224
|
+
/UNIQUE constraint failed/i,
|
|
225
|
+
);
|
|
226
|
+
assert.throws(
|
|
227
|
+
() => db.exec(`
|
|
228
|
+
INSERT INTO workflow_operations (
|
|
229
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
230
|
+
expected_revision, resulting_revision,
|
|
231
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
232
|
+
actor_type, source_transport, request_hash, created_at
|
|
233
|
+
)
|
|
234
|
+
SELECT 'op-same-revision', project_id, 'test.operation', 'idem-2', 0, 1, 0, 0,
|
|
235
|
+
'agent', 'test', 'request-2', '2026-07-12T00:00:05.000Z'
|
|
236
|
+
FROM project_authority WHERE singleton = 1
|
|
237
|
+
`),
|
|
238
|
+
/UNIQUE constraint failed/i,
|
|
239
|
+
);
|
|
240
|
+
assert.throws(
|
|
241
|
+
() => db.exec(`
|
|
242
|
+
INSERT INTO workflow_domain_events (
|
|
243
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
244
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
245
|
+
)
|
|
246
|
+
SELECT 'event-duplicate-index', 'op-1', 1, project_id, 1, 0,
|
|
247
|
+
'test.duplicate', 'project', project_id, '{}', '2026-07-12T00:00:06.000Z'
|
|
248
|
+
FROM project_authority WHERE singleton = 1
|
|
249
|
+
`),
|
|
250
|
+
/UNIQUE constraint failed/i,
|
|
251
|
+
);
|
|
252
|
+
assert.throws(
|
|
253
|
+
() => db.exec(`
|
|
254
|
+
INSERT INTO workflow_domain_events (
|
|
255
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
256
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
257
|
+
)
|
|
258
|
+
SELECT 'event-wrong-revision', 'op-1', 2, project_id, 2, 0,
|
|
259
|
+
'test.wrong-revision', 'project', project_id, '{}', '2026-07-12T00:00:06.000Z'
|
|
260
|
+
FROM project_authority WHERE singleton = 1
|
|
261
|
+
`),
|
|
262
|
+
/FOREIGN KEY constraint failed/i,
|
|
263
|
+
);
|
|
264
|
+
assert.throws(
|
|
265
|
+
() => db.exec(`
|
|
266
|
+
INSERT INTO workflow_outbox (event_id, destination, available_at)
|
|
267
|
+
VALUES ('missing-event', 'projection', '2026-07-12T00:00:07.000Z')
|
|
268
|
+
`),
|
|
269
|
+
/FOREIGN KEY constraint failed/i,
|
|
270
|
+
);
|
|
271
|
+
assert.throws(
|
|
272
|
+
() => db.exec(`
|
|
273
|
+
INSERT INTO workflow_outbox (event_id, destination, available_at)
|
|
274
|
+
VALUES ('event-2', 'projection', '2026-07-12T00:00:08.000Z')
|
|
275
|
+
`),
|
|
276
|
+
/UNIQUE constraint failed/i,
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("operations record the resulting authority epoch used by their events", () => {
|
|
281
|
+
const dbPath = createDatabasePath();
|
|
282
|
+
assert.equal(openDatabase(dbPath), true);
|
|
283
|
+
const db = _getAdapter();
|
|
284
|
+
assert.ok(db);
|
|
285
|
+
|
|
286
|
+
db.exec(`
|
|
287
|
+
INSERT INTO workflow_operations (
|
|
288
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
289
|
+
expected_revision, resulting_revision,
|
|
290
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
291
|
+
actor_type, source_transport, request_hash, created_at
|
|
292
|
+
)
|
|
293
|
+
SELECT 'op-cutover', project_id, 'authority.cutover', 'idem-cutover',
|
|
294
|
+
0, 1, 0, 1, 'agent', 'test', 'request-cutover', '2026-07-12T00:00:01.000Z'
|
|
295
|
+
FROM project_authority WHERE singleton = 1;
|
|
296
|
+
|
|
297
|
+
INSERT INTO workflow_domain_events (
|
|
298
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
299
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
300
|
+
)
|
|
301
|
+
SELECT 'event-cutover', 'op-cutover', 0, project_id, 1, 1,
|
|
302
|
+
'authority.cutover', 'project', project_id, '{}', '2026-07-12T00:00:02.000Z'
|
|
303
|
+
FROM project_authority WHERE singleton = 1;
|
|
304
|
+
|
|
305
|
+
INSERT INTO workflow_operations (
|
|
306
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
307
|
+
expected_revision, resulting_revision,
|
|
308
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
309
|
+
actor_type, source_transport, request_hash, created_at
|
|
310
|
+
)
|
|
311
|
+
SELECT 'op-unchanged-epoch', project_id, 'test.operation', 'idem-unchanged-epoch',
|
|
312
|
+
1, 2, 1, 1, 'agent', 'test', 'request-unchanged', '2026-07-12T00:00:03.000Z'
|
|
313
|
+
FROM project_authority WHERE singleton = 1;
|
|
314
|
+
`);
|
|
315
|
+
|
|
316
|
+
assert.throws(
|
|
317
|
+
() => db.exec(`
|
|
318
|
+
INSERT INTO workflow_domain_events (
|
|
319
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
320
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
321
|
+
)
|
|
322
|
+
SELECT 'event-stale-epoch', 'op-cutover', 1, project_id, 1, 0,
|
|
323
|
+
'authority.stale', 'project', project_id, '{}', '2026-07-12T00:00:04.000Z'
|
|
324
|
+
FROM project_authority WHERE singleton = 1
|
|
325
|
+
`),
|
|
326
|
+
/FOREIGN KEY constraint failed/i,
|
|
327
|
+
);
|
|
328
|
+
assert.throws(
|
|
329
|
+
() => db.exec(`
|
|
330
|
+
INSERT INTO workflow_operations (
|
|
331
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
332
|
+
expected_revision, resulting_revision,
|
|
333
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
334
|
+
actor_type, source_transport, request_hash, created_at
|
|
335
|
+
)
|
|
336
|
+
SELECT 'op-epoch-jump', project_id, 'authority.cutover', 'idem-epoch-jump',
|
|
337
|
+
2, 3, 1, 3, 'agent', 'test', 'request-jump', '2026-07-12T00:00:05.000Z'
|
|
338
|
+
FROM project_authority WHERE singleton = 1
|
|
339
|
+
`),
|
|
340
|
+
/CHECK constraint failed/i,
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("domain events reject updates and deletes", () => {
|
|
345
|
+
const dbPath = createDatabasePath();
|
|
346
|
+
assert.equal(openDatabase(dbPath), true);
|
|
347
|
+
const db = _getAdapter();
|
|
348
|
+
assert.ok(db);
|
|
349
|
+
|
|
350
|
+
db.exec(`
|
|
351
|
+
INSERT INTO workflow_operations (
|
|
352
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
353
|
+
expected_revision, resulting_revision,
|
|
354
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
355
|
+
actor_type, source_transport, request_hash, created_at
|
|
356
|
+
)
|
|
357
|
+
SELECT 'op-immutable', project_id, 'test.operation', 'idem-immutable',
|
|
358
|
+
0, 1, 0, 0, 'agent', 'test', 'request-immutable', '2026-07-12T00:00:01.000Z'
|
|
359
|
+
FROM project_authority WHERE singleton = 1;
|
|
360
|
+
|
|
361
|
+
INSERT INTO workflow_domain_events (
|
|
362
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
363
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
364
|
+
)
|
|
365
|
+
SELECT 'event-immutable', 'op-immutable', 0, project_id, 1, 0,
|
|
366
|
+
'test.recorded', 'project', project_id, '{"original":true}', '2026-07-12T00:00:02.000Z'
|
|
367
|
+
FROM project_authority WHERE singleton = 1;
|
|
368
|
+
`);
|
|
369
|
+
|
|
370
|
+
assert.throws(
|
|
371
|
+
() => db.exec(`UPDATE workflow_domain_events SET payload_json = '{}' WHERE event_id = 'event-immutable'`),
|
|
372
|
+
/workflow domain events are immutable/i,
|
|
373
|
+
);
|
|
374
|
+
assert.throws(
|
|
375
|
+
() => db.exec(`DELETE FROM workflow_domain_events WHERE event_id = 'event-immutable'`),
|
|
376
|
+
/workflow domain events are immutable/i,
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("v30 upgrade preserves legacy rows and creates an independently healthy backup", () => {
|
|
381
|
+
const dbPath = createDatabasePath();
|
|
382
|
+
rewindToV30(dbPath);
|
|
383
|
+
|
|
384
|
+
assert.equal(openDatabase(dbPath), true);
|
|
385
|
+
closeDatabase();
|
|
386
|
+
|
|
387
|
+
let projectId = "";
|
|
388
|
+
const upgraded = openRawDatabase(dbPath);
|
|
389
|
+
try {
|
|
390
|
+
assert.equal(maxSchemaVersion(upgraded), 31);
|
|
391
|
+
assert.equal(tableExists(upgraded, "project_authority"), true);
|
|
392
|
+
assert.equal(upgraded.prepare("SELECT title FROM milestones WHERE id = 'M-LEGACY'").get()?.title, "Preserved legacy milestone");
|
|
393
|
+
assert.equal(upgraded.prepare("SELECT payload_json FROM audit_events WHERE event_id = 'audit-legacy'").get()?.payload_json, '{"kept":true}');
|
|
394
|
+
assert.equal(upgraded.prepare("PRAGMA quick_check").get()?.quick_check, "ok");
|
|
395
|
+
projectId = String(upgraded.prepare("SELECT project_id FROM project_authority").get()?.project_id);
|
|
396
|
+
} finally {
|
|
397
|
+
upgraded.close();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const backup = openRawDatabase(`${dbPath}.backup-v30`);
|
|
401
|
+
try {
|
|
402
|
+
assert.equal(maxSchemaVersion(backup), 30);
|
|
403
|
+
assert.equal(tableExists(backup, "project_authority"), false);
|
|
404
|
+
assert.equal(backup.prepare("SELECT title FROM milestones WHERE id = 'M-LEGACY'").get()?.title, "Preserved legacy milestone");
|
|
405
|
+
assert.equal(backup.prepare("PRAGMA quick_check").get()?.quick_check, "ok");
|
|
406
|
+
} finally {
|
|
407
|
+
backup.close();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const reopened = inspectFromFreshProcess(dbPath);
|
|
411
|
+
assert.deepEqual(reopened, {
|
|
412
|
+
version: 31,
|
|
413
|
+
authority: { project_id: projectId, revision: 0, authority_epoch: 0 },
|
|
414
|
+
legacy: { title: "Preserved legacy milestone" },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const restoredPath = join(dirname(dbPath), "restored.db");
|
|
418
|
+
copyFileSync(`${dbPath}.backup-v30`, restoredPath);
|
|
419
|
+
const restored = inspectFromFreshProcess(restoredPath);
|
|
420
|
+
assert.equal(restored.version, 31);
|
|
421
|
+
assert.deepEqual(restored.legacy, { title: "Preserved legacy milestone" });
|
|
422
|
+
assert.deepEqual(
|
|
423
|
+
Object.fromEntries(
|
|
424
|
+
Object.entries(restored.authority as Record<string, unknown>).filter(([key]) => key !== "project_id"),
|
|
425
|
+
),
|
|
426
|
+
{ revision: 0, authority_epoch: 0 },
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("faulted v30 migration leaves no v31 state and retries cleanly", () => {
|
|
431
|
+
const dbPath = createDatabasePath();
|
|
432
|
+
rewindToV30(dbPath);
|
|
433
|
+
|
|
434
|
+
_setMigrationFaultForTest(true);
|
|
435
|
+
assert.throws(() => openDatabase(dbPath), /migration fault injected/);
|
|
436
|
+
_setMigrationFaultForTest(false);
|
|
437
|
+
|
|
438
|
+
const rolledBack = openRawDatabase(dbPath);
|
|
439
|
+
try {
|
|
440
|
+
assert.equal(maxSchemaVersion(rolledBack), 30);
|
|
441
|
+
assert.equal(tableExists(rolledBack, "project_authority"), false);
|
|
442
|
+
assert.equal(tableExists(rolledBack, "workflow_operations"), false);
|
|
443
|
+
assert.equal(tableExists(rolledBack, "workflow_domain_events"), false);
|
|
444
|
+
assert.equal(tableExists(rolledBack, "workflow_outbox"), false);
|
|
445
|
+
} finally {
|
|
446
|
+
rolledBack.close();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
assert.equal(openDatabase(dbPath), true);
|
|
450
|
+
closeDatabase();
|
|
451
|
+
const retried = openRawDatabase(dbPath);
|
|
452
|
+
try {
|
|
453
|
+
assert.equal(maxSchemaVersion(retried), 31);
|
|
454
|
+
assert.equal(retried.prepare("SELECT COUNT(*) AS count FROM project_authority").get()?.count, 1);
|
|
455
|
+
assert.equal(retried.prepare("SELECT COUNT(*) AS count FROM milestones WHERE id = 'M-LEGACY'").get()?.count, 1);
|
|
456
|
+
} finally {
|
|
457
|
+
retried.close();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
@@ -101,9 +101,9 @@ describe('db-engine migrate guards', () => {
|
|
|
101
101
|
assert.ok(openDatabase(dbPath), 'fresh open should succeed');
|
|
102
102
|
closeDatabase();
|
|
103
103
|
|
|
104
|
-
// Rewind
|
|
105
|
-
//
|
|
106
|
-
const startVersion =
|
|
104
|
+
// Rewind to v28 and remove a v29 column so the multi-step migration has
|
|
105
|
+
// real work to redo even as later schema versions are added.
|
|
106
|
+
const startVersion = 28;
|
|
107
107
|
const raw = openRawSqliteForTest(dbPath);
|
|
108
108
|
raw.exec('DELETE FROM schema_version');
|
|
109
109
|
raw.exec(`INSERT INTO schema_version (version, applied_at) VALUES (${startVersion}, '2026-01-01T00:00:00Z')`);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[9492],{14040:(e,t,l)=>{(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return l(32910)}])},29999:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"styles",{enumerable:!0,get:function(){return l}});let l={error:{fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},desc:{display:"inline-block"},h1:{display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},h2:{fontSize:14,fontWeight:400,lineHeight:"49px",margin:0}};("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},32910:(e,t,l)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return o}});let r=l(18870),n=l(48664),o=function(){return(0,r.jsx)("html",{children:(0,r.jsx)("body",{children:(0,r.jsx)(n.HTTPAccessErrorFallback,{status:404,message:"This page could not be found."})})})};("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},48664:(e,t,l)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"HTTPAccessErrorFallback",{enumerable:!0,get:function(){return o}});let r=l(18870),n=l(29999);function o({status:e,message:t}){return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("title",{children:`${e}: ${t}`}),(0,r.jsx)("div",{style:n.styles.error,children:(0,r.jsxs)("div",{children:[(0,r.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,r.jsx)("h1",{className:"next-error-h1",style:n.styles.h1,children:e}),(0,r.jsx)("div",{style:n.styles.desc,children:(0,r.jsx)("h2",{style:n.styles.h2,children:t})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},e=>{e.O(0,[9990,686,7358],()=>e(e.s=14040)),_N_E=e.O()}]);
|