@tomflow/proflow-task-store-sqlite 0.1.12 → 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 +12 -0
- package/dist/deployment/adapter.d.ts +8 -8
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.js +48 -4
- package/dist/src/phase4-migrations.d.ts +3 -0
- package/dist/src/phase4-migrations.js +357 -0
- package/dist/src/phase4-repositories.d.ts +3 -0
- package/dist/src/phase4-repositories.js +195 -0
- package/package.json +3 -3
- package/proflow.module.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.1.13
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fix(task-store): preserve durable campaign membership
|
|
14
|
+
|
|
3
15
|
## 0.1.12
|
|
4
16
|
|
|
5
17
|
### 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.
|
|
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.
|
|
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.
|
|
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.
|
|
54
|
+
readonly moduleVersion: "0.1.14";
|
|
55
55
|
} | {
|
|
56
56
|
contract: "deployment.result.v1";
|
|
57
57
|
moduleRef: "task-store-sqlite";
|
|
58
|
-
moduleVersion: "0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Task, TaskRepositories
|
|
2
|
-
export { type TaskMigration, type TaskMigrationContext
|
|
3
|
-
export
|
|
1
|
+
import type { Phase4TaskRepositories, Phase4TaskStore, Task, TaskRepositories } from "@tomflow/proflow-task-orchestration";
|
|
2
|
+
export { type TaskMigration, type TaskMigrationContext } from "./migrations.ts";
|
|
3
|
+
export { taskMigrations } from "./phase4-migrations.ts";
|
|
4
|
+
export declare class SqliteTaskStore implements Phase4TaskStore {
|
|
4
5
|
#private;
|
|
5
6
|
constructor(input: {
|
|
6
7
|
databasePath: string;
|
|
@@ -8,6 +9,8 @@ export declare class SqliteTaskStore implements TaskStore {
|
|
|
8
9
|
});
|
|
9
10
|
transaction<T>(work: (repositories: TaskRepositories) => T): T;
|
|
10
11
|
read<T>(work: (repositories: TaskRepositories) => T): T;
|
|
12
|
+
phase4Transaction<T>(work: (repositories: TaskRepositories & Phase4TaskRepositories) => T): T;
|
|
13
|
+
phase4Read<T>(work: (repositories: TaskRepositories & Phase4TaskRepositories) => T): T;
|
|
11
14
|
listReconciliationTaskIds(input: {
|
|
12
15
|
statuses: readonly Task["status"][];
|
|
13
16
|
afterTaskId?: string;
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
|
|
2
|
+
import { taskDocumentRef } from "@tomflow/proflow-task-orchestration";
|
|
3
|
+
import { createPhase4Repositories } from "./phase4-repositories.js";
|
|
4
|
+
export {} from "./migrations.js";
|
|
5
|
+
export { taskMigrations } from "./phase4-migrations.js";
|
|
3
6
|
const json = (value) => JSON.stringify(value);
|
|
4
7
|
const text = (value) => value === null || value === undefined ? null : String(value);
|
|
5
8
|
const number = (value) => Number(value);
|
|
@@ -90,10 +93,19 @@ function history(row) {
|
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
function document(row) {
|
|
96
|
+
const taskId = String(row.task_id);
|
|
97
|
+
const documentType = String(row.document_type);
|
|
93
98
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
documentRef: taskDocumentRef(taskId, documentType),
|
|
100
|
+
taskId,
|
|
101
|
+
documentType,
|
|
96
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),
|
|
97
109
|
filePath: String(row.file_path),
|
|
98
110
|
contentHash: String(row.content_hash),
|
|
99
111
|
updatedByRef: String(row.updated_by_ref),
|
|
@@ -228,7 +240,17 @@ export class SqliteTaskStore {
|
|
|
228
240
|
return row ? document(row) : undefined;
|
|
229
241
|
},
|
|
230
242
|
upsert: (v) => {
|
|
231
|
-
|
|
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");
|
|
232
254
|
},
|
|
233
255
|
listByTask: (taskId) => all("SELECT * FROM task_documents WHERE task_id=? ORDER BY document_type", taskId).map(document),
|
|
234
256
|
},
|
|
@@ -262,6 +284,9 @@ export class SqliteTaskStore {
|
|
|
262
284
|
},
|
|
263
285
|
};
|
|
264
286
|
}
|
|
287
|
+
#phase4Repositories() {
|
|
288
|
+
return createPhase4Repositories(this.#database);
|
|
289
|
+
}
|
|
265
290
|
transaction(work) {
|
|
266
291
|
this.#database.exec("BEGIN IMMEDIATE");
|
|
267
292
|
try {
|
|
@@ -277,6 +302,21 @@ export class SqliteTaskStore {
|
|
|
277
302
|
read(work) {
|
|
278
303
|
return work(this.#repositories());
|
|
279
304
|
}
|
|
305
|
+
phase4Transaction(work) {
|
|
306
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
307
|
+
try {
|
|
308
|
+
const result = work({ ...this.#repositories(), ...this.#phase4Repositories() });
|
|
309
|
+
this.#database.exec("COMMIT");
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
this.#database.exec("ROLLBACK");
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
phase4Read(work) {
|
|
318
|
+
return work({ ...this.#repositories(), ...this.#phase4Repositories() });
|
|
319
|
+
}
|
|
280
320
|
listReconciliationTaskIds(input) {
|
|
281
321
|
const statuses = [...new Set(input.statuses.filter((value) => value.length > 0))];
|
|
282
322
|
if (statuses.length === 0)
|
|
@@ -322,6 +362,10 @@ export class SqliteTaskStore {
|
|
|
322
362
|
"task_messages",
|
|
323
363
|
"task_events",
|
|
324
364
|
"idempotency_records",
|
|
365
|
+
"product_campaigns",
|
|
366
|
+
"product_intents",
|
|
367
|
+
"product_documents",
|
|
368
|
+
"campaign_authorizations",
|
|
325
369
|
];
|
|
326
370
|
return Object.fromEntries(tables.map((table) => [
|
|
327
371
|
table,
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { taskMigrations as legacyTaskMigrations, } from "./migrations.js";
|
|
2
|
+
const legacyTaskColumns = [
|
|
3
|
+
"task_id",
|
|
4
|
+
"task_group_id",
|
|
5
|
+
"sequence_no",
|
|
6
|
+
"title",
|
|
7
|
+
"objective",
|
|
8
|
+
"status",
|
|
9
|
+
"version",
|
|
10
|
+
"plan_version",
|
|
11
|
+
"current_node_id",
|
|
12
|
+
"created_by_ref",
|
|
13
|
+
"created_at",
|
|
14
|
+
"started_at",
|
|
15
|
+
"completed_at",
|
|
16
|
+
"updated_at",
|
|
17
|
+
];
|
|
18
|
+
const phase4TaskColumns = [
|
|
19
|
+
"source_product_intent_ref",
|
|
20
|
+
"campaign_ref",
|
|
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
|
+
];
|
|
37
|
+
const phase4Tables = [
|
|
38
|
+
"product_campaigns",
|
|
39
|
+
"product_intents",
|
|
40
|
+
"product_documents",
|
|
41
|
+
"campaign_authorizations",
|
|
42
|
+
];
|
|
43
|
+
function tableExists(database, table) {
|
|
44
|
+
return Boolean(database
|
|
45
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type='table' AND name=?")
|
|
46
|
+
.get(table));
|
|
47
|
+
}
|
|
48
|
+
function tableColumns(database, table) {
|
|
49
|
+
return new Set(database.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
50
|
+
}
|
|
51
|
+
const legacyV3 = legacyTaskMigrations.find((migration) => migration.version === 3);
|
|
52
|
+
if (!legacyV3)
|
|
53
|
+
throw new Error("TASK_SCHEMA_V3_REQUIRED");
|
|
54
|
+
const v3Compatible = {
|
|
55
|
+
...legacyV3,
|
|
56
|
+
verify: (database) => {
|
|
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;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const phase4Migration = {
|
|
79
|
+
version: 4,
|
|
80
|
+
name: "product_campaign_owner_facts",
|
|
81
|
+
identity: "product_campaign_owner_facts.20260914",
|
|
82
|
+
sql: `
|
|
83
|
+
CREATE TABLE product_campaigns (
|
|
84
|
+
campaign_ref TEXT PRIMARY KEY,
|
|
85
|
+
discussion_ref TEXT NOT NULL,
|
|
86
|
+
goal_revision INTEGER NOT NULL CHECK (goal_revision >= 1),
|
|
87
|
+
goal_summary TEXT NOT NULL,
|
|
88
|
+
scope_json TEXT NOT NULL,
|
|
89
|
+
constraints_json TEXT NOT NULL,
|
|
90
|
+
status TEXT NOT NULL CHECK (status IN ('OPEN','CLOSED')),
|
|
91
|
+
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
92
|
+
created_by_ref TEXT NOT NULL,
|
|
93
|
+
created_at TEXT NOT NULL,
|
|
94
|
+
closed_at TEXT,
|
|
95
|
+
closure_summary TEXT
|
|
96
|
+
);
|
|
97
|
+
CREATE TABLE product_intents (
|
|
98
|
+
intent_ref TEXT PRIMARY KEY,
|
|
99
|
+
campaign_ref TEXT,
|
|
100
|
+
discussion_ref TEXT NOT NULL,
|
|
101
|
+
goal_revision INTEGER NOT NULL CHECK (goal_revision >= 1),
|
|
102
|
+
intent_revision INTEGER NOT NULL CHECK (intent_revision >= 1),
|
|
103
|
+
predecessor_task_id TEXT,
|
|
104
|
+
title TEXT NOT NULL,
|
|
105
|
+
objective TEXT NOT NULL,
|
|
106
|
+
scope_json TEXT NOT NULL,
|
|
107
|
+
constraints_json TEXT NOT NULL,
|
|
108
|
+
plan_json TEXT NOT NULL,
|
|
109
|
+
materialized_task_id TEXT,
|
|
110
|
+
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
111
|
+
created_by_ref TEXT NOT NULL,
|
|
112
|
+
created_at TEXT NOT NULL,
|
|
113
|
+
materialized_at TEXT,
|
|
114
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref),
|
|
115
|
+
FOREIGN KEY (predecessor_task_id) REFERENCES tasks(task_id),
|
|
116
|
+
FOREIGN KEY (materialized_task_id) REFERENCES tasks(task_id)
|
|
117
|
+
);
|
|
118
|
+
CREATE TABLE product_documents (
|
|
119
|
+
document_ref TEXT PRIMARY KEY,
|
|
120
|
+
campaign_ref TEXT NOT NULL,
|
|
121
|
+
intent_ref TEXT,
|
|
122
|
+
document_type TEXT NOT NULL,
|
|
123
|
+
file_path TEXT NOT NULL,
|
|
124
|
+
content_hash TEXT NOT NULL,
|
|
125
|
+
revision INTEGER NOT NULL CHECK (revision >= 1),
|
|
126
|
+
updated_by_ref TEXT NOT NULL,
|
|
127
|
+
updated_at TEXT NOT NULL,
|
|
128
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref) ON DELETE CASCADE,
|
|
129
|
+
FOREIGN KEY (intent_ref) REFERENCES product_intents(intent_ref) ON DELETE CASCADE
|
|
130
|
+
);
|
|
131
|
+
CREATE TABLE campaign_authorizations (
|
|
132
|
+
authorization_ref TEXT PRIMARY KEY,
|
|
133
|
+
campaign_ref TEXT NOT NULL,
|
|
134
|
+
discussion_ref TEXT NOT NULL,
|
|
135
|
+
goal_revision INTEGER NOT NULL CHECK (goal_revision >= 1),
|
|
136
|
+
goal_summary TEXT NOT NULL,
|
|
137
|
+
scope_json TEXT NOT NULL,
|
|
138
|
+
constraints_json TEXT NOT NULL,
|
|
139
|
+
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
140
|
+
created_by_ref TEXT NOT NULL,
|
|
141
|
+
created_at TEXT NOT NULL,
|
|
142
|
+
revoked_at TEXT,
|
|
143
|
+
revoked_by_ref TEXT,
|
|
144
|
+
revoke_reason TEXT,
|
|
145
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref) ON DELETE CASCADE
|
|
146
|
+
);
|
|
147
|
+
ALTER TABLE tasks ADD COLUMN source_product_intent_ref TEXT REFERENCES product_intents(intent_ref);
|
|
148
|
+
ALTER TABLE tasks ADD COLUMN campaign_ref TEXT REFERENCES product_campaigns(campaign_ref);
|
|
149
|
+
CREATE UNIQUE INDEX idx_product_intents_materialized_task ON product_intents(materialized_task_id) WHERE materialized_task_id IS NOT NULL;
|
|
150
|
+
CREATE INDEX idx_product_intents_campaign ON product_intents(campaign_ref, created_at);
|
|
151
|
+
CREATE INDEX idx_product_documents_campaign ON product_documents(campaign_ref, intent_ref);
|
|
152
|
+
CREATE INDEX idx_campaign_authorizations_campaign ON campaign_authorizations(campaign_ref, revoked_at);
|
|
153
|
+
CREATE INDEX idx_tasks_campaign ON tasks(campaign_ref, task_id);
|
|
154
|
+
`,
|
|
155
|
+
verify: (database) => {
|
|
156
|
+
const issues = [];
|
|
157
|
+
for (const table of phase4Tables)
|
|
158
|
+
if (!tableExists(database, table))
|
|
159
|
+
issues.push(`${table}:missing`);
|
|
160
|
+
if (!tableExists(database, "tasks"))
|
|
161
|
+
issues.push("tasks:missing");
|
|
162
|
+
else {
|
|
163
|
+
const columns = tableColumns(database, "tasks");
|
|
164
|
+
for (const column of [...legacyTaskColumns, ...phase4TaskColumns])
|
|
165
|
+
if (!columns.has(column))
|
|
166
|
+
issues.push(`tasks.${column}:missing`);
|
|
167
|
+
}
|
|
168
|
+
const foreignKeyViolations = database
|
|
169
|
+
.prepare("PRAGMA foreign_key_check")
|
|
170
|
+
.all();
|
|
171
|
+
if (foreignKeyViolations.length > 0)
|
|
172
|
+
issues.push(`foreign_key_check:${foreignKeyViolations.length}`);
|
|
173
|
+
return issues;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
function productLinkMismatchCount(database) {
|
|
177
|
+
const row = database
|
|
178
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
179
|
+
FROM tasks AS task
|
|
180
|
+
LEFT JOIN product_intents AS intent
|
|
181
|
+
ON intent.intent_ref = task.source_product_intent_ref
|
|
182
|
+
WHERE (task.campaign_ref IS NOT NULL AND task.source_product_intent_ref IS NULL)
|
|
183
|
+
OR (
|
|
184
|
+
task.source_product_intent_ref IS NOT NULL
|
|
185
|
+
AND (
|
|
186
|
+
intent.intent_ref IS NULL
|
|
187
|
+
OR intent.materialized_task_id IS NOT task.task_id
|
|
188
|
+
OR intent.campaign_ref IS NOT task.campaign_ref
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
OR EXISTS (
|
|
192
|
+
SELECT 1
|
|
193
|
+
FROM product_intents AS materialized
|
|
194
|
+
WHERE materialized.materialized_task_id = task.task_id
|
|
195
|
+
AND (
|
|
196
|
+
task.source_product_intent_ref IS NOT materialized.intent_ref
|
|
197
|
+
OR task.campaign_ref IS NOT materialized.campaign_ref
|
|
198
|
+
)
|
|
199
|
+
)`)
|
|
200
|
+
.get();
|
|
201
|
+
return Number(row.count);
|
|
202
|
+
}
|
|
203
|
+
function triggerExists(database, name) {
|
|
204
|
+
return Boolean(database
|
|
205
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type='trigger' AND name=?")
|
|
206
|
+
.get(name));
|
|
207
|
+
}
|
|
208
|
+
const gapReviewMigration = {
|
|
209
|
+
version: 5,
|
|
210
|
+
name: "product_gap_review_owner_facts",
|
|
211
|
+
identity: "product_gap_review_owner_facts.20260914",
|
|
212
|
+
sql: `
|
|
213
|
+
CREATE TABLE product_gap_reviews (
|
|
214
|
+
review_ref TEXT PRIMARY KEY,
|
|
215
|
+
campaign_ref TEXT NOT NULL,
|
|
216
|
+
intent_ref TEXT NOT NULL UNIQUE,
|
|
217
|
+
task_id TEXT NOT NULL UNIQUE,
|
|
218
|
+
discussion_ref TEXT NOT NULL,
|
|
219
|
+
gap_identity TEXT NOT NULL,
|
|
220
|
+
progress_evidence_refs_json TEXT NOT NULL,
|
|
221
|
+
terminal_task_version INTEGER NOT NULL CHECK (terminal_task_version >= 1),
|
|
222
|
+
created_by_ref TEXT NOT NULL,
|
|
223
|
+
created_at TEXT NOT NULL,
|
|
224
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref) ON DELETE CASCADE,
|
|
225
|
+
FOREIGN KEY (intent_ref) REFERENCES product_intents(intent_ref) ON DELETE CASCADE,
|
|
226
|
+
FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
|
|
227
|
+
);
|
|
228
|
+
CREATE INDEX idx_product_gap_reviews_campaign
|
|
229
|
+
ON product_gap_reviews(campaign_ref, gap_identity, created_at, review_ref);
|
|
230
|
+
`,
|
|
231
|
+
verify: (database) => {
|
|
232
|
+
const issues = [];
|
|
233
|
+
if (!tableExists(database, "product_gap_reviews"))
|
|
234
|
+
issues.push("product_gap_reviews:missing");
|
|
235
|
+
const foreignKeyViolations = database
|
|
236
|
+
.prepare("PRAGMA foreign_key_check")
|
|
237
|
+
.all();
|
|
238
|
+
if (foreignKeyViolations.length > 0)
|
|
239
|
+
issues.push(`foreign_key_check:${foreignKeyViolations.length}`);
|
|
240
|
+
return issues;
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
const taskProductLinkIntegrityMigration = {
|
|
244
|
+
version: 6,
|
|
245
|
+
name: "task_product_link_integrity",
|
|
246
|
+
identity: "task_product_link_integrity.20260928",
|
|
247
|
+
sql: `
|
|
248
|
+
CREATE TRIGGER trg_tasks_product_link_consistency
|
|
249
|
+
BEFORE UPDATE OF source_product_intent_ref, campaign_ref ON tasks
|
|
250
|
+
BEGIN
|
|
251
|
+
SELECT CASE
|
|
252
|
+
WHEN NEW.campaign_ref IS NOT NULL AND NEW.source_product_intent_ref IS NULL
|
|
253
|
+
THEN RAISE(ABORT, 'TASK_PRODUCT_LINK_MISMATCH')
|
|
254
|
+
END;
|
|
255
|
+
SELECT CASE
|
|
256
|
+
WHEN NEW.source_product_intent_ref IS NOT NULL
|
|
257
|
+
AND NOT EXISTS (
|
|
258
|
+
SELECT 1
|
|
259
|
+
FROM product_intents AS intent
|
|
260
|
+
WHERE intent.intent_ref = NEW.source_product_intent_ref
|
|
261
|
+
AND intent.materialized_task_id IS NEW.task_id
|
|
262
|
+
AND intent.campaign_ref IS NEW.campaign_ref
|
|
263
|
+
)
|
|
264
|
+
THEN RAISE(ABORT, 'TASK_PRODUCT_LINK_MISMATCH')
|
|
265
|
+
END;
|
|
266
|
+
END;
|
|
267
|
+
CREATE TRIGGER trg_product_intent_task_link_consistency
|
|
268
|
+
BEFORE UPDATE OF campaign_ref, materialized_task_id ON product_intents
|
|
269
|
+
WHEN EXISTS (
|
|
270
|
+
SELECT 1 FROM tasks AS task
|
|
271
|
+
WHERE task.source_product_intent_ref = OLD.intent_ref
|
|
272
|
+
)
|
|
273
|
+
BEGIN
|
|
274
|
+
SELECT CASE
|
|
275
|
+
WHEN NEW.campaign_ref IS NOT OLD.campaign_ref
|
|
276
|
+
OR NEW.materialized_task_id IS NOT OLD.materialized_task_id
|
|
277
|
+
THEN RAISE(ABORT, 'TASK_PRODUCT_LINK_MISMATCH')
|
|
278
|
+
END;
|
|
279
|
+
END;
|
|
280
|
+
`,
|
|
281
|
+
apply: (database) => {
|
|
282
|
+
const mismatchCount = productLinkMismatchCount(database);
|
|
283
|
+
if (mismatchCount > 0)
|
|
284
|
+
throw new Error(`TASK_PRODUCT_LINK_MISMATCH:${mismatchCount}`);
|
|
285
|
+
},
|
|
286
|
+
verify: (database) => {
|
|
287
|
+
const issues = [];
|
|
288
|
+
for (const trigger of [
|
|
289
|
+
"trg_tasks_product_link_consistency",
|
|
290
|
+
"trg_product_intent_task_link_consistency",
|
|
291
|
+
])
|
|
292
|
+
if (!triggerExists(database, trigger))
|
|
293
|
+
issues.push(`${trigger}:missing`);
|
|
294
|
+
const mismatchCount = productLinkMismatchCount(database);
|
|
295
|
+
if (mismatchCount > 0)
|
|
296
|
+
issues.push(`task_product_link_mismatch:${mismatchCount}`);
|
|
297
|
+
return issues;
|
|
298
|
+
},
|
|
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
|
+
};
|
|
351
|
+
export const taskMigrations = [
|
|
352
|
+
...legacyTaskMigrations.map((migration) => migration.version === 3 ? v3Compatible : migration),
|
|
353
|
+
phase4Migration,
|
|
354
|
+
gapReviewMigration,
|
|
355
|
+
taskProductLinkIntegrityMigration,
|
|
356
|
+
taskDocumentEvidenceIntegrityMigration,
|
|
357
|
+
];
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const text = (value) => value === null || value === undefined ? null : String(value);
|
|
2
|
+
const number = (value) => Number(value);
|
|
3
|
+
const jsonValue = (value) => JSON.parse(String(value));
|
|
4
|
+
const json = (value) => JSON.stringify(value);
|
|
5
|
+
function campaign(row) {
|
|
6
|
+
return {
|
|
7
|
+
campaignRef: String(row.campaign_ref),
|
|
8
|
+
discussionRef: String(row.discussion_ref),
|
|
9
|
+
goalRevision: number(row.goal_revision),
|
|
10
|
+
goalSummary: String(row.goal_summary),
|
|
11
|
+
scope: jsonValue(row.scope_json),
|
|
12
|
+
constraints: jsonValue(row.constraints_json),
|
|
13
|
+
status: row.status,
|
|
14
|
+
version: number(row.version),
|
|
15
|
+
createdByRef: String(row.created_by_ref),
|
|
16
|
+
createdAt: String(row.created_at),
|
|
17
|
+
closedAt: text(row.closed_at),
|
|
18
|
+
closureSummary: text(row.closure_summary),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function intent(row) {
|
|
22
|
+
return {
|
|
23
|
+
intentRef: String(row.intent_ref),
|
|
24
|
+
campaignRef: text(row.campaign_ref),
|
|
25
|
+
discussionRef: String(row.discussion_ref),
|
|
26
|
+
goalRevision: number(row.goal_revision),
|
|
27
|
+
intentRevision: number(row.intent_revision),
|
|
28
|
+
predecessorTaskId: text(row.predecessor_task_id),
|
|
29
|
+
title: String(row.title),
|
|
30
|
+
objective: String(row.objective),
|
|
31
|
+
scope: jsonValue(row.scope_json),
|
|
32
|
+
constraints: jsonValue(row.constraints_json),
|
|
33
|
+
plan: jsonValue(row.plan_json),
|
|
34
|
+
materializedTaskId: text(row.materialized_task_id),
|
|
35
|
+
version: number(row.version),
|
|
36
|
+
createdByRef: String(row.created_by_ref),
|
|
37
|
+
createdAt: String(row.created_at),
|
|
38
|
+
materializedAt: text(row.materialized_at),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function productDocument(row) {
|
|
42
|
+
return {
|
|
43
|
+
documentRef: String(row.document_ref),
|
|
44
|
+
campaignRef: String(row.campaign_ref),
|
|
45
|
+
intentRef: text(row.intent_ref),
|
|
46
|
+
documentType: String(row.document_type),
|
|
47
|
+
filePath: String(row.file_path),
|
|
48
|
+
contentHash: String(row.content_hash),
|
|
49
|
+
revision: number(row.revision),
|
|
50
|
+
updatedByRef: String(row.updated_by_ref),
|
|
51
|
+
updatedAt: String(row.updated_at),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function authorization(row) {
|
|
55
|
+
return {
|
|
56
|
+
authorizationRef: String(row.authorization_ref),
|
|
57
|
+
campaignRef: String(row.campaign_ref),
|
|
58
|
+
discussionRef: String(row.discussion_ref),
|
|
59
|
+
goalRevision: number(row.goal_revision),
|
|
60
|
+
goalSummary: String(row.goal_summary),
|
|
61
|
+
scope: jsonValue(row.scope_json),
|
|
62
|
+
constraints: jsonValue(row.constraints_json),
|
|
63
|
+
version: number(row.version),
|
|
64
|
+
createdByRef: String(row.created_by_ref),
|
|
65
|
+
createdAt: String(row.created_at),
|
|
66
|
+
revokedAt: text(row.revoked_at),
|
|
67
|
+
revokedByRef: text(row.revoked_by_ref),
|
|
68
|
+
revokeReason: text(row.revoke_reason),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function gapReview(row) {
|
|
72
|
+
return {
|
|
73
|
+
reviewRef: String(row.review_ref),
|
|
74
|
+
campaignRef: String(row.campaign_ref),
|
|
75
|
+
intentRef: String(row.intent_ref),
|
|
76
|
+
taskId: String(row.task_id),
|
|
77
|
+
discussionRef: String(row.discussion_ref),
|
|
78
|
+
gapIdentity: String(row.gap_identity),
|
|
79
|
+
progressEvidenceRefs: jsonValue(row.progress_evidence_refs_json),
|
|
80
|
+
terminalTaskVersion: number(row.terminal_task_version),
|
|
81
|
+
createdByRef: String(row.created_by_ref),
|
|
82
|
+
createdAt: String(row.created_at),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function taskLink(row) {
|
|
86
|
+
return {
|
|
87
|
+
taskId: String(row.task_id),
|
|
88
|
+
sourceProductIntentRef: text(row.source_product_intent_ref),
|
|
89
|
+
campaignRef: text(row.campaign_ref),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function createPhase4Repositories(db) {
|
|
93
|
+
const get = (sql, ...params) => db.prepare(sql).get(...params);
|
|
94
|
+
const all = (sql, ...params) => db.prepare(sql).all(...params);
|
|
95
|
+
return {
|
|
96
|
+
productCampaigns: {
|
|
97
|
+
get: (campaignRef) => {
|
|
98
|
+
const row = get("SELECT * FROM product_campaigns WHERE campaign_ref=?", campaignRef);
|
|
99
|
+
return row ? campaign(row) : undefined;
|
|
100
|
+
},
|
|
101
|
+
insert: (value) => {
|
|
102
|
+
db.prepare("INSERT INTO product_campaigns(campaign_ref,discussion_ref,goal_revision,goal_summary,scope_json,constraints_json,status,version,created_by_ref,created_at,closed_at,closure_summary) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)").run(value.campaignRef, value.discussionRef, value.goalRevision, value.goalSummary, json(value.scope), json(value.constraints), value.status, value.version, value.createdByRef, value.createdAt, value.closedAt, value.closureSummary);
|
|
103
|
+
},
|
|
104
|
+
update: (value) => {
|
|
105
|
+
const result = db.prepare("UPDATE product_campaigns SET discussion_ref=?,goal_revision=?,goal_summary=?,scope_json=?,constraints_json=?,status=?,version=?,closed_at=?,closure_summary=? WHERE campaign_ref=? AND version=?").run(value.discussionRef, value.goalRevision, value.goalSummary, json(value.scope), json(value.constraints), value.status, value.version, value.closedAt, value.closureSummary, value.campaignRef, value.version - 1);
|
|
106
|
+
if (Number(result.changes) !== 1)
|
|
107
|
+
throw new Error("PRODUCT_CAMPAIGN_VERSION_CONFLICT");
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
productIntents: {
|
|
111
|
+
get: (intentRef) => {
|
|
112
|
+
const row = get("SELECT * FROM product_intents WHERE intent_ref=?", intentRef);
|
|
113
|
+
return row ? intent(row) : undefined;
|
|
114
|
+
},
|
|
115
|
+
insert: (value) => {
|
|
116
|
+
db.prepare("INSERT INTO product_intents(intent_ref,campaign_ref,discussion_ref,goal_revision,intent_revision,predecessor_task_id,title,objective,scope_json,constraints_json,plan_json,materialized_task_id,version,created_by_ref,created_at,materialized_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)").run(value.intentRef, value.campaignRef, value.discussionRef, value.goalRevision, value.intentRevision, value.predecessorTaskId, value.title, value.objective, json(value.scope), json(value.constraints), json(value.plan), value.materializedTaskId, value.version, value.createdByRef, value.createdAt, value.materializedAt);
|
|
117
|
+
},
|
|
118
|
+
update: (value) => {
|
|
119
|
+
const result = db.prepare("UPDATE product_intents SET campaign_ref=?,discussion_ref=?,goal_revision=?,intent_revision=?,predecessor_task_id=?,title=?,objective=?,scope_json=?,constraints_json=?,plan_json=?,materialized_task_id=?,version=?,materialized_at=? WHERE intent_ref=? AND version=?").run(value.campaignRef, value.discussionRef, value.goalRevision, value.intentRevision, value.predecessorTaskId, value.title, value.objective, json(value.scope), json(value.constraints), json(value.plan), value.materializedTaskId, value.version, value.materializedAt, value.intentRef, value.version - 1);
|
|
120
|
+
if (Number(result.changes) !== 1)
|
|
121
|
+
throw new Error("PRODUCT_INTENT_REVISION_CONFLICT");
|
|
122
|
+
},
|
|
123
|
+
listByCampaign: (campaignRef) => all("SELECT * FROM product_intents WHERE campaign_ref=? ORDER BY created_at,intent_ref", campaignRef).map(intent),
|
|
124
|
+
},
|
|
125
|
+
productDocuments: {
|
|
126
|
+
get: (documentRef) => {
|
|
127
|
+
const row = get("SELECT * FROM product_documents WHERE document_ref=?", documentRef);
|
|
128
|
+
return row ? productDocument(row) : undefined;
|
|
129
|
+
},
|
|
130
|
+
insert: (value) => {
|
|
131
|
+
db.prepare("INSERT INTO product_documents(document_ref,campaign_ref,intent_ref,document_type,file_path,content_hash,revision,updated_by_ref,updated_at) VALUES (?,?,?,?,?,?,?,?,?)").run(value.documentRef, value.campaignRef, value.intentRef, value.documentType, value.filePath, value.contentHash, value.revision, value.updatedByRef, value.updatedAt);
|
|
132
|
+
},
|
|
133
|
+
update: (value) => {
|
|
134
|
+
const result = db.prepare("UPDATE product_documents SET campaign_ref=?,intent_ref=?,document_type=?,file_path=?,content_hash=?,revision=?,updated_by_ref=?,updated_at=? WHERE document_ref=? AND revision=?").run(value.campaignRef, value.intentRef, value.documentType, value.filePath, value.contentHash, value.revision, value.updatedByRef, value.updatedAt, value.documentRef, value.revision - 1);
|
|
135
|
+
if (Number(result.changes) !== 1)
|
|
136
|
+
throw new Error("PRODUCT_DOCUMENT_REVISION_CONFLICT");
|
|
137
|
+
},
|
|
138
|
+
bindIntent: (input) => {
|
|
139
|
+
const result = db.prepare("UPDATE product_documents SET intent_ref=? WHERE document_ref=? AND intent_ref IS NULL AND revision=? AND campaign_ref=(SELECT campaign_ref FROM product_intents WHERE intent_ref=?)").run(input.intentRef, input.documentRef, input.expectedRevision, input.intentRef);
|
|
140
|
+
if (Number(result.changes) !== 1)
|
|
141
|
+
throw new Error("PRODUCT_DOCUMENT_BIND_CONFLICT");
|
|
142
|
+
},
|
|
143
|
+
listByIntent: (intentRef) => all("SELECT * FROM product_documents WHERE intent_ref=? ORDER BY document_type,document_ref", intentRef).map(productDocument),
|
|
144
|
+
},
|
|
145
|
+
campaignAuthorizations: {
|
|
146
|
+
get: (authorizationRef) => {
|
|
147
|
+
const row = get("SELECT * FROM campaign_authorizations WHERE authorization_ref=?", authorizationRef);
|
|
148
|
+
return row ? authorization(row) : undefined;
|
|
149
|
+
},
|
|
150
|
+
insert: (value) => {
|
|
151
|
+
db.prepare("INSERT INTO campaign_authorizations(authorization_ref,campaign_ref,discussion_ref,goal_revision,goal_summary,scope_json,constraints_json,version,created_by_ref,created_at,revoked_at,revoked_by_ref,revoke_reason) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)").run(value.authorizationRef, value.campaignRef, value.discussionRef, value.goalRevision, value.goalSummary, json(value.scope), json(value.constraints), value.version, value.createdByRef, value.createdAt, value.revokedAt, value.revokedByRef, value.revokeReason);
|
|
152
|
+
},
|
|
153
|
+
update: (value) => {
|
|
154
|
+
const result = db.prepare("UPDATE campaign_authorizations SET campaign_ref=?,discussion_ref=?,goal_revision=?,goal_summary=?,scope_json=?,constraints_json=?,version=?,revoked_at=?,revoked_by_ref=?,revoke_reason=? WHERE authorization_ref=? AND version=?").run(value.campaignRef, value.discussionRef, value.goalRevision, value.goalSummary, json(value.scope), json(value.constraints), value.version, value.revokedAt, value.revokedByRef, value.revokeReason, value.authorizationRef, value.version - 1);
|
|
155
|
+
if (Number(result.changes) !== 1)
|
|
156
|
+
throw new Error("CAMPAIGN_AUTHORIZATION_VERSION_CONFLICT");
|
|
157
|
+
},
|
|
158
|
+
listByCampaign: (campaignRef) => all("SELECT * FROM campaign_authorizations WHERE campaign_ref=? ORDER BY created_at,authorization_ref", campaignRef).map(authorization),
|
|
159
|
+
},
|
|
160
|
+
productGapReviews: {
|
|
161
|
+
getByIntent: (intentRef) => {
|
|
162
|
+
const row = get("SELECT * FROM product_gap_reviews WHERE intent_ref=?", intentRef);
|
|
163
|
+
return row ? gapReview(row) : undefined;
|
|
164
|
+
},
|
|
165
|
+
getByTask: (taskId) => {
|
|
166
|
+
const row = get("SELECT * FROM product_gap_reviews WHERE task_id=?", taskId);
|
|
167
|
+
return row ? gapReview(row) : undefined;
|
|
168
|
+
},
|
|
169
|
+
insert: (value) => {
|
|
170
|
+
db.prepare("INSERT INTO product_gap_reviews(review_ref,campaign_ref,intent_ref,task_id,discussion_ref,gap_identity,progress_evidence_refs_json,terminal_task_version,created_by_ref,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)").run(value.reviewRef, value.campaignRef, value.intentRef, value.taskId, value.discussionRef, value.gapIdentity, json(value.progressEvidenceRefs), value.terminalTaskVersion, value.createdByRef, value.createdAt);
|
|
171
|
+
},
|
|
172
|
+
listByCampaign: (campaignRef) => all("SELECT * FROM product_gap_reviews WHERE campaign_ref=? ORDER BY created_at,review_ref", campaignRef).map(gapReview),
|
|
173
|
+
},
|
|
174
|
+
taskProductLinks: {
|
|
175
|
+
get: (taskId) => {
|
|
176
|
+
const row = get("SELECT task_id,source_product_intent_ref,campaign_ref FROM tasks WHERE task_id=?", taskId);
|
|
177
|
+
return row ? taskLink(row) : undefined;
|
|
178
|
+
},
|
|
179
|
+
set: (value) => {
|
|
180
|
+
const result = db.prepare(`UPDATE tasks
|
|
181
|
+
SET source_product_intent_ref=?, campaign_ref=?
|
|
182
|
+
WHERE task_id=?
|
|
183
|
+
AND (
|
|
184
|
+
(source_product_intent_ref IS NULL AND campaign_ref IS NULL)
|
|
185
|
+
OR (source_product_intent_ref IS ? AND campaign_ref IS ?)
|
|
186
|
+
)`).run(value.sourceProductIntentRef, value.campaignRef, value.taskId, value.sourceProductIntentRef, value.campaignRef);
|
|
187
|
+
if (Number(result.changes) === 1)
|
|
188
|
+
return;
|
|
189
|
+
if (!get("SELECT 1 AS present FROM tasks WHERE task_id=?", value.taskId))
|
|
190
|
+
throw new Error("TASK_NOT_FOUND");
|
|
191
|
+
throw new Error("TASK_PRODUCT_LINK_CONFLICT");
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/proflow-task-store-sqlite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./dist/src/index.js",
|
|
10
|
-
"./migrations": "./dist/src/migrations.js",
|
|
10
|
+
"./migrations": "./dist/src/phase4-migrations.js",
|
|
11
11
|
"./deployment/adapter": "./dist/deployment/adapter.js",
|
|
12
12
|
"./deployment/descriptor": "./dist/deployment/descriptor.js"
|
|
13
13
|
},
|
|
@@ -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.
|
|
24
|
+
"@tomflow/proflow-task-orchestration": "^0.1.14"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@tomflow/proflow-task-migration-runner": "^0.1.11"
|
package/proflow.module.json
CHANGED
|
@@ -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.
|
|
6
|
+
"moduleVersion": "0.1.14",
|
|
7
7
|
"kind": "library",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|