@hasna/todos 0.15.29 → 0.15.32
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 +2835 -334
- 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 +1447 -144
- 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 +2031 -135
- 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 +831 -58
- 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 +2022 -126
- 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 +232 -19
- 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 +592 -61
- 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.32",
|
|
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",
|
|
@@ -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
|
}
|
|
@@ -35455,9 +35868,37 @@ function normalizedCallDigest(request) {
|
|
|
35455
35868
|
project_slug: request.project_slug,
|
|
35456
35869
|
project_name: request.project_name,
|
|
35457
35870
|
desired: request.desired,
|
|
35871
|
+
bind_existing: request.bind_existing === true,
|
|
35458
35872
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
35459
35873
|
});
|
|
35460
35874
|
}
|
|
35875
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
35876
|
+
return digestProjectRegistrationValue({
|
|
35877
|
+
authority_route: request.authority_route,
|
|
35878
|
+
package_version: request.package_version,
|
|
35879
|
+
authority_id: request.authority_id,
|
|
35880
|
+
tenant_id: request.tenant_id,
|
|
35881
|
+
corpus_id: request.corpus_id,
|
|
35882
|
+
operation_id: request.operation_id,
|
|
35883
|
+
step_id: request.step_id,
|
|
35884
|
+
resource_kind: request.resource_kind,
|
|
35885
|
+
direction: request.direction,
|
|
35886
|
+
target_selector: request.target_selector,
|
|
35887
|
+
idempotency_key: request.idempotency_key,
|
|
35888
|
+
request_digest: request.request_digest,
|
|
35889
|
+
precondition_digest: request.precondition_digest,
|
|
35890
|
+
project_id: request.project_id,
|
|
35891
|
+
project_slug: request.project_slug,
|
|
35892
|
+
project_name: request.project_name,
|
|
35893
|
+
desired: request.desired,
|
|
35894
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
35895
|
+
});
|
|
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));
|
|
@@ -35989,6 +36502,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35989
36502
|
}
|
|
35990
36503
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
35991
36504
|
}
|
|
36505
|
+
async listProjectResources(request) {
|
|
36506
|
+
const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
|
|
36507
|
+
if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
|
|
36508
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
|
|
36509
|
+
}
|
|
36510
|
+
if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
|
|
36511
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
|
|
36512
|
+
}
|
|
36513
|
+
const includeAnchors = request.include_anchors === true;
|
|
36514
|
+
const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
|
|
36515
|
+
if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
|
|
36516
|
+
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 });
|
|
36517
|
+
}
|
|
36518
|
+
const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
|
|
36519
|
+
if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
|
|
36520
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
|
|
36521
|
+
source_project_id: sourceProjectId,
|
|
36522
|
+
todos_project_id: projectBinding.target_id
|
|
36523
|
+
});
|
|
36524
|
+
}
|
|
36525
|
+
const cursor = decodeProjectResourceCursor(request.cursor, {
|
|
36526
|
+
source_project_id: sourceProjectId,
|
|
36527
|
+
include_anchors: includeAnchors
|
|
36528
|
+
});
|
|
36529
|
+
const collectionInput = {
|
|
36530
|
+
todos_project_id: projectBinding.target_id,
|
|
36531
|
+
task_list_id: taskListBinding.target_id,
|
|
36532
|
+
include_anchors: includeAnchors
|
|
36533
|
+
};
|
|
36534
|
+
const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
36535
|
+
if (cursor && cursor.collection_revision !== collectionRevision) {
|
|
36536
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
|
|
36537
|
+
source_project_id: sourceProjectId,
|
|
36538
|
+
expected_collection_revision: cursor.collection_revision,
|
|
36539
|
+
current_collection_revision: collectionRevision
|
|
36540
|
+
});
|
|
36541
|
+
}
|
|
36542
|
+
const candidates = await this.backend.listProjectResourceCandidates({
|
|
36543
|
+
...collectionInput,
|
|
36544
|
+
after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
|
|
36545
|
+
limit: request.limit + 1
|
|
36546
|
+
});
|
|
36547
|
+
const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
36548
|
+
if (verifiedCollectionRevision !== collectionRevision) {
|
|
36549
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
|
|
36550
|
+
source_project_id: sourceProjectId,
|
|
36551
|
+
expected_collection_revision: collectionRevision,
|
|
36552
|
+
current_collection_revision: verifiedCollectionRevision
|
|
36553
|
+
});
|
|
36554
|
+
}
|
|
36555
|
+
const hasMore = candidates.length > request.limit;
|
|
36556
|
+
const pageCandidates = candidates.slice(0, request.limit);
|
|
36557
|
+
const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
|
|
36558
|
+
const last = pageCandidates.at(-1);
|
|
36559
|
+
return {
|
|
36560
|
+
authority: "todos",
|
|
36561
|
+
route: this.capabilityValue.route,
|
|
36562
|
+
package_version: this.capabilityValue.package_version,
|
|
36563
|
+
authority_id: this.capabilityValue.authority_id,
|
|
36564
|
+
tenant_id: this.capabilityValue.tenant_id,
|
|
36565
|
+
corpus_id: this.capabilityValue.corpus_id,
|
|
36566
|
+
source_project_id: sourceProjectId,
|
|
36567
|
+
todos_project_id: projectBinding.target_id,
|
|
36568
|
+
task_list_id: taskListBinding.target_id,
|
|
36569
|
+
include_anchors: includeAnchors,
|
|
36570
|
+
collection_revision: collectionRevision,
|
|
36571
|
+
limit: request.limit,
|
|
36572
|
+
count: resources.length,
|
|
36573
|
+
resources,
|
|
36574
|
+
has_more: hasMore,
|
|
36575
|
+
next_cursor: hasMore && last ? encodeProjectResourceCursor({
|
|
36576
|
+
source_project_id: sourceProjectId,
|
|
36577
|
+
include_anchors: includeAnchors,
|
|
36578
|
+
collection_revision: collectionRevision,
|
|
36579
|
+
kind_rank: last.kind_rank,
|
|
36580
|
+
target_id: last.target_id
|
|
36581
|
+
}) : null,
|
|
36582
|
+
complete: !hasMore,
|
|
36583
|
+
truncated: false
|
|
36584
|
+
};
|
|
36585
|
+
}
|
|
36586
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
36587
|
+
const startedAt = Date.now();
|
|
36588
|
+
if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
|
|
36589
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
|
|
36590
|
+
}
|
|
36591
|
+
requireString(sourceRequest.package_version, "package_version", {
|
|
36592
|
+
max: 128,
|
|
36593
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
36594
|
+
});
|
|
36595
|
+
assertForwardRequest(sourceRequest, {
|
|
36596
|
+
...this.capabilityValue,
|
|
36597
|
+
package_version: sourceRequest.package_version
|
|
36598
|
+
});
|
|
36599
|
+
const validation = await this.backend.transaction(async (transaction) => {
|
|
36600
|
+
const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
|
|
36601
|
+
if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
|
|
36602
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
36603
|
+
}
|
|
36604
|
+
const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
|
|
36605
|
+
if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
|
|
36606
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
36607
|
+
}
|
|
36608
|
+
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);
|
|
36609
|
+
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)) {
|
|
36610
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
|
|
36611
|
+
}
|
|
36612
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
|
|
36613
|
+
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) {
|
|
36614
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
|
|
36615
|
+
}
|
|
36616
|
+
const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
36617
|
+
if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
|
|
36618
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
|
|
36619
|
+
}
|
|
36620
|
+
let stableMatch = false;
|
|
36621
|
+
if (sourceRequest.resource_kind === "task_list") {
|
|
36622
|
+
stableMatch = taskListRegistrationDigest({
|
|
36623
|
+
...current,
|
|
36624
|
+
updated_at: accepted.result_revision
|
|
36625
|
+
}) === accepted.result_digest;
|
|
36626
|
+
} else {
|
|
36627
|
+
const project = current;
|
|
36628
|
+
if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
|
|
36629
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
|
|
36630
|
+
}
|
|
36631
|
+
for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
|
|
36632
|
+
if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
|
|
36633
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
|
|
36634
|
+
}
|
|
36635
|
+
if (projectRegistrationDigest({
|
|
36636
|
+
...project,
|
|
36637
|
+
task_counter: priorTaskCounter,
|
|
36638
|
+
updated_at: accepted.result_revision
|
|
36639
|
+
}) === accepted.result_digest) {
|
|
36640
|
+
stableMatch = true;
|
|
36641
|
+
break;
|
|
36642
|
+
}
|
|
36643
|
+
}
|
|
36644
|
+
}
|
|
36645
|
+
if (!stableMatch) {
|
|
36646
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
|
|
36647
|
+
}
|
|
36648
|
+
return {
|
|
36649
|
+
valid: true,
|
|
36650
|
+
resource_kind: sourceRequest.resource_kind,
|
|
36651
|
+
target_id: accepted.target_id,
|
|
36652
|
+
source_receipt_id: storedSource.receipt_id,
|
|
36653
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
36654
|
+
source_outcome: storedSource.outcome,
|
|
36655
|
+
created_at: current.created_at,
|
|
36656
|
+
current_revision: current.updated_at,
|
|
36657
|
+
accepted_result_digest: accepted.result_digest
|
|
36658
|
+
};
|
|
36659
|
+
});
|
|
36660
|
+
assertWithinBounds(validation, sourceRequest, startedAt);
|
|
36661
|
+
return validation;
|
|
36662
|
+
}
|
|
35992
36663
|
async storedAcceptedReceipt(request, supplied) {
|
|
35993
36664
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
35994
36665
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -36160,6 +36831,65 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
36160
36831
|
cursorTableName
|
|
36161
36832
|
}), authorityOptions);
|
|
36162
36833
|
}
|
|
36834
|
+
// src/project-registration/adoption-validation.ts
|
|
36835
|
+
var VALIDATION_KEYS = [
|
|
36836
|
+
"valid",
|
|
36837
|
+
"resource_kind",
|
|
36838
|
+
"target_id",
|
|
36839
|
+
"source_receipt_id",
|
|
36840
|
+
"accepted_receipt_id",
|
|
36841
|
+
"source_outcome",
|
|
36842
|
+
"created_at",
|
|
36843
|
+
"current_revision",
|
|
36844
|
+
"accepted_result_digest"
|
|
36845
|
+
];
|
|
36846
|
+
function isRecord2(value) {
|
|
36847
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
36848
|
+
}
|
|
36849
|
+
function isNonEmptyString(value) {
|
|
36850
|
+
return typeof value === "string" && value.length > 0;
|
|
36851
|
+
}
|
|
36852
|
+
function hasExactKeys(value, expected) {
|
|
36853
|
+
const actual = Object.keys(value).sort();
|
|
36854
|
+
const wanted = [...expected].sort();
|
|
36855
|
+
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
|
|
36856
|
+
}
|
|
36857
|
+
function adoptionRejected(message) {
|
|
36858
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", `TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED: ${message}`);
|
|
36859
|
+
}
|
|
36860
|
+
function assertTodosPriorRegistrationAdoptionValidationEnvelope(value, input) {
|
|
36861
|
+
if (!isRecord2(input) || !hasExactKeys(input, [
|
|
36862
|
+
"source_request",
|
|
36863
|
+
"source_receipt",
|
|
36864
|
+
"current_record"
|
|
36865
|
+
])) {
|
|
36866
|
+
adoptionRejected("prior-adoption validation input is incomplete");
|
|
36867
|
+
}
|
|
36868
|
+
const request = input["source_request"];
|
|
36869
|
+
const receipt = input["source_receipt"];
|
|
36870
|
+
const current = input["current_record"];
|
|
36871
|
+
if (!isRecord2(request) || !isRecord2(receipt) || !isRecord2(current)) {
|
|
36872
|
+
adoptionRejected("prior-adoption validation input records are incomplete");
|
|
36873
|
+
}
|
|
36874
|
+
const resourceKind = request["resource_kind"];
|
|
36875
|
+
const sourceOutcome = receipt["outcome"];
|
|
36876
|
+
const acceptedReceiptId = sourceOutcome === "accepted" ? receipt["receipt_id"] : sourceOutcome === "duplicate_of_accepted" ? receipt["duplicate_of_receipt_id"] : null;
|
|
36877
|
+
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"]) {
|
|
36878
|
+
adoptionRejected("prior-adoption validation input does not carry one complete accepted receipt and current target incarnation");
|
|
36879
|
+
}
|
|
36880
|
+
if (sourceOutcome === "accepted" && receipt["duplicate_of_receipt_id"] !== null || sourceOutcome === "duplicate_of_accepted" && receipt["duplicate_of_receipt_id"] !== acceptedReceiptId) {
|
|
36881
|
+
adoptionRejected("prior-adoption validation source receipt lineage is incomplete");
|
|
36882
|
+
}
|
|
36883
|
+
if (!isRecord2(value) || !hasExactKeys(value, ["validation"]) || !isRecord2(value["validation"]) || !hasExactKeys(value["validation"], VALIDATION_KEYS)) {
|
|
36884
|
+
adoptionRejected("prior-adoption validation response envelope is incomplete");
|
|
36885
|
+
}
|
|
36886
|
+
const validation = value["validation"];
|
|
36887
|
+
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"]) {
|
|
36888
|
+
adoptionRejected("prior-adoption validation response does not prove the exact accepted receipt and current target");
|
|
36889
|
+
}
|
|
36890
|
+
return validation;
|
|
36891
|
+
}
|
|
36892
|
+
|
|
36163
36893
|
// src/project-registration/http.ts
|
|
36164
36894
|
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
36165
36895
|
function json(body, status = 200) {
|
|
@@ -36206,6 +36936,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
36206
36936
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
36207
36937
|
return json({ capability: await authority.capability() });
|
|
36208
36938
|
}
|
|
36939
|
+
if (action === "resources" && method === "GET") {
|
|
36940
|
+
const sourceProjectId = url.searchParams.get("source_project_id");
|
|
36941
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
36942
|
+
const includeAnchorsRaw = url.searchParams.get("include_anchors");
|
|
36943
|
+
const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
|
|
36944
|
+
return json({
|
|
36945
|
+
page: await authority.listProjectResources({
|
|
36946
|
+
source_project_id: sourceProjectId,
|
|
36947
|
+
limit,
|
|
36948
|
+
include_anchors: includeAnchors,
|
|
36949
|
+
cursor: url.searchParams.get("cursor") ?? undefined
|
|
36950
|
+
})
|
|
36951
|
+
});
|
|
36952
|
+
}
|
|
36209
36953
|
if (method !== "POST")
|
|
36210
36954
|
return json({ error: "method not allowed" }, 405);
|
|
36211
36955
|
const body = await readJson(req);
|
|
@@ -36228,6 +36972,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
36228
36972
|
record: await authority.readExact(body)
|
|
36229
36973
|
});
|
|
36230
36974
|
}
|
|
36975
|
+
if (action === "validate-prior-adoption") {
|
|
36976
|
+
const input = body;
|
|
36977
|
+
return json({
|
|
36978
|
+
validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
|
|
36979
|
+
});
|
|
36980
|
+
}
|
|
36231
36981
|
if (action === "compensate") {
|
|
36232
36982
|
return json({
|
|
36233
36983
|
receipt: await authority.compensate(body)
|
|
@@ -36302,6 +37052,28 @@ class TodosProjectRegistrationHttpClient {
|
|
|
36302
37052
|
async lookupReceipt(request) {
|
|
36303
37053
|
return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
|
|
36304
37054
|
}
|
|
37055
|
+
async listProjectResources(request) {
|
|
37056
|
+
const query = new URLSearchParams({
|
|
37057
|
+
source_project_id: request.source_project_id,
|
|
37058
|
+
limit: String(request.limit),
|
|
37059
|
+
include_anchors: String(request.include_anchors === true),
|
|
37060
|
+
...request.cursor ? { cursor: request.cursor } : {}
|
|
37061
|
+
});
|
|
37062
|
+
const body = await this.request(`/resources?${query.toString()}`);
|
|
37063
|
+
return body.page;
|
|
37064
|
+
}
|
|
37065
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
37066
|
+
const input = {
|
|
37067
|
+
source_request: withoutTarget(sourceRequest),
|
|
37068
|
+
source_receipt: sourceReceipt,
|
|
37069
|
+
current_record: currentRecord
|
|
37070
|
+
};
|
|
37071
|
+
const body = await this.request("/validate-prior-adoption", {
|
|
37072
|
+
method: "POST",
|
|
37073
|
+
body: JSON.stringify(input)
|
|
37074
|
+
});
|
|
37075
|
+
return assertTodosPriorRegistrationAdoptionValidationEnvelope(body, input);
|
|
37076
|
+
}
|
|
36305
37077
|
async compensate(request) {
|
|
36306
37078
|
const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
36307
37079
|
return body.receipt;
|
|
@@ -40295,7 +41067,9 @@ init_types();
|
|
|
40295
41067
|
|
|
40296
41068
|
// src/task-manifest/types.ts
|
|
40297
41069
|
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
|
|
41070
|
+
var TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1";
|
|
40298
41071
|
var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
|
|
41072
|
+
var TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1";
|
|
40299
41073
|
function supportsIdempotentOutboxDelivery(capability2) {
|
|
40300
41074
|
return capability2 !== null && typeof capability2 === "object" && capability2["idempotent_outbox_delivery"] === true;
|
|
40301
41075
|
}
|
|
@@ -40325,6 +41099,8 @@ var TODOS_TASK_MANIFEST_BOUNDS = {
|
|
|
40325
41099
|
};
|
|
40326
41100
|
var key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
40327
41101
|
var identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
41102
|
+
var digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
|
|
41103
|
+
var idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
|
|
40328
41104
|
var uuid2 = exports_external.string().uuid();
|
|
40329
41105
|
var scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
40330
41106
|
var boundedScalarRecord = (limit, field2) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
@@ -40367,7 +41143,9 @@ var effect = exports_external.object({
|
|
|
40367
41143
|
var schema = exports_external.object({
|
|
40368
41144
|
version: exports_external.literal(1),
|
|
40369
41145
|
operation_id: identifier,
|
|
40370
|
-
|
|
41146
|
+
step_id: identifier,
|
|
41147
|
+
idempotency_key: idempotencyKey,
|
|
41148
|
+
precondition_digest: digest,
|
|
40371
41149
|
project_id: uuid2,
|
|
40372
41150
|
task_list_id: uuid2.optional(),
|
|
40373
41151
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -40383,7 +41161,10 @@ var schema = exports_external.object({
|
|
|
40383
41161
|
}).strict();
|
|
40384
41162
|
var compensationSchema = exports_external.object({
|
|
40385
41163
|
receipt_id: uuid2,
|
|
40386
|
-
|
|
41164
|
+
operation_id: identifier,
|
|
41165
|
+
step_id: identifier,
|
|
41166
|
+
idempotency_key: idempotencyKey,
|
|
41167
|
+
precondition_digest: digest,
|
|
40387
41168
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
40388
41169
|
}).strict();
|
|
40389
41170
|
var bindingLookupSchema = exports_external.object({
|
|
@@ -40468,10 +41249,31 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
40468
41249
|
}
|
|
40469
41250
|
|
|
40470
41251
|
// src/task-manifest/plan-slug.ts
|
|
41252
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
40471
41253
|
function taskManifestPlanSlug(manifest, planId) {
|
|
40472
41254
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
40473
41255
|
return `${base}-${planId}`;
|
|
40474
41256
|
}
|
|
41257
|
+
function sqliteLegacyTaskManifestPlanSlug(rows, planId, targetBase) {
|
|
41258
|
+
const target = rows.find((row) => row.id === planId);
|
|
41259
|
+
if (!target)
|
|
41260
|
+
return null;
|
|
41261
|
+
const used = new Set;
|
|
41262
|
+
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));
|
|
41263
|
+
for (const row of ordered) {
|
|
41264
|
+
const base = row.id === planId ? normalizeSlug(targetBase ?? row.name) || "plan" : normalizeSlug(row.slug || row.name) || "plan";
|
|
41265
|
+
let candidate = base;
|
|
41266
|
+
let suffix = 2;
|
|
41267
|
+
while (used.has(candidate)) {
|
|
41268
|
+
candidate = `${base}-${suffix}`;
|
|
41269
|
+
suffix += 1;
|
|
41270
|
+
}
|
|
41271
|
+
if (row.id === planId)
|
|
41272
|
+
return candidate;
|
|
41273
|
+
used.add(candidate);
|
|
41274
|
+
}
|
|
41275
|
+
return null;
|
|
41276
|
+
}
|
|
40475
41277
|
|
|
40476
41278
|
// src/task-manifest/backend.ts
|
|
40477
41279
|
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 +41287,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
40485
41287
|
const row = rows[0];
|
|
40486
41288
|
const bindingVersion = Number(row.binding_version);
|
|
40487
41289
|
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") {
|
|
41290
|
+
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
41291
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
40490
41292
|
}
|
|
40491
41293
|
return {
|
|
40492
41294
|
plan_id: planId,
|
|
41295
|
+
operation_id: row.binding_operation_id,
|
|
41296
|
+
step_id: row.binding_step_id,
|
|
40493
41297
|
apply_receipt_id: row.apply_receipt_id,
|
|
40494
41298
|
binding_version: bindingVersion,
|
|
40495
41299
|
state
|
|
@@ -40597,9 +41401,15 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40597
41401
|
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
40598
41402
|
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
40599
41403
|
operation_id TEXT NOT NULL,
|
|
41404
|
+
step_id TEXT NOT NULL,
|
|
40600
41405
|
idempotency_key TEXT NOT NULL,
|
|
40601
41406
|
request_digest TEXT NOT NULL,
|
|
41407
|
+
precondition_digest TEXT NOT NULL,
|
|
40602
41408
|
result_digest TEXT NOT NULL,
|
|
41409
|
+
slug_provenance TEXT,
|
|
41410
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
41411
|
+
reason TEXT,
|
|
41412
|
+
duplicate_of_receipt_id TEXT,
|
|
40603
41413
|
binding_version INTEGER NOT NULL,
|
|
40604
41414
|
apply_receipt_id TEXT,
|
|
40605
41415
|
manifest_json TEXT,
|
|
@@ -40610,9 +41420,13 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40610
41420
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
40611
41421
|
operation_id TEXT PRIMARY KEY,
|
|
40612
41422
|
tenant_id TEXT NOT NULL,
|
|
41423
|
+
step_id TEXT NOT NULL,
|
|
40613
41424
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
40614
41425
|
request_digest TEXT NOT NULL,
|
|
41426
|
+
precondition_digest TEXT NOT NULL,
|
|
40615
41427
|
result_digest TEXT NOT NULL,
|
|
41428
|
+
slug_provenance TEXT,
|
|
41429
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
40616
41430
|
apply_receipt_id TEXT NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
40617
41431
|
manifest_json TEXT NOT NULL,
|
|
40618
41432
|
result_json TEXT NOT NULL,
|
|
@@ -40633,6 +41447,27 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40633
41447
|
created_at TEXT NOT NULL,
|
|
40634
41448
|
delivered_at TEXT
|
|
40635
41449
|
);
|
|
41450
|
+
CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
41451
|
+
receipt_id TEXT PRIMARY KEY,
|
|
41452
|
+
tenant_id TEXT NOT NULL,
|
|
41453
|
+
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
41454
|
+
route TEXT NOT NULL,
|
|
41455
|
+
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
41456
|
+
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
41457
|
+
operation_id TEXT NOT NULL,
|
|
41458
|
+
step_id TEXT NOT NULL,
|
|
41459
|
+
idempotency_key TEXT NOT NULL,
|
|
41460
|
+
request_digest TEXT NOT NULL,
|
|
41461
|
+
precondition_digest TEXT NOT NULL,
|
|
41462
|
+
result_digest TEXT NOT NULL,
|
|
41463
|
+
outcome TEXT NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
41464
|
+
reason TEXT NOT NULL,
|
|
41465
|
+
binding_version INTEGER NOT NULL,
|
|
41466
|
+
apply_receipt_id TEXT,
|
|
41467
|
+
manifest_json TEXT,
|
|
41468
|
+
result_json TEXT NOT NULL,
|
|
41469
|
+
created_at TEXT NOT NULL
|
|
41470
|
+
);
|
|
40636
41471
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_outbox_receipt
|
|
40637
41472
|
ON todos_task_manifest_outbox(apply_receipt_id, status);
|
|
40638
41473
|
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_receipts_immutable_update
|
|
@@ -40643,6 +41478,14 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40643
41478
|
BEFORE DELETE ON todos_task_manifest_receipts BEGIN
|
|
40644
41479
|
SELECT RAISE(ABORT, 'todos task manifest receipts are immutable');
|
|
40645
41480
|
END;
|
|
41481
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_update
|
|
41482
|
+
BEFORE UPDATE ON todos_task_manifest_terminal_receipts BEGIN
|
|
41483
|
+
SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
|
|
41484
|
+
END;
|
|
41485
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_delete
|
|
41486
|
+
BEFORE DELETE ON todos_task_manifest_terminal_receipts BEGIN
|
|
41487
|
+
SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
|
|
41488
|
+
END;
|
|
40646
41489
|
`;
|
|
40647
41490
|
}
|
|
40648
41491
|
function sqliteTableHasColumn(db, tableName, columnName) {
|
|
@@ -40659,6 +41502,24 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
40659
41502
|
if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
|
|
40660
41503
|
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
|
|
40661
41504
|
}
|
|
41505
|
+
if (!sqliteTableHasColumn(db, tableName, "slug_provenance")) {
|
|
41506
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN slug_provenance TEXT`);
|
|
41507
|
+
}
|
|
41508
|
+
}
|
|
41509
|
+
const defaults = [
|
|
41510
|
+
["todos_task_manifest_receipts", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
|
|
41511
|
+
["todos_task_manifest_receipts", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
|
|
41512
|
+
["todos_task_manifest_receipts", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"],
|
|
41513
|
+
["todos_task_manifest_receipts", "reason", "TEXT"],
|
|
41514
|
+
["todos_task_manifest_receipts", "duplicate_of_receipt_id", "TEXT"],
|
|
41515
|
+
["todos_task_manifest_bindings", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
|
|
41516
|
+
["todos_task_manifest_bindings", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
|
|
41517
|
+
["todos_task_manifest_bindings", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"]
|
|
41518
|
+
];
|
|
41519
|
+
for (const [tableName, columnName, definition] of defaults) {
|
|
41520
|
+
if (!sqliteTableHasColumn(db, tableName, columnName)) {
|
|
41521
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${definition}`);
|
|
41522
|
+
}
|
|
40662
41523
|
}
|
|
40663
41524
|
db.exec(`
|
|
40664
41525
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
|
|
@@ -40668,6 +41529,12 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
40668
41529
|
tenant_id,
|
|
40669
41530
|
json_extract(result_json, '$.graph.plan_id')
|
|
40670
41531
|
);
|
|
41532
|
+
DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_lookup;
|
|
41533
|
+
DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_identity;
|
|
41534
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_lookup
|
|
41535
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
|
|
41536
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_identity
|
|
41537
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
|
|
40671
41538
|
`);
|
|
40672
41539
|
}
|
|
40673
41540
|
function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
@@ -40681,9 +41548,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40681
41548
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
40682
41549
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
40683
41550
|
operation_id text NOT NULL,
|
|
41551
|
+
step_id text NOT NULL,
|
|
40684
41552
|
idempotency_key text NOT NULL,
|
|
40685
41553
|
request_digest text NOT NULL,
|
|
41554
|
+
precondition_digest text NOT NULL,
|
|
40686
41555
|
result_digest text NOT NULL,
|
|
41556
|
+
slug_provenance text,
|
|
41557
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
41558
|
+
reason text,
|
|
41559
|
+
duplicate_of_receipt_id text,
|
|
40687
41560
|
binding_version integer NOT NULL,
|
|
40688
41561
|
apply_receipt_id text,
|
|
40689
41562
|
manifest_json jsonb,
|
|
@@ -40695,12 +41568,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40695
41568
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
40696
41569
|
`ALTER TABLE todos_task_manifest_receipts
|
|
40697
41570
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
41571
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41572
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
41573
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41574
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
41575
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41576
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
41577
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41578
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
41579
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41580
|
+
ADD COLUMN IF NOT EXISTS reason text`,
|
|
41581
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
41582
|
+
ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
|
|
40698
41583
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
40699
41584
|
operation_id text PRIMARY KEY,
|
|
40700
41585
|
tenant_id text NOT NULL,
|
|
41586
|
+
step_id text NOT NULL,
|
|
40701
41587
|
idempotency_key text NOT NULL UNIQUE,
|
|
40702
41588
|
request_digest text NOT NULL,
|
|
41589
|
+
precondition_digest text NOT NULL,
|
|
40703
41590
|
result_digest text NOT NULL,
|
|
41591
|
+
slug_provenance text,
|
|
41592
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
40704
41593
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
40705
41594
|
manifest_json jsonb NOT NULL,
|
|
40706
41595
|
result_json jsonb NOT NULL,
|
|
@@ -40714,6 +41603,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40714
41603
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
40715
41604
|
`ALTER TABLE todos_task_manifest_bindings
|
|
40716
41605
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
41606
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41607
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
41608
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41609
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
41610
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41611
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
41612
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
41613
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
40717
41614
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
40718
41615
|
id text PRIMARY KEY,
|
|
40719
41616
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -40725,10 +41622,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40725
41622
|
created_at timestamptz NOT NULL,
|
|
40726
41623
|
delivered_at timestamptz
|
|
40727
41624
|
)`,
|
|
41625
|
+
`CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
41626
|
+
receipt_id text PRIMARY KEY,
|
|
41627
|
+
tenant_id text NOT NULL,
|
|
41628
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
41629
|
+
route text NOT NULL,
|
|
41630
|
+
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
41631
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
41632
|
+
operation_id text NOT NULL,
|
|
41633
|
+
step_id text NOT NULL,
|
|
41634
|
+
idempotency_key text NOT NULL,
|
|
41635
|
+
request_digest text NOT NULL,
|
|
41636
|
+
precondition_digest text NOT NULL,
|
|
41637
|
+
result_digest text NOT NULL,
|
|
41638
|
+
outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
41639
|
+
reason text NOT NULL,
|
|
41640
|
+
binding_version integer NOT NULL,
|
|
41641
|
+
apply_receipt_id text,
|
|
41642
|
+
manifest_json jsonb,
|
|
41643
|
+
result_json jsonb NOT NULL,
|
|
41644
|
+
created_at timestamptz NOT NULL
|
|
41645
|
+
)`,
|
|
40728
41646
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
40729
41647
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
40730
41648
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
40731
41649
|
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
41650
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
|
|
41651
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
|
|
41652
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
|
|
41653
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
41654
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
|
|
41655
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
40732
41656
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
40733
41657
|
ON todos_task_manifest_bindings(
|
|
40734
41658
|
tenant_id,
|
|
@@ -40741,6 +41665,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40741
41665
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
40742
41666
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
40743
41667
|
BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
|
|
41668
|
+
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
|
|
41669
|
+
`DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
|
|
41670
|
+
`CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
|
|
41671
|
+
BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
|
|
40744
41672
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
40745
41673
|
];
|
|
40746
41674
|
}
|
|
@@ -40752,7 +41680,76 @@ function fault(faults, point) {
|
|
|
40752
41680
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
40753
41681
|
}
|
|
40754
41682
|
function parseApplyResult(value, duplicate) {
|
|
40755
|
-
|
|
41683
|
+
const parsed = JSON.parse(value);
|
|
41684
|
+
return {
|
|
41685
|
+
...parsed,
|
|
41686
|
+
duplicate,
|
|
41687
|
+
receipt: {
|
|
41688
|
+
...parsed.receipt,
|
|
41689
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
41690
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
41691
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
41692
|
+
reason: parsed.receipt.reason ?? null,
|
|
41693
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
41694
|
+
}
|
|
41695
|
+
};
|
|
41696
|
+
}
|
|
41697
|
+
function terminalApplyResult(input, reason) {
|
|
41698
|
+
const receipt = {
|
|
41699
|
+
receipt_id: input.terminal_receipt_id,
|
|
41700
|
+
authority: "todos",
|
|
41701
|
+
route: "todos.task-manifest.v1",
|
|
41702
|
+
schema_version: 1,
|
|
41703
|
+
kind: "apply",
|
|
41704
|
+
operation_id: input.manifest.operation_id,
|
|
41705
|
+
step_id: input.manifest.step_id,
|
|
41706
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
41707
|
+
request_digest: input.request_digest,
|
|
41708
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
41709
|
+
result_digest: canonicalDigest({
|
|
41710
|
+
outcome: "terminal_nonacceptance",
|
|
41711
|
+
reason,
|
|
41712
|
+
operation_id: input.manifest.operation_id,
|
|
41713
|
+
step_id: input.manifest.step_id,
|
|
41714
|
+
request_digest: input.request_digest
|
|
41715
|
+
}),
|
|
41716
|
+
outcome: "terminal_nonacceptance",
|
|
41717
|
+
reason,
|
|
41718
|
+
duplicate_of_receipt_id: null,
|
|
41719
|
+
binding_version: 0,
|
|
41720
|
+
apply_receipt_id: null,
|
|
41721
|
+
created_at: input.now
|
|
41722
|
+
};
|
|
41723
|
+
return {
|
|
41724
|
+
duplicate: false,
|
|
41725
|
+
receipt,
|
|
41726
|
+
graph: input.graph,
|
|
41727
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
41728
|
+
outbox_ids: [],
|
|
41729
|
+
result_digest: receipt.result_digest
|
|
41730
|
+
};
|
|
41731
|
+
}
|
|
41732
|
+
function validateCompensationPlanSlug(db, manifest, planId, slugProvenance) {
|
|
41733
|
+
const plan = db.query("SELECT id, project_id, name, slug, created_at FROM plans WHERE id = ? LIMIT 1").get(planId);
|
|
41734
|
+
if (!plan) {
|
|
41735
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
41736
|
+
}
|
|
41737
|
+
if (slugProvenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
41738
|
+
if (plan.slug !== taskManifestPlanSlug(manifest, planId)) {
|
|
41739
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
41740
|
+
}
|
|
41741
|
+
return;
|
|
41742
|
+
}
|
|
41743
|
+
if (slugProvenance !== null && slugProvenance !== undefined) {
|
|
41744
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
41745
|
+
}
|
|
41746
|
+
if (plan.slug === null)
|
|
41747
|
+
return;
|
|
41748
|
+
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);
|
|
41749
|
+
const expected = sqliteLegacyTaskManifestPlanSlug(rows, planId, manifest.plan.key || manifest.plan.name);
|
|
41750
|
+
if (expected === null || plan.slug !== expected) {
|
|
41751
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy plan slug was not produced by SQLite allocation");
|
|
41752
|
+
}
|
|
40756
41753
|
}
|
|
40757
41754
|
|
|
40758
41755
|
class SqliteTodosTaskManifestBackend {
|
|
@@ -40792,33 +41789,49 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40792
41789
|
async apply(input, faults) {
|
|
40793
41790
|
return this.serialized(() => {
|
|
40794
41791
|
const { manifest } = input;
|
|
41792
|
+
const terminal = this.db.query(`SELECT result_json
|
|
41793
|
+
FROM todos_task_manifest_terminal_receipts
|
|
41794
|
+
WHERE tenant_id = ?
|
|
41795
|
+
AND kind = 'apply'
|
|
41796
|
+
AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
|
|
41797
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
41798
|
+
LIMIT 1`).get(this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id);
|
|
41799
|
+
if (terminal)
|
|
41800
|
+
return parseApplyResult(terminal.result_json, true);
|
|
40795
41801
|
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
41802
|
if (binding) {
|
|
40797
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
40798
|
-
|
|
41803
|
+
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) {
|
|
41804
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
41805
|
+
}
|
|
41806
|
+
if (binding["outcome"] === "terminal_nonacceptance") {
|
|
41807
|
+
const terminalResult = this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41808
|
+
return { ...terminalResult, duplicate: true };
|
|
40799
41809
|
}
|
|
40800
41810
|
if (binding["state"] !== "applied") {
|
|
40801
|
-
|
|
41811
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
40802
41812
|
}
|
|
40803
41813
|
return parseApplyResult(String(binding["result_json"]), true);
|
|
40804
41814
|
}
|
|
40805
41815
|
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
41816
|
if (idempotency)
|
|
40807
|
-
|
|
41817
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
41818
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
41819
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
41820
|
+
}
|
|
40808
41821
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
40809
|
-
|
|
41822
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
40810
41823
|
}
|
|
40811
41824
|
if (!this.db.query("SELECT 1 AS found FROM projects WHERE id = ? LIMIT 1").get(manifest.project_id)) {
|
|
40812
|
-
|
|
41825
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
40813
41826
|
}
|
|
40814
41827
|
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
|
-
|
|
41828
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
40816
41829
|
}
|
|
40817
41830
|
const allIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids];
|
|
40818
41831
|
for (const id of allIds) {
|
|
40819
41832
|
for (const table of ["plans", "tasks", "task_comments", "task_verifications"]) {
|
|
40820
41833
|
if (this.db.query(`SELECT 1 AS found FROM ${table} WHERE id = ? LIMIT 1`).get(id)) {
|
|
40821
|
-
|
|
41834
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
40822
41835
|
}
|
|
40823
41836
|
}
|
|
40824
41837
|
}
|
|
@@ -40874,9 +41887,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40874
41887
|
schema_version: 1,
|
|
40875
41888
|
kind: "apply",
|
|
40876
41889
|
operation_id: manifest.operation_id,
|
|
41890
|
+
step_id: manifest.step_id,
|
|
40877
41891
|
idempotency_key: manifest.idempotency_key,
|
|
40878
41892
|
request_digest: input.request_digest,
|
|
41893
|
+
precondition_digest: manifest.precondition_digest,
|
|
40879
41894
|
result_digest: input.result_digest,
|
|
41895
|
+
outcome: "accepted",
|
|
41896
|
+
reason: null,
|
|
41897
|
+
duplicate_of_receipt_id: null,
|
|
40880
41898
|
binding_version: 1,
|
|
40881
41899
|
apply_receipt_id: null,
|
|
40882
41900
|
created_at: input.now
|
|
@@ -40892,9 +41910,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40892
41910
|
const resultJson = canonicalJson(result);
|
|
40893
41911
|
const manifestJson = canonicalJson(manifest);
|
|
40894
41912
|
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
|
-
|
|
41913
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
41914
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
41915
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
41916
|
+
) 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
41917
|
for (const entry2 of input.outbox) {
|
|
40899
41918
|
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
40900
41919
|
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
@@ -40902,18 +41921,37 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40902
41921
|
}
|
|
40903
41922
|
fault(faults, "after_outbox_write");
|
|
40904
41923
|
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);
|
|
41924
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest, result_digest,
|
|
41925
|
+
slug_provenance, outcome, apply_receipt_id, manifest_json, result_json, state, version, created_at, updated_at
|
|
41926
|
+
) 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
41927
|
fault(faults, "after_receipt_write");
|
|
40909
41928
|
return result;
|
|
40910
41929
|
});
|
|
40911
41930
|
}
|
|
41931
|
+
persistTerminal(input, reason) {
|
|
41932
|
+
const result = terminalApplyResult(input, reason);
|
|
41933
|
+
const resultJson = canonicalJson(result);
|
|
41934
|
+
this.db.query(`INSERT OR IGNORE INTO todos_task_manifest_terminal_receipts (
|
|
41935
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
41936
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
41937
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
41938
|
+
) 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);
|
|
41939
|
+
const stored = this.db.query(`SELECT receipt_id, result_json
|
|
41940
|
+
FROM todos_task_manifest_terminal_receipts
|
|
41941
|
+
WHERE tenant_id = ? AND kind = 'apply'
|
|
41942
|
+
AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
|
|
41943
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
41944
|
+
LIMIT 1`).get(this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id);
|
|
41945
|
+
return stored ? parseApplyResult(stored.result_json, stored.receipt_id !== result.receipt.receipt_id) : result;
|
|
41946
|
+
}
|
|
40912
41947
|
async readExact(receiptId2) {
|
|
40913
41948
|
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 (
|
|
41949
|
+
if (row)
|
|
41950
|
+
return parseApplyResult(row.result_json, false);
|
|
41951
|
+
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);
|
|
41952
|
+
if (!terminal)
|
|
40915
41953
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
40916
|
-
return parseApplyResult(
|
|
41954
|
+
return parseApplyResult(terminal.result_json, false);
|
|
40917
41955
|
}
|
|
40918
41956
|
async lookupBindingByPlanId(planId) {
|
|
40919
41957
|
const rows = this.db.query(`
|
|
@@ -40923,6 +41961,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40923
41961
|
b.version AS binding_version,
|
|
40924
41962
|
b.tenant_id AS binding_tenant_id,
|
|
40925
41963
|
b.operation_id AS binding_operation_id,
|
|
41964
|
+
b.step_id AS binding_step_id,
|
|
40926
41965
|
json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
|
|
40927
41966
|
r.tenant_id AS receipt_tenant_id,
|
|
40928
41967
|
r.authority AS receipt_authority,
|
|
@@ -40930,6 +41969,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40930
41969
|
r.schema_version AS receipt_schema_version,
|
|
40931
41970
|
r.kind AS receipt_kind,
|
|
40932
41971
|
r.operation_id AS receipt_operation_id,
|
|
41972
|
+
r.step_id AS receipt_step_id,
|
|
40933
41973
|
json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
|
|
40934
41974
|
FROM todos_task_manifest_bindings b
|
|
40935
41975
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -40992,6 +42032,12 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40992
42032
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
40993
42033
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
40994
42034
|
}
|
|
42035
|
+
const storedStepId = String(row["step_id"] ?? "legacy-apply");
|
|
42036
|
+
const storedRequestDigest = String(row["request_digest"]);
|
|
42037
|
+
const storedPreconditionDigest = String(row["precondition_digest"] ?? "0".repeat(64));
|
|
42038
|
+
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"]) {
|
|
42039
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
42040
|
+
}
|
|
40995
42041
|
if (binding["state"] !== "applied")
|
|
40996
42042
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
40997
42043
|
const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -41002,10 +42048,16 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41002
42048
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
41003
42049
|
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
41004
42050
|
const manifest = JSON.parse(String(row["manifest_json"]));
|
|
42051
|
+
const manifestRecord = manifest;
|
|
42052
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
41005
42053
|
const expectedEffects = [
|
|
41006
42054
|
{
|
|
41007
42055
|
topic: "todos.task-manifest.applied",
|
|
41008
|
-
payload: {
|
|
42056
|
+
payload: {
|
|
42057
|
+
operation_id: manifest.operation_id,
|
|
42058
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
42059
|
+
project_id: manifest.project_id
|
|
42060
|
+
}
|
|
41009
42061
|
},
|
|
41010
42062
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
41011
42063
|
];
|
|
@@ -41045,8 +42097,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41045
42097
|
if (canonicalJson(actualReadback) !== canonicalJson(applyResult.readback)) {
|
|
41046
42098
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: graph changed since apply", { actualReadback });
|
|
41047
42099
|
}
|
|
41048
|
-
const
|
|
41049
|
-
|
|
42100
|
+
const slugProvenance = row["slug_provenance"];
|
|
42101
|
+
validateCompensationPlanSlug(this.db, manifest, applyResult.graph.plan_id, slugProvenance);
|
|
42102
|
+
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);
|
|
42103
|
+
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
42104
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
41051
42105
|
}
|
|
41052
42106
|
for (const task3 of manifest.tasks) {
|
|
@@ -41123,9 +42177,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41123
42177
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
41124
42178
|
const resultJson = canonicalJson(result);
|
|
41125
42179
|
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
|
-
|
|
42180
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
42181
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
42182
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42183
|
+
) 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
42184
|
const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
|
|
41130
42185
|
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
42186
|
if (updated.changes !== 1) {
|
|
@@ -41159,6 +42214,39 @@ function safeIdentifier2(value, field2) {
|
|
|
41159
42214
|
function parseJson2(value) {
|
|
41160
42215
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
41161
42216
|
}
|
|
42217
|
+
function parseApplyResult2(value, duplicate) {
|
|
42218
|
+
const parsed = parseJson2(value);
|
|
42219
|
+
return {
|
|
42220
|
+
...parsed,
|
|
42221
|
+
duplicate,
|
|
42222
|
+
receipt: {
|
|
42223
|
+
...parsed.receipt,
|
|
42224
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
42225
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
42226
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
42227
|
+
reason: parsed.receipt.reason ?? null,
|
|
42228
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
42229
|
+
}
|
|
42230
|
+
};
|
|
42231
|
+
}
|
|
42232
|
+
function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
|
|
42233
|
+
if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
42234
|
+
const expected = taskManifestPlanSlug(manifest, planId);
|
|
42235
|
+
if (slug !== expected) {
|
|
42236
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
|
|
42237
|
+
}
|
|
42238
|
+
return expected;
|
|
42239
|
+
}
|
|
42240
|
+
if (provenance !== null && provenance !== undefined) {
|
|
42241
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
42242
|
+
}
|
|
42243
|
+
if (slug === null || slug === undefined)
|
|
42244
|
+
return null;
|
|
42245
|
+
if (slug !== null && slug !== undefined) {
|
|
42246
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
|
|
42247
|
+
}
|
|
42248
|
+
return null;
|
|
42249
|
+
}
|
|
41162
42250
|
function timestamp3(value) {
|
|
41163
42251
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
41164
42252
|
}
|
|
@@ -41166,6 +42254,41 @@ function fault2(faults, point) {
|
|
|
41166
42254
|
if (faults.points.has(point))
|
|
41167
42255
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
41168
42256
|
}
|
|
42257
|
+
function terminalApplyResult2(input, reason) {
|
|
42258
|
+
const receipt = {
|
|
42259
|
+
receipt_id: input.terminal_receipt_id,
|
|
42260
|
+
authority: "todos",
|
|
42261
|
+
route: "todos.task-manifest.v1",
|
|
42262
|
+
schema_version: 1,
|
|
42263
|
+
kind: "apply",
|
|
42264
|
+
operation_id: input.manifest.operation_id,
|
|
42265
|
+
step_id: input.manifest.step_id,
|
|
42266
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
42267
|
+
request_digest: input.request_digest,
|
|
42268
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
42269
|
+
result_digest: canonicalDigest({
|
|
42270
|
+
outcome: "terminal_nonacceptance",
|
|
42271
|
+
reason,
|
|
42272
|
+
operation_id: input.manifest.operation_id,
|
|
42273
|
+
step_id: input.manifest.step_id,
|
|
42274
|
+
request_digest: input.request_digest
|
|
42275
|
+
}),
|
|
42276
|
+
outcome: "terminal_nonacceptance",
|
|
42277
|
+
reason,
|
|
42278
|
+
duplicate_of_receipt_id: null,
|
|
42279
|
+
binding_version: 0,
|
|
42280
|
+
apply_receipt_id: null,
|
|
42281
|
+
created_at: input.now
|
|
42282
|
+
};
|
|
42283
|
+
return {
|
|
42284
|
+
duplicate: false,
|
|
42285
|
+
receipt,
|
|
42286
|
+
graph: input.graph,
|
|
42287
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
42288
|
+
outbox_ids: [],
|
|
42289
|
+
result_digest: receipt.result_digest
|
|
42290
|
+
};
|
|
42291
|
+
}
|
|
41169
42292
|
function receiptFromRow3(row) {
|
|
41170
42293
|
return {
|
|
41171
42294
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -41174,9 +42297,14 @@ function receiptFromRow3(row) {
|
|
|
41174
42297
|
schema_version: 1,
|
|
41175
42298
|
kind: row["kind"],
|
|
41176
42299
|
operation_id: String(row["operation_id"]),
|
|
42300
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
41177
42301
|
idempotency_key: String(row["idempotency_key"]),
|
|
41178
42302
|
request_digest: String(row["request_digest"]),
|
|
42303
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
41179
42304
|
result_digest: String(row["result_digest"]),
|
|
42305
|
+
outcome: row["outcome"] ?? "accepted",
|
|
42306
|
+
reason: row["reason"] == null ? null : row["reason"],
|
|
42307
|
+
duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
|
|
41180
42308
|
binding_version: Number(row["binding_version"]),
|
|
41181
42309
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
41182
42310
|
created_at: timestamp3(row["created_at"])
|
|
@@ -41294,46 +42422,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41294
42422
|
now4
|
|
41295
42423
|
]);
|
|
41296
42424
|
}
|
|
42425
|
+
async persistTerminal(tx, input, reason) {
|
|
42426
|
+
const result = terminalApplyResult2(input, reason);
|
|
42427
|
+
const resultJson = canonicalJson(result);
|
|
42428
|
+
await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
|
|
42429
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
42430
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
42431
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42432
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
|
|
42433
|
+
'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
|
|
42434
|
+
ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
|
|
42435
|
+
result.receipt.receipt_id,
|
|
42436
|
+
this.tenantId,
|
|
42437
|
+
input.manifest.operation_id,
|
|
42438
|
+
input.manifest.step_id,
|
|
42439
|
+
input.manifest.idempotency_key,
|
|
42440
|
+
input.request_digest,
|
|
42441
|
+
input.manifest.precondition_digest,
|
|
42442
|
+
result.receipt.result_digest,
|
|
42443
|
+
reason,
|
|
42444
|
+
canonicalJson(input.manifest),
|
|
42445
|
+
resultJson,
|
|
42446
|
+
input.now
|
|
42447
|
+
]);
|
|
42448
|
+
const stored = await tx.query(`SELECT receipt_id, result_json
|
|
42449
|
+
FROM todos_task_manifest_terminal_receipts
|
|
42450
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
42451
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
42452
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
42453
|
+
LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
|
|
42454
|
+
return stored.rows[0] ? parseApplyResult2(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
|
|
42455
|
+
}
|
|
41297
42456
|
async apply(input, faults) {
|
|
41298
42457
|
await this.ensureSchema();
|
|
41299
42458
|
return this.client.transaction(async (tx) => {
|
|
41300
42459
|
const { manifest } = input;
|
|
41301
42460
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
41302
42461
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
42462
|
+
const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
|
|
42463
|
+
WHERE tenant_id = $1
|
|
42464
|
+
AND kind = 'apply'
|
|
42465
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
42466
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
42467
|
+
LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
|
|
42468
|
+
if (terminal.rows[0]) {
|
|
42469
|
+
return parseApplyResult2(terminal.rows[0]["result_json"], true);
|
|
42470
|
+
}
|
|
41303
42471
|
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
42472
|
if (existing.rows[0]) {
|
|
41305
42473
|
const binding = existing.rows[0];
|
|
41306
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
41307
|
-
|
|
42474
|
+
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) {
|
|
42475
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
41308
42476
|
}
|
|
41309
42477
|
if (binding["state"] !== "applied") {
|
|
41310
|
-
|
|
42478
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41311
42479
|
}
|
|
41312
|
-
return
|
|
42480
|
+
return parseApplyResult2(binding["result_json"], true);
|
|
41313
42481
|
}
|
|
41314
42482
|
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
42483
|
if (reused.rows[0])
|
|
41316
|
-
|
|
42484
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
42485
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
42486
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
42487
|
+
}
|
|
41317
42488
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
41318
|
-
|
|
42489
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
41319
42490
|
}
|
|
41320
42491
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
41321
42492
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
41322
42493
|
if (!project.rows[0])
|
|
41323
|
-
|
|
42494
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
41324
42495
|
if (manifest.task_list_id) {
|
|
41325
42496
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
41326
42497
|
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
42498
|
const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
|
|
41328
42499
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
41329
|
-
|
|
42500
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
41330
42501
|
}
|
|
41331
42502
|
}
|
|
41332
42503
|
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
42504
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
41334
42505
|
WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
41335
42506
|
if (conflict.rows[0])
|
|
41336
|
-
|
|
42507
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41337
42508
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
41338
42509
|
fault2(faults, "after_plan_write");
|
|
41339
42510
|
for (const task3 of manifest.tasks) {
|
|
@@ -41403,9 +42574,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41403
42574
|
schema_version: 1,
|
|
41404
42575
|
kind: "apply",
|
|
41405
42576
|
operation_id: manifest.operation_id,
|
|
42577
|
+
step_id: manifest.step_id,
|
|
41406
42578
|
idempotency_key: manifest.idempotency_key,
|
|
41407
42579
|
request_digest: input.request_digest,
|
|
42580
|
+
precondition_digest: manifest.precondition_digest,
|
|
41408
42581
|
result_digest: input.result_digest,
|
|
42582
|
+
outcome: "accepted",
|
|
42583
|
+
reason: null,
|
|
42584
|
+
duplicate_of_receipt_id: null,
|
|
41409
42585
|
binding_version: 1,
|
|
41410
42586
|
apply_receipt_id: null,
|
|
41411
42587
|
created_at: input.now
|
|
@@ -41422,14 +42598,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41422
42598
|
const resultJson = canonicalJson(result);
|
|
41423
42599
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
41424
42600
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
41425
|
-
|
|
41426
|
-
|
|
42601
|
+
step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
|
|
42602
|
+
reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42603
|
+
) 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
42604
|
input.receipt_id,
|
|
41428
42605
|
this.tenantId,
|
|
41429
42606
|
manifest.operation_id,
|
|
41430
42607
|
manifest.idempotency_key,
|
|
42608
|
+
manifest.step_id,
|
|
41431
42609
|
input.request_digest,
|
|
42610
|
+
manifest.precondition_digest,
|
|
41432
42611
|
input.result_digest,
|
|
42612
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
41433
42613
|
manifestJson,
|
|
41434
42614
|
resultJson,
|
|
41435
42615
|
input.now
|
|
@@ -41448,14 +42628,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41448
42628
|
}
|
|
41449
42629
|
fault2(faults, "after_outbox_write");
|
|
41450
42630
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
41451
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
41452
|
-
|
|
41453
|
-
|
|
42631
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
|
|
42632
|
+
result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
|
|
42633
|
+
state, version, created_at, updated_at
|
|
42634
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
|
|
41454
42635
|
manifest.operation_id,
|
|
41455
42636
|
this.tenantId,
|
|
42637
|
+
manifest.step_id,
|
|
41456
42638
|
manifest.idempotency_key,
|
|
41457
42639
|
input.request_digest,
|
|
42640
|
+
manifest.precondition_digest,
|
|
41458
42641
|
input.result_digest,
|
|
42642
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
41459
42643
|
input.receipt_id,
|
|
41460
42644
|
manifestJson,
|
|
41461
42645
|
resultJson,
|
|
@@ -41468,9 +42652,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41468
42652
|
async readExact(receiptId2) {
|
|
41469
42653
|
await this.ensureSchema();
|
|
41470
42654
|
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 (
|
|
42655
|
+
if (result.rows[0])
|
|
42656
|
+
return parseApplyResult2(result.rows[0]["result_json"], false);
|
|
42657
|
+
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]);
|
|
42658
|
+
if (!terminal.rows[0])
|
|
41472
42659
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
41473
|
-
return
|
|
42660
|
+
return parseApplyResult2(terminal.rows[0]["result_json"], false);
|
|
41474
42661
|
}
|
|
41475
42662
|
async lookupBindingByPlanId(planId) {
|
|
41476
42663
|
await this.ensureSchema();
|
|
@@ -41481,6 +42668,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41481
42668
|
b.version AS binding_version,
|
|
41482
42669
|
b.tenant_id AS binding_tenant_id,
|
|
41483
42670
|
b.operation_id AS binding_operation_id,
|
|
42671
|
+
b.step_id AS binding_step_id,
|
|
41484
42672
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
41485
42673
|
r.tenant_id AS receipt_tenant_id,
|
|
41486
42674
|
r.authority AS receipt_authority,
|
|
@@ -41488,6 +42676,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41488
42676
|
r.schema_version AS receipt_schema_version,
|
|
41489
42677
|
r.kind AS receipt_kind,
|
|
41490
42678
|
r.operation_id AS receipt_operation_id,
|
|
42679
|
+
r.step_id AS receipt_step_id,
|
|
41491
42680
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
41492
42681
|
FROM todos_task_manifest_bindings b
|
|
41493
42682
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -41574,6 +42763,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41574
42763
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
41575
42764
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
41576
42765
|
}
|
|
42766
|
+
const appliedReceipt = receiptFromRow3(applyRow);
|
|
42767
|
+
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"]) {
|
|
42768
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
42769
|
+
}
|
|
41577
42770
|
if (binding["state"] !== "applied")
|
|
41578
42771
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
41579
42772
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -41588,12 +42781,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41588
42781
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
41589
42782
|
if (delivered.rows[0])
|
|
41590
42783
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
41591
|
-
const applyResult =
|
|
42784
|
+
const applyResult = parseApplyResult2(applyRow["result_json"], false);
|
|
41592
42785
|
const manifest = parseJson2(applyRow["manifest_json"]);
|
|
42786
|
+
const manifestRecord = manifest;
|
|
42787
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
41593
42788
|
const expectedEffects = [
|
|
41594
42789
|
{
|
|
41595
42790
|
topic: "todos.task-manifest.applied",
|
|
41596
|
-
payload: {
|
|
42791
|
+
payload: {
|
|
42792
|
+
operation_id: manifest.operation_id,
|
|
42793
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
42794
|
+
project_id: manifest.project_id
|
|
42795
|
+
}
|
|
41597
42796
|
},
|
|
41598
42797
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
41599
42798
|
];
|
|
@@ -41641,9 +42840,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41641
42840
|
}
|
|
41642
42841
|
const appliedAt = receiptFromRow3(applyRow).created_at;
|
|
41643
42842
|
const expectedPayloads = new Map;
|
|
42843
|
+
const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
42844
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
42845
|
+
LIMIT 1`, [this.service, applyResult.graph.plan_id]);
|
|
42846
|
+
const actualPlan = planRow.rows[0] ? parseJson2(planRow.rows[0]["payload"]) : null;
|
|
42847
|
+
const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
|
|
42848
|
+
planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
|
|
41644
42849
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
41645
42850
|
type: "plans",
|
|
41646
|
-
payload: canonicalJson(
|
|
42851
|
+
payload: canonicalJson(planExpected)
|
|
41647
42852
|
});
|
|
41648
42853
|
for (const task3 of manifest.tasks)
|
|
41649
42854
|
expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
|
|
@@ -41743,14 +42948,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41743
42948
|
const readback = await this.readback(tx, applyResult.graph);
|
|
41744
42949
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
41745
42950
|
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
|
-
|
|
42951
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
42952
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
42953
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
42954
|
+
) 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
42955
|
compensationReceiptId,
|
|
41750
42956
|
this.tenantId,
|
|
41751
42957
|
receipt.operation_id,
|
|
42958
|
+
receipt.step_id,
|
|
41752
42959
|
input.idempotency_key,
|
|
41753
42960
|
requestDigest,
|
|
42961
|
+
input.precondition_digest,
|
|
41754
42962
|
receipt.result_digest,
|
|
41755
42963
|
receipt.binding_version,
|
|
41756
42964
|
input.receipt_id,
|
|
@@ -41810,36 +43018,83 @@ function resolveTenantId(value) {
|
|
|
41810
43018
|
}
|
|
41811
43019
|
return tenantId;
|
|
41812
43020
|
}
|
|
43021
|
+
function taskManifestRequestDigest(manifest) {
|
|
43022
|
+
const { idempotency_key: _idempotencyKey, ...request } = manifest;
|
|
43023
|
+
return canonicalDigest(request);
|
|
43024
|
+
}
|
|
43025
|
+
function taskManifestCompensationRequestDigest(request) {
|
|
43026
|
+
return canonicalDigest(request);
|
|
43027
|
+
}
|
|
43028
|
+
function deriveTodosTaskManifestApplyPreconditionDigest(input) {
|
|
43029
|
+
return canonicalDigest({
|
|
43030
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
43031
|
+
direction: "apply",
|
|
43032
|
+
operation_id: input.operation_id,
|
|
43033
|
+
step_id: input.step_id,
|
|
43034
|
+
project_id: input.project_id,
|
|
43035
|
+
task_list_id: input.task_list_id ?? null,
|
|
43036
|
+
expected_binding_version: input.if_binding_version ?? 0
|
|
43037
|
+
});
|
|
43038
|
+
}
|
|
43039
|
+
function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
|
|
43040
|
+
return canonicalDigest({
|
|
43041
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
43042
|
+
direction: "compensate",
|
|
43043
|
+
operation_id: input.operation_id,
|
|
43044
|
+
step_id: input.step_id,
|
|
43045
|
+
apply_receipt_id: input.receipt_id,
|
|
43046
|
+
expected_binding_version: input.if_binding_version
|
|
43047
|
+
});
|
|
43048
|
+
}
|
|
43049
|
+
function deriveTodosTaskManifestIdempotencyKey(input) {
|
|
43050
|
+
return `tmk_${canonicalDigest({
|
|
43051
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
43052
|
+
...input
|
|
43053
|
+
}).slice(0, 48)}`;
|
|
43054
|
+
}
|
|
41813
43055
|
function normalize(input, now4) {
|
|
41814
43056
|
const parsed = parseTodosTaskManifest(input);
|
|
41815
43057
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
41816
43058
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
41817
43059
|
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
43060
|
}
|
|
43061
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
43062
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
41819
43063
|
const manifest = sanitizeManifest(parsed);
|
|
43064
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
|
|
43065
|
+
if (manifest.precondition_digest !== expectedPreconditionDigest) {
|
|
43066
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
43067
|
+
}
|
|
43068
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
43069
|
+
operation_id: manifest.operation_id,
|
|
43070
|
+
step_id: manifest.step_id,
|
|
43071
|
+
direction: "apply",
|
|
43072
|
+
target_selector: manifest.project_id,
|
|
43073
|
+
request_digest,
|
|
43074
|
+
precondition_digest: manifest.precondition_digest
|
|
43075
|
+
});
|
|
41820
43076
|
const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
|
|
41821
43077
|
task3.key,
|
|
41822
|
-
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task3.key)
|
|
43078
|
+
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task3.key)
|
|
41823
43079
|
]));
|
|
41824
43080
|
const graph = {
|
|
41825
|
-
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
|
|
43081
|
+
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
|
|
41826
43082
|
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)))),
|
|
43083
|
+
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)))),
|
|
43084
|
+
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
43085
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
41830
43086
|
};
|
|
41831
|
-
const request_digest = canonicalDigest(parsed);
|
|
41832
43087
|
const effectInputs = [
|
|
41833
43088
|
{
|
|
41834
43089
|
topic: "todos.task-manifest.applied",
|
|
41835
|
-
payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
|
|
43090
|
+
payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
|
|
41836
43091
|
},
|
|
41837
43092
|
...manifest.effects ?? []
|
|
41838
43093
|
];
|
|
41839
43094
|
const outbox = effectInputs.map((effect2, index) => {
|
|
41840
43095
|
const payload = { ...effect2.payload };
|
|
41841
43096
|
return {
|
|
41842
|
-
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
|
|
43097
|
+
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
|
|
41843
43098
|
topic: effect2.topic,
|
|
41844
43099
|
payload,
|
|
41845
43100
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -41849,11 +43104,14 @@ function normalize(input, now4) {
|
|
|
41849
43104
|
return {
|
|
41850
43105
|
manifest,
|
|
41851
43106
|
request_digest,
|
|
43107
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
41852
43108
|
result_digest,
|
|
41853
|
-
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
|
|
43109
|
+
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
43110
|
+
terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
41854
43111
|
graph,
|
|
41855
43112
|
outbox,
|
|
41856
|
-
now: now4
|
|
43113
|
+
now: now4,
|
|
43114
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
41857
43115
|
};
|
|
41858
43116
|
}
|
|
41859
43117
|
function sanitizeManifest(manifest) {
|
|
@@ -41911,6 +43169,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41911
43169
|
tenant_id: this.tenantId,
|
|
41912
43170
|
backend: this.backend.kind,
|
|
41913
43171
|
deterministic_ids: true,
|
|
43172
|
+
operation_step_identity: true,
|
|
43173
|
+
deterministic_idempotency_keys: true,
|
|
43174
|
+
terminal_nonacceptance_receipts: true,
|
|
43175
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
41914
43176
|
immutable_receipts: true,
|
|
41915
43177
|
transactional_outbox: true,
|
|
41916
43178
|
idempotent_outbox_delivery: true,
|
|
@@ -41940,7 +43202,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41940
43202
|
async apply(input) {
|
|
41941
43203
|
const normalized = normalize(input, this.now());
|
|
41942
43204
|
const faults = await this.prepareFaults();
|
|
41943
|
-
|
|
43205
|
+
const result = this.bounded(await this.backend.apply(normalized, faults));
|
|
43206
|
+
if (result.receipt.outcome === "terminal_nonacceptance") {
|
|
43207
|
+
throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
|
|
43208
|
+
}
|
|
43209
|
+
return result;
|
|
41944
43210
|
}
|
|
41945
43211
|
readExact(receiptId2) {
|
|
41946
43212
|
if (!receiptId2 || receiptId2.length > 200) {
|
|
@@ -41973,18 +43239,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41973
43239
|
async compensate(input) {
|
|
41974
43240
|
const request = parseTodosTaskManifestCompensation(input);
|
|
41975
43241
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
41976
|
-
|
|
41977
|
-
|
|
43242
|
+
if (applied.receipt.outcome !== "accepted") {
|
|
43243
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
|
|
43244
|
+
}
|
|
43245
|
+
if (request.operation_id !== applied.receipt.operation_id) {
|
|
43246
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
|
|
43247
|
+
}
|
|
43248
|
+
if (request.step_id === applied.receipt.step_id) {
|
|
43249
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
|
|
43250
|
+
}
|
|
43251
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
|
|
43252
|
+
if (request.precondition_digest !== expectedPreconditionDigest) {
|
|
43253
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
43254
|
+
}
|
|
43255
|
+
const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
|
|
43256
|
+
const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
|
|
43257
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
43258
|
+
operation_id: request.operation_id,
|
|
43259
|
+
step_id: request.step_id,
|
|
43260
|
+
direction: "compensate",
|
|
43261
|
+
target_selector: request.receipt_id,
|
|
43262
|
+
request_digest: requestDigest,
|
|
43263
|
+
precondition_digest: request.precondition_digest
|
|
43264
|
+
});
|
|
43265
|
+
if (request.idempotency_key !== expectedIdempotencyKey) {
|
|
43266
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
|
|
43267
|
+
}
|
|
43268
|
+
const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
|
|
41978
43269
|
const receipt = {
|
|
41979
43270
|
receipt_id: compensationReceiptId,
|
|
41980
43271
|
authority: "todos",
|
|
41981
43272
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
41982
43273
|
schema_version: 1,
|
|
41983
43274
|
kind: "compensate",
|
|
41984
|
-
operation_id:
|
|
43275
|
+
operation_id: request.operation_id,
|
|
43276
|
+
step_id: request.step_id,
|
|
41985
43277
|
idempotency_key: request.idempotency_key,
|
|
41986
43278
|
request_digest: requestDigest,
|
|
43279
|
+
precondition_digest: request.precondition_digest,
|
|
41987
43280
|
result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
|
|
43281
|
+
outcome: "accepted",
|
|
43282
|
+
reason: null,
|
|
43283
|
+
duplicate_of_receipt_id: null,
|
|
41988
43284
|
binding_version: request.if_binding_version + 1,
|
|
41989
43285
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
41990
43286
|
created_at: this.now()
|
|
@@ -42166,7 +43462,7 @@ function createTodosTaskManifestHttpClient(options) {
|
|
|
42166
43462
|
return new TodosTaskManifestHttpClient(options);
|
|
42167
43463
|
}
|
|
42168
43464
|
// src/ai-tools.ts
|
|
42169
|
-
import { createHash as
|
|
43465
|
+
import { createHash as createHash16 } from "crypto";
|
|
42170
43466
|
// src/cli/cloud-router.ts
|
|
42171
43467
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
42172
43468
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
@@ -43474,7 +44770,7 @@ function deriveTodosAiUpdateTaskApprovalIdentity(input) {
|
|
|
43474
44770
|
expected_version: input.expected_version,
|
|
43475
44771
|
patch
|
|
43476
44772
|
});
|
|
43477
|
-
const payloadDigest =
|
|
44773
|
+
const payloadDigest = createHash16("sha256").update(canonical).digest("hex");
|
|
43478
44774
|
return {
|
|
43479
44775
|
ref: `todos-ai:update_task:${payloadDigest}`,
|
|
43480
44776
|
payload_digest: payloadDigest
|
|
@@ -43629,9 +44925,9 @@ function normalizeUpdateTaskInput(input) {
|
|
|
43629
44925
|
if (!Object.hasOwn(record, "expected_version")) {
|
|
43630
44926
|
throw new Error("expected_version is required");
|
|
43631
44927
|
}
|
|
43632
|
-
const
|
|
43633
|
-
const idempotencyBytes = ENCODER.encode(
|
|
43634
|
-
if (idempotencyBytes < TODOS_AI_UPDATE_TASK_LIMITS.min_idempotency_key_bytes || !/^[A-Za-z0-9._:-]+$/.test(
|
|
44928
|
+
const idempotencyKey2 = boundedRequiredString(record, "idempotency_key", TODOS_AI_UPDATE_TASK_LIMITS.max_idempotency_key_bytes);
|
|
44929
|
+
const idempotencyBytes = ENCODER.encode(idempotencyKey2).byteLength;
|
|
44930
|
+
if (idempotencyBytes < TODOS_AI_UPDATE_TASK_LIMITS.min_idempotency_key_bytes || !/^[A-Za-z0-9._:-]+$/.test(idempotencyKey2)) {
|
|
43635
44931
|
throw new Error("idempotency_key must be a bounded stable identifier");
|
|
43636
44932
|
}
|
|
43637
44933
|
const patchValue = record["patch"];
|
|
@@ -43650,7 +44946,7 @@ function normalizeUpdateTaskInput(input) {
|
|
|
43650
44946
|
expected_version: expectedVersion,
|
|
43651
44947
|
patch,
|
|
43652
44948
|
changed_fields: changedFields,
|
|
43653
|
-
idempotency_key:
|
|
44949
|
+
idempotency_key: idempotencyKey2,
|
|
43654
44950
|
payload_digest: identity.payload_digest,
|
|
43655
44951
|
approval_ref: identity.ref
|
|
43656
44952
|
};
|
|
@@ -44331,7 +45627,7 @@ init_task_lifecycle();
|
|
|
44331
45627
|
init_task_crud();
|
|
44332
45628
|
init_redaction();
|
|
44333
45629
|
import { Database as Database3 } from "bun:sqlite";
|
|
44334
|
-
import { createHash as
|
|
45630
|
+
import { createHash as createHash17 } from "crypto";
|
|
44335
45631
|
import { existsSync as existsSync9, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
44336
45632
|
import { basename as basename2, dirname as dirname6, join as join9, resolve as resolve10 } from "path";
|
|
44337
45633
|
|
|
@@ -44614,8 +45910,8 @@ function normalizePath3(input) {
|
|
|
44614
45910
|
return resolve10(input);
|
|
44615
45911
|
}
|
|
44616
45912
|
function sourceStoreId(sourceDbPath) {
|
|
44617
|
-
const
|
|
44618
|
-
return `sqlite:${
|
|
45913
|
+
const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
45914
|
+
return `sqlite:${digest2}`;
|
|
44619
45915
|
}
|
|
44620
45916
|
function inferSourceRepoPath(sourceDbPath) {
|
|
44621
45917
|
const normalized = normalizePath3(sourceDbPath);
|
|
@@ -46576,7 +47872,7 @@ init_comments();
|
|
|
46576
47872
|
|
|
46577
47873
|
// src/db/api-keys.ts
|
|
46578
47874
|
init_database();
|
|
46579
|
-
import { createHash as
|
|
47875
|
+
import { createHash as createHash18, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
46580
47876
|
function rowToRecord(row) {
|
|
46581
47877
|
return {
|
|
46582
47878
|
id: row.id,
|
|
@@ -46590,7 +47886,7 @@ function rowToRecord(row) {
|
|
|
46590
47886
|
};
|
|
46591
47887
|
}
|
|
46592
47888
|
function hashApiKey(key2) {
|
|
46593
|
-
return
|
|
47889
|
+
return createHash18("sha256").update(key2).digest("hex");
|
|
46594
47890
|
}
|
|
46595
47891
|
function safeEqualHex(a, b) {
|
|
46596
47892
|
if (a.length !== b.length)
|
|
@@ -50491,7 +51787,7 @@ init_database();
|
|
|
50491
51787
|
init_tasks();
|
|
50492
51788
|
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
|
|
50493
51789
|
import { basename as basename5 } from "path";
|
|
50494
|
-
import { createHash as
|
|
51790
|
+
import { createHash as createHash19 } from "crypto";
|
|
50495
51791
|
init_secret_redaction();
|
|
50496
51792
|
var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
|
|
50497
51793
|
var INTAKE_SOURCE_TYPES = [
|
|
@@ -50504,7 +51800,7 @@ var INTAKE_SOURCE_TYPES = [
|
|
|
50504
51800
|
];
|
|
50505
51801
|
var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
|
|
50506
51802
|
function fingerprint2(text) {
|
|
50507
|
-
return
|
|
51803
|
+
return createHash19("sha256").update(text).digest("hex").slice(0, 16);
|
|
50508
51804
|
}
|
|
50509
51805
|
function loadRawContent(input) {
|
|
50510
51806
|
if (input.github_url) {
|
|
@@ -55964,7 +57260,7 @@ init_database();
|
|
|
55964
57260
|
init_tasks();
|
|
55965
57261
|
init_redaction();
|
|
55966
57262
|
init_sync_utils();
|
|
55967
|
-
import { createHash as
|
|
57263
|
+
import { createHash as createHash20 } from "crypto";
|
|
55968
57264
|
import { existsSync as existsSync22, readFileSync as readFileSync20, statSync as statSync9 } from "fs";
|
|
55969
57265
|
import { hostname as hostname3, platform, arch } from "os";
|
|
55970
57266
|
import { dirname as dirname15, join as join18, resolve as resolve17 } from "path";
|
|
@@ -55986,7 +57282,7 @@ var CONFIG_FILES = [
|
|
|
55986
57282
|
"dashboard/vite.config.ts"
|
|
55987
57283
|
];
|
|
55988
57284
|
function sha2566(value) {
|
|
55989
|
-
return
|
|
57285
|
+
return createHash20("sha256").update(value).digest("hex");
|
|
55990
57286
|
}
|
|
55991
57287
|
function fileRecord(root, relativePath) {
|
|
55992
57288
|
const path = join18(root, relativePath);
|
|
@@ -56101,8 +57397,8 @@ function defaultSnapshotDir() {
|
|
|
56101
57397
|
return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
|
|
56102
57398
|
}
|
|
56103
57399
|
function snapshotWithId(snapshot) {
|
|
56104
|
-
const
|
|
56105
|
-
return { id: `env_${
|
|
57400
|
+
const digest2 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
57401
|
+
return { id: `env_${digest2}`, ...snapshot };
|
|
56106
57402
|
}
|
|
56107
57403
|
function captureEnvironmentSnapshot(input = {}) {
|
|
56108
57404
|
const root = resolve17(input.root || process.cwd());
|
|
@@ -56238,7 +57534,7 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
|
|
|
56238
57534
|
init_database();
|
|
56239
57535
|
init_projects();
|
|
56240
57536
|
init_plans();
|
|
56241
|
-
import { createHash as
|
|
57537
|
+
import { createHash as createHash21 } from "crypto";
|
|
56242
57538
|
import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "fs";
|
|
56243
57539
|
import { dirname as dirname16, join as join19 } from "path";
|
|
56244
57540
|
var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
|
|
@@ -56294,7 +57590,7 @@ function rowToDecisionRecord(row) {
|
|
|
56294
57590
|
}
|
|
56295
57591
|
function stableSnapshotHash(payload) {
|
|
56296
57592
|
const { captured_at: _capturedAt, ...rest } = payload;
|
|
56297
|
-
return
|
|
57593
|
+
return createHash21("sha256").update(JSON.stringify(rest)).digest("hex");
|
|
56298
57594
|
}
|
|
56299
57595
|
function createDecisionRecord(input, db) {
|
|
56300
57596
|
const d = db || getDatabase();
|
|
@@ -60787,7 +62083,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
60787
62083
|
init_tasks();
|
|
60788
62084
|
init_task_files();
|
|
60789
62085
|
import { existsSync as existsSync27, readFileSync as readFileSync24, statSync as statSync10 } from "fs";
|
|
60790
|
-
import { createHash as
|
|
62086
|
+
import { createHash as createHash22 } from "crypto";
|
|
60791
62087
|
import { relative as relative6, resolve as resolve18, join as join25 } from "path";
|
|
60792
62088
|
var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
60793
62089
|
var DEFAULT_EXTENSIONS = new Set([
|
|
@@ -60852,7 +62148,7 @@ var SKIP_DIRS2 = new Set([
|
|
|
60852
62148
|
".parcel-cache"
|
|
60853
62149
|
]);
|
|
60854
62150
|
function stableHash(value) {
|
|
60855
|
-
return
|
|
62151
|
+
return createHash22("sha256").update(value).digest("hex");
|
|
60856
62152
|
}
|
|
60857
62153
|
function normalizePathForMatch(value) {
|
|
60858
62154
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -61750,7 +63046,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
|
|
|
61750
63046
|
}
|
|
61751
63047
|
// src/lib/agent-replay-simulator.ts
|
|
61752
63048
|
init_redaction();
|
|
61753
|
-
import { createHash as
|
|
63049
|
+
import { createHash as createHash23 } from "crypto";
|
|
61754
63050
|
import { readFileSync as readFileSync25 } from "fs";
|
|
61755
63051
|
function isObject(value) {
|
|
61756
63052
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -61772,7 +63068,7 @@ function stable2(value) {
|
|
|
61772
63068
|
return Object.fromEntries(Object.keys(value).sort().map((key2) => [key2, stable2(value[key2])]));
|
|
61773
63069
|
}
|
|
61774
63070
|
function fingerprint3(value) {
|
|
61775
|
-
return
|
|
63071
|
+
return createHash23("sha256").update(JSON.stringify(stable2(value))).digest("hex");
|
|
61776
63072
|
}
|
|
61777
63073
|
function unpackFixture(input) {
|
|
61778
63074
|
if (!isObject(input))
|
|
@@ -62011,7 +63307,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
|
|
|
62011
63307
|
}
|
|
62012
63308
|
// src/lib/local-extensions.ts
|
|
62013
63309
|
init_config2();
|
|
62014
|
-
import { createHash as
|
|
63310
|
+
import { createHash as createHash24, createVerify } from "crypto";
|
|
62015
63311
|
import { existsSync as existsSync28, readdirSync as readdirSync6, readFileSync as readFileSync26, statSync as statSync11 } from "fs";
|
|
62016
63312
|
import { basename as basename6, join as join26, resolve as resolve19 } from "path";
|
|
62017
63313
|
init_redaction();
|
|
@@ -62099,7 +63395,7 @@ function parseJson3(path) {
|
|
|
62099
63395
|
return JSON.parse(readFileSync26(path, "utf8"));
|
|
62100
63396
|
}
|
|
62101
63397
|
function sha2567(bytes) {
|
|
62102
|
-
return `sha256:${
|
|
63398
|
+
return `sha256:${createHash24("sha256").update(bytes).digest("hex")}`;
|
|
62103
63399
|
}
|
|
62104
63400
|
function compareVersions(a, b) {
|
|
62105
63401
|
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -64714,6 +66010,8 @@ export {
|
|
|
64714
66010
|
testTerminalNotificationRule,
|
|
64715
66011
|
testLocalEventHook,
|
|
64716
66012
|
tasksFromTemplate,
|
|
66013
|
+
taskManifestRequestDigest,
|
|
66014
|
+
taskManifestCompensationRequestDigest,
|
|
64717
66015
|
taskFromTemplate,
|
|
64718
66016
|
tagToPriority,
|
|
64719
66017
|
syncWithAgents,
|
|
@@ -65448,6 +66746,9 @@ export {
|
|
|
65448
66746
|
detectInboxSourceType,
|
|
65449
66747
|
detectCyclesFromEdges,
|
|
65450
66748
|
describeTerminalNotificationRule,
|
|
66749
|
+
deriveTodosTaskManifestIdempotencyKey,
|
|
66750
|
+
deriveTodosTaskManifestCompensationPreconditionDigest,
|
|
66751
|
+
deriveTodosTaskManifestApplyPreconditionDigest,
|
|
65451
66752
|
deriveTodosProjectRegistrationIdempotencyKey,
|
|
65452
66753
|
deriveTodosAiUpdateTaskApprovalIdentity,
|
|
65453
66754
|
deriveInboxTitle,
|
|
@@ -65692,6 +66993,8 @@ export {
|
|
|
65692
66993
|
TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
65693
66994
|
TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
65694
66995
|
TODOS_TASK_MANIFEST_ROUTE,
|
|
66996
|
+
TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
66997
|
+
TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
65695
66998
|
TODOS_TASK_MANIFEST_BOUNDS,
|
|
65696
66999
|
TODOS_STORAGE_TABLES,
|
|
65697
67000
|
TODOS_STORAGE_FALLBACK_ENV,
|