@hasna/todos 0.15.29 → 0.15.33
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/dist/cli/cloud-router.d.ts +13 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +2 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2861 -342
- package/dist/contracts.js +52 -2
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1466 -147
- package/dist/lib/task-parent-integrity.d.ts +14 -0
- package/dist/lib/task-parent-integrity.d.ts.map +1 -0
- package/dist/mcp/index.js +2050 -138
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/project-registration/adoption-validation.d.ts +3 -0
- package/dist/project-registration/adoption-validation.d.ts.map +1 -0
- package/dist/project-registration/authority.d.ts +3 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/backend.d.ts +24 -1
- package/dist/project-registration/backend.d.ts.map +1 -1
- package/dist/project-registration/http.d.ts +3 -1
- package/dist/project-registration/http.d.ts.map +1 -1
- package/dist/project-registration/index.d.ts +2 -1
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/page-validation.d.ts +5 -0
- package/dist/project-registration/page-validation.d.ts.map +1 -0
- package/dist/project-registration/postgres.d.ts +13 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts +13 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration/types.d.ts +68 -1
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +849 -60
- package/dist/registry.js +52 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +151 -0
- package/dist/sdk/v1.generated.d.ts +282 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +2041 -129
- package/dist/server/openapi.d.ts +2856 -1357
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +8 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +233 -20
- package/dist/task-manifest/authority.d.ts +13 -1
- package/dist/task-manifest/authority.d.ts.map +1 -1
- package/dist/task-manifest/backend.d.ts +5 -0
- package/dist/task-manifest/backend.d.ts.map +1 -1
- package/dist/task-manifest/index.d.ts +2 -2
- package/dist/task-manifest/index.d.ts.map +1 -1
- package/dist/task-manifest/plan-slug.d.ts +20 -0
- package/dist/task-manifest/plan-slug.d.ts.map +1 -1
- package/dist/task-manifest/postgres.d.ts +1 -0
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/schema-sql.d.ts.map +1 -1
- package/dist/task-manifest/schema.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -0
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest/types.d.ts +21 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +593 -62
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7917,6 +7917,46 @@ function guardPlanRowsSqlite(planIds, db) {
|
|
|
7917
7917
|
}
|
|
7918
7918
|
}
|
|
7919
7919
|
|
|
7920
|
+
// src/lib/task-parent-integrity.ts
|
|
7921
|
+
function parentCycleError(taskId, parentId) {
|
|
7922
|
+
return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
|
|
7923
|
+
}
|
|
7924
|
+
function assertTaskParentIntegrity(taskId, parentId, getTask) {
|
|
7925
|
+
if (parentId === undefined || parentId === null)
|
|
7926
|
+
return;
|
|
7927
|
+
const visited = new Set;
|
|
7928
|
+
let cursor = parentId;
|
|
7929
|
+
while (cursor) {
|
|
7930
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
7931
|
+
throw parentCycleError(taskId, parentId);
|
|
7932
|
+
}
|
|
7933
|
+
visited.add(cursor);
|
|
7934
|
+
const parent = getTask(cursor);
|
|
7935
|
+
if (!parent)
|
|
7936
|
+
throw new TaskNotFoundError(cursor);
|
|
7937
|
+
cursor = parent.parent_id;
|
|
7938
|
+
}
|
|
7939
|
+
}
|
|
7940
|
+
async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
|
|
7941
|
+
if (parentId === undefined || parentId === null)
|
|
7942
|
+
return;
|
|
7943
|
+
const visited = new Set;
|
|
7944
|
+
let cursor = parentId;
|
|
7945
|
+
while (cursor) {
|
|
7946
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
7947
|
+
throw parentCycleError(taskId, parentId);
|
|
7948
|
+
}
|
|
7949
|
+
visited.add(cursor);
|
|
7950
|
+
const parent = await getTask(cursor);
|
|
7951
|
+
if (!parent)
|
|
7952
|
+
throw new TaskNotFoundError(cursor);
|
|
7953
|
+
cursor = parent.parent_id;
|
|
7954
|
+
}
|
|
7955
|
+
}
|
|
7956
|
+
var init_task_parent_integrity = __esm(() => {
|
|
7957
|
+
init_types();
|
|
7958
|
+
});
|
|
7959
|
+
|
|
7920
7960
|
// src/lib/creator-identity.ts
|
|
7921
7961
|
function canonicalAgentRef(value) {
|
|
7922
7962
|
return value.trim().toLowerCase();
|
|
@@ -9440,6 +9480,7 @@ function createTaskStored(input, d) {
|
|
|
9440
9480
|
let id = uuid();
|
|
9441
9481
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
9442
9482
|
try {
|
|
9483
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9443
9484
|
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, created_by, assigned_from_project, task_type, machine_id)
|
|
9444
9485
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9445
9486
|
id,
|
|
@@ -9818,6 +9859,7 @@ function updateTaskStored(id, input, db) {
|
|
|
9818
9859
|
throw new VersionConflictError(id, input.version, task.version);
|
|
9819
9860
|
}
|
|
9820
9861
|
input = sanitizeUpdateTaskInput(input);
|
|
9862
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9821
9863
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
|
|
9822
9864
|
const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
|
|
9823
9865
|
if (linkedProjectId) {
|
|
@@ -9871,6 +9913,10 @@ function updateTaskStored(id, input, db) {
|
|
|
9871
9913
|
sets.push("project_id = ?");
|
|
9872
9914
|
params.push(input.project_id);
|
|
9873
9915
|
}
|
|
9916
|
+
if (input.parent_id !== undefined) {
|
|
9917
|
+
sets.push("parent_id = ?");
|
|
9918
|
+
params.push(input.parent_id);
|
|
9919
|
+
}
|
|
9874
9920
|
if (input.assigned_to !== undefined) {
|
|
9875
9921
|
sets.push("assigned_to = ?");
|
|
9876
9922
|
params.push(input.assigned_to);
|
|
@@ -9986,6 +10032,8 @@ function updateTaskStored(id, input, db) {
|
|
|
9986
10032
|
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
9987
10033
|
if (input.title !== undefined && input.title !== task.title)
|
|
9988
10034
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
10035
|
+
if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
|
|
10036
|
+
logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
|
|
9989
10037
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
9990
10038
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
9991
10039
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
@@ -10043,7 +10091,8 @@ function updateTask(id, input, db) {
|
|
|
10043
10091
|
if (!before)
|
|
10044
10092
|
throw new TaskNotFoundError(id);
|
|
10045
10093
|
const guardedPlanIds = [before.plan_id, input.plan_id];
|
|
10046
|
-
|
|
10094
|
+
const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
|
|
10095
|
+
if (!needsSerializedWrite)
|
|
10047
10096
|
return updateTaskStored(id, input, d);
|
|
10048
10097
|
return d.transaction(() => {
|
|
10049
10098
|
guardPlanRowsSqlite(guardedPlanIds, d);
|
|
@@ -10079,6 +10128,7 @@ var init_task_crud = __esm(() => {
|
|
|
10079
10128
|
init_checklists();
|
|
10080
10129
|
init_storage_tombstones();
|
|
10081
10130
|
init_prewrite_secrets();
|
|
10131
|
+
init_task_parent_integrity();
|
|
10082
10132
|
});
|
|
10083
10133
|
|
|
10084
10134
|
// src/db/task-status.ts
|
|
@@ -12783,7 +12833,7 @@ var init_dispatches = __esm(() => {
|
|
|
12783
12833
|
// package.json
|
|
12784
12834
|
var package_default = {
|
|
12785
12835
|
name: "@hasna/todos",
|
|
12786
|
-
version: "0.15.
|
|
12836
|
+
version: "0.15.33",
|
|
12787
12837
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12788
12838
|
type: "module",
|
|
12789
12839
|
main: "dist/index.js",
|
|
@@ -27129,7 +27179,7 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
|
|
|
27129
27179
|
AS $$ SELECT unaccent('unaccent', $1) $$`,
|
|
27130
27180
|
`CREATE OR REPLACE FUNCTION todos_try_timestamptz(text)
|
|
27131
27181
|
RETURNS timestamptz
|
|
27132
|
-
LANGUAGE plpgsql IMMUTABLE PARALLEL
|
|
27182
|
+
LANGUAGE plpgsql IMMUTABLE PARALLEL UNSAFE
|
|
27133
27183
|
SET DateStyle TO 'ISO, YMD'
|
|
27134
27184
|
AS $$
|
|
27135
27185
|
BEGIN
|
|
@@ -27505,6 +27555,7 @@ function assertSafeIdentifier(value) {
|
|
|
27505
27555
|
|
|
27506
27556
|
// src/storage/postgres-adapter.ts
|
|
27507
27557
|
init_redaction();
|
|
27558
|
+
init_task_parent_integrity();
|
|
27508
27559
|
|
|
27509
27560
|
// src/task-manifest/canonical.ts
|
|
27510
27561
|
import { createHash as createHash11 } from "crypto";
|
|
@@ -27548,8 +27599,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
27548
27599
|
resolveRef: (ref) => store.resolveTaskRef(ref),
|
|
27549
27600
|
list: (filter = {}) => store.listTasks(filter),
|
|
27550
27601
|
count: (filter = {}) => store.countTasks(filter),
|
|
27551
|
-
update: (id, input) => updateTask2(id, input, store),
|
|
27552
|
-
delete: (id, context) => store.
|
|
27602
|
+
update: (id, input, context) => updateTask2(id, input, store, context),
|
|
27603
|
+
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
27553
27604
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
27554
27605
|
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
27555
27606
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
@@ -28082,22 +28133,70 @@ class PostgresJsonRecordStore {
|
|
|
28082
28133
|
return "identical";
|
|
28083
28134
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
28084
28135
|
}
|
|
28085
|
-
async
|
|
28136
|
+
async withTaskParentIntegrityTransaction(fn) {
|
|
28137
|
+
if (typeof this.options.client.transaction !== "function") {
|
|
28138
|
+
throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
|
|
28139
|
+
}
|
|
28140
|
+
return this.options.client.transaction(async (client) => {
|
|
28141
|
+
await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
|
|
28142
|
+
return fn(client);
|
|
28143
|
+
});
|
|
28144
|
+
}
|
|
28145
|
+
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
|
|
28086
28146
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
28087
|
-
if (planIds.length === 0)
|
|
28147
|
+
if (planIds.length === 0 && !parentGuard)
|
|
28088
28148
|
return this.upsert("tasks", value, context);
|
|
28089
28149
|
await this.ensureSchema();
|
|
28150
|
+
if (parentGuard && !queryClient) {
|
|
28151
|
+
return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
|
|
28152
|
+
}
|
|
28153
|
+
const client = queryClient ?? this.options.client;
|
|
28090
28154
|
const updatedAt = value.updated_at;
|
|
28091
28155
|
const targetPlanId = value.plan_id;
|
|
28092
|
-
const result = await
|
|
28156
|
+
const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
|
|
28157
|
+
locked_task AS MATERIALIZED (
|
|
28158
|
+
SELECT payload FROM ${this.tableName}
|
|
28159
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
|
|
28160
|
+
FOR UPDATE
|
|
28161
|
+
),
|
|
28093
28162
|
locked_plans AS MATERIALIZED (
|
|
28094
28163
|
SELECT object_id, payload FROM ${this.tableName}
|
|
28095
28164
|
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
|
|
28096
28165
|
AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
|
|
28097
28166
|
ORDER BY object_id
|
|
28098
28167
|
FOR UPDATE
|
|
28168
|
+
), parent_chain(object_id, payload, path, cycle) AS (
|
|
28169
|
+
SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
|
|
28170
|
+
FROM ${this.tableName} AS parent
|
|
28171
|
+
WHERE $10::boolean
|
|
28172
|
+
AND $11::text IS NOT NULL
|
|
28173
|
+
AND parent.service = $1
|
|
28174
|
+
AND parent.object_type = 'tasks'
|
|
28175
|
+
AND parent.object_id = $11
|
|
28176
|
+
AND parent.deleted_at IS NULL
|
|
28177
|
+
UNION ALL
|
|
28178
|
+
SELECT ancestor.object_id,
|
|
28179
|
+
ancestor.payload,
|
|
28180
|
+
chain.path || ancestor.object_id,
|
|
28181
|
+
ancestor.object_id = ANY(chain.path)
|
|
28182
|
+
FROM parent_chain AS chain
|
|
28183
|
+
JOIN ${this.tableName} AS ancestor
|
|
28184
|
+
ON ancestor.service = $1
|
|
28185
|
+
AND ancestor.object_type = 'tasks'
|
|
28186
|
+
AND ancestor.object_id = chain.payload->>'parent_id'
|
|
28187
|
+
AND ancestor.deleted_at IS NULL
|
|
28188
|
+
WHERE NOT chain.cycle
|
|
28099
28189
|
), validation AS (
|
|
28100
28190
|
SELECT
|
|
28191
|
+
(NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
|
|
28192
|
+
(NOT $10::boolean OR NOT $13::boolean
|
|
28193
|
+
OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
|
|
28194
|
+
(NOT $10::boolean OR $11::text IS NULL
|
|
28195
|
+
OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
|
|
28196
|
+
(NOT $10::boolean OR $11::text IS NULL
|
|
28197
|
+
OR ($11::text <> $2
|
|
28198
|
+
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
28199
|
+
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
28101
28200
|
(SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
|
|
28102
28201
|
($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
28103
28202
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
@@ -28118,7 +28217,13 @@ class PostgresJsonRecordStore {
|
|
|
28118
28217
|
)
|
|
28119
28218
|
SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
|
|
28120
28219
|
FROM guarded
|
|
28121
|
-
WHERE guarded.
|
|
28220
|
+
WHERE guarded.task_found
|
|
28221
|
+
AND guarded.version_matches
|
|
28222
|
+
AND guarded.parent_found
|
|
28223
|
+
AND guarded.parent_acyclic
|
|
28224
|
+
AND guarded.all_plans_found
|
|
28225
|
+
AND guarded.target_plan_found
|
|
28226
|
+
AND NOT guarded.project_conflict
|
|
28122
28227
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
28123
28228
|
payload = EXCLUDED.payload,
|
|
28124
28229
|
updated_at = EXCLUDED.updated_at,
|
|
@@ -28131,8 +28236,10 @@ class PostgresJsonRecordStore {
|
|
|
28131
28236
|
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
28132
28237
|
RETURNING payload
|
|
28133
28238
|
)
|
|
28134
|
-
SELECT guarded.
|
|
28135
|
-
|
|
28239
|
+
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
28240
|
+
guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
|
|
28241
|
+
(SELECT payload FROM stored) AS payload,
|
|
28242
|
+
(SELECT payload FROM locked_task) AS current_payload
|
|
28136
28243
|
FROM guarded`, [
|
|
28137
28244
|
this.service,
|
|
28138
28245
|
value.id,
|
|
@@ -28142,9 +28249,26 @@ class PostgresJsonRecordStore {
|
|
|
28142
28249
|
numberValue3(value.version),
|
|
28143
28250
|
jsonbParam(planIds),
|
|
28144
28251
|
targetPlanId,
|
|
28145
|
-
explicitProject
|
|
28252
|
+
explicitProject,
|
|
28253
|
+
Boolean(parentGuard),
|
|
28254
|
+
parentGuard?.parentId ?? null,
|
|
28255
|
+
parentGuard?.expectedVersion ?? null,
|
|
28256
|
+
parentGuard?.operation === "update"
|
|
28146
28257
|
]);
|
|
28147
28258
|
const row = result.rows[0];
|
|
28259
|
+
if (parentGuard && !row?.task_found) {
|
|
28260
|
+
throw new TaskNotFoundError(value.id);
|
|
28261
|
+
}
|
|
28262
|
+
if (parentGuard && !row?.version_matches) {
|
|
28263
|
+
const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
|
|
28264
|
+
throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
|
|
28265
|
+
}
|
|
28266
|
+
if (parentGuard && !row?.parent_found && parentGuard.parentId) {
|
|
28267
|
+
throw new TaskNotFoundError(parentGuard.parentId);
|
|
28268
|
+
}
|
|
28269
|
+
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
28270
|
+
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
28271
|
+
}
|
|
28148
28272
|
if (!row?.all_plans_found || !row.target_plan_found) {
|
|
28149
28273
|
throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
|
|
28150
28274
|
}
|
|
@@ -28152,6 +28276,8 @@ class PostgresJsonRecordStore {
|
|
|
28152
28276
|
throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
|
|
28153
28277
|
}
|
|
28154
28278
|
if (!row.payload) {
|
|
28279
|
+
if (row.current_payload)
|
|
28280
|
+
return payloadRecord2(row.current_payload);
|
|
28155
28281
|
return await requireRecord("tasks", value.id, this);
|
|
28156
28282
|
}
|
|
28157
28283
|
return payloadRecord2(row.payload);
|
|
@@ -28477,6 +28603,80 @@ class PostgresJsonRecordStore {
|
|
|
28477
28603
|
version: numberValue3(existing["version"])
|
|
28478
28604
|
}, context);
|
|
28479
28605
|
}
|
|
28606
|
+
async deleteTaskHierarchy(id, context = {}) {
|
|
28607
|
+
await this.ensureSchema();
|
|
28608
|
+
return this.withTaskParentIntegrityTransaction(async (client) => {
|
|
28609
|
+
const timestamp3 = new Date().toISOString();
|
|
28610
|
+
const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
|
|
28611
|
+
task_tree(object_id, path, cycle) AS (
|
|
28612
|
+
SELECT task.object_id, ARRAY[task.object_id], false
|
|
28613
|
+
FROM ${this.tableName} AS task
|
|
28614
|
+
WHERE task.service = $1
|
|
28615
|
+
AND task.object_type = 'tasks'
|
|
28616
|
+
AND task.object_id = $2
|
|
28617
|
+
AND task.deleted_at IS NULL
|
|
28618
|
+
UNION ALL
|
|
28619
|
+
SELECT child.object_id,
|
|
28620
|
+
tree.path || child.object_id,
|
|
28621
|
+
child.object_id = ANY(tree.path)
|
|
28622
|
+
FROM task_tree AS tree
|
|
28623
|
+
JOIN ${this.tableName} AS child
|
|
28624
|
+
ON child.service = $1
|
|
28625
|
+
AND child.object_type = 'tasks'
|
|
28626
|
+
AND child.payload->>'parent_id' = tree.object_id
|
|
28627
|
+
AND child.deleted_at IS NULL
|
|
28628
|
+
WHERE NOT tree.cycle
|
|
28629
|
+
), tombstoned AS (
|
|
28630
|
+
UPDATE ${this.tableName} AS task
|
|
28631
|
+
SET deleted_at = $3::timestamptz,
|
|
28632
|
+
updated_at = $3::timestamptz,
|
|
28633
|
+
source_machine_id = $4
|
|
28634
|
+
WHERE task.service = $1
|
|
28635
|
+
AND task.object_type = 'tasks'
|
|
28636
|
+
AND task.deleted_at IS NULL
|
|
28637
|
+
AND task.object_id IN (
|
|
28638
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
28639
|
+
)
|
|
28640
|
+
RETURNING task.object_id
|
|
28641
|
+
), tombstoned_related AS (
|
|
28642
|
+
UPDATE ${this.tableName} AS related
|
|
28643
|
+
SET deleted_at = $3::timestamptz,
|
|
28644
|
+
updated_at = $3::timestamptz,
|
|
28645
|
+
source_machine_id = $4
|
|
28646
|
+
WHERE related.service = $1
|
|
28647
|
+
AND related.deleted_at IS NULL
|
|
28648
|
+
AND (
|
|
28649
|
+
(
|
|
28650
|
+
related.object_type = 'dependencies'
|
|
28651
|
+
AND (
|
|
28652
|
+
related.payload->>'task_id' IN (
|
|
28653
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
28654
|
+
)
|
|
28655
|
+
OR related.payload->>'depends_on' IN (
|
|
28656
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
28657
|
+
)
|
|
28658
|
+
)
|
|
28659
|
+
)
|
|
28660
|
+
OR (
|
|
28661
|
+
related.object_type IN ('comments', 'verifications', 'commits', 'refs')
|
|
28662
|
+
AND related.payload->>'task_id' IN (
|
|
28663
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
28664
|
+
)
|
|
28665
|
+
)
|
|
28666
|
+
)
|
|
28667
|
+
RETURNING related.object_id
|
|
28668
|
+
)
|
|
28669
|
+
SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
|
|
28670
|
+
(SELECT count(*) FROM tombstoned) AS deleted_count,
|
|
28671
|
+
(SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
|
|
28672
|
+
this.service,
|
|
28673
|
+
id,
|
|
28674
|
+
timestamp3,
|
|
28675
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
28676
|
+
]);
|
|
28677
|
+
return Boolean(result.rows[0]?.found);
|
|
28678
|
+
});
|
|
28679
|
+
}
|
|
28480
28680
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
28481
28681
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
28482
28682
|
return value ? assertPlanProjectLinkReceipt(value) : null;
|
|
@@ -28897,9 +29097,8 @@ class PostgresJsonRecordStore {
|
|
|
28897
29097
|
}
|
|
28898
29098
|
async function createTask2(input, store, context) {
|
|
28899
29099
|
const timestamp3 = new Date().toISOString();
|
|
28900
|
-
|
|
28901
|
-
|
|
28902
|
-
}
|
|
29100
|
+
const taskId = randomUUID3();
|
|
29101
|
+
await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
|
|
28903
29102
|
const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
|
|
28904
29103
|
const requestedProjectId = input.project_id ?? context?.projectId ?? null;
|
|
28905
29104
|
if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
|
|
@@ -28908,7 +29107,7 @@ async function createTask2(input, store, context) {
|
|
|
28908
29107
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
28909
29108
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
28910
29109
|
const task2 = {
|
|
28911
|
-
id:
|
|
29110
|
+
id: taskId,
|
|
28912
29111
|
short_id: shortId,
|
|
28913
29112
|
project_id: effectiveProjectId,
|
|
28914
29113
|
parent_id: input.parent_id ?? null,
|
|
@@ -28964,15 +29163,16 @@ async function createTask2(input, store, context) {
|
|
|
28964
29163
|
synced_at: null,
|
|
28965
29164
|
archived_at: null
|
|
28966
29165
|
};
|
|
28967
|
-
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, task2.plan_id ? [task2.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
|
|
29166
|
+
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, task2.plan_id ? [task2.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context, input.parent_id ? { operation: "create", expectedVersion: 0, parentId: input.parent_id } : undefined);
|
|
28968
29167
|
await logTaskChange2(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
|
|
28969
29168
|
return storedTask;
|
|
28970
29169
|
}
|
|
28971
|
-
async function updateTask2(id, input, store) {
|
|
29170
|
+
async function updateTask2(id, input, store, context) {
|
|
28972
29171
|
const existing = await requireRecord("tasks", id, store);
|
|
28973
29172
|
if (existing.version !== input.version) {
|
|
28974
|
-
throw new
|
|
29173
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
28975
29174
|
}
|
|
29175
|
+
await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
|
|
28976
29176
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
|
|
28977
29177
|
const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
|
|
28978
29178
|
if (linkedPlan?.project_id) {
|
|
@@ -28997,10 +29197,19 @@ async function updateTask2(id, input, store) {
|
|
|
28997
29197
|
metadata: input.metadata ?? existing.metadata,
|
|
28998
29198
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
28999
29199
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
29200
|
+
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
29000
29201
|
created_by: existing.created_by,
|
|
29001
29202
|
completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
|
|
29002
29203
|
};
|
|
29003
|
-
|
|
29204
|
+
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
29205
|
+
operation: "update",
|
|
29206
|
+
expectedVersion: input.version,
|
|
29207
|
+
parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
|
|
29208
|
+
});
|
|
29209
|
+
if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
|
|
29210
|
+
await logTaskChange2(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
|
|
29211
|
+
}
|
|
29212
|
+
return storedTask;
|
|
29004
29213
|
}
|
|
29005
29214
|
async function startTask2(id, agentId, store) {
|
|
29006
29215
|
const task2 = await requireRecord("tasks", id, store);
|
|
@@ -29064,7 +29273,11 @@ async function patchTask(task2, patch, store) {
|
|
|
29064
29273
|
version: task2.version + 1,
|
|
29065
29274
|
updated_at: new Date().toISOString()
|
|
29066
29275
|
};
|
|
29067
|
-
return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id")
|
|
29276
|
+
return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
|
|
29277
|
+
operation: "update",
|
|
29278
|
+
expectedVersion: task2.version,
|
|
29279
|
+
parentId: updated.parent_id
|
|
29280
|
+
});
|
|
29068
29281
|
}
|
|
29069
29282
|
var CLOUD_LOCK_EXPIRY_MINUTES = 30;
|
|
29070
29283
|
function sameCloudLockHolder(stored, incoming) {
|
|
@@ -34184,7 +34397,7 @@ function createLocalPrGroupLedger(db = getDatabase()) {
|
|
|
34184
34397
|
return new PrGroupLedger(new SqlitePrGroupLedgerPersistence(db));
|
|
34185
34398
|
}
|
|
34186
34399
|
// src/project-registration/authority.ts
|
|
34187
|
-
import { createHash as
|
|
34400
|
+
import { createHash as createHash15 } from "crypto";
|
|
34188
34401
|
// src/project-registration/types.ts
|
|
34189
34402
|
var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1";
|
|
34190
34403
|
var TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
|
|
@@ -34478,6 +34691,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34478
34691
|
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
34479
34692
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
34480
34693
|
LIMIT 1
|
|
34694
|
+
FOR UPDATE
|
|
34481
34695
|
`, [this.service, path, taskListSlug]);
|
|
34482
34696
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
34483
34697
|
}
|
|
@@ -34488,6 +34702,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34488
34702
|
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
34489
34703
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
34490
34704
|
LIMIT 1
|
|
34705
|
+
FOR UPDATE
|
|
34491
34706
|
`, [this.service, projectId, slug]);
|
|
34492
34707
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
34493
34708
|
}
|
|
@@ -34498,10 +34713,24 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34498
34713
|
return await this.storage.taskLists.create(input);
|
|
34499
34714
|
}
|
|
34500
34715
|
async getProject(id) {
|
|
34501
|
-
|
|
34716
|
+
const result = await this.client.query(`
|
|
34717
|
+
SELECT payload FROM ${this.tableName}
|
|
34718
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2
|
|
34719
|
+
AND deleted_at IS NULL
|
|
34720
|
+
LIMIT 1
|
|
34721
|
+
FOR SHARE
|
|
34722
|
+
`, [this.service, id]);
|
|
34723
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
34502
34724
|
}
|
|
34503
34725
|
async getTaskList(id) {
|
|
34504
|
-
|
|
34726
|
+
const result = await this.client.query(`
|
|
34727
|
+
SELECT payload FROM ${this.tableName}
|
|
34728
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
|
|
34729
|
+
AND deleted_at IS NULL
|
|
34730
|
+
LIMIT 1
|
|
34731
|
+
FOR SHARE
|
|
34732
|
+
`, [this.service, id]);
|
|
34733
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
34505
34734
|
}
|
|
34506
34735
|
async lockCompensationWrites() {
|
|
34507
34736
|
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
@@ -34581,11 +34810,102 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
34581
34810
|
async getTaskList(id) {
|
|
34582
34811
|
return (await this.direct()).getTaskList(id);
|
|
34583
34812
|
}
|
|
34813
|
+
async getProjectResourceCollectionRevision(input) {
|
|
34814
|
+
await this.ensureSchema();
|
|
34815
|
+
const result = await this.client.query(`
|
|
34816
|
+
WITH resources(kind_rank, target_id, revision) AS (
|
|
34817
|
+
SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
34818
|
+
FROM ${this.tableName}
|
|
34819
|
+
WHERE service = $1 AND object_type = 'projects'
|
|
34820
|
+
AND deleted_at IS NULL AND object_id = $2
|
|
34821
|
+
UNION ALL
|
|
34822
|
+
SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
34823
|
+
FROM ${this.tableName}
|
|
34824
|
+
WHERE service = $1 AND object_type = 'task_lists'
|
|
34825
|
+
AND deleted_at IS NULL AND object_id = $3
|
|
34826
|
+
AND payload->>'project_id' = $2
|
|
34827
|
+
UNION ALL
|
|
34828
|
+
SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
34829
|
+
FROM ${this.tableName}
|
|
34830
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'plans'
|
|
34831
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
34832
|
+
UNION ALL
|
|
34833
|
+
SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
34834
|
+
FROM ${this.tableName}
|
|
34835
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
|
|
34836
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
34837
|
+
)
|
|
34838
|
+
SELECT 'md5:' || md5(COALESCE(string_agg(
|
|
34839
|
+
kind_rank::text || chr(31) || target_id || chr(31) || revision,
|
|
34840
|
+
chr(30) ORDER BY kind_rank ASC, target_id ASC
|
|
34841
|
+
), '')) AS revision
|
|
34842
|
+
FROM resources
|
|
34843
|
+
`, [
|
|
34844
|
+
this.service,
|
|
34845
|
+
input.todos_project_id,
|
|
34846
|
+
input.task_list_id,
|
|
34847
|
+
input.include_anchors
|
|
34848
|
+
]);
|
|
34849
|
+
const revision = result.rows[0]?.revision;
|
|
34850
|
+
if (!revision) {
|
|
34851
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
|
|
34852
|
+
}
|
|
34853
|
+
return revision;
|
|
34854
|
+
}
|
|
34855
|
+
async listProjectResourceCandidates(input) {
|
|
34856
|
+
await this.ensureSchema();
|
|
34857
|
+
const afterRank = input.after?.kind_rank ?? -1;
|
|
34858
|
+
const afterId = input.after?.target_id ?? "";
|
|
34859
|
+
const result = await this.client.query(`
|
|
34860
|
+
WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
|
|
34861
|
+
SELECT 'project'::text, 0, object_id, NULL::text,
|
|
34862
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
34863
|
+
FROM ${this.tableName}
|
|
34864
|
+
WHERE service = $1 AND object_type = 'projects'
|
|
34865
|
+
AND deleted_at IS NULL AND object_id = $2
|
|
34866
|
+
UNION ALL
|
|
34867
|
+
SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
|
|
34868
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
34869
|
+
FROM ${this.tableName}
|
|
34870
|
+
WHERE service = $1 AND object_type = 'task_lists'
|
|
34871
|
+
AND deleted_at IS NULL AND object_id = $3
|
|
34872
|
+
AND payload->>'project_id' = $2
|
|
34873
|
+
UNION ALL
|
|
34874
|
+
SELECT 'plan'::text, 2, object_id, payload->>'project_id',
|
|
34875
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
34876
|
+
FROM ${this.tableName}
|
|
34877
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'plans'
|
|
34878
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
34879
|
+
UNION ALL
|
|
34880
|
+
SELECT 'task'::text, 3, object_id,
|
|
34881
|
+
COALESCE(payload->>'plan_id', payload->>'project_id'),
|
|
34882
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
34883
|
+
FROM ${this.tableName}
|
|
34884
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
|
|
34885
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
34886
|
+
)
|
|
34887
|
+
SELECT kind, kind_rank, target_id, parent_id, revision
|
|
34888
|
+
FROM resources
|
|
34889
|
+
WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
|
|
34890
|
+
ORDER BY kind_rank ASC, target_id ASC
|
|
34891
|
+
LIMIT $7
|
|
34892
|
+
`, [
|
|
34893
|
+
this.service,
|
|
34894
|
+
input.todos_project_id,
|
|
34895
|
+
input.task_list_id,
|
|
34896
|
+
input.include_anchors,
|
|
34897
|
+
afterRank,
|
|
34898
|
+
afterId,
|
|
34899
|
+
input.limit
|
|
34900
|
+
]);
|
|
34901
|
+
return result.rows;
|
|
34902
|
+
}
|
|
34584
34903
|
}
|
|
34585
34904
|
|
|
34586
34905
|
// src/project-registration/sqlite.ts
|
|
34587
34906
|
init_database();
|
|
34588
34907
|
init_storage_tombstones();
|
|
34908
|
+
import { createHash as createHash14 } from "crypto";
|
|
34589
34909
|
var sqliteTransactionTails2 = new WeakMap;
|
|
34590
34910
|
var PROJECT_REFERENCE_COLUMNS = new Set([
|
|
34591
34911
|
"project_id",
|
|
@@ -35239,6 +35559,64 @@ class SqliteTodosProjectRegistrationBackend {
|
|
|
35239
35559
|
getTaskList(id) {
|
|
35240
35560
|
return this.direct.getTaskList(id);
|
|
35241
35561
|
}
|
|
35562
|
+
async getProjectResourceCollectionRevision(input) {
|
|
35563
|
+
const digest = createHash14("sha256");
|
|
35564
|
+
const rows = this.db.query(`
|
|
35565
|
+
WITH resources(kind_rank, target_id, revision) AS (
|
|
35566
|
+
SELECT 0, id, updated_at
|
|
35567
|
+
FROM projects
|
|
35568
|
+
WHERE id = ?
|
|
35569
|
+
UNION ALL
|
|
35570
|
+
SELECT 1, id, updated_at
|
|
35571
|
+
FROM task_lists
|
|
35572
|
+
WHERE id = ? AND project_id = ?
|
|
35573
|
+
UNION ALL
|
|
35574
|
+
SELECT 2, id, updated_at
|
|
35575
|
+
FROM plans
|
|
35576
|
+
WHERE ? = 1 AND project_id = ?
|
|
35577
|
+
UNION ALL
|
|
35578
|
+
SELECT 3, id, updated_at
|
|
35579
|
+
FROM tasks
|
|
35580
|
+
WHERE ? = 1 AND project_id = ?
|
|
35581
|
+
)
|
|
35582
|
+
SELECT kind_rank, target_id, revision
|
|
35583
|
+
FROM resources
|
|
35584
|
+
ORDER BY kind_rank ASC, target_id ASC
|
|
35585
|
+
`).iterate(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id);
|
|
35586
|
+
for (const row of rows) {
|
|
35587
|
+
digest.update(`${row.kind_rank}\x00${row.target_id}\x00${row.revision}
|
|
35588
|
+
`);
|
|
35589
|
+
}
|
|
35590
|
+
return `sha256:${digest.digest("hex")}`;
|
|
35591
|
+
}
|
|
35592
|
+
async listProjectResourceCandidates(input) {
|
|
35593
|
+
const afterRank = input.after?.kind_rank ?? -1;
|
|
35594
|
+
const afterId = input.after?.target_id ?? "";
|
|
35595
|
+
return this.db.query(`
|
|
35596
|
+
WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
|
|
35597
|
+
SELECT 'project', 0, id, NULL, updated_at
|
|
35598
|
+
FROM projects
|
|
35599
|
+
WHERE id = ?
|
|
35600
|
+
UNION ALL
|
|
35601
|
+
SELECT 'task_list', 1, id, project_id, updated_at
|
|
35602
|
+
FROM task_lists
|
|
35603
|
+
WHERE id = ? AND project_id = ?
|
|
35604
|
+
UNION ALL
|
|
35605
|
+
SELECT 'plan', 2, id, project_id, updated_at
|
|
35606
|
+
FROM plans
|
|
35607
|
+
WHERE ? = 1 AND project_id = ?
|
|
35608
|
+
UNION ALL
|
|
35609
|
+
SELECT 'task', 3, id, COALESCE(plan_id, project_id), updated_at
|
|
35610
|
+
FROM tasks
|
|
35611
|
+
WHERE ? = 1 AND project_id = ?
|
|
35612
|
+
)
|
|
35613
|
+
SELECT kind, kind_rank, target_id, parent_id, revision
|
|
35614
|
+
FROM resources
|
|
35615
|
+
WHERE kind_rank > ? OR (kind_rank = ? AND target_id > ?)
|
|
35616
|
+
ORDER BY kind_rank ASC, target_id ASC
|
|
35617
|
+
LIMIT ?
|
|
35618
|
+
`).all(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, afterRank, afterRank, afterId, input.limit);
|
|
35619
|
+
}
|
|
35242
35620
|
}
|
|
35243
35621
|
|
|
35244
35622
|
// src/project-registration/authority.ts
|
|
@@ -35250,6 +35628,8 @@ var AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
|
35250
35628
|
var PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
35251
35629
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
35252
35630
|
var IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
35631
|
+
var PROJECT_RESOURCE_PAGE_LIMIT = 500;
|
|
35632
|
+
var PROJECT_RESOURCE_CURSOR_VERSION = 1;
|
|
35253
35633
|
|
|
35254
35634
|
class WriteBoundaryError extends Error {
|
|
35255
35635
|
point;
|
|
@@ -35277,7 +35657,7 @@ function canonicalize4(value) {
|
|
|
35277
35657
|
return out;
|
|
35278
35658
|
}
|
|
35279
35659
|
function digestProjectRegistrationValue(value) {
|
|
35280
|
-
return
|
|
35660
|
+
return createHash15("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
35281
35661
|
}
|
|
35282
35662
|
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
35283
35663
|
return `prk_${digestProjectRegistrationValue({
|
|
@@ -35389,35 +35769,68 @@ function projectRecord(project) {
|
|
|
35389
35769
|
return {
|
|
35390
35770
|
target_id: project.id,
|
|
35391
35771
|
revision: project.updated_at,
|
|
35392
|
-
digest:
|
|
35393
|
-
id: project.id,
|
|
35394
|
-
name: project.name,
|
|
35395
|
-
path: project.path,
|
|
35396
|
-
description: project.description,
|
|
35397
|
-
task_list_id: project.task_list_id,
|
|
35398
|
-
task_prefix: project.task_prefix,
|
|
35399
|
-
task_counter: project.task_counter,
|
|
35400
|
-
created_at: project.created_at,
|
|
35401
|
-
updated_at: project.updated_at
|
|
35402
|
-
})
|
|
35772
|
+
digest: projectRegistrationDigest(project)
|
|
35403
35773
|
};
|
|
35404
35774
|
}
|
|
35405
35775
|
function taskListRecord(taskList) {
|
|
35406
35776
|
return {
|
|
35407
35777
|
target_id: taskList.id,
|
|
35408
35778
|
revision: taskList.updated_at,
|
|
35409
|
-
digest:
|
|
35410
|
-
|
|
35411
|
-
|
|
35412
|
-
|
|
35413
|
-
|
|
35414
|
-
|
|
35415
|
-
|
|
35416
|
-
|
|
35417
|
-
|
|
35779
|
+
digest: taskListRegistrationDigest(taskList)
|
|
35780
|
+
};
|
|
35781
|
+
}
|
|
35782
|
+
function boundExistingProjectRecord(project) {
|
|
35783
|
+
return {
|
|
35784
|
+
target_id: project.id,
|
|
35785
|
+
revision: project.created_at,
|
|
35786
|
+
digest: projectRegistrationDigest({
|
|
35787
|
+
...project,
|
|
35788
|
+
updated_at: project.created_at
|
|
35418
35789
|
})
|
|
35419
35790
|
};
|
|
35420
35791
|
}
|
|
35792
|
+
function boundExistingTaskListRecord(taskList) {
|
|
35793
|
+
return {
|
|
35794
|
+
target_id: taskList.id,
|
|
35795
|
+
revision: taskList.created_at,
|
|
35796
|
+
digest: taskListRegistrationDigest({
|
|
35797
|
+
...taskList,
|
|
35798
|
+
updated_at: taskList.created_at
|
|
35799
|
+
})
|
|
35800
|
+
};
|
|
35801
|
+
}
|
|
35802
|
+
function projectRegistrationDigest(project) {
|
|
35803
|
+
return digestProjectRegistrationValue({
|
|
35804
|
+
id: project.id,
|
|
35805
|
+
name: project.name,
|
|
35806
|
+
path: project.path,
|
|
35807
|
+
description: project.description,
|
|
35808
|
+
task_list_id: project.task_list_id,
|
|
35809
|
+
task_prefix: project.task_prefix,
|
|
35810
|
+
task_counter: project.task_counter,
|
|
35811
|
+
created_at: project.created_at,
|
|
35812
|
+
updated_at: project.updated_at
|
|
35813
|
+
});
|
|
35814
|
+
}
|
|
35815
|
+
function taskListRegistrationDigest(taskList) {
|
|
35816
|
+
return digestProjectRegistrationValue({
|
|
35817
|
+
id: taskList.id,
|
|
35818
|
+
project_id: taskList.project_id,
|
|
35819
|
+
slug: taskList.slug,
|
|
35820
|
+
name: taskList.name,
|
|
35821
|
+
description: taskList.description,
|
|
35822
|
+
metadata: taskList.metadata,
|
|
35823
|
+
created_at: taskList.created_at,
|
|
35824
|
+
updated_at: taskList.updated_at
|
|
35825
|
+
});
|
|
35826
|
+
}
|
|
35827
|
+
function canonicalValuesEqual(left, right) {
|
|
35828
|
+
try {
|
|
35829
|
+
return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
|
|
35830
|
+
} catch {
|
|
35831
|
+
return false;
|
|
35832
|
+
}
|
|
35833
|
+
}
|
|
35421
35834
|
function receiptId(input) {
|
|
35422
35835
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
35423
35836
|
}
|
|
@@ -35437,6 +35850,29 @@ function assertCapabilityRequest(request, capability2) {
|
|
|
35437
35850
|
}
|
|
35438
35851
|
}
|
|
35439
35852
|
function normalizedCallDigest(request) {
|
|
35853
|
+
return digestProjectRegistrationValue({
|
|
35854
|
+
authority_route: request.authority_route,
|
|
35855
|
+
package_version: request.package_version,
|
|
35856
|
+
authority_id: request.authority_id,
|
|
35857
|
+
tenant_id: request.tenant_id,
|
|
35858
|
+
corpus_id: request.corpus_id,
|
|
35859
|
+
operation_id: request.operation_id,
|
|
35860
|
+
step_id: request.step_id,
|
|
35861
|
+
resource_kind: request.resource_kind,
|
|
35862
|
+
direction: request.direction,
|
|
35863
|
+
target_selector: request.target_selector,
|
|
35864
|
+
idempotency_key: request.idempotency_key,
|
|
35865
|
+
request_digest: request.request_digest,
|
|
35866
|
+
precondition_digest: request.precondition_digest,
|
|
35867
|
+
project_id: request.project_id,
|
|
35868
|
+
project_slug: request.project_slug,
|
|
35869
|
+
project_name: request.project_name,
|
|
35870
|
+
desired: request.desired,
|
|
35871
|
+
bind_existing: request.bind_existing === true,
|
|
35872
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
35873
|
+
});
|
|
35874
|
+
}
|
|
35875
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
35440
35876
|
return digestProjectRegistrationValue({
|
|
35441
35877
|
authority_route: request.authority_route,
|
|
35442
35878
|
package_version: request.package_version,
|
|
@@ -35458,6 +35894,11 @@ function normalizedCallDigest(request) {
|
|
|
35458
35894
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
35459
35895
|
});
|
|
35460
35896
|
}
|
|
35897
|
+
function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
|
|
35898
|
+
if (accepted.normalized_call_digest === callDigest)
|
|
35899
|
+
return true;
|
|
35900
|
+
return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
|
|
35901
|
+
}
|
|
35461
35902
|
function assertCommonRequest(request, capability2) {
|
|
35462
35903
|
assertBounds(request);
|
|
35463
35904
|
assertResourceKind(request.resource_kind);
|
|
@@ -35499,6 +35940,9 @@ function assertCommonRequest(request, capability2) {
|
|
|
35499
35940
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
35500
35941
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
35501
35942
|
}
|
|
35943
|
+
if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
|
|
35944
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
|
|
35945
|
+
}
|
|
35502
35946
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
35503
35947
|
operation_id: request.operation_id,
|
|
35504
35948
|
step_id: request.step_id,
|
|
@@ -35520,7 +35964,7 @@ function assertForwardRequest(request, capability2) {
|
|
|
35520
35964
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
35521
35965
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
35522
35966
|
target_selector: request.target_selector,
|
|
35523
|
-
expected: "absent"
|
|
35967
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
35524
35968
|
});
|
|
35525
35969
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
35526
35970
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -35599,7 +36043,7 @@ function receiptBase(request, callDigest, capability2) {
|
|
|
35599
36043
|
normalized_call_digest: callDigest
|
|
35600
36044
|
};
|
|
35601
36045
|
}
|
|
35602
|
-
function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt2) {
|
|
36046
|
+
function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt2, createdByOperation = true) {
|
|
35603
36047
|
return makeReceipt({
|
|
35604
36048
|
...receiptBase(request, callDigest, capability2),
|
|
35605
36049
|
outcome: "accepted",
|
|
@@ -35609,7 +36053,7 @@ function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt
|
|
|
35609
36053
|
result_digest: record.digest,
|
|
35610
36054
|
duplicate_of_receipt_id: null,
|
|
35611
36055
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
35612
|
-
created_by_operation:
|
|
36056
|
+
created_by_operation: createdByOperation
|
|
35613
36057
|
}, createdAt2);
|
|
35614
36058
|
}
|
|
35615
36059
|
function makeDuplicateReceipt(request, callDigest, capability2, accepted, createdAt2) {
|
|
@@ -35671,6 +36115,53 @@ function bindingFor(request, callDigest, timestamp3, capability2) {
|
|
|
35671
36115
|
updated_at: timestamp3
|
|
35672
36116
|
};
|
|
35673
36117
|
}
|
|
36118
|
+
function encodeProjectResourceCursor(input) {
|
|
36119
|
+
return Buffer.from(JSON.stringify({
|
|
36120
|
+
version: PROJECT_RESOURCE_CURSOR_VERSION,
|
|
36121
|
+
...input
|
|
36122
|
+
}), "utf8").toString("base64url");
|
|
36123
|
+
}
|
|
36124
|
+
function decodeProjectResourceCursor(cursor, expected) {
|
|
36125
|
+
if (!cursor)
|
|
36126
|
+
return null;
|
|
36127
|
+
let parsed;
|
|
36128
|
+
try {
|
|
36129
|
+
parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
36130
|
+
} catch {
|
|
36131
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
|
|
36132
|
+
}
|
|
36133
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
36134
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
|
|
36135
|
+
}
|
|
36136
|
+
const value = parsed;
|
|
36137
|
+
if (value["version"] !== PROJECT_RESOURCE_CURSOR_VERSION || value["source_project_id"] !== expected.source_project_id || value["include_anchors"] !== expected.include_anchors || !Number.isSafeInteger(value["kind_rank"]) || Number(value["kind_rank"]) < 0 || Number(value["kind_rank"]) > 3 || typeof value["target_id"] !== "string" || !UUID_PATTERN.test(value["target_id"]) || typeof value["collection_revision"] !== "string" || value["collection_revision"].length < 16 || value["collection_revision"].length > 128) {
|
|
36138
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
|
|
36139
|
+
}
|
|
36140
|
+
return {
|
|
36141
|
+
kind_rank: Number(value["kind_rank"]),
|
|
36142
|
+
target_id: value["target_id"],
|
|
36143
|
+
collection_revision: value["collection_revision"]
|
|
36144
|
+
};
|
|
36145
|
+
}
|
|
36146
|
+
function projectResourceFromCandidate(sourceProjectId, candidate) {
|
|
36147
|
+
const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
|
|
36148
|
+
return {
|
|
36149
|
+
source_project_id: sourceProjectId,
|
|
36150
|
+
kind: candidate.kind,
|
|
36151
|
+
scope,
|
|
36152
|
+
target_id: candidate.target_id,
|
|
36153
|
+
parent_id: candidate.parent_id,
|
|
36154
|
+
revision: candidate.revision,
|
|
36155
|
+
digest: digestProjectRegistrationValue({
|
|
36156
|
+
source_project_id: sourceProjectId,
|
|
36157
|
+
kind: candidate.kind,
|
|
36158
|
+
scope,
|
|
36159
|
+
target_id: candidate.target_id,
|
|
36160
|
+
parent_id: candidate.parent_id,
|
|
36161
|
+
revision: candidate.revision
|
|
36162
|
+
})
|
|
36163
|
+
};
|
|
36164
|
+
}
|
|
35674
36165
|
|
|
35675
36166
|
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
35676
36167
|
backend;
|
|
@@ -35692,6 +36183,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35692
36183
|
immutable_receipts: true,
|
|
35693
36184
|
exact_terminal_lookup: true,
|
|
35694
36185
|
exact_readback: true,
|
|
36186
|
+
bind_existing_adoption: true,
|
|
36187
|
+
prior_registration_adoption_validation: true,
|
|
36188
|
+
project_resource_enumeration: true,
|
|
36189
|
+
project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
|
|
35695
36190
|
conditional_inverse: true,
|
|
35696
36191
|
ambiguous_outcome_reconciliation: true
|
|
35697
36192
|
};
|
|
@@ -35752,7 +36247,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35752
36247
|
if (!accepted2) {
|
|
35753
36248
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
35754
36249
|
}
|
|
35755
|
-
if (accepted2
|
|
36250
|
+
if (!acceptedCallMatches(request, accepted2, callDigest)) {
|
|
35756
36251
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
35757
36252
|
}
|
|
35758
36253
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
@@ -35766,7 +36261,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35766
36261
|
});
|
|
35767
36262
|
if (!accepted)
|
|
35768
36263
|
return null;
|
|
35769
|
-
if (accepted
|
|
36264
|
+
if (acceptedCallMatches(request, accepted, callDigest)) {
|
|
35770
36265
|
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
35771
36266
|
}
|
|
35772
36267
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
@@ -35777,6 +36272,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35777
36272
|
const slug2 = taskListSlug(request.project_slug);
|
|
35778
36273
|
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
35779
36274
|
if (conflict2) {
|
|
36275
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
|
|
36276
|
+
return {
|
|
36277
|
+
record: boundExistingProjectRecord(conflict2),
|
|
36278
|
+
created_by_operation: false
|
|
36279
|
+
};
|
|
36280
|
+
}
|
|
35780
36281
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
35781
36282
|
}
|
|
35782
36283
|
await this.fault("before_object_write", request);
|
|
@@ -35788,7 +36289,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35788
36289
|
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
35789
36290
|
});
|
|
35790
36291
|
await this.fault("after_object_write", request);
|
|
35791
|
-
return
|
|
36292
|
+
return {
|
|
36293
|
+
record: projectRecord(project),
|
|
36294
|
+
created_by_operation: true
|
|
36295
|
+
};
|
|
35792
36296
|
}
|
|
35793
36297
|
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
35794
36298
|
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
@@ -35802,6 +36306,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35802
36306
|
const slug = taskListSlug(request.project_slug);
|
|
35803
36307
|
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
35804
36308
|
if (conflict) {
|
|
36309
|
+
if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
|
|
36310
|
+
return {
|
|
36311
|
+
record: boundExistingTaskListRecord(conflict),
|
|
36312
|
+
created_by_operation: false
|
|
36313
|
+
};
|
|
36314
|
+
}
|
|
35805
36315
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
35806
36316
|
}
|
|
35807
36317
|
await this.fault("before_object_write", request);
|
|
@@ -35818,7 +36328,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35818
36328
|
if (taskList.project_id !== todosProjectId) {
|
|
35819
36329
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
35820
36330
|
}
|
|
35821
|
-
return
|
|
36331
|
+
return {
|
|
36332
|
+
record: taskListRecord(taskList),
|
|
36333
|
+
created_by_operation: true
|
|
36334
|
+
};
|
|
35822
36335
|
}
|
|
35823
36336
|
async create(request) {
|
|
35824
36337
|
const startedAt = Date.now();
|
|
@@ -35840,9 +36353,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35840
36353
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
|
|
35841
36354
|
if (!claimed) {
|
|
35842
36355
|
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
35843
|
-
if (binding?.state === "accepted" && binding.
|
|
36356
|
+
if (binding?.state === "accepted" && binding.accepted_receipt_id) {
|
|
35844
36357
|
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
35845
|
-
if (accepted2) {
|
|
36358
|
+
if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
|
|
35846
36359
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
35847
36360
|
}
|
|
35848
36361
|
}
|
|
@@ -35853,15 +36366,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35853
36366
|
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
35854
36367
|
return recordOrTerminal;
|
|
35855
36368
|
}
|
|
35856
|
-
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
|
|
36369
|
+
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
|
|
35857
36370
|
await this.fault("before_receipt_write", request);
|
|
35858
36371
|
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
35859
36372
|
await this.fault("after_receipt_write", request);
|
|
35860
36373
|
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
35861
|
-
target_id: recordOrTerminal.target_id,
|
|
36374
|
+
target_id: recordOrTerminal.record.target_id,
|
|
35862
36375
|
accepted_receipt_id: stored.receipt_id,
|
|
35863
|
-
result_revision: recordOrTerminal.revision,
|
|
35864
|
-
result_digest: recordOrTerminal.digest,
|
|
36376
|
+
result_revision: recordOrTerminal.record.revision,
|
|
36377
|
+
result_digest: recordOrTerminal.record.digest,
|
|
35865
36378
|
updated_at: this.now()
|
|
35866
36379
|
});
|
|
35867
36380
|
return stored;
|
|
@@ -35909,7 +36422,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35909
36422
|
direction: request.direction
|
|
35910
36423
|
});
|
|
35911
36424
|
if (accepted) {
|
|
35912
|
-
return accepted
|
|
36425
|
+
return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
35913
36426
|
}
|
|
35914
36427
|
const timestamp3 = this.now();
|
|
35915
36428
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
|
|
@@ -35942,9 +36455,14 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35942
36455
|
if (request.max_items !== 1) {
|
|
35943
36456
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
35944
36457
|
}
|
|
35945
|
-
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id
|
|
36458
|
+
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id) {
|
|
35946
36459
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
35947
36460
|
}
|
|
36461
|
+
requireString(request.corpus_id, "corpus_id", {
|
|
36462
|
+
min: 3,
|
|
36463
|
+
max: 128,
|
|
36464
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
36465
|
+
});
|
|
35948
36466
|
requireString(request.authority_route, "authority_route", {
|
|
35949
36467
|
min: 3,
|
|
35950
36468
|
max: 128,
|
|
@@ -35975,6 +36493,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35975
36493
|
}
|
|
35976
36494
|
const receipt = await this.backend.getReceiptForLookup({
|
|
35977
36495
|
...authorityScope(this.capabilityValue),
|
|
36496
|
+
corpus_id: request.corpus_id,
|
|
35978
36497
|
route: request.authority_route,
|
|
35979
36498
|
package_version: request.package_version,
|
|
35980
36499
|
operation_id: request.operation_id,
|
|
@@ -35989,6 +36508,174 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35989
36508
|
}
|
|
35990
36509
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
35991
36510
|
}
|
|
36511
|
+
async listProjectResources(request) {
|
|
36512
|
+
const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
|
|
36513
|
+
if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
|
|
36514
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
|
|
36515
|
+
}
|
|
36516
|
+
if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
|
|
36517
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
|
|
36518
|
+
}
|
|
36519
|
+
const includeAnchors = request.include_anchors === true;
|
|
36520
|
+
const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
|
|
36521
|
+
if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
|
|
36522
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted Todos project binding exists for this exact Projects workspace id", { source_project_id: sourceProjectId });
|
|
36523
|
+
}
|
|
36524
|
+
const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
|
|
36525
|
+
if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
|
|
36526
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
|
|
36527
|
+
source_project_id: sourceProjectId,
|
|
36528
|
+
todos_project_id: projectBinding.target_id
|
|
36529
|
+
});
|
|
36530
|
+
}
|
|
36531
|
+
const cursor = decodeProjectResourceCursor(request.cursor, {
|
|
36532
|
+
source_project_id: sourceProjectId,
|
|
36533
|
+
include_anchors: includeAnchors
|
|
36534
|
+
});
|
|
36535
|
+
const collectionInput = {
|
|
36536
|
+
todos_project_id: projectBinding.target_id,
|
|
36537
|
+
task_list_id: taskListBinding.target_id,
|
|
36538
|
+
include_anchors: includeAnchors
|
|
36539
|
+
};
|
|
36540
|
+
const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
36541
|
+
if (cursor && cursor.collection_revision !== collectionRevision) {
|
|
36542
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
|
|
36543
|
+
source_project_id: sourceProjectId,
|
|
36544
|
+
expected_collection_revision: cursor.collection_revision,
|
|
36545
|
+
current_collection_revision: collectionRevision
|
|
36546
|
+
});
|
|
36547
|
+
}
|
|
36548
|
+
const candidates = await this.backend.listProjectResourceCandidates({
|
|
36549
|
+
...collectionInput,
|
|
36550
|
+
after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
|
|
36551
|
+
limit: request.limit + 1
|
|
36552
|
+
});
|
|
36553
|
+
const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
36554
|
+
if (verifiedCollectionRevision !== collectionRevision) {
|
|
36555
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
|
|
36556
|
+
source_project_id: sourceProjectId,
|
|
36557
|
+
expected_collection_revision: collectionRevision,
|
|
36558
|
+
current_collection_revision: verifiedCollectionRevision
|
|
36559
|
+
});
|
|
36560
|
+
}
|
|
36561
|
+
const hasMore = candidates.length > request.limit;
|
|
36562
|
+
const pageCandidates = candidates.slice(0, request.limit);
|
|
36563
|
+
const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
|
|
36564
|
+
const last = pageCandidates.at(-1);
|
|
36565
|
+
return {
|
|
36566
|
+
authority: "todos",
|
|
36567
|
+
route: this.capabilityValue.route,
|
|
36568
|
+
package_version: this.capabilityValue.package_version,
|
|
36569
|
+
authority_id: this.capabilityValue.authority_id,
|
|
36570
|
+
tenant_id: this.capabilityValue.tenant_id,
|
|
36571
|
+
corpus_id: this.capabilityValue.corpus_id,
|
|
36572
|
+
source_project_id: sourceProjectId,
|
|
36573
|
+
todos_project_id: projectBinding.target_id,
|
|
36574
|
+
task_list_id: taskListBinding.target_id,
|
|
36575
|
+
include_anchors: includeAnchors,
|
|
36576
|
+
collection_revision: collectionRevision,
|
|
36577
|
+
limit: request.limit,
|
|
36578
|
+
count: resources.length,
|
|
36579
|
+
resources,
|
|
36580
|
+
has_more: hasMore,
|
|
36581
|
+
next_cursor: hasMore && last ? encodeProjectResourceCursor({
|
|
36582
|
+
source_project_id: sourceProjectId,
|
|
36583
|
+
include_anchors: includeAnchors,
|
|
36584
|
+
collection_revision: collectionRevision,
|
|
36585
|
+
kind_rank: last.kind_rank,
|
|
36586
|
+
target_id: last.target_id
|
|
36587
|
+
}) : null,
|
|
36588
|
+
complete: !hasMore,
|
|
36589
|
+
truncated: false
|
|
36590
|
+
};
|
|
36591
|
+
}
|
|
36592
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
36593
|
+
const startedAt = Date.now();
|
|
36594
|
+
if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
|
|
36595
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
|
|
36596
|
+
}
|
|
36597
|
+
requireString(sourceRequest.package_version, "package_version", {
|
|
36598
|
+
max: 128,
|
|
36599
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
36600
|
+
});
|
|
36601
|
+
requireString(sourceRequest.corpus_id, "corpus_id", {
|
|
36602
|
+
min: 3,
|
|
36603
|
+
max: 128,
|
|
36604
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
36605
|
+
});
|
|
36606
|
+
assertForwardRequest(sourceRequest, {
|
|
36607
|
+
...this.capabilityValue,
|
|
36608
|
+
package_version: sourceRequest.package_version,
|
|
36609
|
+
corpus_id: sourceRequest.corpus_id
|
|
36610
|
+
});
|
|
36611
|
+
const validation = await this.backend.transaction(async (transaction) => {
|
|
36612
|
+
const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
|
|
36613
|
+
if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
|
|
36614
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
36615
|
+
}
|
|
36616
|
+
const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
|
|
36617
|
+
if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
|
|
36618
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
36619
|
+
}
|
|
36620
|
+
const receiptLineageMatches = (receipt) => receipt.authority === "todos" && receipt.route === sourceRequest.authority_route && receipt.package_version === sourceRequest.package_version && receipt.authority_id === sourceRequest.authority_id && receipt.tenant_id === sourceRequest.tenant_id && receipt.corpus_id === sourceRequest.corpus_id && receipt.operation_id === sourceRequest.operation_id && receipt.step_id === sourceRequest.step_id && receipt.resource_kind === sourceRequest.resource_kind && receipt.direction === "forward" && receipt.target_selector === sourceRequest.target_selector && receipt.idempotency_key === sourceRequest.idempotency_key && receipt.request_digest === sourceRequest.request_digest && receipt.precondition_digest === sourceRequest.precondition_digest && acceptedCallMatches(sourceRequest, receipt);
|
|
36621
|
+
if (!receiptLineageMatches(storedSource) || !receiptLineageMatches(accepted) || storedSource.outcome === "duplicate_of_accepted" && (storedSource.duplicate_of_receipt_id !== accepted.receipt_id || storedSource.target_id !== accepted.target_id || storedSource.result_revision !== accepted.result_revision || storedSource.result_digest !== accepted.result_digest)) {
|
|
36622
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
|
|
36623
|
+
}
|
|
36624
|
+
const binding = await transaction.getBinding({
|
|
36625
|
+
authority_id: sourceRequest.authority_id,
|
|
36626
|
+
tenant_id: sourceRequest.tenant_id,
|
|
36627
|
+
corpus_id: sourceRequest.corpus_id
|
|
36628
|
+
}, sourceRequest.resource_kind, sourceRequest.target_selector);
|
|
36629
|
+
if (!binding || binding.state !== "accepted" || binding.operation_id !== sourceRequest.operation_id || binding.step_id !== sourceRequest.step_id || binding.direction !== "forward" || binding.idempotency_key !== sourceRequest.idempotency_key || binding.request_digest !== sourceRequest.request_digest || binding.precondition_digest !== sourceRequest.precondition_digest || binding.normalized_call_digest !== accepted.normalized_call_digest || binding.target_id !== accepted.target_id || binding.accepted_receipt_id !== accepted.receipt_id || binding.result_revision !== accepted.result_revision || binding.result_digest !== accepted.result_digest) {
|
|
36630
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
|
|
36631
|
+
}
|
|
36632
|
+
const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
36633
|
+
if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
|
|
36634
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
|
|
36635
|
+
}
|
|
36636
|
+
let stableMatch = false;
|
|
36637
|
+
if (sourceRequest.resource_kind === "task_list") {
|
|
36638
|
+
stableMatch = taskListRegistrationDigest({
|
|
36639
|
+
...current,
|
|
36640
|
+
updated_at: accepted.result_revision
|
|
36641
|
+
}) === accepted.result_digest;
|
|
36642
|
+
} else {
|
|
36643
|
+
const project = current;
|
|
36644
|
+
if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
|
|
36645
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
|
|
36646
|
+
}
|
|
36647
|
+
for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
|
|
36648
|
+
if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
|
|
36649
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
|
|
36650
|
+
}
|
|
36651
|
+
if (projectRegistrationDigest({
|
|
36652
|
+
...project,
|
|
36653
|
+
task_counter: priorTaskCounter,
|
|
36654
|
+
updated_at: accepted.result_revision
|
|
36655
|
+
}) === accepted.result_digest) {
|
|
36656
|
+
stableMatch = true;
|
|
36657
|
+
break;
|
|
36658
|
+
}
|
|
36659
|
+
}
|
|
36660
|
+
}
|
|
36661
|
+
if (!stableMatch) {
|
|
36662
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
|
|
36663
|
+
}
|
|
36664
|
+
return {
|
|
36665
|
+
valid: true,
|
|
36666
|
+
resource_kind: sourceRequest.resource_kind,
|
|
36667
|
+
target_id: accepted.target_id,
|
|
36668
|
+
source_receipt_id: storedSource.receipt_id,
|
|
36669
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
36670
|
+
source_outcome: storedSource.outcome,
|
|
36671
|
+
created_at: current.created_at,
|
|
36672
|
+
current_revision: current.updated_at,
|
|
36673
|
+
accepted_result_digest: accepted.result_digest
|
|
36674
|
+
};
|
|
36675
|
+
});
|
|
36676
|
+
assertWithinBounds(validation, sourceRequest, startedAt);
|
|
36677
|
+
return validation;
|
|
36678
|
+
}
|
|
35992
36679
|
async storedAcceptedReceipt(request, supplied) {
|
|
35993
36680
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
35994
36681
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -36160,6 +36847,65 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
36160
36847
|
cursorTableName
|
|
36161
36848
|
}), authorityOptions);
|
|
36162
36849
|
}
|
|
36850
|
+
// src/project-registration/adoption-validation.ts
|
|
36851
|
+
var VALIDATION_KEYS = [
|
|
36852
|
+
"valid",
|
|
36853
|
+
"resource_kind",
|
|
36854
|
+
"target_id",
|
|
36855
|
+
"source_receipt_id",
|
|
36856
|
+
"accepted_receipt_id",
|
|
36857
|
+
"source_outcome",
|
|
36858
|
+
"created_at",
|
|
36859
|
+
"current_revision",
|
|
36860
|
+
"accepted_result_digest"
|
|
36861
|
+
];
|
|
36862
|
+
function isRecord2(value) {
|
|
36863
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
36864
|
+
}
|
|
36865
|
+
function isNonEmptyString(value) {
|
|
36866
|
+
return typeof value === "string" && value.length > 0;
|
|
36867
|
+
}
|
|
36868
|
+
function hasExactKeys(value, expected) {
|
|
36869
|
+
const actual = Object.keys(value).sort();
|
|
36870
|
+
const wanted = [...expected].sort();
|
|
36871
|
+
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
|
|
36872
|
+
}
|
|
36873
|
+
function adoptionRejected(message) {
|
|
36874
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", `TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED: ${message}`);
|
|
36875
|
+
}
|
|
36876
|
+
function assertTodosPriorRegistrationAdoptionValidationEnvelope(value, input) {
|
|
36877
|
+
if (!isRecord2(input) || !hasExactKeys(input, [
|
|
36878
|
+
"source_request",
|
|
36879
|
+
"source_receipt",
|
|
36880
|
+
"current_record"
|
|
36881
|
+
])) {
|
|
36882
|
+
adoptionRejected("prior-adoption validation input is incomplete");
|
|
36883
|
+
}
|
|
36884
|
+
const request = input["source_request"];
|
|
36885
|
+
const receipt = input["source_receipt"];
|
|
36886
|
+
const current = input["current_record"];
|
|
36887
|
+
if (!isRecord2(request) || !isRecord2(receipt) || !isRecord2(current)) {
|
|
36888
|
+
adoptionRejected("prior-adoption validation input records are incomplete");
|
|
36889
|
+
}
|
|
36890
|
+
const resourceKind = request["resource_kind"];
|
|
36891
|
+
const sourceOutcome = receipt["outcome"];
|
|
36892
|
+
const acceptedReceiptId = sourceOutcome === "accepted" ? receipt["receipt_id"] : sourceOutcome === "duplicate_of_accepted" ? receipt["duplicate_of_receipt_id"] : null;
|
|
36893
|
+
if (resourceKind !== "project" && resourceKind !== "task_list" || request["direction"] !== "forward" || sourceOutcome !== "accepted" && sourceOutcome !== "duplicate_of_accepted" || !isNonEmptyString(acceptedReceiptId) || !isNonEmptyString(receipt["receipt_id"]) || !isNonEmptyString(receipt["target_id"]) || !isNonEmptyString(receipt["result_revision"]) || !isNonEmptyString(receipt["result_digest"]) || !isNonEmptyString(current["id"]) || !isNonEmptyString(current["created_at"]) || !isNonEmptyString(current["updated_at"]) || receipt["authority"] !== "todos" || receipt["route"] !== request["authority_route"] || receipt["package_version"] !== request["package_version"] || receipt["authority_id"] !== request["authority_id"] || receipt["tenant_id"] !== request["tenant_id"] || receipt["corpus_id"] !== request["corpus_id"] || receipt["operation_id"] !== request["operation_id"] || receipt["step_id"] !== request["step_id"] || receipt["resource_kind"] !== resourceKind || receipt["direction"] !== "forward" || receipt["idempotency_key"] !== request["idempotency_key"] || receipt["request_digest"] !== request["request_digest"] || receipt["precondition_digest"] !== request["precondition_digest"] || receipt["accepted_receipt_id"] !== null || receipt["target_id"] !== current["id"] || receipt["result_revision"] !== current["created_at"]) {
|
|
36894
|
+
adoptionRejected("prior-adoption validation input does not carry one complete accepted receipt and current target incarnation");
|
|
36895
|
+
}
|
|
36896
|
+
if (sourceOutcome === "accepted" && receipt["duplicate_of_receipt_id"] !== null || sourceOutcome === "duplicate_of_accepted" && receipt["duplicate_of_receipt_id"] !== acceptedReceiptId) {
|
|
36897
|
+
adoptionRejected("prior-adoption validation source receipt lineage is incomplete");
|
|
36898
|
+
}
|
|
36899
|
+
if (!isRecord2(value) || !hasExactKeys(value, ["validation"]) || !isRecord2(value["validation"]) || !hasExactKeys(value["validation"], VALIDATION_KEYS)) {
|
|
36900
|
+
adoptionRejected("prior-adoption validation response envelope is incomplete");
|
|
36901
|
+
}
|
|
36902
|
+
const validation = value["validation"];
|
|
36903
|
+
if (validation["valid"] !== true || validation["resource_kind"] !== resourceKind || validation["target_id"] !== current["id"] || validation["source_receipt_id"] !== receipt["receipt_id"] || validation["accepted_receipt_id"] !== acceptedReceiptId || validation["source_outcome"] !== sourceOutcome || validation["created_at"] !== current["created_at"] || validation["current_revision"] !== current["updated_at"] || validation["accepted_result_digest"] !== receipt["result_digest"]) {
|
|
36904
|
+
adoptionRejected("prior-adoption validation response does not prove the exact accepted receipt and current target");
|
|
36905
|
+
}
|
|
36906
|
+
return validation;
|
|
36907
|
+
}
|
|
36908
|
+
|
|
36163
36909
|
// src/project-registration/http.ts
|
|
36164
36910
|
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
36165
36911
|
function json(body, status = 200) {
|
|
@@ -36206,6 +36952,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
36206
36952
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
36207
36953
|
return json({ capability: await authority.capability() });
|
|
36208
36954
|
}
|
|
36955
|
+
if (action === "resources" && method === "GET") {
|
|
36956
|
+
const sourceProjectId = url.searchParams.get("source_project_id");
|
|
36957
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
36958
|
+
const includeAnchorsRaw = url.searchParams.get("include_anchors");
|
|
36959
|
+
const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
|
|
36960
|
+
return json({
|
|
36961
|
+
page: await authority.listProjectResources({
|
|
36962
|
+
source_project_id: sourceProjectId,
|
|
36963
|
+
limit,
|
|
36964
|
+
include_anchors: includeAnchors,
|
|
36965
|
+
cursor: url.searchParams.get("cursor") ?? undefined
|
|
36966
|
+
})
|
|
36967
|
+
});
|
|
36968
|
+
}
|
|
36209
36969
|
if (method !== "POST")
|
|
36210
36970
|
return json({ error: "method not allowed" }, 405);
|
|
36211
36971
|
const body = await readJson(req);
|
|
@@ -36228,6 +36988,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
36228
36988
|
record: await authority.readExact(body)
|
|
36229
36989
|
});
|
|
36230
36990
|
}
|
|
36991
|
+
if (action === "validate-prior-adoption") {
|
|
36992
|
+
const input = body;
|
|
36993
|
+
return json({
|
|
36994
|
+
validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
|
|
36995
|
+
});
|
|
36996
|
+
}
|
|
36231
36997
|
if (action === "compensate") {
|
|
36232
36998
|
return json({
|
|
36233
36999
|
receipt: await authority.compensate(body)
|
|
@@ -36302,6 +37068,28 @@ class TodosProjectRegistrationHttpClient {
|
|
|
36302
37068
|
async lookupReceipt(request) {
|
|
36303
37069
|
return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
|
|
36304
37070
|
}
|
|
37071
|
+
async listProjectResources(request) {
|
|
37072
|
+
const query = new URLSearchParams({
|
|
37073
|
+
source_project_id: request.source_project_id,
|
|
37074
|
+
limit: String(request.limit),
|
|
37075
|
+
include_anchors: String(request.include_anchors === true),
|
|
37076
|
+
...request.cursor ? { cursor: request.cursor } : {}
|
|
37077
|
+
});
|
|
37078
|
+
const body = await this.request(`/resources?${query.toString()}`);
|
|
37079
|
+
return body.page;
|
|
37080
|
+
}
|
|
37081
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
37082
|
+
const input = {
|
|
37083
|
+
source_request: withoutTarget(sourceRequest),
|
|
37084
|
+
source_receipt: sourceReceipt,
|
|
37085
|
+
current_record: currentRecord
|
|
37086
|
+
};
|
|
37087
|
+
const body = await this.request("/validate-prior-adoption", {
|
|
37088
|
+
method: "POST",
|
|
37089
|
+
body: JSON.stringify(input)
|
|
37090
|
+
});
|
|
37091
|
+
return assertTodosPriorRegistrationAdoptionValidationEnvelope(body, input);
|
|
37092
|
+
}
|
|
36305
37093
|
async compensate(request) {
|
|
36306
37094
|
const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
36307
37095
|
return body.receipt;
|
|
@@ -40295,7 +41083,9 @@ init_types();
|
|
|
40295
41083
|
|
|
40296
41084
|
// src/task-manifest/types.ts
|
|
40297
41085
|
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
|
|
41086
|
+
var TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1";
|
|
40298
41087
|
var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
|
|
41088
|
+
var TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1";
|
|
40299
41089
|
function supportsIdempotentOutboxDelivery(capability2) {
|
|
40300
41090
|
return capability2 !== null && typeof capability2 === "object" && capability2["idempotent_outbox_delivery"] === true;
|
|
40301
41091
|
}
|
|
@@ -40325,6 +41115,8 @@ var TODOS_TASK_MANIFEST_BOUNDS = {
|
|
|
40325
41115
|
};
|
|
40326
41116
|
var key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
40327
41117
|
var identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
41118
|
+
var digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
|
|
41119
|
+
var idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
|
|
40328
41120
|
var uuid2 = exports_external.string().uuid();
|
|
40329
41121
|
var scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
40330
41122
|
var boundedScalarRecord = (limit, field2) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
@@ -40367,7 +41159,9 @@ var effect = exports_external.object({
|
|
|
40367
41159
|
var schema = exports_external.object({
|
|
40368
41160
|
version: exports_external.literal(1),
|
|
40369
41161
|
operation_id: identifier,
|
|
40370
|
-
|
|
41162
|
+
step_id: identifier,
|
|
41163
|
+
idempotency_key: idempotencyKey,
|
|
41164
|
+
precondition_digest: digest,
|
|
40371
41165
|
project_id: uuid2,
|
|
40372
41166
|
task_list_id: uuid2.optional(),
|
|
40373
41167
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -40383,7 +41177,10 @@ var schema = exports_external.object({
|
|
|
40383
41177
|
}).strict();
|
|
40384
41178
|
var compensationSchema = exports_external.object({
|
|
40385
41179
|
receipt_id: uuid2,
|
|
40386
|
-
|
|
41180
|
+
operation_id: identifier,
|
|
41181
|
+
step_id: identifier,
|
|
41182
|
+
idempotency_key: idempotencyKey,
|
|
41183
|
+
precondition_digest: digest,
|
|
40387
41184
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
40388
41185
|
}).strict();
|
|
40389
41186
|
var bindingLookupSchema = exports_external.object({
|
|
@@ -40468,10 +41265,31 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
40468
41265
|
}
|
|
40469
41266
|
|
|
40470
41267
|
// src/task-manifest/plan-slug.ts
|
|
41268
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
40471
41269
|
function taskManifestPlanSlug(manifest, planId) {
|
|
40472
41270
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
40473
41271
|
return `${base}-${planId}`;
|
|
40474
41272
|
}
|
|
41273
|
+
function sqliteLegacyTaskManifestPlanSlug(rows, planId, targetBase) {
|
|
41274
|
+
const target = rows.find((row) => row.id === planId);
|
|
41275
|
+
if (!target)
|
|
41276
|
+
return null;
|
|
41277
|
+
const used = new Set;
|
|
41278
|
+
const ordered = [...rows].filter((row) => row.project_id === target.project_id).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
|
|
41279
|
+
for (const row of ordered) {
|
|
41280
|
+
const base = row.id === planId ? normalizeSlug(targetBase ?? row.name) || "plan" : normalizeSlug(row.slug || row.name) || "plan";
|
|
41281
|
+
let candidate = base;
|
|
41282
|
+
let suffix = 2;
|
|
41283
|
+
while (used.has(candidate)) {
|
|
41284
|
+
candidate = `${base}-${suffix}`;
|
|
41285
|
+
suffix += 1;
|
|
41286
|
+
}
|
|
41287
|
+
if (row.id === planId)
|
|
41288
|
+
return candidate;
|
|
41289
|
+
used.add(candidate);
|
|
41290
|
+
}
|
|
41291
|
+
return null;
|
|
41292
|
+
}
|
|
40475
41293
|
|
|
40476
41294
|
// src/task-manifest/backend.ts
|
|
40477
41295
|
var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -40485,11 +41303,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
40485
41303
|
const row = rows[0];
|
|
40486
41304
|
const bindingVersion = Number(row.binding_version);
|
|
40487
41305
|
const state = row.state;
|
|
40488
|
-
if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
|
|
41306
|
+
if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || row.binding_step_id !== row.receipt_step_id || typeof row.binding_operation_id !== "string" || typeof row.binding_step_id !== "string" || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
|
|
40489
41307
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
40490
41308
|
}
|
|
40491
41309
|
return {
|
|
40492
41310
|
plan_id: planId,
|
|
41311
|
+
operation_id: row.binding_operation_id,
|
|
41312
|
+
step_id: row.binding_step_id,
|
|
40493
41313
|
apply_receipt_id: row.apply_receipt_id,
|
|
40494
41314
|
binding_version: bindingVersion,
|
|
40495
41315
|
state
|
|
@@ -40597,9 +41417,15 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40597
41417
|
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
40598
41418
|
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
40599
41419
|
operation_id TEXT NOT NULL,
|
|
41420
|
+
step_id TEXT NOT NULL,
|
|
40600
41421
|
idempotency_key TEXT NOT NULL,
|
|
40601
41422
|
request_digest TEXT NOT NULL,
|
|
41423
|
+
precondition_digest TEXT NOT NULL,
|
|
40602
41424
|
result_digest TEXT NOT NULL,
|
|
41425
|
+
slug_provenance TEXT,
|
|
41426
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
41427
|
+
reason TEXT,
|
|
41428
|
+
duplicate_of_receipt_id TEXT,
|
|
40603
41429
|
binding_version INTEGER NOT NULL,
|
|
40604
41430
|
apply_receipt_id TEXT,
|
|
40605
41431
|
manifest_json TEXT,
|
|
@@ -40610,9 +41436,13 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40610
41436
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
40611
41437
|
operation_id TEXT PRIMARY KEY,
|
|
40612
41438
|
tenant_id TEXT NOT NULL,
|
|
41439
|
+
step_id TEXT NOT NULL,
|
|
40613
41440
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
40614
41441
|
request_digest TEXT NOT NULL,
|
|
41442
|
+
precondition_digest TEXT NOT NULL,
|
|
40615
41443
|
result_digest TEXT NOT NULL,
|
|
41444
|
+
slug_provenance TEXT,
|
|
41445
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
40616
41446
|
apply_receipt_id TEXT NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
40617
41447
|
manifest_json TEXT NOT NULL,
|
|
40618
41448
|
result_json TEXT NOT NULL,
|
|
@@ -40633,6 +41463,27 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40633
41463
|
created_at TEXT NOT NULL,
|
|
40634
41464
|
delivered_at TEXT
|
|
40635
41465
|
);
|
|
41466
|
+
CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
41467
|
+
receipt_id TEXT PRIMARY KEY,
|
|
41468
|
+
tenant_id TEXT NOT NULL,
|
|
41469
|
+
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
41470
|
+
route TEXT NOT NULL,
|
|
41471
|
+
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
41472
|
+
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
41473
|
+
operation_id TEXT NOT NULL,
|
|
41474
|
+
step_id TEXT NOT NULL,
|
|
41475
|
+
idempotency_key TEXT NOT NULL,
|
|
41476
|
+
request_digest TEXT NOT NULL,
|
|
41477
|
+
precondition_digest TEXT NOT NULL,
|
|
41478
|
+
result_digest TEXT NOT NULL,
|
|
41479
|
+
outcome TEXT NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
41480
|
+
reason TEXT NOT NULL,
|
|
41481
|
+
binding_version INTEGER NOT NULL,
|
|
41482
|
+
apply_receipt_id TEXT,
|
|
41483
|
+
manifest_json TEXT,
|
|
41484
|
+
result_json TEXT NOT NULL,
|
|
41485
|
+
created_at TEXT NOT NULL
|
|
41486
|
+
);
|
|
40636
41487
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_outbox_receipt
|
|
40637
41488
|
ON todos_task_manifest_outbox(apply_receipt_id, status);
|
|
40638
41489
|
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_receipts_immutable_update
|
|
@@ -40643,6 +41494,14 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40643
41494
|
BEFORE DELETE ON todos_task_manifest_receipts BEGIN
|
|
40644
41495
|
SELECT RAISE(ABORT, 'todos task manifest receipts are immutable');
|
|
40645
41496
|
END;
|
|
41497
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_update
|
|
41498
|
+
BEFORE UPDATE ON todos_task_manifest_terminal_receipts BEGIN
|
|
41499
|
+
SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
|
|
41500
|
+
END;
|
|
41501
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_delete
|
|
41502
|
+
BEFORE DELETE ON todos_task_manifest_terminal_receipts BEGIN
|
|
41503
|
+
SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
|
|
41504
|
+
END;
|
|
40646
41505
|
`;
|
|
40647
41506
|
}
|
|
40648
41507
|
function sqliteTableHasColumn(db, tableName, columnName) {
|
|
@@ -40659,6 +41518,24 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
40659
41518
|
if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
|
|
40660
41519
|
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
|
|
40661
41520
|
}
|
|
41521
|
+
if (!sqliteTableHasColumn(db, tableName, "slug_provenance")) {
|
|
41522
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN slug_provenance TEXT`);
|
|
41523
|
+
}
|
|
41524
|
+
}
|
|
41525
|
+
const defaults = [
|
|
41526
|
+
["todos_task_manifest_receipts", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
|
|
41527
|
+
["todos_task_manifest_receipts", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
|
|
41528
|
+
["todos_task_manifest_receipts", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"],
|
|
41529
|
+
["todos_task_manifest_receipts", "reason", "TEXT"],
|
|
41530
|
+
["todos_task_manifest_receipts", "duplicate_of_receipt_id", "TEXT"],
|
|
41531
|
+
["todos_task_manifest_bindings", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
|
|
41532
|
+
["todos_task_manifest_bindings", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
|
|
41533
|
+
["todos_task_manifest_bindings", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"]
|
|
41534
|
+
];
|
|
41535
|
+
for (const [tableName, columnName, definition] of defaults) {
|
|
41536
|
+
if (!sqliteTableHasColumn(db, tableName, columnName)) {
|
|
41537
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${definition}`);
|
|
41538
|
+
}
|
|
40662
41539
|
}
|
|
40663
41540
|
db.exec(`
|
|
40664
41541
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
|
|
@@ -40668,6 +41545,12 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
40668
41545
|
tenant_id,
|
|
40669
41546
|
json_extract(result_json, '$.graph.plan_id')
|
|
40670
41547
|
);
|
|
41548
|
+
DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_lookup;
|
|
41549
|
+
DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_identity;
|
|
41550
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_lookup
|
|
41551
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
|
|
41552
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_identity
|
|
41553
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
|
|
40671
41554
|
`);
|
|
40672
41555
|
}
|
|
40673
41556
|
function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
@@ -40681,9 +41564,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40681
41564
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
40682
41565
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
40683
41566
|
operation_id text NOT NULL,
|
|
41567
|
+
step_id text NOT NULL,
|
|
40684
41568
|
idempotency_key text NOT NULL,
|
|
40685
41569
|
request_digest text NOT NULL,
|
|
41570
|
+
precondition_digest text NOT NULL,
|
|
40686
41571
|
result_digest text NOT NULL,
|
|
41572
|
+
slug_provenance text,
|
|
41573
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
41574
|
+
reason text,
|
|
41575
|
+
duplicate_of_receipt_id text,
|
|
40687
41576
|
binding_version integer NOT NULL,
|
|
40688
41577
|
apply_receipt_id text,
|
|
40689
41578
|
manifest_json jsonb,
|
|
@@ -40695,12 +41584,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40695
41584
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
40696
41585
|
`ALTER TABLE todos_task_manifest_receipts
|
|
40697
41586
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
41587
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41588
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
41589
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41590
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
41591
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41592
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
41593
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41594
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
41595
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41596
|
+
ADD COLUMN IF NOT EXISTS reason text`,
|
|
41597
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41598
|
+
ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
|
|
40698
41599
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
40699
41600
|
operation_id text PRIMARY KEY,
|
|
40700
41601
|
tenant_id text NOT NULL,
|
|
41602
|
+
step_id text NOT NULL,
|
|
40701
41603
|
idempotency_key text NOT NULL UNIQUE,
|
|
40702
41604
|
request_digest text NOT NULL,
|
|
41605
|
+
precondition_digest text NOT NULL,
|
|
40703
41606
|
result_digest text NOT NULL,
|
|
41607
|
+
slug_provenance text,
|
|
41608
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
40704
41609
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
40705
41610
|
manifest_json jsonb NOT NULL,
|
|
40706
41611
|
result_json jsonb NOT NULL,
|
|
@@ -40714,6 +41619,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40714
41619
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
40715
41620
|
`ALTER TABLE todos_task_manifest_bindings
|
|
40716
41621
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
41622
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41623
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
41624
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41625
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
41626
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41627
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
41628
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41629
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
40717
41630
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
40718
41631
|
id text PRIMARY KEY,
|
|
40719
41632
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -40725,10 +41638,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40725
41638
|
created_at timestamptz NOT NULL,
|
|
40726
41639
|
delivered_at timestamptz
|
|
40727
41640
|
)`,
|
|
41641
|
+
`CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
41642
|
+
receipt_id text PRIMARY KEY,
|
|
41643
|
+
tenant_id text NOT NULL,
|
|
41644
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
41645
|
+
route text NOT NULL,
|
|
41646
|
+
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
41647
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
41648
|
+
operation_id text NOT NULL,
|
|
41649
|
+
step_id text NOT NULL,
|
|
41650
|
+
idempotency_key text NOT NULL,
|
|
41651
|
+
request_digest text NOT NULL,
|
|
41652
|
+
precondition_digest text NOT NULL,
|
|
41653
|
+
result_digest text NOT NULL,
|
|
41654
|
+
outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
41655
|
+
reason text NOT NULL,
|
|
41656
|
+
binding_version integer NOT NULL,
|
|
41657
|
+
apply_receipt_id text,
|
|
41658
|
+
manifest_json jsonb,
|
|
41659
|
+
result_json jsonb NOT NULL,
|
|
41660
|
+
created_at timestamptz NOT NULL
|
|
41661
|
+
)`,
|
|
40728
41662
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
40729
41663
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
40730
41664
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
40731
41665
|
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
41666
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
|
|
41667
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
|
|
41668
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
|
|
41669
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
41670
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
|
|
41671
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
40732
41672
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
40733
41673
|
ON todos_task_manifest_bindings(
|
|
40734
41674
|
tenant_id,
|
|
@@ -40741,6 +41681,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40741
41681
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
40742
41682
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
40743
41683
|
BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
|
|
41684
|
+
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
|
|
41685
|
+
`DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
|
|
41686
|
+
`CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
|
|
41687
|
+
BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
|
|
40744
41688
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
40745
41689
|
];
|
|
40746
41690
|
}
|
|
@@ -40752,7 +41696,76 @@ function fault(faults, point) {
|
|
|
40752
41696
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
40753
41697
|
}
|
|
40754
41698
|
function parseApplyResult(value, duplicate) {
|
|
40755
|
-
|
|
41699
|
+
const parsed = JSON.parse(value);
|
|
41700
|
+
return {
|
|
41701
|
+
...parsed,
|
|
41702
|
+
duplicate,
|
|
41703
|
+
receipt: {
|
|
41704
|
+
...parsed.receipt,
|
|
41705
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
41706
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
41707
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
41708
|
+
reason: parsed.receipt.reason ?? null,
|
|
41709
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
41710
|
+
}
|
|
41711
|
+
};
|
|
41712
|
+
}
|
|
41713
|
+
function terminalApplyResult(input, reason) {
|
|
41714
|
+
const receipt = {
|
|
41715
|
+
receipt_id: input.terminal_receipt_id,
|
|
41716
|
+
authority: "todos",
|
|
41717
|
+
route: "todos.task-manifest.v1",
|
|
41718
|
+
schema_version: 1,
|
|
41719
|
+
kind: "apply",
|
|
41720
|
+
operation_id: input.manifest.operation_id,
|
|
41721
|
+
step_id: input.manifest.step_id,
|
|
41722
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
41723
|
+
request_digest: input.request_digest,
|
|
41724
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
41725
|
+
result_digest: canonicalDigest({
|
|
41726
|
+
outcome: "terminal_nonacceptance",
|
|
41727
|
+
reason,
|
|
41728
|
+
operation_id: input.manifest.operation_id,
|
|
41729
|
+
step_id: input.manifest.step_id,
|
|
41730
|
+
request_digest: input.request_digest
|
|
41731
|
+
}),
|
|
41732
|
+
outcome: "terminal_nonacceptance",
|
|
41733
|
+
reason,
|
|
41734
|
+
duplicate_of_receipt_id: null,
|
|
41735
|
+
binding_version: 0,
|
|
41736
|
+
apply_receipt_id: null,
|
|
41737
|
+
created_at: input.now
|
|
41738
|
+
};
|
|
41739
|
+
return {
|
|
41740
|
+
duplicate: false,
|
|
41741
|
+
receipt,
|
|
41742
|
+
graph: input.graph,
|
|
41743
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
41744
|
+
outbox_ids: [],
|
|
41745
|
+
result_digest: receipt.result_digest
|
|
41746
|
+
};
|
|
41747
|
+
}
|
|
41748
|
+
function validateCompensationPlanSlug(db, manifest, planId, slugProvenance) {
|
|
41749
|
+
const plan = db.query("SELECT id, project_id, name, slug, created_at FROM plans WHERE id = ? LIMIT 1").get(planId);
|
|
41750
|
+
if (!plan) {
|
|
41751
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
41752
|
+
}
|
|
41753
|
+
if (slugProvenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
41754
|
+
if (plan.slug !== taskManifestPlanSlug(manifest, planId)) {
|
|
41755
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
41756
|
+
}
|
|
41757
|
+
return;
|
|
41758
|
+
}
|
|
41759
|
+
if (slugProvenance !== null && slugProvenance !== undefined) {
|
|
41760
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
41761
|
+
}
|
|
41762
|
+
if (plan.slug === null)
|
|
41763
|
+
return;
|
|
41764
|
+
const rows = db.query("SELECT id, project_id, name, slug, created_at FROM plans WHERE project_id IS ? ORDER BY created_at ASC, id ASC").all(plan.project_id);
|
|
41765
|
+
const expected = sqliteLegacyTaskManifestPlanSlug(rows, planId, manifest.plan.key || manifest.plan.name);
|
|
41766
|
+
if (expected === null || plan.slug !== expected) {
|
|
41767
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy plan slug was not produced by SQLite allocation");
|
|
41768
|
+
}
|
|
40756
41769
|
}
|
|
40757
41770
|
|
|
40758
41771
|
class SqliteTodosTaskManifestBackend {
|
|
@@ -40792,33 +41805,49 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40792
41805
|
async apply(input, faults) {
|
|
40793
41806
|
return this.serialized(() => {
|
|
40794
41807
|
const { manifest } = input;
|
|
41808
|
+
const terminal = this.db.query(`SELECT result_json
|
|
41809
|
+
FROM todos_task_manifest_terminal_receipts
|
|
41810
|
+
WHERE tenant_id = ?
|
|
41811
|
+
AND kind = 'apply'
|
|
41812
|
+
AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
|
|
41813
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
41814
|
+
LIMIT 1`).get(this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id);
|
|
41815
|
+
if (terminal)
|
|
41816
|
+
return parseApplyResult(terminal.result_json, true);
|
|
40795
41817
|
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = ? AND operation_id = ? LIMIT 1").get(this.tenantId, manifest.operation_id);
|
|
40796
41818
|
if (binding) {
|
|
40797
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
40798
|
-
|
|
41819
|
+
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest || binding["step_id"] !== manifest.step_id || binding["precondition_digest"] !== manifest.precondition_digest) {
|
|
41820
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
41821
|
+
}
|
|
41822
|
+
if (binding["outcome"] === "terminal_nonacceptance") {
|
|
41823
|
+
const terminalResult = this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41824
|
+
return { ...terminalResult, duplicate: true };
|
|
40799
41825
|
}
|
|
40800
41826
|
if (binding["state"] !== "applied") {
|
|
40801
|
-
|
|
41827
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
40802
41828
|
}
|
|
40803
41829
|
return parseApplyResult(String(binding["result_json"]), true);
|
|
40804
41830
|
}
|
|
40805
41831
|
const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE tenant_id = ? AND idempotency_key = ? LIMIT 1").get(this.tenantId, manifest.idempotency_key);
|
|
40806
41832
|
if (idempotency)
|
|
40807
|
-
|
|
41833
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
41834
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
41835
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
41836
|
+
}
|
|
40808
41837
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
40809
|
-
|
|
41838
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
40810
41839
|
}
|
|
40811
41840
|
if (!this.db.query("SELECT 1 AS found FROM projects WHERE id = ? LIMIT 1").get(manifest.project_id)) {
|
|
40812
|
-
|
|
41841
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
40813
41842
|
}
|
|
40814
41843
|
if (manifest.task_list_id && !this.db.query("SELECT 1 AS found FROM task_lists WHERE id = ? AND project_id = ? LIMIT 1").get(manifest.task_list_id, manifest.project_id)) {
|
|
40815
|
-
|
|
41844
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
40816
41845
|
}
|
|
40817
41846
|
const allIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids];
|
|
40818
41847
|
for (const id of allIds) {
|
|
40819
41848
|
for (const table of ["plans", "tasks", "task_comments", "task_verifications"]) {
|
|
40820
41849
|
if (this.db.query(`SELECT 1 AS found FROM ${table} WHERE id = ? LIMIT 1`).get(id)) {
|
|
40821
|
-
|
|
41850
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
40822
41851
|
}
|
|
40823
41852
|
}
|
|
40824
41853
|
}
|
|
@@ -40874,9 +41903,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40874
41903
|
schema_version: 1,
|
|
40875
41904
|
kind: "apply",
|
|
40876
41905
|
operation_id: manifest.operation_id,
|
|
41906
|
+
step_id: manifest.step_id,
|
|
40877
41907
|
idempotency_key: manifest.idempotency_key,
|
|
40878
41908
|
request_digest: input.request_digest,
|
|
41909
|
+
precondition_digest: manifest.precondition_digest,
|
|
40879
41910
|
result_digest: input.result_digest,
|
|
41911
|
+
outcome: "accepted",
|
|
41912
|
+
reason: null,
|
|
41913
|
+
duplicate_of_receipt_id: null,
|
|
40880
41914
|
binding_version: 1,
|
|
40881
41915
|
apply_receipt_id: null,
|
|
40882
41916
|
created_at: input.now
|
|
@@ -40892,9 +41926,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40892
41926
|
const resultJson = canonicalJson(result);
|
|
40893
41927
|
const manifestJson = canonicalJson(manifest);
|
|
40894
41928
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40895
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40896
|
-
request_digest,
|
|
40897
|
-
|
|
41929
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
41930
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
41931
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
41932
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, ?, ?, ?, 'accepted', NULL, NULL, 1, NULL, ?, ?, ?)`).run(input.receipt_id, this.tenantId, manifest.operation_id, manifest.step_id, manifest.idempotency_key, input.request_digest, manifest.precondition_digest, input.result_digest, TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE, manifestJson, resultJson, input.now);
|
|
40898
41933
|
for (const entry2 of input.outbox) {
|
|
40899
41934
|
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
40900
41935
|
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
@@ -40902,18 +41937,37 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40902
41937
|
}
|
|
40903
41938
|
fault(faults, "after_outbox_write");
|
|
40904
41939
|
this.db.query(`INSERT INTO todos_task_manifest_bindings (
|
|
40905
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
40906
|
-
manifest_json, result_json, state, version, created_at, updated_at
|
|
40907
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.idempotency_key, input.request_digest, input.result_digest, input.receipt_id, manifestJson, resultJson, input.now, input.now);
|
|
41940
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest, result_digest,
|
|
41941
|
+
slug_provenance, outcome, apply_receipt_id, manifest_json, result_json, state, version, created_at, updated_at
|
|
41942
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.step_id, manifest.idempotency_key, input.request_digest, manifest.precondition_digest, input.result_digest, TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE, input.receipt_id, manifestJson, resultJson, input.now, input.now);
|
|
40908
41943
|
fault(faults, "after_receipt_write");
|
|
40909
41944
|
return result;
|
|
40910
41945
|
});
|
|
40911
41946
|
}
|
|
41947
|
+
persistTerminal(input, reason) {
|
|
41948
|
+
const result = terminalApplyResult(input, reason);
|
|
41949
|
+
const resultJson = canonicalJson(result);
|
|
41950
|
+
this.db.query(`INSERT OR IGNORE INTO todos_task_manifest_terminal_receipts (
|
|
41951
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
41952
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
41953
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
41954
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, ?, ?, 'terminal_nonacceptance', ?, 0, NULL, ?, ?, ?)`).run(result.receipt.receipt_id, this.tenantId, input.manifest.operation_id, input.manifest.step_id, input.manifest.idempotency_key, input.request_digest, input.manifest.precondition_digest, result.receipt.result_digest, reason, canonicalJson(input.manifest), resultJson, input.now);
|
|
41955
|
+
const stored = this.db.query(`SELECT receipt_id, result_json
|
|
41956
|
+
FROM todos_task_manifest_terminal_receipts
|
|
41957
|
+
WHERE tenant_id = ? AND kind = 'apply'
|
|
41958
|
+
AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
|
|
41959
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
41960
|
+
LIMIT 1`).get(this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id);
|
|
41961
|
+
return stored ? parseApplyResult(stored.result_json, stored.receipt_id !== result.receipt.receipt_id) : result;
|
|
41962
|
+
}
|
|
40912
41963
|
async readExact(receiptId2) {
|
|
40913
41964
|
const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, receiptId2);
|
|
40914
|
-
if (
|
|
41965
|
+
if (row)
|
|
41966
|
+
return parseApplyResult(row.result_json, false);
|
|
41967
|
+
const terminal = this.db.query("SELECT result_json FROM todos_task_manifest_terminal_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, receiptId2);
|
|
41968
|
+
if (!terminal)
|
|
40915
41969
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
40916
|
-
return parseApplyResult(
|
|
41970
|
+
return parseApplyResult(terminal.result_json, false);
|
|
40917
41971
|
}
|
|
40918
41972
|
async lookupBindingByPlanId(planId) {
|
|
40919
41973
|
const rows = this.db.query(`
|
|
@@ -40923,6 +41977,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40923
41977
|
b.version AS binding_version,
|
|
40924
41978
|
b.tenant_id AS binding_tenant_id,
|
|
40925
41979
|
b.operation_id AS binding_operation_id,
|
|
41980
|
+
b.step_id AS binding_step_id,
|
|
40926
41981
|
json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
|
|
40927
41982
|
r.tenant_id AS receipt_tenant_id,
|
|
40928
41983
|
r.authority AS receipt_authority,
|
|
@@ -40930,6 +41985,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40930
41985
|
r.schema_version AS receipt_schema_version,
|
|
40931
41986
|
r.kind AS receipt_kind,
|
|
40932
41987
|
r.operation_id AS receipt_operation_id,
|
|
41988
|
+
r.step_id AS receipt_step_id,
|
|
40933
41989
|
json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
|
|
40934
41990
|
FROM todos_task_manifest_bindings b
|
|
40935
41991
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -40992,6 +42048,12 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40992
42048
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
40993
42049
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
40994
42050
|
}
|
|
42051
|
+
const storedStepId = String(row["step_id"] ?? "legacy-apply");
|
|
42052
|
+
const storedRequestDigest = String(row["request_digest"]);
|
|
42053
|
+
const storedPreconditionDigest = String(row["precondition_digest"] ?? "0".repeat(64));
|
|
42054
|
+
if (String(row["receipt_id"]) !== input.receipt_id || String(row["operation_id"]) !== input.operation_id || String(binding["operation_id"]) !== String(row["operation_id"]) || String(binding["step_id"] ?? "legacy-apply") !== storedStepId || String(binding["idempotency_key"]) !== String(row["idempotency_key"]) || String(binding["request_digest"]) !== storedRequestDigest || String(binding["precondition_digest"] ?? "0".repeat(64)) !== storedPreconditionDigest || String(binding["apply_receipt_id"]) !== input.receipt_id || binding["slug_provenance"] !== row["slug_provenance"]) {
|
|
42055
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
42056
|
+
}
|
|
40995
42057
|
if (binding["state"] !== "applied")
|
|
40996
42058
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
40997
42059
|
const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -41002,10 +42064,16 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41002
42064
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
41003
42065
|
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
41004
42066
|
const manifest = JSON.parse(String(row["manifest_json"]));
|
|
42067
|
+
const manifestRecord = manifest;
|
|
42068
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
41005
42069
|
const expectedEffects = [
|
|
41006
42070
|
{
|
|
41007
42071
|
topic: "todos.task-manifest.applied",
|
|
41008
|
-
payload: {
|
|
42072
|
+
payload: {
|
|
42073
|
+
operation_id: manifest.operation_id,
|
|
42074
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
42075
|
+
project_id: manifest.project_id
|
|
42076
|
+
}
|
|
41009
42077
|
},
|
|
41010
42078
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
41011
42079
|
];
|
|
@@ -41045,8 +42113,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41045
42113
|
if (canonicalJson(actualReadback) !== canonicalJson(applyResult.readback)) {
|
|
41046
42114
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: graph changed since apply", { actualReadback });
|
|
41047
42115
|
}
|
|
41048
|
-
const
|
|
41049
|
-
|
|
42116
|
+
const slugProvenance = row["slug_provenance"];
|
|
42117
|
+
validateCompensationPlanSlug(this.db, manifest, applyResult.graph.plan_id, slugProvenance);
|
|
42118
|
+
const plan = this.db.query("SELECT project_id, name, description, status, task_list_id FROM plans WHERE id = ? LIMIT 1").get(applyResult.graph.plan_id);
|
|
42119
|
+
if (!plan || plan["project_id"] !== manifest.project_id || plan["name"] !== manifest.plan.name || plan["description"] !== (manifest.plan.description ?? null) || plan["status"] !== (manifest.plan.status ?? "active") || plan["task_list_id"] !== (manifest.task_list_id ?? null)) {
|
|
41050
42120
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
41051
42121
|
}
|
|
41052
42122
|
for (const task3 of manifest.tasks) {
|
|
@@ -41123,9 +42193,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41123
42193
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
41124
42194
|
const resultJson = canonicalJson(result);
|
|
41125
42195
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
41126
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
41127
|
-
request_digest,
|
|
41128
|
-
|
|
42196
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
42197
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
42198
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42199
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, 'accepted', NULL, NULL, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, this.tenantId, receipt.operation_id, receipt.step_id, input.idempotency_key, requestDigest, input.precondition_digest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson, now4);
|
|
41129
42200
|
const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
|
|
41130
42201
|
WHERE tenant_id = ? AND operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now4, this.tenantId, receipt.operation_id, input.if_binding_version);
|
|
41131
42202
|
if (updated.changes !== 1) {
|
|
@@ -41159,6 +42230,39 @@ function safeIdentifier2(value, field2) {
|
|
|
41159
42230
|
function parseJson2(value) {
|
|
41160
42231
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
41161
42232
|
}
|
|
42233
|
+
function parseApplyResult2(value, duplicate) {
|
|
42234
|
+
const parsed = parseJson2(value);
|
|
42235
|
+
return {
|
|
42236
|
+
...parsed,
|
|
42237
|
+
duplicate,
|
|
42238
|
+
receipt: {
|
|
42239
|
+
...parsed.receipt,
|
|
42240
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
42241
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
42242
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
42243
|
+
reason: parsed.receipt.reason ?? null,
|
|
42244
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
42245
|
+
}
|
|
42246
|
+
};
|
|
42247
|
+
}
|
|
42248
|
+
function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
|
|
42249
|
+
if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
42250
|
+
const expected = taskManifestPlanSlug(manifest, planId);
|
|
42251
|
+
if (slug !== expected) {
|
|
42252
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
|
|
42253
|
+
}
|
|
42254
|
+
return expected;
|
|
42255
|
+
}
|
|
42256
|
+
if (provenance !== null && provenance !== undefined) {
|
|
42257
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
42258
|
+
}
|
|
42259
|
+
if (slug === null || slug === undefined)
|
|
42260
|
+
return null;
|
|
42261
|
+
if (slug !== null && slug !== undefined) {
|
|
42262
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
|
|
42263
|
+
}
|
|
42264
|
+
return null;
|
|
42265
|
+
}
|
|
41162
42266
|
function timestamp3(value) {
|
|
41163
42267
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
41164
42268
|
}
|
|
@@ -41166,6 +42270,41 @@ function fault2(faults, point) {
|
|
|
41166
42270
|
if (faults.points.has(point))
|
|
41167
42271
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
41168
42272
|
}
|
|
42273
|
+
function terminalApplyResult2(input, reason) {
|
|
42274
|
+
const receipt = {
|
|
42275
|
+
receipt_id: input.terminal_receipt_id,
|
|
42276
|
+
authority: "todos",
|
|
42277
|
+
route: "todos.task-manifest.v1",
|
|
42278
|
+
schema_version: 1,
|
|
42279
|
+
kind: "apply",
|
|
42280
|
+
operation_id: input.manifest.operation_id,
|
|
42281
|
+
step_id: input.manifest.step_id,
|
|
42282
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
42283
|
+
request_digest: input.request_digest,
|
|
42284
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
42285
|
+
result_digest: canonicalDigest({
|
|
42286
|
+
outcome: "terminal_nonacceptance",
|
|
42287
|
+
reason,
|
|
42288
|
+
operation_id: input.manifest.operation_id,
|
|
42289
|
+
step_id: input.manifest.step_id,
|
|
42290
|
+
request_digest: input.request_digest
|
|
42291
|
+
}),
|
|
42292
|
+
outcome: "terminal_nonacceptance",
|
|
42293
|
+
reason,
|
|
42294
|
+
duplicate_of_receipt_id: null,
|
|
42295
|
+
binding_version: 0,
|
|
42296
|
+
apply_receipt_id: null,
|
|
42297
|
+
created_at: input.now
|
|
42298
|
+
};
|
|
42299
|
+
return {
|
|
42300
|
+
duplicate: false,
|
|
42301
|
+
receipt,
|
|
42302
|
+
graph: input.graph,
|
|
42303
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
42304
|
+
outbox_ids: [],
|
|
42305
|
+
result_digest: receipt.result_digest
|
|
42306
|
+
};
|
|
42307
|
+
}
|
|
41169
42308
|
function receiptFromRow3(row) {
|
|
41170
42309
|
return {
|
|
41171
42310
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -41174,9 +42313,14 @@ function receiptFromRow3(row) {
|
|
|
41174
42313
|
schema_version: 1,
|
|
41175
42314
|
kind: row["kind"],
|
|
41176
42315
|
operation_id: String(row["operation_id"]),
|
|
42316
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
41177
42317
|
idempotency_key: String(row["idempotency_key"]),
|
|
41178
42318
|
request_digest: String(row["request_digest"]),
|
|
42319
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
41179
42320
|
result_digest: String(row["result_digest"]),
|
|
42321
|
+
outcome: row["outcome"] ?? "accepted",
|
|
42322
|
+
reason: row["reason"] == null ? null : row["reason"],
|
|
42323
|
+
duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
|
|
41180
42324
|
binding_version: Number(row["binding_version"]),
|
|
41181
42325
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
41182
42326
|
created_at: timestamp3(row["created_at"])
|
|
@@ -41294,46 +42438,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41294
42438
|
now4
|
|
41295
42439
|
]);
|
|
41296
42440
|
}
|
|
42441
|
+
async persistTerminal(tx, input, reason) {
|
|
42442
|
+
const result = terminalApplyResult2(input, reason);
|
|
42443
|
+
const resultJson = canonicalJson(result);
|
|
42444
|
+
await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
|
|
42445
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
42446
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
42447
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42448
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
|
|
42449
|
+
'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
|
|
42450
|
+
ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
|
|
42451
|
+
result.receipt.receipt_id,
|
|
42452
|
+
this.tenantId,
|
|
42453
|
+
input.manifest.operation_id,
|
|
42454
|
+
input.manifest.step_id,
|
|
42455
|
+
input.manifest.idempotency_key,
|
|
42456
|
+
input.request_digest,
|
|
42457
|
+
input.manifest.precondition_digest,
|
|
42458
|
+
result.receipt.result_digest,
|
|
42459
|
+
reason,
|
|
42460
|
+
canonicalJson(input.manifest),
|
|
42461
|
+
resultJson,
|
|
42462
|
+
input.now
|
|
42463
|
+
]);
|
|
42464
|
+
const stored = await tx.query(`SELECT receipt_id, result_json
|
|
42465
|
+
FROM todos_task_manifest_terminal_receipts
|
|
42466
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
42467
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
42468
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
42469
|
+
LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
|
|
42470
|
+
return stored.rows[0] ? parseApplyResult2(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
|
|
42471
|
+
}
|
|
41297
42472
|
async apply(input, faults) {
|
|
41298
42473
|
await this.ensureSchema();
|
|
41299
42474
|
return this.client.transaction(async (tx) => {
|
|
41300
42475
|
const { manifest } = input;
|
|
41301
42476
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
41302
42477
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
42478
|
+
const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
|
|
42479
|
+
WHERE tenant_id = $1
|
|
42480
|
+
AND kind = 'apply'
|
|
42481
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
42482
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
42483
|
+
LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
|
|
42484
|
+
if (terminal.rows[0]) {
|
|
42485
|
+
return parseApplyResult2(terminal.rows[0]["result_json"], true);
|
|
42486
|
+
}
|
|
41303
42487
|
const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND operation_id = $2 LIMIT 1 FOR UPDATE", [this.tenantId, manifest.operation_id]);
|
|
41304
42488
|
if (existing.rows[0]) {
|
|
41305
42489
|
const binding = existing.rows[0];
|
|
41306
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
41307
|
-
|
|
42490
|
+
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest || binding["step_id"] !== manifest.step_id || binding["precondition_digest"] !== manifest.precondition_digest) {
|
|
42491
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
41308
42492
|
}
|
|
41309
42493
|
if (binding["state"] !== "applied") {
|
|
41310
|
-
|
|
42494
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41311
42495
|
}
|
|
41312
|
-
return
|
|
42496
|
+
return parseApplyResult2(binding["result_json"], true);
|
|
41313
42497
|
}
|
|
41314
42498
|
const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND idempotency_key = $2 LIMIT 1", [this.tenantId, manifest.idempotency_key]);
|
|
41315
42499
|
if (reused.rows[0])
|
|
41316
|
-
|
|
42500
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
42501
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
42502
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
42503
|
+
}
|
|
41317
42504
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
41318
|
-
|
|
42505
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
41319
42506
|
}
|
|
41320
42507
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
41321
42508
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
41322
42509
|
if (!project.rows[0])
|
|
41323
|
-
|
|
42510
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
41324
42511
|
if (manifest.task_list_id) {
|
|
41325
42512
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
41326
42513
|
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.task_list_id]);
|
|
41327
42514
|
const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
|
|
41328
42515
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
41329
|
-
|
|
42516
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
41330
42517
|
}
|
|
41331
42518
|
}
|
|
41332
42519
|
const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
|
|
41333
42520
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
41334
42521
|
WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
41335
42522
|
if (conflict.rows[0])
|
|
41336
|
-
|
|
42523
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41337
42524
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
41338
42525
|
fault2(faults, "after_plan_write");
|
|
41339
42526
|
for (const task3 of manifest.tasks) {
|
|
@@ -41403,9 +42590,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41403
42590
|
schema_version: 1,
|
|
41404
42591
|
kind: "apply",
|
|
41405
42592
|
operation_id: manifest.operation_id,
|
|
42593
|
+
step_id: manifest.step_id,
|
|
41406
42594
|
idempotency_key: manifest.idempotency_key,
|
|
41407
42595
|
request_digest: input.request_digest,
|
|
42596
|
+
precondition_digest: manifest.precondition_digest,
|
|
41408
42597
|
result_digest: input.result_digest,
|
|
42598
|
+
outcome: "accepted",
|
|
42599
|
+
reason: null,
|
|
42600
|
+
duplicate_of_receipt_id: null,
|
|
41409
42601
|
binding_version: 1,
|
|
41410
42602
|
apply_receipt_id: null,
|
|
41411
42603
|
created_at: input.now
|
|
@@ -41422,14 +42614,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41422
42614
|
const resultJson = canonicalJson(result);
|
|
41423
42615
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
41424
42616
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
41425
|
-
|
|
41426
|
-
|
|
42617
|
+
step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
|
|
42618
|
+
reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42619
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8, $9, 'accepted', NULL, NULL, 1, NULL, $10::jsonb, $11::jsonb, $12)`, [
|
|
41427
42620
|
input.receipt_id,
|
|
41428
42621
|
this.tenantId,
|
|
41429
42622
|
manifest.operation_id,
|
|
41430
42623
|
manifest.idempotency_key,
|
|
42624
|
+
manifest.step_id,
|
|
41431
42625
|
input.request_digest,
|
|
42626
|
+
manifest.precondition_digest,
|
|
41432
42627
|
input.result_digest,
|
|
42628
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
41433
42629
|
manifestJson,
|
|
41434
42630
|
resultJson,
|
|
41435
42631
|
input.now
|
|
@@ -41448,14 +42644,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41448
42644
|
}
|
|
41449
42645
|
fault2(faults, "after_outbox_write");
|
|
41450
42646
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
41451
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
41452
|
-
|
|
41453
|
-
|
|
42647
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
|
|
42648
|
+
result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
|
|
42649
|
+
state, version, created_at, updated_at
|
|
42650
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
|
|
41454
42651
|
manifest.operation_id,
|
|
41455
42652
|
this.tenantId,
|
|
42653
|
+
manifest.step_id,
|
|
41456
42654
|
manifest.idempotency_key,
|
|
41457
42655
|
input.request_digest,
|
|
42656
|
+
manifest.precondition_digest,
|
|
41458
42657
|
input.result_digest,
|
|
42658
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
41459
42659
|
input.receipt_id,
|
|
41460
42660
|
manifestJson,
|
|
41461
42661
|
resultJson,
|
|
@@ -41468,9 +42668,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41468
42668
|
async readExact(receiptId2) {
|
|
41469
42669
|
await this.ensureSchema();
|
|
41470
42670
|
const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
|
|
41471
|
-
if (
|
|
42671
|
+
if (result.rows[0])
|
|
42672
|
+
return parseApplyResult2(result.rows[0]["result_json"], false);
|
|
42673
|
+
const terminal = await this.client.query("SELECT result_json FROM todos_task_manifest_terminal_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
|
|
42674
|
+
if (!terminal.rows[0])
|
|
41472
42675
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
41473
|
-
return
|
|
42676
|
+
return parseApplyResult2(terminal.rows[0]["result_json"], false);
|
|
41474
42677
|
}
|
|
41475
42678
|
async lookupBindingByPlanId(planId) {
|
|
41476
42679
|
await this.ensureSchema();
|
|
@@ -41481,6 +42684,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41481
42684
|
b.version AS binding_version,
|
|
41482
42685
|
b.tenant_id AS binding_tenant_id,
|
|
41483
42686
|
b.operation_id AS binding_operation_id,
|
|
42687
|
+
b.step_id AS binding_step_id,
|
|
41484
42688
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
41485
42689
|
r.tenant_id AS receipt_tenant_id,
|
|
41486
42690
|
r.authority AS receipt_authority,
|
|
@@ -41488,6 +42692,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41488
42692
|
r.schema_version AS receipt_schema_version,
|
|
41489
42693
|
r.kind AS receipt_kind,
|
|
41490
42694
|
r.operation_id AS receipt_operation_id,
|
|
42695
|
+
r.step_id AS receipt_step_id,
|
|
41491
42696
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
41492
42697
|
FROM todos_task_manifest_bindings b
|
|
41493
42698
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -41574,6 +42779,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41574
42779
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
41575
42780
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
41576
42781
|
}
|
|
42782
|
+
const appliedReceipt = receiptFromRow3(applyRow);
|
|
42783
|
+
if (appliedReceipt.receipt_id !== input.receipt_id || appliedReceipt.operation_id !== input.operation_id || String(binding["operation_id"]) !== appliedReceipt.operation_id || String(binding["step_id"] ?? "legacy-apply") !== appliedReceipt.step_id || String(binding["idempotency_key"]) !== appliedReceipt.idempotency_key || String(binding["request_digest"]) !== appliedReceipt.request_digest || String(binding["precondition_digest"] ?? "0".repeat(64)) !== appliedReceipt.precondition_digest || String(binding["apply_receipt_id"]) !== input.receipt_id || binding["slug_provenance"] !== applyRow["slug_provenance"]) {
|
|
42784
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
42785
|
+
}
|
|
41577
42786
|
if (binding["state"] !== "applied")
|
|
41578
42787
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
41579
42788
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -41588,12 +42797,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41588
42797
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
41589
42798
|
if (delivered.rows[0])
|
|
41590
42799
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
41591
|
-
const applyResult =
|
|
42800
|
+
const applyResult = parseApplyResult2(applyRow["result_json"], false);
|
|
41592
42801
|
const manifest = parseJson2(applyRow["manifest_json"]);
|
|
42802
|
+
const manifestRecord = manifest;
|
|
42803
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
41593
42804
|
const expectedEffects = [
|
|
41594
42805
|
{
|
|
41595
42806
|
topic: "todos.task-manifest.applied",
|
|
41596
|
-
payload: {
|
|
42807
|
+
payload: {
|
|
42808
|
+
operation_id: manifest.operation_id,
|
|
42809
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
42810
|
+
project_id: manifest.project_id
|
|
42811
|
+
}
|
|
41597
42812
|
},
|
|
41598
42813
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
41599
42814
|
];
|
|
@@ -41641,9 +42856,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41641
42856
|
}
|
|
41642
42857
|
const appliedAt = receiptFromRow3(applyRow).created_at;
|
|
41643
42858
|
const expectedPayloads = new Map;
|
|
42859
|
+
const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
42860
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
42861
|
+
LIMIT 1`, [this.service, applyResult.graph.plan_id]);
|
|
42862
|
+
const actualPlan = planRow.rows[0] ? parseJson2(planRow.rows[0]["payload"]) : null;
|
|
42863
|
+
const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
|
|
42864
|
+
planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
|
|
41644
42865
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
41645
42866
|
type: "plans",
|
|
41646
|
-
payload: canonicalJson(
|
|
42867
|
+
payload: canonicalJson(planExpected)
|
|
41647
42868
|
});
|
|
41648
42869
|
for (const task3 of manifest.tasks)
|
|
41649
42870
|
expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
|
|
@@ -41743,14 +42964,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41743
42964
|
const readback = await this.readback(tx, applyResult.graph);
|
|
41744
42965
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
41745
42966
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
41746
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
41747
|
-
request_digest,
|
|
41748
|
-
|
|
42967
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
42968
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
42969
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42970
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, 'accepted', NULL, NULL, $9, $10, NULL, $11::jsonb, $12)`, [
|
|
41749
42971
|
compensationReceiptId,
|
|
41750
42972
|
this.tenantId,
|
|
41751
42973
|
receipt.operation_id,
|
|
42974
|
+
receipt.step_id,
|
|
41752
42975
|
input.idempotency_key,
|
|
41753
42976
|
requestDigest,
|
|
42977
|
+
input.precondition_digest,
|
|
41754
42978
|
receipt.result_digest,
|
|
41755
42979
|
receipt.binding_version,
|
|
41756
42980
|
input.receipt_id,
|
|
@@ -41810,36 +43034,83 @@ function resolveTenantId(value) {
|
|
|
41810
43034
|
}
|
|
41811
43035
|
return tenantId;
|
|
41812
43036
|
}
|
|
43037
|
+
function taskManifestRequestDigest(manifest) {
|
|
43038
|
+
const { idempotency_key: _idempotencyKey, ...request } = manifest;
|
|
43039
|
+
return canonicalDigest(request);
|
|
43040
|
+
}
|
|
43041
|
+
function taskManifestCompensationRequestDigest(request) {
|
|
43042
|
+
return canonicalDigest(request);
|
|
43043
|
+
}
|
|
43044
|
+
function deriveTodosTaskManifestApplyPreconditionDigest(input) {
|
|
43045
|
+
return canonicalDigest({
|
|
43046
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
43047
|
+
direction: "apply",
|
|
43048
|
+
operation_id: input.operation_id,
|
|
43049
|
+
step_id: input.step_id,
|
|
43050
|
+
project_id: input.project_id,
|
|
43051
|
+
task_list_id: input.task_list_id ?? null,
|
|
43052
|
+
expected_binding_version: input.if_binding_version ?? 0
|
|
43053
|
+
});
|
|
43054
|
+
}
|
|
43055
|
+
function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
|
|
43056
|
+
return canonicalDigest({
|
|
43057
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
43058
|
+
direction: "compensate",
|
|
43059
|
+
operation_id: input.operation_id,
|
|
43060
|
+
step_id: input.step_id,
|
|
43061
|
+
apply_receipt_id: input.receipt_id,
|
|
43062
|
+
expected_binding_version: input.if_binding_version
|
|
43063
|
+
});
|
|
43064
|
+
}
|
|
43065
|
+
function deriveTodosTaskManifestIdempotencyKey(input) {
|
|
43066
|
+
return `tmk_${canonicalDigest({
|
|
43067
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
43068
|
+
...input
|
|
43069
|
+
}).slice(0, 48)}`;
|
|
43070
|
+
}
|
|
41813
43071
|
function normalize(input, now4) {
|
|
41814
43072
|
const parsed = parseTodosTaskManifest(input);
|
|
41815
43073
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
41816
43074
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
41817
43075
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BOUNDS_EXCEEDED", `Task manifest requires ${requestBytes} bytes but the bound is ${TODOS_TASK_MANIFEST_BOUNDS.request_bytes}`, { request_bytes: requestBytes, request_byte_limit: TODOS_TASK_MANIFEST_BOUNDS.request_bytes });
|
|
41818
43076
|
}
|
|
43077
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
43078
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
41819
43079
|
const manifest = sanitizeManifest(parsed);
|
|
43080
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
|
|
43081
|
+
if (manifest.precondition_digest !== expectedPreconditionDigest) {
|
|
43082
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
43083
|
+
}
|
|
43084
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
43085
|
+
operation_id: manifest.operation_id,
|
|
43086
|
+
step_id: manifest.step_id,
|
|
43087
|
+
direction: "apply",
|
|
43088
|
+
target_selector: manifest.project_id,
|
|
43089
|
+
request_digest,
|
|
43090
|
+
precondition_digest: manifest.precondition_digest
|
|
43091
|
+
});
|
|
41820
43092
|
const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
|
|
41821
43093
|
task3.key,
|
|
41822
|
-
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task3.key)
|
|
43094
|
+
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task3.key)
|
|
41823
43095
|
]));
|
|
41824
43096
|
const graph = {
|
|
41825
|
-
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
|
|
43097
|
+
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
|
|
41826
43098
|
task_ids,
|
|
41827
|
-
comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task3.key, String(index)))),
|
|
41828
|
-
verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task3.key, String(index)))),
|
|
43099
|
+
comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "comment", task3.key, String(index)))),
|
|
43100
|
+
verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "verification", task3.key, String(index)))),
|
|
41829
43101
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
41830
43102
|
};
|
|
41831
|
-
const request_digest = canonicalDigest(parsed);
|
|
41832
43103
|
const effectInputs = [
|
|
41833
43104
|
{
|
|
41834
43105
|
topic: "todos.task-manifest.applied",
|
|
41835
|
-
payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
|
|
43106
|
+
payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
|
|
41836
43107
|
},
|
|
41837
43108
|
...manifest.effects ?? []
|
|
41838
43109
|
];
|
|
41839
43110
|
const outbox = effectInputs.map((effect2, index) => {
|
|
41840
43111
|
const payload = { ...effect2.payload };
|
|
41841
43112
|
return {
|
|
41842
|
-
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
|
|
43113
|
+
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
|
|
41843
43114
|
topic: effect2.topic,
|
|
41844
43115
|
payload,
|
|
41845
43116
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -41849,11 +43120,14 @@ function normalize(input, now4) {
|
|
|
41849
43120
|
return {
|
|
41850
43121
|
manifest,
|
|
41851
43122
|
request_digest,
|
|
43123
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
41852
43124
|
result_digest,
|
|
41853
|
-
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
|
|
43125
|
+
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
43126
|
+
terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
41854
43127
|
graph,
|
|
41855
43128
|
outbox,
|
|
41856
|
-
now: now4
|
|
43129
|
+
now: now4,
|
|
43130
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
41857
43131
|
};
|
|
41858
43132
|
}
|
|
41859
43133
|
function sanitizeManifest(manifest) {
|
|
@@ -41911,6 +43185,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41911
43185
|
tenant_id: this.tenantId,
|
|
41912
43186
|
backend: this.backend.kind,
|
|
41913
43187
|
deterministic_ids: true,
|
|
43188
|
+
operation_step_identity: true,
|
|
43189
|
+
deterministic_idempotency_keys: true,
|
|
43190
|
+
terminal_nonacceptance_receipts: true,
|
|
43191
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
41914
43192
|
immutable_receipts: true,
|
|
41915
43193
|
transactional_outbox: true,
|
|
41916
43194
|
idempotent_outbox_delivery: true,
|
|
@@ -41940,7 +43218,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41940
43218
|
async apply(input) {
|
|
41941
43219
|
const normalized = normalize(input, this.now());
|
|
41942
43220
|
const faults = await this.prepareFaults();
|
|
41943
|
-
|
|
43221
|
+
const result = this.bounded(await this.backend.apply(normalized, faults));
|
|
43222
|
+
if (result.receipt.outcome === "terminal_nonacceptance") {
|
|
43223
|
+
throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
|
|
43224
|
+
}
|
|
43225
|
+
return result;
|
|
41944
43226
|
}
|
|
41945
43227
|
readExact(receiptId2) {
|
|
41946
43228
|
if (!receiptId2 || receiptId2.length > 200) {
|
|
@@ -41973,18 +43255,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41973
43255
|
async compensate(input) {
|
|
41974
43256
|
const request = parseTodosTaskManifestCompensation(input);
|
|
41975
43257
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
41976
|
-
|
|
41977
|
-
|
|
43258
|
+
if (applied.receipt.outcome !== "accepted") {
|
|
43259
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
|
|
43260
|
+
}
|
|
43261
|
+
if (request.operation_id !== applied.receipt.operation_id) {
|
|
43262
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
|
|
43263
|
+
}
|
|
43264
|
+
if (request.step_id === applied.receipt.step_id) {
|
|
43265
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
|
|
43266
|
+
}
|
|
43267
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
|
|
43268
|
+
if (request.precondition_digest !== expectedPreconditionDigest) {
|
|
43269
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
43270
|
+
}
|
|
43271
|
+
const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
|
|
43272
|
+
const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
|
|
43273
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
43274
|
+
operation_id: request.operation_id,
|
|
43275
|
+
step_id: request.step_id,
|
|
43276
|
+
direction: "compensate",
|
|
43277
|
+
target_selector: request.receipt_id,
|
|
43278
|
+
request_digest: requestDigest,
|
|
43279
|
+
precondition_digest: request.precondition_digest
|
|
43280
|
+
});
|
|
43281
|
+
if (request.idempotency_key !== expectedIdempotencyKey) {
|
|
43282
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
|
|
43283
|
+
}
|
|
43284
|
+
const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
|
|
41978
43285
|
const receipt = {
|
|
41979
43286
|
receipt_id: compensationReceiptId,
|
|
41980
43287
|
authority: "todos",
|
|
41981
43288
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
41982
43289
|
schema_version: 1,
|
|
41983
43290
|
kind: "compensate",
|
|
41984
|
-
operation_id:
|
|
43291
|
+
operation_id: request.operation_id,
|
|
43292
|
+
step_id: request.step_id,
|
|
41985
43293
|
idempotency_key: request.idempotency_key,
|
|
41986
43294
|
request_digest: requestDigest,
|
|
43295
|
+
precondition_digest: request.precondition_digest,
|
|
41987
43296
|
result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
|
|
43297
|
+
outcome: "accepted",
|
|
43298
|
+
reason: null,
|
|
43299
|
+
duplicate_of_receipt_id: null,
|
|
41988
43300
|
binding_version: request.if_binding_version + 1,
|
|
41989
43301
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
41990
43302
|
created_at: this.now()
|
|
@@ -42166,7 +43478,7 @@ function createTodosTaskManifestHttpClient(options) {
|
|
|
42166
43478
|
return new TodosTaskManifestHttpClient(options);
|
|
42167
43479
|
}
|
|
42168
43480
|
// src/ai-tools.ts
|
|
42169
|
-
import { createHash as
|
|
43481
|
+
import { createHash as createHash16 } from "crypto";
|
|
42170
43482
|
// src/cli/cloud-router.ts
|
|
42171
43483
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
42172
43484
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
@@ -42404,7 +43716,7 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
42404
43716
|
return await request();
|
|
42405
43717
|
} catch (error) {
|
|
42406
43718
|
const status2 = error && typeof error === "object" ? error.status : undefined;
|
|
42407
|
-
if (status2 === 404) {
|
|
43719
|
+
if (status2 === 404 || status2 === 405) {
|
|
42408
43720
|
const body2 = error && typeof error === "object" ? error.body : undefined;
|
|
42409
43721
|
const code = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.code : undefined;
|
|
42410
43722
|
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
@@ -43474,7 +44786,7 @@ function deriveTodosAiUpdateTaskApprovalIdentity(input) {
|
|
|
43474
44786
|
expected_version: input.expected_version,
|
|
43475
44787
|
patch
|
|
43476
44788
|
});
|
|
43477
|
-
const payloadDigest =
|
|
44789
|
+
const payloadDigest = createHash16("sha256").update(canonical).digest("hex");
|
|
43478
44790
|
return {
|
|
43479
44791
|
ref: `todos-ai:update_task:${payloadDigest}`,
|
|
43480
44792
|
payload_digest: payloadDigest
|
|
@@ -43629,9 +44941,9 @@ function normalizeUpdateTaskInput(input) {
|
|
|
43629
44941
|
if (!Object.hasOwn(record, "expected_version")) {
|
|
43630
44942
|
throw new Error("expected_version is required");
|
|
43631
44943
|
}
|
|
43632
|
-
const
|
|
43633
|
-
const idempotencyBytes = ENCODER.encode(
|
|
43634
|
-
if (idempotencyBytes < TODOS_AI_UPDATE_TASK_LIMITS.min_idempotency_key_bytes || !/^[A-Za-z0-9._:-]+$/.test(
|
|
44944
|
+
const idempotencyKey2 = boundedRequiredString(record, "idempotency_key", TODOS_AI_UPDATE_TASK_LIMITS.max_idempotency_key_bytes);
|
|
44945
|
+
const idempotencyBytes = ENCODER.encode(idempotencyKey2).byteLength;
|
|
44946
|
+
if (idempotencyBytes < TODOS_AI_UPDATE_TASK_LIMITS.min_idempotency_key_bytes || !/^[A-Za-z0-9._:-]+$/.test(idempotencyKey2)) {
|
|
43635
44947
|
throw new Error("idempotency_key must be a bounded stable identifier");
|
|
43636
44948
|
}
|
|
43637
44949
|
const patchValue = record["patch"];
|
|
@@ -43650,7 +44962,7 @@ function normalizeUpdateTaskInput(input) {
|
|
|
43650
44962
|
expected_version: expectedVersion,
|
|
43651
44963
|
patch,
|
|
43652
44964
|
changed_fields: changedFields,
|
|
43653
|
-
idempotency_key:
|
|
44965
|
+
idempotency_key: idempotencyKey2,
|
|
43654
44966
|
payload_digest: identity.payload_digest,
|
|
43655
44967
|
approval_ref: identity.ref
|
|
43656
44968
|
};
|
|
@@ -44331,7 +45643,7 @@ init_task_lifecycle();
|
|
|
44331
45643
|
init_task_crud();
|
|
44332
45644
|
init_redaction();
|
|
44333
45645
|
import { Database as Database3 } from "bun:sqlite";
|
|
44334
|
-
import { createHash as
|
|
45646
|
+
import { createHash as createHash17 } from "crypto";
|
|
44335
45647
|
import { existsSync as existsSync9, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
44336
45648
|
import { basename as basename2, dirname as dirname6, join as join9, resolve as resolve10 } from "path";
|
|
44337
45649
|
|
|
@@ -44614,8 +45926,8 @@ function normalizePath3(input) {
|
|
|
44614
45926
|
return resolve10(input);
|
|
44615
45927
|
}
|
|
44616
45928
|
function sourceStoreId(sourceDbPath) {
|
|
44617
|
-
const
|
|
44618
|
-
return `sqlite:${
|
|
45929
|
+
const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
45930
|
+
return `sqlite:${digest2}`;
|
|
44619
45931
|
}
|
|
44620
45932
|
function inferSourceRepoPath(sourceDbPath) {
|
|
44621
45933
|
const normalized = normalizePath3(sourceDbPath);
|
|
@@ -46576,7 +47888,7 @@ init_comments();
|
|
|
46576
47888
|
|
|
46577
47889
|
// src/db/api-keys.ts
|
|
46578
47890
|
init_database();
|
|
46579
|
-
import { createHash as
|
|
47891
|
+
import { createHash as createHash18, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
46580
47892
|
function rowToRecord(row) {
|
|
46581
47893
|
return {
|
|
46582
47894
|
id: row.id,
|
|
@@ -46590,7 +47902,7 @@ function rowToRecord(row) {
|
|
|
46590
47902
|
};
|
|
46591
47903
|
}
|
|
46592
47904
|
function hashApiKey(key2) {
|
|
46593
|
-
return
|
|
47905
|
+
return createHash18("sha256").update(key2).digest("hex");
|
|
46594
47906
|
}
|
|
46595
47907
|
function safeEqualHex(a, b) {
|
|
46596
47908
|
if (a.length !== b.length)
|
|
@@ -50491,7 +51803,7 @@ init_database();
|
|
|
50491
51803
|
init_tasks();
|
|
50492
51804
|
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
|
|
50493
51805
|
import { basename as basename5 } from "path";
|
|
50494
|
-
import { createHash as
|
|
51806
|
+
import { createHash as createHash19 } from "crypto";
|
|
50495
51807
|
init_secret_redaction();
|
|
50496
51808
|
var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
|
|
50497
51809
|
var INTAKE_SOURCE_TYPES = [
|
|
@@ -50504,7 +51816,7 @@ var INTAKE_SOURCE_TYPES = [
|
|
|
50504
51816
|
];
|
|
50505
51817
|
var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
|
|
50506
51818
|
function fingerprint2(text) {
|
|
50507
|
-
return
|
|
51819
|
+
return createHash19("sha256").update(text).digest("hex").slice(0, 16);
|
|
50508
51820
|
}
|
|
50509
51821
|
function loadRawContent(input) {
|
|
50510
51822
|
if (input.github_url) {
|
|
@@ -55964,7 +57276,7 @@ init_database();
|
|
|
55964
57276
|
init_tasks();
|
|
55965
57277
|
init_redaction();
|
|
55966
57278
|
init_sync_utils();
|
|
55967
|
-
import { createHash as
|
|
57279
|
+
import { createHash as createHash20 } from "crypto";
|
|
55968
57280
|
import { existsSync as existsSync22, readFileSync as readFileSync20, statSync as statSync9 } from "fs";
|
|
55969
57281
|
import { hostname as hostname3, platform, arch } from "os";
|
|
55970
57282
|
import { dirname as dirname15, join as join18, resolve as resolve17 } from "path";
|
|
@@ -55986,7 +57298,7 @@ var CONFIG_FILES = [
|
|
|
55986
57298
|
"dashboard/vite.config.ts"
|
|
55987
57299
|
];
|
|
55988
57300
|
function sha2566(value) {
|
|
55989
|
-
return
|
|
57301
|
+
return createHash20("sha256").update(value).digest("hex");
|
|
55990
57302
|
}
|
|
55991
57303
|
function fileRecord(root, relativePath) {
|
|
55992
57304
|
const path = join18(root, relativePath);
|
|
@@ -56101,8 +57413,8 @@ function defaultSnapshotDir() {
|
|
|
56101
57413
|
return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
|
|
56102
57414
|
}
|
|
56103
57415
|
function snapshotWithId(snapshot) {
|
|
56104
|
-
const
|
|
56105
|
-
return { id: `env_${
|
|
57416
|
+
const digest2 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
57417
|
+
return { id: `env_${digest2}`, ...snapshot };
|
|
56106
57418
|
}
|
|
56107
57419
|
function captureEnvironmentSnapshot(input = {}) {
|
|
56108
57420
|
const root = resolve17(input.root || process.cwd());
|
|
@@ -56238,7 +57550,7 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
|
|
|
56238
57550
|
init_database();
|
|
56239
57551
|
init_projects();
|
|
56240
57552
|
init_plans();
|
|
56241
|
-
import { createHash as
|
|
57553
|
+
import { createHash as createHash21 } from "crypto";
|
|
56242
57554
|
import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "fs";
|
|
56243
57555
|
import { dirname as dirname16, join as join19 } from "path";
|
|
56244
57556
|
var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
|
|
@@ -56294,7 +57606,7 @@ function rowToDecisionRecord(row) {
|
|
|
56294
57606
|
}
|
|
56295
57607
|
function stableSnapshotHash(payload) {
|
|
56296
57608
|
const { captured_at: _capturedAt, ...rest } = payload;
|
|
56297
|
-
return
|
|
57609
|
+
return createHash21("sha256").update(JSON.stringify(rest)).digest("hex");
|
|
56298
57610
|
}
|
|
56299
57611
|
function createDecisionRecord(input, db) {
|
|
56300
57612
|
const d = db || getDatabase();
|
|
@@ -60787,7 +62099,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
60787
62099
|
init_tasks();
|
|
60788
62100
|
init_task_files();
|
|
60789
62101
|
import { existsSync as existsSync27, readFileSync as readFileSync24, statSync as statSync10 } from "fs";
|
|
60790
|
-
import { createHash as
|
|
62102
|
+
import { createHash as createHash22 } from "crypto";
|
|
60791
62103
|
import { relative as relative6, resolve as resolve18, join as join25 } from "path";
|
|
60792
62104
|
var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
60793
62105
|
var DEFAULT_EXTENSIONS = new Set([
|
|
@@ -60852,7 +62164,7 @@ var SKIP_DIRS2 = new Set([
|
|
|
60852
62164
|
".parcel-cache"
|
|
60853
62165
|
]);
|
|
60854
62166
|
function stableHash(value) {
|
|
60855
|
-
return
|
|
62167
|
+
return createHash22("sha256").update(value).digest("hex");
|
|
60856
62168
|
}
|
|
60857
62169
|
function normalizePathForMatch(value) {
|
|
60858
62170
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -61750,7 +63062,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
|
|
|
61750
63062
|
}
|
|
61751
63063
|
// src/lib/agent-replay-simulator.ts
|
|
61752
63064
|
init_redaction();
|
|
61753
|
-
import { createHash as
|
|
63065
|
+
import { createHash as createHash23 } from "crypto";
|
|
61754
63066
|
import { readFileSync as readFileSync25 } from "fs";
|
|
61755
63067
|
function isObject(value) {
|
|
61756
63068
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -61772,7 +63084,7 @@ function stable2(value) {
|
|
|
61772
63084
|
return Object.fromEntries(Object.keys(value).sort().map((key2) => [key2, stable2(value[key2])]));
|
|
61773
63085
|
}
|
|
61774
63086
|
function fingerprint3(value) {
|
|
61775
|
-
return
|
|
63087
|
+
return createHash23("sha256").update(JSON.stringify(stable2(value))).digest("hex");
|
|
61776
63088
|
}
|
|
61777
63089
|
function unpackFixture(input) {
|
|
61778
63090
|
if (!isObject(input))
|
|
@@ -62011,7 +63323,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
|
|
|
62011
63323
|
}
|
|
62012
63324
|
// src/lib/local-extensions.ts
|
|
62013
63325
|
init_config2();
|
|
62014
|
-
import { createHash as
|
|
63326
|
+
import { createHash as createHash24, createVerify } from "crypto";
|
|
62015
63327
|
import { existsSync as existsSync28, readdirSync as readdirSync6, readFileSync as readFileSync26, statSync as statSync11 } from "fs";
|
|
62016
63328
|
import { basename as basename6, join as join26, resolve as resolve19 } from "path";
|
|
62017
63329
|
init_redaction();
|
|
@@ -62099,7 +63411,7 @@ function parseJson3(path) {
|
|
|
62099
63411
|
return JSON.parse(readFileSync26(path, "utf8"));
|
|
62100
63412
|
}
|
|
62101
63413
|
function sha2567(bytes) {
|
|
62102
|
-
return `sha256:${
|
|
63414
|
+
return `sha256:${createHash24("sha256").update(bytes).digest("hex")}`;
|
|
62103
63415
|
}
|
|
62104
63416
|
function compareVersions(a, b) {
|
|
62105
63417
|
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -64714,6 +66026,8 @@ export {
|
|
|
64714
66026
|
testTerminalNotificationRule,
|
|
64715
66027
|
testLocalEventHook,
|
|
64716
66028
|
tasksFromTemplate,
|
|
66029
|
+
taskManifestRequestDigest,
|
|
66030
|
+
taskManifestCompensationRequestDigest,
|
|
64717
66031
|
taskFromTemplate,
|
|
64718
66032
|
tagToPriority,
|
|
64719
66033
|
syncWithAgents,
|
|
@@ -65448,6 +66762,9 @@ export {
|
|
|
65448
66762
|
detectInboxSourceType,
|
|
65449
66763
|
detectCyclesFromEdges,
|
|
65450
66764
|
describeTerminalNotificationRule,
|
|
66765
|
+
deriveTodosTaskManifestIdempotencyKey,
|
|
66766
|
+
deriveTodosTaskManifestCompensationPreconditionDigest,
|
|
66767
|
+
deriveTodosTaskManifestApplyPreconditionDigest,
|
|
65451
66768
|
deriveTodosProjectRegistrationIdempotencyKey,
|
|
65452
66769
|
deriveTodosAiUpdateTaskApprovalIdentity,
|
|
65453
66770
|
deriveInboxTitle,
|
|
@@ -65692,6 +67009,8 @@ export {
|
|
|
65692
67009
|
TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
65693
67010
|
TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
65694
67011
|
TODOS_TASK_MANIFEST_ROUTE,
|
|
67012
|
+
TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
67013
|
+
TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
65695
67014
|
TODOS_TASK_MANIFEST_BOUNDS,
|
|
65696
67015
|
TODOS_STORAGE_TABLES,
|
|
65697
67016
|
TODOS_STORAGE_FALLBACK_ENV,
|