@tomflow/proflow-task-store-sqlite 0.1.13 → 0.1.14

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @tomflow/proflow-task-store-sqlite
2
2
 
3
+ ## 0.1.14
4
+
5
+ ### Patch Changes
6
+
7
+ - fix(task-store): make task evidence non-destructive
8
+
3
9
  ## 0.1.13
4
10
 
5
11
  ### Patch Changes
@@ -7,7 +7,7 @@ export declare const behaviorAdapter: {
7
7
  ok: true;
8
8
  status: "SUCCEEDED";
9
9
  moduleRef: "task-store-sqlite";
10
- moduleVersion: "0.1.13";
10
+ moduleVersion: "0.1.14";
11
11
  data: {
12
12
  databasePath: string;
13
13
  };
@@ -20,7 +20,7 @@ export declare const behaviorAdapter: {
20
20
  readonly ok: true;
21
21
  readonly status: "SUCCEEDED";
22
22
  readonly moduleRef: "task-store-sqlite";
23
- readonly moduleVersion: "0.1.13";
23
+ readonly moduleVersion: "0.1.14";
24
24
  };
25
25
  observedEffects: never[];
26
26
  }>;
@@ -30,7 +30,7 @@ export declare const behaviorAdapter: {
30
30
  ok: true;
31
31
  status: "SUCCEEDED";
32
32
  moduleRef: "task-store-sqlite";
33
- moduleVersion: "0.1.13";
33
+ moduleVersion: "0.1.14";
34
34
  data: {
35
35
  setupStatus: "FAILED" | "READY";
36
36
  runtimeStatus: "NOT_APPLICABLE";
@@ -51,11 +51,11 @@ export declare const behaviorAdapter: {
51
51
  readonly ok: true;
52
52
  readonly status: "SUCCEEDED";
53
53
  readonly moduleRef: "task-store-sqlite";
54
- readonly moduleVersion: "0.1.13";
54
+ readonly moduleVersion: "0.1.14";
55
55
  } | {
56
56
  contract: "deployment.result.v1";
57
57
  moduleRef: "task-store-sqlite";
58
- moduleVersion: "0.1.13";
58
+ moduleVersion: "0.1.14";
59
59
  ok: false;
60
60
  status: "FAILED";
61
61
  error: {
@@ -72,7 +72,7 @@ export declare const behaviorAdapter: {
72
72
  ok: true;
73
73
  status: "SUCCEEDED";
74
74
  moduleRef: "task-store-sqlite";
75
- moduleVersion: "0.1.13";
75
+ moduleVersion: "0.1.14";
76
76
  data: {
77
77
  docs: string;
78
78
  };
@@ -85,7 +85,7 @@ export declare const behaviorAdapter: {
85
85
  readonly ok: true;
86
86
  readonly status: "SUCCEEDED";
87
87
  readonly moduleRef: "task-store-sqlite";
88
- readonly moduleVersion: "0.1.13";
88
+ readonly moduleVersion: "0.1.14";
89
89
  };
90
90
  observedEffects: never[];
91
91
  }>;
@@ -95,7 +95,7 @@ export declare const behaviorAdapter: {
95
95
  readonly ok: true;
96
96
  readonly status: "SUCCEEDED";
97
97
  readonly moduleRef: "task-store-sqlite";
98
- readonly moduleVersion: "0.1.13";
98
+ readonly moduleVersion: "0.1.14";
99
99
  };
100
100
  observedEffects: never[];
101
101
  }>;
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "task-store-sqlite";
5
5
  readonly packageName: "@tomflow/proflow-task-store-sqlite";
6
- readonly moduleVersion: "0.1.13";
6
+ readonly moduleVersion: "0.1.14";
7
7
  readonly kind: "library";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -3,7 +3,7 @@ export const descriptor = {
3
3
  contractVersion: "1.0.0",
4
4
  moduleRef: "task-store-sqlite",
5
5
  packageName: "@tomflow/proflow-task-store-sqlite",
6
- moduleVersion: "0.1.13",
6
+ moduleVersion: "0.1.14",
7
7
  kind: "library",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
package/dist/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
+ import { taskDocumentRef } from "@tomflow/proflow-task-orchestration";
2
3
  import { createPhase4Repositories } from "./phase4-repositories.js";
3
4
  export {} from "./migrations.js";
4
5
  export { taskMigrations } from "./phase4-migrations.js";
@@ -92,10 +93,19 @@ function history(row) {
92
93
  };
93
94
  }
94
95
  function document(row) {
96
+ const taskId = String(row.task_id);
97
+ const documentType = String(row.document_type);
95
98
  return {
96
- taskId: String(row.task_id),
97
- documentType: String(row.document_type),
99
+ documentRef: taskDocumentRef(taskId, documentType),
100
+ taskId,
101
+ documentType,
98
102
  sourceNodeId: text(row.source_node_id),
103
+ sourceNodeRunNo: row.source_node_run_no === null ? null : number(row.source_node_run_no),
104
+ revision: number(row.revision),
105
+ sourceDocumentRef: text(row.source_document_ref),
106
+ sourceDocumentRevision: row.source_document_revision === null
107
+ ? null
108
+ : number(row.source_document_revision),
99
109
  filePath: String(row.file_path),
100
110
  contentHash: String(row.content_hash),
101
111
  updatedByRef: String(row.updated_by_ref),
@@ -230,7 +240,17 @@ export class SqliteTaskStore {
230
240
  return row ? document(row) : undefined;
231
241
  },
232
242
  upsert: (v) => {
233
- db.prepare("INSERT INTO task_documents VALUES (?,?,?,?,?,?,?) ON CONFLICT(task_id,document_type) DO UPDATE SET source_node_id=excluded.source_node_id,file_path=excluded.file_path,content_hash=excluded.content_hash,updated_by_ref=excluded.updated_by_ref,updated_at=excluded.updated_at").run(v.taskId, v.documentType, v.sourceNodeId, v.filePath, v.contentHash, v.updatedByRef, v.updatedAt);
243
+ if (v.documentRef !== taskDocumentRef(v.taskId, v.documentType))
244
+ throw new Error("TASK_DOCUMENT_IDENTITY_CONFLICT");
245
+ if (!Number.isSafeInteger(v.revision) || v.revision < 1)
246
+ throw new Error("TASK_DOCUMENT_REVISION_CONFLICT");
247
+ if (v.revision === 1) {
248
+ db.prepare("INSERT INTO task_documents(task_id,document_type,source_node_id,file_path,content_hash,updated_by_ref,updated_at,source_node_run_no,revision,source_document_ref,source_document_revision) VALUES (?,?,?,?,?,?,?,?,?,?,?)").run(v.taskId, v.documentType, v.sourceNodeId, v.filePath, v.contentHash, v.updatedByRef, v.updatedAt, v.sourceNodeRunNo, v.revision, v.sourceDocumentRef, v.sourceDocumentRevision);
249
+ return;
250
+ }
251
+ const result = db.prepare("UPDATE task_documents SET source_node_id=?,file_path=?,content_hash=?,updated_by_ref=?,updated_at=?,source_node_run_no=?,revision=?,source_document_ref=?,source_document_revision=? WHERE task_id=? AND document_type=? AND revision=?").run(v.sourceNodeId, v.filePath, v.contentHash, v.updatedByRef, v.updatedAt, v.sourceNodeRunNo, v.revision, v.sourceDocumentRef, v.sourceDocumentRevision, v.taskId, v.documentType, v.revision - 1);
252
+ if (Number(result.changes) !== 1)
253
+ throw new Error("TASK_DOCUMENT_REVISION_CONFLICT");
234
254
  },
235
255
  listByTask: (taskId) => all("SELECT * FROM task_documents WHERE task_id=? ORDER BY document_type", taskId).map(document),
236
256
  },
@@ -19,6 +19,21 @@ const phase4TaskColumns = [
19
19
  "source_product_intent_ref",
20
20
  "campaign_ref",
21
21
  ];
22
+ const legacyTaskDocumentColumns = [
23
+ "task_id",
24
+ "document_type",
25
+ "source_node_id",
26
+ "file_path",
27
+ "content_hash",
28
+ "updated_by_ref",
29
+ "updated_at",
30
+ ];
31
+ const evidenceTaskDocumentColumns = [
32
+ "source_node_run_no",
33
+ "revision",
34
+ "source_document_ref",
35
+ "source_document_revision",
36
+ ];
22
37
  const phase4Tables = [
23
38
  "product_campaigns",
24
39
  "product_intents",
@@ -39,16 +54,25 @@ if (!legacyV3)
39
54
  const v3Compatible = {
40
55
  ...legacyV3,
41
56
  verify: (database) => {
42
- const issues = [...(legacyV3.verify?.(database) ?? [])];
43
- if (!tableExists(database, "tasks"))
44
- return issues;
45
- const columns = tableColumns(database, "tasks");
46
- const phase4ShapePresent = columns.size === legacyTaskColumns.length + phase4TaskColumns.length &&
47
- legacyTaskColumns.every((column) => columns.has(column)) &&
48
- phase4TaskColumns.every((column) => columns.has(column));
49
- return phase4ShapePresent
50
- ? issues.filter((issue) => issue !== "tasks:column_shape_mismatch")
51
- : issues;
57
+ let issues = [...(legacyV3.verify?.(database) ?? [])];
58
+ if (tableExists(database, "tasks")) {
59
+ const columns = tableColumns(database, "tasks");
60
+ const phase4ShapePresent = columns.size === legacyTaskColumns.length + phase4TaskColumns.length &&
61
+ legacyTaskColumns.every((column) => columns.has(column)) &&
62
+ phase4TaskColumns.every((column) => columns.has(column));
63
+ if (phase4ShapePresent)
64
+ issues = issues.filter((issue) => issue !== "tasks:column_shape_mismatch");
65
+ }
66
+ if (tableExists(database, "task_documents")) {
67
+ const columns = tableColumns(database, "task_documents");
68
+ const evidenceShapePresent = columns.size ===
69
+ legacyTaskDocumentColumns.length + evidenceTaskDocumentColumns.length &&
70
+ legacyTaskDocumentColumns.every((column) => columns.has(column)) &&
71
+ evidenceTaskDocumentColumns.every((column) => columns.has(column));
72
+ if (evidenceShapePresent)
73
+ issues = issues.filter((issue) => issue !== "task_documents:column_shape_mismatch");
74
+ }
75
+ return issues;
52
76
  },
53
77
  };
54
78
  const phase4Migration = {
@@ -273,9 +297,61 @@ END;
273
297
  return issues;
274
298
  },
275
299
  };
300
+ const taskDocumentEvidenceIntegrityMigration = {
301
+ version: 7,
302
+ name: "task_document_evidence_integrity",
303
+ identity: "task_document_evidence_integrity.20260928",
304
+ sql: [
305
+ "ALTER TABLE task_documents ADD COLUMN source_node_run_no INTEGER CHECK (source_node_run_no IS NULL OR source_node_run_no >= 1);",
306
+ "ALTER TABLE task_documents ADD COLUMN revision INTEGER NOT NULL DEFAULT 1 CHECK (revision >= 1);",
307
+ "ALTER TABLE task_documents ADD COLUMN source_document_ref TEXT;",
308
+ "ALTER TABLE task_documents ADD COLUMN source_document_revision INTEGER CHECK (source_document_revision IS NULL OR source_document_revision >= 1);",
309
+ "CREATE TRIGGER trg_task_documents_insert_evidence BEFORE INSERT ON task_documents BEGIN "
310
+ + "SELECT CASE WHEN NEW.revision <> 1 THEN RAISE(ABORT, 'TASK_DOCUMENT_REVISION_CONFLICT') END; "
311
+ + "SELECT CASE WHEN (NEW.source_node_id IS NULL) <> (NEW.source_node_run_no IS NULL) OR (NEW.source_document_ref IS NULL) <> (NEW.source_document_revision IS NULL) OR (NEW.source_node_id IS NOT NULL AND NEW.source_document_ref IS NOT NULL) THEN RAISE(ABORT, 'TASK_DOCUMENT_EVIDENCE_INVALID') END; "
312
+ + "SELECT CASE WHEN NEW.source_node_id IS NULL AND NOT EXISTS (SELECT 1 FROM tasks AS task WHERE task.task_id=NEW.task_id AND task.status='PENDING') THEN RAISE(ABORT, 'TASK_DOCUMENT_EVIDENCE_INVALID') END; "
313
+ + "SELECT CASE WHEN NEW.source_node_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM nodes AS node JOIN tasks AS task ON task.task_id=node.task_id WHERE node.node_id=NEW.source_node_id AND node.task_id=NEW.task_id AND node.run_no=NEW.source_node_run_no AND node.status='IN_PROGRESS' AND node.worker_ref IS NOT NULL AND task.status='ACTIVE' AND task.current_node_id=node.node_id AND NEW.updated_by_ref='worker:' || node.worker_ref AND EXISTS (SELECT 1 FROM json_each(node.output_documents_json) WHERE value=NEW.document_type)) THEN RAISE(ABORT, 'TASK_DOCUMENT_EVIDENCE_INVALID') END; "
314
+ + "SELECT CASE WHEN NEW.source_document_ref IS NOT NULL AND NOT EXISTS (SELECT 1 FROM product_documents AS source JOIN product_intents AS intent ON intent.intent_ref=source.intent_ref WHERE source.document_ref=NEW.source_document_ref AND source.revision=NEW.source_document_revision AND source.file_path=NEW.file_path AND source.content_hash=NEW.content_hash AND source.updated_by_ref=NEW.updated_by_ref AND intent.materialized_task_id=NEW.task_id) THEN RAISE(ABORT, 'TASK_DOCUMENT_SOURCE_MISMATCH') END; END;",
315
+ "CREATE TRIGGER trg_task_documents_update_evidence BEFORE UPDATE ON task_documents BEGIN "
316
+ + "SELECT CASE WHEN NEW.task_id IS NOT OLD.task_id OR NEW.document_type IS NOT OLD.document_type THEN RAISE(ABORT, 'TASK_DOCUMENT_IDENTITY_CONFLICT') END; "
317
+ + "SELECT CASE WHEN NEW.revision <> OLD.revision + 1 THEN RAISE(ABORT, 'TASK_DOCUMENT_REVISION_CONFLICT') END; "
318
+ + "SELECT CASE WHEN (NEW.source_node_id IS NULL) <> (NEW.source_node_run_no IS NULL) OR (NEW.source_document_ref IS NULL) <> (NEW.source_document_revision IS NULL) OR (NEW.source_node_id IS NOT NULL AND NEW.source_document_ref IS NOT NULL) THEN RAISE(ABORT, 'TASK_DOCUMENT_EVIDENCE_INVALID') END; "
319
+ + "SELECT CASE WHEN NEW.source_node_id IS NULL AND NOT EXISTS (SELECT 1 FROM tasks AS task WHERE task.task_id=NEW.task_id AND task.status='PENDING') THEN RAISE(ABORT, 'TASK_DOCUMENT_EVIDENCE_INVALID') END; "
320
+ + "SELECT CASE WHEN NEW.source_node_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM nodes AS node JOIN tasks AS task ON task.task_id=node.task_id WHERE node.node_id=NEW.source_node_id AND node.task_id=NEW.task_id AND node.run_no=NEW.source_node_run_no AND node.status='IN_PROGRESS' AND node.worker_ref IS NOT NULL AND task.status='ACTIVE' AND task.current_node_id=node.node_id AND NEW.updated_by_ref='worker:' || node.worker_ref AND EXISTS (SELECT 1 FROM json_each(node.output_documents_json) WHERE value=NEW.document_type)) THEN RAISE(ABORT, 'TASK_DOCUMENT_EVIDENCE_INVALID') END; "
321
+ + "SELECT CASE WHEN NEW.source_document_ref IS NOT NULL AND NOT EXISTS (SELECT 1 FROM product_documents AS source JOIN product_intents AS intent ON intent.intent_ref=source.intent_ref WHERE source.document_ref=NEW.source_document_ref AND source.revision=NEW.source_document_revision AND source.file_path=NEW.file_path AND source.content_hash=NEW.content_hash AND source.updated_by_ref=NEW.updated_by_ref AND intent.materialized_task_id=NEW.task_id) THEN RAISE(ABORT, 'TASK_DOCUMENT_SOURCE_MISMATCH') END; END;",
322
+ "CREATE TRIGGER trg_product_document_transferred_immutable BEFORE UPDATE OF revision,file_path,content_hash,updated_by_ref ON product_documents WHEN EXISTS (SELECT 1 FROM task_documents WHERE source_document_ref=OLD.document_ref) BEGIN SELECT RAISE(ABORT, 'PRODUCT_DOCUMENT_ALREADY_TRANSFERRED'); END;",
323
+ "CREATE TRIGGER trg_task_documents_delete_evidence BEFORE DELETE ON task_documents BEGIN SELECT RAISE(ABORT, 'TASK_DOCUMENT_DELETE_FORBIDDEN'); END;",
324
+ "CREATE TRIGGER trg_product_document_transferred_delete_forbidden BEFORE DELETE ON product_documents WHEN EXISTS (SELECT 1 FROM task_documents WHERE source_document_ref=OLD.document_ref) BEGIN SELECT RAISE(ABORT, 'PRODUCT_DOCUMENT_DELETE_FORBIDDEN'); END;",
325
+ ].join("\n"),
326
+ verify: (database) => {
327
+ const issues = [];
328
+ if (!tableExists(database, "task_documents"))
329
+ return ["task_documents:missing"];
330
+ const columns = tableColumns(database, "task_documents");
331
+ for (const column of evidenceTaskDocumentColumns)
332
+ if (!columns.has(column))
333
+ issues.push("task_documents." + column + ":missing");
334
+ for (const trigger of [
335
+ "trg_task_documents_insert_evidence",
336
+ "trg_task_documents_update_evidence",
337
+ "trg_product_document_transferred_immutable",
338
+ "trg_task_documents_delete_evidence",
339
+ "trg_product_document_transferred_delete_forbidden",
340
+ ])
341
+ if (!triggerExists(database, trigger))
342
+ issues.push(trigger + ":missing");
343
+ const invalidRevision = database
344
+ .prepare("SELECT COUNT(*) AS count FROM task_documents WHERE revision < 1")
345
+ .get();
346
+ if (Number(invalidRevision.count) !== 0)
347
+ issues.push("task_documents.revision:invalid");
348
+ return issues;
349
+ },
350
+ };
276
351
  export const taskMigrations = [
277
352
  ...legacyTaskMigrations.map((migration) => migration.version === 3 ? v3Compatible : migration),
278
353
  phase4Migration,
279
354
  gapReviewMigration,
280
355
  taskProductLinkIntegrityMigration,
356
+ taskDocumentEvidenceIntegrityMigration,
281
357
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-task-store-sqlite",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "@tomflow/proflow-module-contract": "^0.1.13",
24
- "@tomflow/proflow-task-orchestration": "^0.1.13"
24
+ "@tomflow/proflow-task-orchestration": "^0.1.14"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@tomflow/proflow-task-migration-runner": "^0.1.11"
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "task-store-sqlite",
5
5
  "packageName": "@tomflow/proflow-task-store-sqlite",
6
- "moduleVersion": "0.1.13",
6
+ "moduleVersion": "0.1.14",
7
7
  "kind": "library",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",