@tomflow/proflow-task-migration-runner 0.1.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 ADDED
@@ -0,0 +1,3 @@
1
+ # @tomflow/proflow-task-migration-runner
2
+
3
+ 确定性发现、执行并验证 Task schema migrations;SQL 由 task-store-sqlite 提供。
@@ -0,0 +1,4 @@
1
+ {
2
+ "contract": "proflow.conformance.v1",
3
+ "levels": ["C1", "C2", "C3"]
4
+ }
@@ -0,0 +1,67 @@
1
+ export declare const behaviorAdapter: {
2
+ readonly describe: () => {
3
+ result: {
4
+ readonly contract: "deployment.result.v1";
5
+ readonly ok: true;
6
+ readonly status: "SUCCEEDED";
7
+ readonly moduleRef: "task-migration-runner";
8
+ readonly moduleVersion: "0.1.0";
9
+ };
10
+ observedEffects: never[];
11
+ };
12
+ readonly preflight: () => {
13
+ result: {
14
+ readonly contract: "deployment.result.v1";
15
+ readonly ok: true;
16
+ readonly status: "SUCCEEDED";
17
+ readonly moduleRef: "task-migration-runner";
18
+ readonly moduleVersion: "0.1.0";
19
+ };
20
+ observedEffects: never[];
21
+ };
22
+ readonly status: () => {
23
+ result: {
24
+ readonly contract: "deployment.result.v1";
25
+ readonly ok: true;
26
+ readonly status: "SUCCEEDED";
27
+ readonly moduleRef: "task-migration-runner";
28
+ readonly moduleVersion: "0.1.0";
29
+ };
30
+ observedEffects: never[];
31
+ };
32
+ readonly verify: () => {
33
+ result: {
34
+ contract: "deployment.result.v1";
35
+ ok: true;
36
+ status: "SUCCEEDED";
37
+ moduleRef: "task-migration-runner";
38
+ moduleVersion: "0.1.0";
39
+ checks: {
40
+ id: string;
41
+ status: string;
42
+ message: string;
43
+ }[];
44
+ };
45
+ observedEffects: never[];
46
+ };
47
+ readonly doctor: () => {
48
+ result: {
49
+ readonly contract: "deployment.result.v1";
50
+ readonly ok: true;
51
+ readonly status: "SUCCEEDED";
52
+ readonly moduleRef: "task-migration-runner";
53
+ readonly moduleVersion: "0.1.0";
54
+ };
55
+ observedEffects: never[];
56
+ };
57
+ readonly migrate: () => {
58
+ result: {
59
+ readonly contract: "deployment.result.v1";
60
+ readonly ok: true;
61
+ readonly status: "SUCCEEDED";
62
+ readonly moduleRef: "task-migration-runner";
63
+ readonly moduleVersion: "0.1.0";
64
+ };
65
+ observedEffects: string[];
66
+ };
67
+ };
@@ -0,0 +1,31 @@
1
+ import { descriptor } from "./descriptor.js";
2
+ const result = {
3
+ contract: "deployment.result.v1",
4
+ ok: true,
5
+ status: "SUCCEEDED",
6
+ moduleRef: descriptor.moduleRef,
7
+ moduleVersion: descriptor.moduleVersion,
8
+ };
9
+ export const behaviorAdapter = {
10
+ describe: () => ({ result, observedEffects: [] }),
11
+ preflight: () => ({ result, observedEffects: [] }),
12
+ status: () => ({ result, observedEffects: [] }),
13
+ verify: () => ({
14
+ result: {
15
+ ...result,
16
+ checks: [
17
+ {
18
+ id: "migration-state-pass",
19
+ status: "PASS",
20
+ message: "Migration verification primitive is available",
21
+ },
22
+ ],
23
+ },
24
+ observedEffects: [],
25
+ }),
26
+ doctor: () => ({ result, observedEffects: [] }),
27
+ migrate: () => ({
28
+ result,
29
+ observedEffects: ["Applies Task Store migration SQL to SQLite"],
30
+ }),
31
+ };
@@ -0,0 +1,48 @@
1
+ export declare const descriptor: {
2
+ readonly contract: "module";
3
+ readonly contractVersion: "1.0.0";
4
+ readonly moduleRef: "task-migration-runner";
5
+ readonly packageName: "@tomflow/proflow-task-migration-runner";
6
+ readonly moduleVersion: "0.1.0";
7
+ readonly kind: "cli";
8
+ readonly templateVersion: "1.0.0";
9
+ readonly platformCompatibility: ">=1.0.0 <2.0.0";
10
+ readonly installClass: "core";
11
+ readonly identity: {
12
+ readonly domain: "task-orchestration";
13
+ readonly summary: "Deterministically discovers, applies and verifies Task Store SQLite schema migrations.";
14
+ };
15
+ readonly provides: readonly [];
16
+ readonly requires: readonly [];
17
+ readonly requirements: readonly [{
18
+ readonly kind: "runtime";
19
+ readonly runtime: "node";
20
+ readonly versionRange: ">=24.19.0";
21
+ }];
22
+ readonly configSlots: readonly [{
23
+ readonly key: "databasePath";
24
+ readonly type: "path";
25
+ readonly required: true;
26
+ readonly description: "Task SQLite database path";
27
+ }];
28
+ readonly lifecycle: {
29
+ readonly supported: readonly ["describe", "preflight", "status", "verify", "doctor", "migrate"];
30
+ };
31
+ readonly verification: {
32
+ readonly checks: readonly [{
33
+ readonly id: "migration-state-pass";
34
+ readonly description: "Migration state matches the Task schema";
35
+ readonly lifecycle: "verify";
36
+ }];
37
+ };
38
+ readonly effects: readonly [{
39
+ readonly kind: "filesystem";
40
+ readonly description: "Applies Task Store migration SQL to SQLite";
41
+ readonly retention: "preserve";
42
+ }];
43
+ readonly documentation: readonly [{
44
+ readonly id: "overview";
45
+ readonly path: "./README.md";
46
+ readonly description: "Package-owned module overview";
47
+ }];
48
+ };
@@ -0,0 +1,61 @@
1
+ export const descriptor = {
2
+ contract: "module",
3
+ contractVersion: "1.0.0",
4
+ moduleRef: "task-migration-runner",
5
+ packageName: "@tomflow/proflow-task-migration-runner",
6
+ moduleVersion: "0.1.0",
7
+ kind: "cli",
8
+ templateVersion: "1.0.0",
9
+ platformCompatibility: ">=1.0.0 <2.0.0",
10
+ installClass: "core",
11
+ identity: {
12
+ domain: "task-orchestration",
13
+ summary: "Deterministically discovers, applies and verifies Task Store SQLite schema migrations.",
14
+ },
15
+ provides: [],
16
+ requires: [],
17
+ requirements: [
18
+ { kind: "runtime", runtime: "node", versionRange: ">=24.19.0" },
19
+ ],
20
+ configSlots: [
21
+ {
22
+ key: "databasePath",
23
+ type: "path",
24
+ required: true,
25
+ description: "Task SQLite database path",
26
+ },
27
+ ],
28
+ lifecycle: {
29
+ supported: [
30
+ "describe",
31
+ "preflight",
32
+ "status",
33
+ "verify",
34
+ "doctor",
35
+ "migrate",
36
+ ],
37
+ },
38
+ verification: {
39
+ checks: [
40
+ {
41
+ id: "migration-state-pass",
42
+ description: "Migration state matches the Task schema",
43
+ lifecycle: "verify",
44
+ },
45
+ ],
46
+ },
47
+ effects: [
48
+ {
49
+ kind: "filesystem",
50
+ description: "Applies Task Store migration SQL to SQLite",
51
+ retention: "preserve",
52
+ },
53
+ ],
54
+ documentation: [
55
+ {
56
+ id: "overview",
57
+ path: "./README.md",
58
+ description: "Package-owned module overview",
59
+ },
60
+ ],
61
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { runCli } from "./index.ts";
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import { runCli } from "./index.js";
4
+ export { runCli } from "./index.js";
5
+ if (process.argv[1] !== undefined &&
6
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
7
+ const output = await runCli(process.argv.slice(2));
8
+ process.stdout.write(`${output}\n`);
9
+ if (JSON.parse(output).ok !== true)
10
+ process.exitCode = 1;
11
+ }
@@ -0,0 +1,41 @@
1
+ import { type TaskMigration, type TaskMigrationContext } from "@tomflow/proflow-task-store-sqlite/migrations";
2
+ export interface MigrationInput {
3
+ databasePath: string;
4
+ migrations: readonly TaskMigration[];
5
+ context?: TaskMigrationContext;
6
+ }
7
+ export interface MigrationResult {
8
+ contract: "task-migration";
9
+ contractVersion: "1.0.0";
10
+ ok: boolean;
11
+ applied: number[];
12
+ pending: number[];
13
+ error?: {
14
+ code: "MIGRATION_FAILED" | "MIGRATION_VERIFY_FAILED";
15
+ message: string;
16
+ };
17
+ }
18
+ export interface MigrationStatus {
19
+ contract: "task-migration";
20
+ contractVersion: "1.0.0";
21
+ appliedVersions: number[];
22
+ pendingVersions: number[];
23
+ metadataDrift: Array<{
24
+ version: number;
25
+ expectedName: string;
26
+ actualName: string;
27
+ }>;
28
+ checksumDrift: Array<{
29
+ version: number;
30
+ expectedChecksum: string;
31
+ actualChecksum: string;
32
+ }>;
33
+ legacyMetadataVersions: number[];
34
+ missingTables: string[];
35
+ schemaDrift: string[];
36
+ }
37
+ export declare function discoverMigrations(migrations: readonly TaskMigration[]): TaskMigration[];
38
+ export declare function getMigrationStatus(input: MigrationInput): MigrationStatus;
39
+ export declare function applyMigrations(input: MigrationInput): MigrationResult;
40
+ export declare function verifyMigrations(input: MigrationInput): MigrationResult;
41
+ export declare function runCli(args: string[]): Promise<string>;
@@ -0,0 +1,401 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { taskMigrations, } from "@tomflow/proflow-task-store-sqlite/migrations";
6
+ import { descriptor as moduleDescriptor } from "../deployment/descriptor.js";
7
+ export function discoverMigrations(migrations) {
8
+ const sorted = [...migrations].sort((left, right) => left.version - right.version);
9
+ const versions = new Set();
10
+ for (const migration of sorted) {
11
+ if (!Number.isSafeInteger(migration.version) || migration.version < 1)
12
+ throw new TypeError("migration version must be a positive integer");
13
+ if (!/^[a-z][a-z0-9_]*$/.test(migration.name))
14
+ throw new TypeError("migration name must be stable snake_case");
15
+ if (versions.has(migration.version))
16
+ throw new TypeError(`duplicate migration version ${migration.version}`);
17
+ if (migration.apply !== undefined && !migration.identity)
18
+ throw new TypeError(`custom migration ${migration.version} requires a stable identity`);
19
+ if (migration.coversLegacyVersions !== undefined) {
20
+ if (migration.apply === undefined)
21
+ throw new TypeError(`migration ${migration.version} cannot cover legacy versions without an owner compatibility apply`);
22
+ if (migration.coversLegacyVersions.some((version) => !Number.isInteger(version) ||
23
+ version < 1 ||
24
+ version >= migration.version))
25
+ throw new TypeError(`migration ${migration.version} has an invalid legacy coverage declaration`);
26
+ }
27
+ versions.add(migration.version);
28
+ }
29
+ return sorted;
30
+ }
31
+ function migrationChecksum(migration) {
32
+ const semanticIdentity = `${migration.identity ?? "sql"}\0${migration.sql}`;
33
+ return `sha256:${createHash("sha256")
34
+ .update(`${migration.version}\0${migration.name}\0${semanticIdentity}`)
35
+ .digest("hex")}`;
36
+ }
37
+ function migrationMetadataColumns(database) {
38
+ return new Set(database.prepare("PRAGMA table_info(schema_migrations)").all().map((row) => row.name));
39
+ }
40
+ function openDatabase(databasePath) {
41
+ if (databasePath !== ":memory:")
42
+ mkdirSync(dirname(databasePath), { recursive: true });
43
+ const database = new DatabaseSync(databasePath);
44
+ database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 2500;");
45
+ database.exec("CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, checksum TEXT, applied_at TEXT NOT NULL);");
46
+ const columns = migrationMetadataColumns(database);
47
+ if (!columns.has("checksum")) {
48
+ // Preserve historical migration metadata in place. SQLite appends the
49
+ // nullable checksum column to pre-checksum installations; physical column
50
+ // ordinal is not part of the migration contract and must not be normalized
51
+ // with a non-transactional rename/create/copy/drop sequence. This keeps a
52
+ // crash from stranding authoritative migration history in a side table.
53
+ database.exec("ALTER TABLE schema_migrations ADD COLUMN checksum TEXT");
54
+ }
55
+ return database;
56
+ }
57
+ function appliedVersions(database) {
58
+ return database
59
+ .prepare("SELECT version FROM schema_migrations ORDER BY version")
60
+ .all().map((row) => row.version);
61
+ }
62
+ function expectedTableNames(migrations) {
63
+ return [
64
+ ...new Set(migrations.flatMap((migration) => [
65
+ ...migration.sql.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_][A-Za-z0-9_]*)/gi),
66
+ ].map((match) => match[1] ?? ""))),
67
+ ].filter(Boolean);
68
+ }
69
+ function inspectionStatus(database, migrations) {
70
+ const migrationTable = database
71
+ .prepare("SELECT 1 AS present FROM sqlite_master WHERE type='table' AND name='schema_migrations'")
72
+ .get();
73
+ const metadataColumns = migrationTable
74
+ ? migrationMetadataColumns(database)
75
+ : new Set();
76
+ const appliedRows = migrationTable
77
+ ? database
78
+ .prepare(metadataColumns.has("checksum")
79
+ ? "SELECT version, name, checksum FROM schema_migrations ORDER BY version"
80
+ : "SELECT version, name, NULL AS checksum FROM schema_migrations ORDER BY version")
81
+ .all()
82
+ : [];
83
+ const appliedSet = new Set(appliedRows.map((row) => row.version));
84
+ const expectedTables = expectedTableNames(migrations);
85
+ const actualTables = new Set(database
86
+ .prepare("SELECT name FROM sqlite_master WHERE type='table'")
87
+ .all().map((row) => row.name));
88
+ return {
89
+ contract: "task-migration",
90
+ contractVersion: "1.0.0",
91
+ appliedVersions: appliedRows.map((row) => row.version),
92
+ pendingVersions: migrations
93
+ .filter((migration) => !appliedSet.has(migration.version))
94
+ .map((migration) => migration.version),
95
+ metadataDrift: migrations.flatMap((migration) => {
96
+ const actual = appliedRows.find((row) => row.version === migration.version);
97
+ const coveredLegacyIdentity = actual?.checksum === null &&
98
+ migrations.some((barrier) => barrier.version > migration.version &&
99
+ appliedSet.has(barrier.version) &&
100
+ barrier.apply !== undefined &&
101
+ barrier.coversLegacyVersions?.includes(migration.version) === true);
102
+ return actual !== undefined &&
103
+ !coveredLegacyIdentity &&
104
+ actual.name !== migration.name
105
+ ? [
106
+ {
107
+ version: migration.version,
108
+ expectedName: migration.name,
109
+ actualName: actual.name,
110
+ },
111
+ ]
112
+ : [];
113
+ }),
114
+ checksumDrift: migrations.flatMap((migration) => {
115
+ const actual = appliedRows.find((row) => row.version === migration.version);
116
+ const expectedChecksum = migrationChecksum(migration);
117
+ return actual?.checksum !== null &&
118
+ actual?.checksum !== undefined &&
119
+ actual.checksum !== expectedChecksum
120
+ ? [
121
+ {
122
+ version: migration.version,
123
+ expectedChecksum,
124
+ actualChecksum: actual.checksum,
125
+ },
126
+ ]
127
+ : [];
128
+ }),
129
+ legacyMetadataVersions: appliedRows
130
+ .filter((row) => row.checksum === null)
131
+ .map((row) => row.version),
132
+ missingTables: expectedTables.filter((table) => !actualTables.has(table)),
133
+ schemaDrift: migrations.flatMap((migration) => appliedSet.has(migration.version) && migration.verify
134
+ ? [...migration.verify(database)].map((issue) => `v${migration.version}:${issue}`)
135
+ : []),
136
+ };
137
+ }
138
+ export function getMigrationStatus(input) {
139
+ const migrations = discoverMigrations(input.migrations);
140
+ if (input.databasePath !== ":memory:" && !existsSync(input.databasePath)) {
141
+ return {
142
+ contract: "task-migration",
143
+ contractVersion: "1.0.0",
144
+ appliedVersions: [],
145
+ pendingVersions: migrations.map((migration) => migration.version),
146
+ metadataDrift: [],
147
+ checksumDrift: [],
148
+ legacyMetadataVersions: [],
149
+ missingTables: expectedTableNames(migrations),
150
+ schemaDrift: [],
151
+ };
152
+ }
153
+ const database = new DatabaseSync(input.databasePath, { readOnly: true });
154
+ try {
155
+ return inspectionStatus(database, migrations);
156
+ }
157
+ finally {
158
+ database.close();
159
+ }
160
+ }
161
+ export function applyMigrations(input) {
162
+ const migrations = discoverMigrations(input.migrations);
163
+ const database = openDatabase(input.databasePath);
164
+ const appliedNow = [];
165
+ try {
166
+ const alreadyApplied = new Set(appliedVersions(database));
167
+ for (const migration of migrations) {
168
+ if (alreadyApplied.has(migration.version)) {
169
+ const row = database
170
+ .prepare("SELECT name, checksum FROM schema_migrations WHERE version = ?")
171
+ .get(migration.version);
172
+ const legacyIdentityCoveredByCompatibility = row?.checksum === null &&
173
+ migrations.some((barrier) => barrier.version > migration.version &&
174
+ barrier.apply !== undefined &&
175
+ barrier.coversLegacyVersions?.includes(migration.version) ===
176
+ true);
177
+ if (!row ||
178
+ (!legacyIdentityCoveredByCompatibility && row.name !== migration.name))
179
+ return {
180
+ contract: "task-migration",
181
+ contractVersion: "1.0.0",
182
+ ok: false,
183
+ applied: appliedNow,
184
+ pending: migrations
185
+ .filter((item) => !alreadyApplied.has(item.version))
186
+ .map((item) => item.version),
187
+ error: {
188
+ code: "MIGRATION_FAILED",
189
+ message: `migration ${migration.version} name drift: expected ${migration.name}, found ${row?.name ?? "missing"}`,
190
+ },
191
+ };
192
+ if (row.checksum === null && migration.apply !== undefined)
193
+ return {
194
+ contract: "task-migration",
195
+ contractVersion: "1.0.0",
196
+ ok: false,
197
+ applied: appliedNow,
198
+ pending: migrations
199
+ .filter((item) => !alreadyApplied.has(item.version))
200
+ .map((item) => item.version),
201
+ error: {
202
+ code: "MIGRATION_FAILED",
203
+ message: `compatibility migration ${migration.version} is missing checksum identity`,
204
+ },
205
+ };
206
+ if (row.checksum !== null &&
207
+ row.checksum !== migrationChecksum(migration))
208
+ return {
209
+ contract: "task-migration",
210
+ contractVersion: "1.0.0",
211
+ ok: false,
212
+ applied: appliedNow,
213
+ pending: migrations
214
+ .filter((item) => !alreadyApplied.has(item.version))
215
+ .map((item) => item.version),
216
+ error: {
217
+ code: "MIGRATION_FAILED",
218
+ message: `migration ${migration.version} checksum drift`,
219
+ },
220
+ };
221
+ // Rows created by a pre-checksum runner remain explicitly legacy. They are
222
+ // not backfilled with the current SQL checksum because the historical SQL
223
+ // cannot be proven. A later compatibility migration validates/upgrades the
224
+ // real schema instead.
225
+ continue;
226
+ }
227
+ database.exec("BEGIN IMMEDIATE");
228
+ try {
229
+ if (migration.sql.trim().length > 0)
230
+ database.exec(migration.sql);
231
+ migration.apply?.(database, input.context ?? {});
232
+ database
233
+ .prepare("INSERT INTO schema_migrations(version, name, checksum, applied_at) VALUES (?, ?, ?, ?)")
234
+ .run(migration.version, migration.name, migrationChecksum(migration), new Date().toISOString());
235
+ database.exec("COMMIT");
236
+ appliedNow.push(migration.version);
237
+ alreadyApplied.add(migration.version);
238
+ }
239
+ catch (error) {
240
+ database.exec("ROLLBACK");
241
+ return {
242
+ contract: "task-migration",
243
+ contractVersion: "1.0.0",
244
+ ok: false,
245
+ applied: appliedNow,
246
+ pending: migrations
247
+ .filter((item) => !alreadyApplied.has(item.version))
248
+ .map((item) => item.version),
249
+ error: {
250
+ code: "MIGRATION_FAILED",
251
+ message: error instanceof Error ? error.message : "migration failed",
252
+ },
253
+ };
254
+ }
255
+ }
256
+ return {
257
+ contract: "task-migration",
258
+ contractVersion: "1.0.0",
259
+ ok: true,
260
+ applied: appliedNow,
261
+ pending: [],
262
+ };
263
+ }
264
+ finally {
265
+ database.close();
266
+ }
267
+ }
268
+ export function verifyMigrations(input) {
269
+ const status = getMigrationStatus(input);
270
+ if (input.databasePath !== ":memory:" && !existsSync(input.databasePath)) {
271
+ return {
272
+ contract: "task-migration",
273
+ contractVersion: "1.0.0",
274
+ ok: false,
275
+ applied: [],
276
+ pending: status.pendingVersions,
277
+ error: {
278
+ code: "MIGRATION_VERIFY_FAILED",
279
+ message: "database does not exist",
280
+ },
281
+ };
282
+ }
283
+ const database = new DatabaseSync(input.databasePath, { readOnly: true });
284
+ try {
285
+ const integrity = database.prepare("PRAGMA integrity_check").get();
286
+ const appliedSet = new Set(status.appliedVersions);
287
+ const legacyMetadataCoveredByCompatibilityBarrier = status.legacyMetadataVersions.length === 0 ||
288
+ status.legacyMetadataVersions.every((legacyVersion) => input.migrations.some((migration) => migration.version > legacyVersion &&
289
+ appliedSet.has(migration.version) &&
290
+ migration.apply !== undefined &&
291
+ migration.coversLegacyVersions?.includes(legacyVersion) === true));
292
+ const ok = status.pendingVersions.length === 0 &&
293
+ status.metadataDrift.length === 0 &&
294
+ status.checksumDrift.length === 0 &&
295
+ legacyMetadataCoveredByCompatibilityBarrier &&
296
+ status.missingTables.length === 0 &&
297
+ status.schemaDrift.length === 0 &&
298
+ integrity?.integrity_check === "ok";
299
+ return {
300
+ contract: "task-migration",
301
+ contractVersion: "1.0.0",
302
+ ok,
303
+ applied: status.appliedVersions,
304
+ pending: status.pendingVersions,
305
+ ...(ok
306
+ ? {}
307
+ : {
308
+ error: {
309
+ code: "MIGRATION_VERIFY_FAILED",
310
+ message: "migration state or SQLite integrity does not match",
311
+ },
312
+ }),
313
+ };
314
+ }
315
+ finally {
316
+ database.close();
317
+ }
318
+ }
319
+ export async function runCli(args) {
320
+ if (args.includes("--help") || args.includes("-h")) {
321
+ return JSON.stringify({
322
+ contract: "deployment.result.v1",
323
+ ok: true,
324
+ status: "SUCCEEDED",
325
+ moduleRef: moduleDescriptor.moduleRef,
326
+ moduleVersion: moduleDescriptor.moduleVersion,
327
+ data: {
328
+ usage: "proflow-task-migrate [apply|status|verify] --database <path> [--legacy-role-map <json>]",
329
+ },
330
+ });
331
+ }
332
+ const command = args.find((item) => ["apply", "status", "verify"].includes(item)) ??
333
+ "status";
334
+ const databaseIndex = args.indexOf("--database");
335
+ const databasePath = databaseIndex >= 0 ? (args[databaseIndex + 1] ?? ":memory:") : ":memory:";
336
+ const legacyRoleMapIndex = args.indexOf("--legacy-role-map");
337
+ let migrationContext;
338
+ if (legacyRoleMapIndex >= 0) {
339
+ const mapPath = args[legacyRoleMapIndex + 1];
340
+ if (!mapPath)
341
+ throw new TypeError("--legacy-role-map requires a JSON file");
342
+ const parsed = JSON.parse(readFileSync(mapPath, "utf8"));
343
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object")
344
+ throw new TypeError("legacy role map must be a JSON object");
345
+ const record = parsed;
346
+ const structured = "roleMap" in record || "roleBindings" in record;
347
+ migrationContext = structured
348
+ ? {
349
+ ...(record.roleMap && typeof record.roleMap === "object"
350
+ ? {
351
+ legacyRoleMap: record.roleMap,
352
+ }
353
+ : {}),
354
+ ...(record.roleBindings && typeof record.roleBindings === "object"
355
+ ? {
356
+ legacyRoleBindings: record.roleBindings,
357
+ }
358
+ : {}),
359
+ }
360
+ : { legacyRoleMap: record };
361
+ }
362
+ let success = true;
363
+ let message = "migration status is readable";
364
+ if (command === "apply") {
365
+ const result = applyMigrations({
366
+ databasePath,
367
+ migrations: taskMigrations,
368
+ ...(migrationContext ? { context: migrationContext } : {}),
369
+ });
370
+ success = result.ok;
371
+ message = result.ok
372
+ ? "migrations applied"
373
+ : (result.error?.message ?? "migration failed");
374
+ }
375
+ else if (command === "verify") {
376
+ const result = verifyMigrations({
377
+ databasePath,
378
+ migrations: taskMigrations,
379
+ });
380
+ success = result.ok;
381
+ message = result.ok
382
+ ? "migrations verified"
383
+ : (result.error?.message ?? "migration verify failed");
384
+ }
385
+ else {
386
+ getMigrationStatus({ databasePath, migrations: taskMigrations });
387
+ }
388
+ return JSON.stringify({
389
+ contract: "deployment.result.v1",
390
+ ok: success,
391
+ status: success ? "SUCCEEDED" : "FAILED",
392
+ moduleRef: moduleDescriptor.moduleRef,
393
+ moduleVersion: moduleDescriptor.moduleVersion,
394
+ checks: [
395
+ { id: "migration-state", status: success ? "PASS" : "FAIL", message },
396
+ ],
397
+ ...(success
398
+ ? {}
399
+ : { error: { code: "COMMAND_FAILED", message, retryable: false } }),
400
+ });
401
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@tomflow/proflow-task-migration-runner",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "exports": {
9
+ ".": "./dist/src/index.js",
10
+ "./cli": "./dist/src/cli.js",
11
+ "./deployment/adapter": "./dist/deployment/adapter.js",
12
+ "./deployment/descriptor": "./dist/deployment/descriptor.js"
13
+ },
14
+ "bin": {
15
+ "proflow-task-migrate": "./dist/src/cli.js",
16
+ "proflow-task-migration-runner": "./self-install.mjs"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "proflow.module.json",
21
+ "conformance.json",
22
+ "README.md",
23
+ "self-install.mjs"
24
+ ],
25
+ "dependencies": {
26
+ "@tomflow/proflow-task-store-sqlite": "^0.1.0",
27
+ "@tomflow/proflow-module-contract": "^0.1.0"
28
+ },
29
+ "description": "Deterministically discovers, applies and verifies Task Store SQLite schema migrations.",
30
+ "keywords": [
31
+ "proflow",
32
+ "proflow-module",
33
+ "task-orchestration"
34
+ ],
35
+ "proflow": {
36
+ "module": true,
37
+ "installClass": "core",
38
+ "installRequires": [
39
+ "@tomflow/proflow-module-contract",
40
+ "@tomflow/proflow-task-store-sqlite"
41
+ ],
42
+ "descriptor": "./dist/deployment/descriptor.js",
43
+ "manifest": "./proflow.module.json"
44
+ },
45
+ "scripts": {
46
+ "test": "node --test tests/**/*.test.ts",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "contract": "module",
3
+ "contractVersion": "1.0.0",
4
+ "moduleRef": "task-migration-runner",
5
+ "packageName": "@tomflow/proflow-task-migration-runner",
6
+ "moduleVersion": "0.1.0",
7
+ "kind": "cli",
8
+ "templateVersion": "1.0.0",
9
+ "platformCompatibility": ">=1.0.0 <2.0.0",
10
+ "installClass": "core",
11
+ "identity": {
12
+ "domain": "task-orchestration",
13
+ "summary": "Deterministically discovers, applies and verifies Task Store SQLite schema migrations."
14
+ },
15
+ "provides": [],
16
+ "requires": [],
17
+ "requirements": [
18
+ {
19
+ "kind": "runtime",
20
+ "runtime": "node",
21
+ "versionRange": ">=24.19.0"
22
+ }
23
+ ],
24
+ "configSlots": [
25
+ {
26
+ "key": "databasePath",
27
+ "type": "path",
28
+ "required": true,
29
+ "description": "Task SQLite database path"
30
+ }
31
+ ],
32
+ "lifecycle": {
33
+ "supported": [
34
+ "describe",
35
+ "preflight",
36
+ "status",
37
+ "verify",
38
+ "doctor",
39
+ "migrate"
40
+ ]
41
+ },
42
+ "verification": {
43
+ "checks": [
44
+ {
45
+ "id": "migration-state-pass",
46
+ "description": "Migration state matches the Task schema",
47
+ "lifecycle": "verify"
48
+ }
49
+ ]
50
+ },
51
+ "effects": [
52
+ {
53
+ "kind": "filesystem",
54
+ "description": "Applies Task Store migration SQL to SQLite",
55
+ "retention": "preserve"
56
+ }
57
+ ],
58
+ "documentation": [
59
+ {
60
+ "id": "overview",
61
+ "path": "./README.md",
62
+ "description": "Package-owned module overview"
63
+ }
64
+ ]
65
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ const [command, ...rest] = process.argv.slice(2);
5
+ const usage = "Usage: npx @tomflow/proflow-task-migration-runner install\n";
6
+ if (command === "--help" || command === "-h") {
7
+ process.stdout.write(usage);
8
+ process.exit(0);
9
+ }
10
+ if (command !== "install" || rest.length > 0) {
11
+ process.stderr.write(usage);
12
+ process.exit(2);
13
+ }
14
+ const executable = process.platform === "win32" ? "npx.cmd" : "npx";
15
+ const result = spawnSync(
16
+ executable,
17
+ [
18
+ "--yes",
19
+ "@tomflow/proflow-platform-cli",
20
+ "install",
21
+ "@tomflow/proflow-task-migration-runner",
22
+ ],
23
+ { cwd: process.cwd(), env: process.env, stdio: "inherit" },
24
+ );
25
+ if (result.error) throw result.error;
26
+ process.exit(result.status ?? 1);