@tomflow/proflow-task-store-sqlite 0.1.12 → 0.1.13
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 +6 -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 +25 -1
- package/dist/src/phase4-migrations.d.ts +3 -0
- package/dist/src/phase4-migrations.js +281 -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
|
@@ -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.13";
|
|
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.13";
|
|
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.13";
|
|
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.13";
|
|
55
55
|
} | {
|
|
56
56
|
contract: "deployment.result.v1";
|
|
57
57
|
moduleRef: "task-store-sqlite";
|
|
58
|
-
moduleVersion: "0.1.
|
|
58
|
+
moduleVersion: "0.1.13";
|
|
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.13";
|
|
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.13";
|
|
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.13";
|
|
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.13";
|
|
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.13",
|
|
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,7 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
|
|
2
|
+
import { createPhase4Repositories } from "./phase4-repositories.js";
|
|
3
|
+
export {} from "./migrations.js";
|
|
4
|
+
export { taskMigrations } from "./phase4-migrations.js";
|
|
3
5
|
const json = (value) => JSON.stringify(value);
|
|
4
6
|
const text = (value) => value === null || value === undefined ? null : String(value);
|
|
5
7
|
const number = (value) => Number(value);
|
|
@@ -262,6 +264,9 @@ export class SqliteTaskStore {
|
|
|
262
264
|
},
|
|
263
265
|
};
|
|
264
266
|
}
|
|
267
|
+
#phase4Repositories() {
|
|
268
|
+
return createPhase4Repositories(this.#database);
|
|
269
|
+
}
|
|
265
270
|
transaction(work) {
|
|
266
271
|
this.#database.exec("BEGIN IMMEDIATE");
|
|
267
272
|
try {
|
|
@@ -277,6 +282,21 @@ export class SqliteTaskStore {
|
|
|
277
282
|
read(work) {
|
|
278
283
|
return work(this.#repositories());
|
|
279
284
|
}
|
|
285
|
+
phase4Transaction(work) {
|
|
286
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
287
|
+
try {
|
|
288
|
+
const result = work({ ...this.#repositories(), ...this.#phase4Repositories() });
|
|
289
|
+
this.#database.exec("COMMIT");
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
this.#database.exec("ROLLBACK");
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
phase4Read(work) {
|
|
298
|
+
return work({ ...this.#repositories(), ...this.#phase4Repositories() });
|
|
299
|
+
}
|
|
280
300
|
listReconciliationTaskIds(input) {
|
|
281
301
|
const statuses = [...new Set(input.statuses.filter((value) => value.length > 0))];
|
|
282
302
|
if (statuses.length === 0)
|
|
@@ -322,6 +342,10 @@ export class SqliteTaskStore {
|
|
|
322
342
|
"task_messages",
|
|
323
343
|
"task_events",
|
|
324
344
|
"idempotency_records",
|
|
345
|
+
"product_campaigns",
|
|
346
|
+
"product_intents",
|
|
347
|
+
"product_documents",
|
|
348
|
+
"campaign_authorizations",
|
|
325
349
|
];
|
|
326
350
|
return Object.fromEntries(tables.map((table) => [
|
|
327
351
|
table,
|
|
@@ -0,0 +1,281 @@
|
|
|
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 phase4Tables = [
|
|
23
|
+
"product_campaigns",
|
|
24
|
+
"product_intents",
|
|
25
|
+
"product_documents",
|
|
26
|
+
"campaign_authorizations",
|
|
27
|
+
];
|
|
28
|
+
function tableExists(database, table) {
|
|
29
|
+
return Boolean(database
|
|
30
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type='table' AND name=?")
|
|
31
|
+
.get(table));
|
|
32
|
+
}
|
|
33
|
+
function tableColumns(database, table) {
|
|
34
|
+
return new Set(database.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
35
|
+
}
|
|
36
|
+
const legacyV3 = legacyTaskMigrations.find((migration) => migration.version === 3);
|
|
37
|
+
if (!legacyV3)
|
|
38
|
+
throw new Error("TASK_SCHEMA_V3_REQUIRED");
|
|
39
|
+
const v3Compatible = {
|
|
40
|
+
...legacyV3,
|
|
41
|
+
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;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const phase4Migration = {
|
|
55
|
+
version: 4,
|
|
56
|
+
name: "product_campaign_owner_facts",
|
|
57
|
+
identity: "product_campaign_owner_facts.20260914",
|
|
58
|
+
sql: `
|
|
59
|
+
CREATE TABLE product_campaigns (
|
|
60
|
+
campaign_ref TEXT PRIMARY KEY,
|
|
61
|
+
discussion_ref TEXT NOT NULL,
|
|
62
|
+
goal_revision INTEGER NOT NULL CHECK (goal_revision >= 1),
|
|
63
|
+
goal_summary TEXT NOT NULL,
|
|
64
|
+
scope_json TEXT NOT NULL,
|
|
65
|
+
constraints_json TEXT NOT NULL,
|
|
66
|
+
status TEXT NOT NULL CHECK (status IN ('OPEN','CLOSED')),
|
|
67
|
+
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
68
|
+
created_by_ref TEXT NOT NULL,
|
|
69
|
+
created_at TEXT NOT NULL,
|
|
70
|
+
closed_at TEXT,
|
|
71
|
+
closure_summary TEXT
|
|
72
|
+
);
|
|
73
|
+
CREATE TABLE product_intents (
|
|
74
|
+
intent_ref TEXT PRIMARY KEY,
|
|
75
|
+
campaign_ref TEXT,
|
|
76
|
+
discussion_ref TEXT NOT NULL,
|
|
77
|
+
goal_revision INTEGER NOT NULL CHECK (goal_revision >= 1),
|
|
78
|
+
intent_revision INTEGER NOT NULL CHECK (intent_revision >= 1),
|
|
79
|
+
predecessor_task_id TEXT,
|
|
80
|
+
title TEXT NOT NULL,
|
|
81
|
+
objective TEXT NOT NULL,
|
|
82
|
+
scope_json TEXT NOT NULL,
|
|
83
|
+
constraints_json TEXT NOT NULL,
|
|
84
|
+
plan_json TEXT NOT NULL,
|
|
85
|
+
materialized_task_id TEXT,
|
|
86
|
+
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
87
|
+
created_by_ref TEXT NOT NULL,
|
|
88
|
+
created_at TEXT NOT NULL,
|
|
89
|
+
materialized_at TEXT,
|
|
90
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref),
|
|
91
|
+
FOREIGN KEY (predecessor_task_id) REFERENCES tasks(task_id),
|
|
92
|
+
FOREIGN KEY (materialized_task_id) REFERENCES tasks(task_id)
|
|
93
|
+
);
|
|
94
|
+
CREATE TABLE product_documents (
|
|
95
|
+
document_ref TEXT PRIMARY KEY,
|
|
96
|
+
campaign_ref TEXT NOT NULL,
|
|
97
|
+
intent_ref TEXT,
|
|
98
|
+
document_type TEXT NOT NULL,
|
|
99
|
+
file_path TEXT NOT NULL,
|
|
100
|
+
content_hash TEXT NOT NULL,
|
|
101
|
+
revision INTEGER NOT NULL CHECK (revision >= 1),
|
|
102
|
+
updated_by_ref TEXT NOT NULL,
|
|
103
|
+
updated_at TEXT NOT NULL,
|
|
104
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref) ON DELETE CASCADE,
|
|
105
|
+
FOREIGN KEY (intent_ref) REFERENCES product_intents(intent_ref) ON DELETE CASCADE
|
|
106
|
+
);
|
|
107
|
+
CREATE TABLE campaign_authorizations (
|
|
108
|
+
authorization_ref TEXT PRIMARY KEY,
|
|
109
|
+
campaign_ref TEXT NOT NULL,
|
|
110
|
+
discussion_ref TEXT NOT NULL,
|
|
111
|
+
goal_revision INTEGER NOT NULL CHECK (goal_revision >= 1),
|
|
112
|
+
goal_summary TEXT NOT NULL,
|
|
113
|
+
scope_json TEXT NOT NULL,
|
|
114
|
+
constraints_json TEXT NOT NULL,
|
|
115
|
+
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
116
|
+
created_by_ref TEXT NOT NULL,
|
|
117
|
+
created_at TEXT NOT NULL,
|
|
118
|
+
revoked_at TEXT,
|
|
119
|
+
revoked_by_ref TEXT,
|
|
120
|
+
revoke_reason TEXT,
|
|
121
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref) ON DELETE CASCADE
|
|
122
|
+
);
|
|
123
|
+
ALTER TABLE tasks ADD COLUMN source_product_intent_ref TEXT REFERENCES product_intents(intent_ref);
|
|
124
|
+
ALTER TABLE tasks ADD COLUMN campaign_ref TEXT REFERENCES product_campaigns(campaign_ref);
|
|
125
|
+
CREATE UNIQUE INDEX idx_product_intents_materialized_task ON product_intents(materialized_task_id) WHERE materialized_task_id IS NOT NULL;
|
|
126
|
+
CREATE INDEX idx_product_intents_campaign ON product_intents(campaign_ref, created_at);
|
|
127
|
+
CREATE INDEX idx_product_documents_campaign ON product_documents(campaign_ref, intent_ref);
|
|
128
|
+
CREATE INDEX idx_campaign_authorizations_campaign ON campaign_authorizations(campaign_ref, revoked_at);
|
|
129
|
+
CREATE INDEX idx_tasks_campaign ON tasks(campaign_ref, task_id);
|
|
130
|
+
`,
|
|
131
|
+
verify: (database) => {
|
|
132
|
+
const issues = [];
|
|
133
|
+
for (const table of phase4Tables)
|
|
134
|
+
if (!tableExists(database, table))
|
|
135
|
+
issues.push(`${table}:missing`);
|
|
136
|
+
if (!tableExists(database, "tasks"))
|
|
137
|
+
issues.push("tasks:missing");
|
|
138
|
+
else {
|
|
139
|
+
const columns = tableColumns(database, "tasks");
|
|
140
|
+
for (const column of [...legacyTaskColumns, ...phase4TaskColumns])
|
|
141
|
+
if (!columns.has(column))
|
|
142
|
+
issues.push(`tasks.${column}:missing`);
|
|
143
|
+
}
|
|
144
|
+
const foreignKeyViolations = database
|
|
145
|
+
.prepare("PRAGMA foreign_key_check")
|
|
146
|
+
.all();
|
|
147
|
+
if (foreignKeyViolations.length > 0)
|
|
148
|
+
issues.push(`foreign_key_check:${foreignKeyViolations.length}`);
|
|
149
|
+
return issues;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
function productLinkMismatchCount(database) {
|
|
153
|
+
const row = database
|
|
154
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
155
|
+
FROM tasks AS task
|
|
156
|
+
LEFT JOIN product_intents AS intent
|
|
157
|
+
ON intent.intent_ref = task.source_product_intent_ref
|
|
158
|
+
WHERE (task.campaign_ref IS NOT NULL AND task.source_product_intent_ref IS NULL)
|
|
159
|
+
OR (
|
|
160
|
+
task.source_product_intent_ref IS NOT NULL
|
|
161
|
+
AND (
|
|
162
|
+
intent.intent_ref IS NULL
|
|
163
|
+
OR intent.materialized_task_id IS NOT task.task_id
|
|
164
|
+
OR intent.campaign_ref IS NOT task.campaign_ref
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
OR EXISTS (
|
|
168
|
+
SELECT 1
|
|
169
|
+
FROM product_intents AS materialized
|
|
170
|
+
WHERE materialized.materialized_task_id = task.task_id
|
|
171
|
+
AND (
|
|
172
|
+
task.source_product_intent_ref IS NOT materialized.intent_ref
|
|
173
|
+
OR task.campaign_ref IS NOT materialized.campaign_ref
|
|
174
|
+
)
|
|
175
|
+
)`)
|
|
176
|
+
.get();
|
|
177
|
+
return Number(row.count);
|
|
178
|
+
}
|
|
179
|
+
function triggerExists(database, name) {
|
|
180
|
+
return Boolean(database
|
|
181
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type='trigger' AND name=?")
|
|
182
|
+
.get(name));
|
|
183
|
+
}
|
|
184
|
+
const gapReviewMigration = {
|
|
185
|
+
version: 5,
|
|
186
|
+
name: "product_gap_review_owner_facts",
|
|
187
|
+
identity: "product_gap_review_owner_facts.20260914",
|
|
188
|
+
sql: `
|
|
189
|
+
CREATE TABLE product_gap_reviews (
|
|
190
|
+
review_ref TEXT PRIMARY KEY,
|
|
191
|
+
campaign_ref TEXT NOT NULL,
|
|
192
|
+
intent_ref TEXT NOT NULL UNIQUE,
|
|
193
|
+
task_id TEXT NOT NULL UNIQUE,
|
|
194
|
+
discussion_ref TEXT NOT NULL,
|
|
195
|
+
gap_identity TEXT NOT NULL,
|
|
196
|
+
progress_evidence_refs_json TEXT NOT NULL,
|
|
197
|
+
terminal_task_version INTEGER NOT NULL CHECK (terminal_task_version >= 1),
|
|
198
|
+
created_by_ref TEXT NOT NULL,
|
|
199
|
+
created_at TEXT NOT NULL,
|
|
200
|
+
FOREIGN KEY (campaign_ref) REFERENCES product_campaigns(campaign_ref) ON DELETE CASCADE,
|
|
201
|
+
FOREIGN KEY (intent_ref) REFERENCES product_intents(intent_ref) ON DELETE CASCADE,
|
|
202
|
+
FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
|
|
203
|
+
);
|
|
204
|
+
CREATE INDEX idx_product_gap_reviews_campaign
|
|
205
|
+
ON product_gap_reviews(campaign_ref, gap_identity, created_at, review_ref);
|
|
206
|
+
`,
|
|
207
|
+
verify: (database) => {
|
|
208
|
+
const issues = [];
|
|
209
|
+
if (!tableExists(database, "product_gap_reviews"))
|
|
210
|
+
issues.push("product_gap_reviews:missing");
|
|
211
|
+
const foreignKeyViolations = database
|
|
212
|
+
.prepare("PRAGMA foreign_key_check")
|
|
213
|
+
.all();
|
|
214
|
+
if (foreignKeyViolations.length > 0)
|
|
215
|
+
issues.push(`foreign_key_check:${foreignKeyViolations.length}`);
|
|
216
|
+
return issues;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
const taskProductLinkIntegrityMigration = {
|
|
220
|
+
version: 6,
|
|
221
|
+
name: "task_product_link_integrity",
|
|
222
|
+
identity: "task_product_link_integrity.20260928",
|
|
223
|
+
sql: `
|
|
224
|
+
CREATE TRIGGER trg_tasks_product_link_consistency
|
|
225
|
+
BEFORE UPDATE OF source_product_intent_ref, campaign_ref ON tasks
|
|
226
|
+
BEGIN
|
|
227
|
+
SELECT CASE
|
|
228
|
+
WHEN NEW.campaign_ref IS NOT NULL AND NEW.source_product_intent_ref IS NULL
|
|
229
|
+
THEN RAISE(ABORT, 'TASK_PRODUCT_LINK_MISMATCH')
|
|
230
|
+
END;
|
|
231
|
+
SELECT CASE
|
|
232
|
+
WHEN NEW.source_product_intent_ref IS NOT NULL
|
|
233
|
+
AND NOT EXISTS (
|
|
234
|
+
SELECT 1
|
|
235
|
+
FROM product_intents AS intent
|
|
236
|
+
WHERE intent.intent_ref = NEW.source_product_intent_ref
|
|
237
|
+
AND intent.materialized_task_id IS NEW.task_id
|
|
238
|
+
AND intent.campaign_ref IS NEW.campaign_ref
|
|
239
|
+
)
|
|
240
|
+
THEN RAISE(ABORT, 'TASK_PRODUCT_LINK_MISMATCH')
|
|
241
|
+
END;
|
|
242
|
+
END;
|
|
243
|
+
CREATE TRIGGER trg_product_intent_task_link_consistency
|
|
244
|
+
BEFORE UPDATE OF campaign_ref, materialized_task_id ON product_intents
|
|
245
|
+
WHEN EXISTS (
|
|
246
|
+
SELECT 1 FROM tasks AS task
|
|
247
|
+
WHERE task.source_product_intent_ref = OLD.intent_ref
|
|
248
|
+
)
|
|
249
|
+
BEGIN
|
|
250
|
+
SELECT CASE
|
|
251
|
+
WHEN NEW.campaign_ref IS NOT OLD.campaign_ref
|
|
252
|
+
OR NEW.materialized_task_id IS NOT OLD.materialized_task_id
|
|
253
|
+
THEN RAISE(ABORT, 'TASK_PRODUCT_LINK_MISMATCH')
|
|
254
|
+
END;
|
|
255
|
+
END;
|
|
256
|
+
`,
|
|
257
|
+
apply: (database) => {
|
|
258
|
+
const mismatchCount = productLinkMismatchCount(database);
|
|
259
|
+
if (mismatchCount > 0)
|
|
260
|
+
throw new Error(`TASK_PRODUCT_LINK_MISMATCH:${mismatchCount}`);
|
|
261
|
+
},
|
|
262
|
+
verify: (database) => {
|
|
263
|
+
const issues = [];
|
|
264
|
+
for (const trigger of [
|
|
265
|
+
"trg_tasks_product_link_consistency",
|
|
266
|
+
"trg_product_intent_task_link_consistency",
|
|
267
|
+
])
|
|
268
|
+
if (!triggerExists(database, trigger))
|
|
269
|
+
issues.push(`${trigger}:missing`);
|
|
270
|
+
const mismatchCount = productLinkMismatchCount(database);
|
|
271
|
+
if (mismatchCount > 0)
|
|
272
|
+
issues.push(`task_product_link_mismatch:${mismatchCount}`);
|
|
273
|
+
return issues;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
export const taskMigrations = [
|
|
277
|
+
...legacyTaskMigrations.map((migration) => migration.version === 3 ? v3Compatible : migration),
|
|
278
|
+
phase4Migration,
|
|
279
|
+
gapReviewMigration,
|
|
280
|
+
taskProductLinkIntegrityMigration,
|
|
281
|
+
];
|
|
@@ -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.13",
|
|
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.13"
|
|
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.13",
|
|
7
7
|
"kind": "library",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|