@hasna/todos 0.15.28 → 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 +15 -0
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +5 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -0
- 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 +13040 -10368
- package/dist/contracts.js +64 -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 +1489 -149
- 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 +2073 -140
- 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 +26 -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/schema.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 +873 -63
- package/dist/registry.js +64 -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 +2064 -131
- 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 +244 -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
|
@@ -77,6 +77,12 @@ function sqliteTodosProjectRegistrationSchemaSql() {
|
|
|
77
77
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
78
78
|
resource_kind, direction, idempotency_key
|
|
79
79
|
);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_source_identity
|
|
81
|
+
ON todos_project_registration_receipts (
|
|
82
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
83
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
84
|
+
target_selector
|
|
85
|
+
);
|
|
80
86
|
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
81
87
|
ON todos_project_registration_receipts (
|
|
82
88
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -176,6 +182,12 @@ function postgresTodosProjectRegistrationSchemaSql() {
|
|
|
176
182
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
177
183
|
resource_kind, direction, idempotency_key
|
|
178
184
|
)`,
|
|
185
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_source_identity_idx
|
|
186
|
+
ON todos_project_registration_receipts (
|
|
187
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
188
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
189
|
+
target_selector
|
|
190
|
+
)`,
|
|
179
191
|
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
180
192
|
ON todos_project_registration_receipts (
|
|
181
193
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -7905,6 +7917,46 @@ function guardPlanRowsSqlite(planIds, db) {
|
|
|
7905
7917
|
}
|
|
7906
7918
|
}
|
|
7907
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
|
+
|
|
7908
7960
|
// src/lib/creator-identity.ts
|
|
7909
7961
|
function canonicalAgentRef(value) {
|
|
7910
7962
|
return value.trim().toLowerCase();
|
|
@@ -9428,6 +9480,7 @@ function createTaskStored(input, d) {
|
|
|
9428
9480
|
let id = uuid();
|
|
9429
9481
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
9430
9482
|
try {
|
|
9483
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9431
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)
|
|
9432
9485
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9433
9486
|
id,
|
|
@@ -9806,6 +9859,7 @@ function updateTaskStored(id, input, db) {
|
|
|
9806
9859
|
throw new VersionConflictError(id, input.version, task.version);
|
|
9807
9860
|
}
|
|
9808
9861
|
input = sanitizeUpdateTaskInput(input);
|
|
9862
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9809
9863
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
|
|
9810
9864
|
const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
|
|
9811
9865
|
if (linkedProjectId) {
|
|
@@ -9859,6 +9913,10 @@ function updateTaskStored(id, input, db) {
|
|
|
9859
9913
|
sets.push("project_id = ?");
|
|
9860
9914
|
params.push(input.project_id);
|
|
9861
9915
|
}
|
|
9916
|
+
if (input.parent_id !== undefined) {
|
|
9917
|
+
sets.push("parent_id = ?");
|
|
9918
|
+
params.push(input.parent_id);
|
|
9919
|
+
}
|
|
9862
9920
|
if (input.assigned_to !== undefined) {
|
|
9863
9921
|
sets.push("assigned_to = ?");
|
|
9864
9922
|
params.push(input.assigned_to);
|
|
@@ -9974,6 +10032,8 @@ function updateTaskStored(id, input, db) {
|
|
|
9974
10032
|
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
9975
10033
|
if (input.title !== undefined && input.title !== task.title)
|
|
9976
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);
|
|
9977
10037
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
9978
10038
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
9979
10039
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
@@ -10031,7 +10091,8 @@ function updateTask(id, input, db) {
|
|
|
10031
10091
|
if (!before)
|
|
10032
10092
|
throw new TaskNotFoundError(id);
|
|
10033
10093
|
const guardedPlanIds = [before.plan_id, input.plan_id];
|
|
10034
|
-
|
|
10094
|
+
const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
|
|
10095
|
+
if (!needsSerializedWrite)
|
|
10035
10096
|
return updateTaskStored(id, input, d);
|
|
10036
10097
|
return d.transaction(() => {
|
|
10037
10098
|
guardPlanRowsSqlite(guardedPlanIds, d);
|
|
@@ -10067,6 +10128,7 @@ var init_task_crud = __esm(() => {
|
|
|
10067
10128
|
init_checklists();
|
|
10068
10129
|
init_storage_tombstones();
|
|
10069
10130
|
init_prewrite_secrets();
|
|
10131
|
+
init_task_parent_integrity();
|
|
10070
10132
|
});
|
|
10071
10133
|
|
|
10072
10134
|
// src/db/task-status.ts
|
|
@@ -12771,7 +12833,7 @@ var init_dispatches = __esm(() => {
|
|
|
12771
12833
|
// package.json
|
|
12772
12834
|
var package_default = {
|
|
12773
12835
|
name: "@hasna/todos",
|
|
12774
|
-
version: "0.15.
|
|
12836
|
+
version: "0.15.32",
|
|
12775
12837
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12776
12838
|
type: "module",
|
|
12777
12839
|
main: "dist/index.js",
|
|
@@ -27493,6 +27555,7 @@ function assertSafeIdentifier(value) {
|
|
|
27493
27555
|
|
|
27494
27556
|
// src/storage/postgres-adapter.ts
|
|
27495
27557
|
init_redaction();
|
|
27558
|
+
init_task_parent_integrity();
|
|
27496
27559
|
|
|
27497
27560
|
// src/task-manifest/canonical.ts
|
|
27498
27561
|
import { createHash as createHash11 } from "crypto";
|
|
@@ -27536,8 +27599,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
27536
27599
|
resolveRef: (ref) => store.resolveTaskRef(ref),
|
|
27537
27600
|
list: (filter = {}) => store.listTasks(filter),
|
|
27538
27601
|
count: (filter = {}) => store.countTasks(filter),
|
|
27539
|
-
update: (id, input) => updateTask2(id, input, store),
|
|
27540
|
-
delete: (id, context) => store.
|
|
27602
|
+
update: (id, input, context) => updateTask2(id, input, store, context),
|
|
27603
|
+
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
27541
27604
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
27542
27605
|
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
27543
27606
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
@@ -28070,22 +28133,70 @@ class PostgresJsonRecordStore {
|
|
|
28070
28133
|
return "identical";
|
|
28071
28134
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
28072
28135
|
}
|
|
28073
|
-
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) {
|
|
28074
28146
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
28075
|
-
if (planIds.length === 0)
|
|
28147
|
+
if (planIds.length === 0 && !parentGuard)
|
|
28076
28148
|
return this.upsert("tasks", value, context);
|
|
28077
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;
|
|
28078
28154
|
const updatedAt = value.updated_at;
|
|
28079
28155
|
const targetPlanId = value.plan_id;
|
|
28080
|
-
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
|
+
),
|
|
28081
28162
|
locked_plans AS MATERIALIZED (
|
|
28082
28163
|
SELECT object_id, payload FROM ${this.tableName}
|
|
28083
28164
|
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
|
|
28084
28165
|
AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
|
|
28085
28166
|
ORDER BY object_id
|
|
28086
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
|
|
28087
28189
|
), validation AS (
|
|
28088
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,
|
|
28089
28200
|
(SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
|
|
28090
28201
|
($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
28091
28202
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
@@ -28106,7 +28217,13 @@ class PostgresJsonRecordStore {
|
|
|
28106
28217
|
)
|
|
28107
28218
|
SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
|
|
28108
28219
|
FROM guarded
|
|
28109
|
-
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
|
|
28110
28227
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
28111
28228
|
payload = EXCLUDED.payload,
|
|
28112
28229
|
updated_at = EXCLUDED.updated_at,
|
|
@@ -28119,8 +28236,10 @@ class PostgresJsonRecordStore {
|
|
|
28119
28236
|
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
28120
28237
|
RETURNING payload
|
|
28121
28238
|
)
|
|
28122
|
-
SELECT guarded.
|
|
28123
|
-
|
|
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
|
|
28124
28243
|
FROM guarded`, [
|
|
28125
28244
|
this.service,
|
|
28126
28245
|
value.id,
|
|
@@ -28130,9 +28249,26 @@ class PostgresJsonRecordStore {
|
|
|
28130
28249
|
numberValue3(value.version),
|
|
28131
28250
|
jsonbParam(planIds),
|
|
28132
28251
|
targetPlanId,
|
|
28133
|
-
explicitProject
|
|
28252
|
+
explicitProject,
|
|
28253
|
+
Boolean(parentGuard),
|
|
28254
|
+
parentGuard?.parentId ?? null,
|
|
28255
|
+
parentGuard?.expectedVersion ?? null,
|
|
28256
|
+
parentGuard?.operation === "update"
|
|
28134
28257
|
]);
|
|
28135
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
|
+
}
|
|
28136
28272
|
if (!row?.all_plans_found || !row.target_plan_found) {
|
|
28137
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 });
|
|
28138
28274
|
}
|
|
@@ -28140,6 +28276,8 @@ class PostgresJsonRecordStore {
|
|
|
28140
28276
|
throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
|
|
28141
28277
|
}
|
|
28142
28278
|
if (!row.payload) {
|
|
28279
|
+
if (row.current_payload)
|
|
28280
|
+
return payloadRecord2(row.current_payload);
|
|
28143
28281
|
return await requireRecord("tasks", value.id, this);
|
|
28144
28282
|
}
|
|
28145
28283
|
return payloadRecord2(row.payload);
|
|
@@ -28465,6 +28603,80 @@ class PostgresJsonRecordStore {
|
|
|
28465
28603
|
version: numberValue3(existing["version"])
|
|
28466
28604
|
}, context);
|
|
28467
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
|
+
}
|
|
28468
28680
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
28469
28681
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
28470
28682
|
return value ? assertPlanProjectLinkReceipt(value) : null;
|
|
@@ -28885,9 +29097,8 @@ class PostgresJsonRecordStore {
|
|
|
28885
29097
|
}
|
|
28886
29098
|
async function createTask2(input, store, context) {
|
|
28887
29099
|
const timestamp3 = new Date().toISOString();
|
|
28888
|
-
|
|
28889
|
-
|
|
28890
|
-
}
|
|
29100
|
+
const taskId = randomUUID3();
|
|
29101
|
+
await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
|
|
28891
29102
|
const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
|
|
28892
29103
|
const requestedProjectId = input.project_id ?? context?.projectId ?? null;
|
|
28893
29104
|
if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
|
|
@@ -28896,7 +29107,7 @@ async function createTask2(input, store, context) {
|
|
|
28896
29107
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
28897
29108
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
28898
29109
|
const task2 = {
|
|
28899
|
-
id:
|
|
29110
|
+
id: taskId,
|
|
28900
29111
|
short_id: shortId,
|
|
28901
29112
|
project_id: effectiveProjectId,
|
|
28902
29113
|
parent_id: input.parent_id ?? null,
|
|
@@ -28952,15 +29163,16 @@ async function createTask2(input, store, context) {
|
|
|
28952
29163
|
synced_at: null,
|
|
28953
29164
|
archived_at: null
|
|
28954
29165
|
};
|
|
28955
|
-
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);
|
|
28956
29167
|
await logTaskChange2(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
|
|
28957
29168
|
return storedTask;
|
|
28958
29169
|
}
|
|
28959
|
-
async function updateTask2(id, input, store) {
|
|
29170
|
+
async function updateTask2(id, input, store, context) {
|
|
28960
29171
|
const existing = await requireRecord("tasks", id, store);
|
|
28961
29172
|
if (existing.version !== input.version) {
|
|
28962
|
-
throw new
|
|
29173
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
28963
29174
|
}
|
|
29175
|
+
await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
|
|
28964
29176
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
|
|
28965
29177
|
const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
|
|
28966
29178
|
if (linkedPlan?.project_id) {
|
|
@@ -28985,10 +29197,19 @@ async function updateTask2(id, input, store) {
|
|
|
28985
29197
|
metadata: input.metadata ?? existing.metadata,
|
|
28986
29198
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
28987
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,
|
|
28988
29201
|
created_by: existing.created_by,
|
|
28989
29202
|
completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
|
|
28990
29203
|
};
|
|
28991
|
-
|
|
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;
|
|
28992
29213
|
}
|
|
28993
29214
|
async function startTask2(id, agentId, store) {
|
|
28994
29215
|
const task2 = await requireRecord("tasks", id, store);
|
|
@@ -29052,7 +29273,11 @@ async function patchTask(task2, patch, store) {
|
|
|
29052
29273
|
version: task2.version + 1,
|
|
29053
29274
|
updated_at: new Date().toISOString()
|
|
29054
29275
|
};
|
|
29055
|
-
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
|
+
});
|
|
29056
29281
|
}
|
|
29057
29282
|
var CLOUD_LOCK_EXPIRY_MINUTES = 30;
|
|
29058
29283
|
function sameCloudLockHolder(stored, incoming) {
|
|
@@ -34172,7 +34397,7 @@ function createLocalPrGroupLedger(db = getDatabase()) {
|
|
|
34172
34397
|
return new PrGroupLedger(new SqlitePrGroupLedgerPersistence(db));
|
|
34173
34398
|
}
|
|
34174
34399
|
// src/project-registration/authority.ts
|
|
34175
|
-
import { createHash as
|
|
34400
|
+
import { createHash as createHash15 } from "crypto";
|
|
34176
34401
|
// src/project-registration/types.ts
|
|
34177
34402
|
var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1";
|
|
34178
34403
|
var TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
|
|
@@ -34252,8 +34477,9 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34252
34477
|
const result = await this.client.query(`
|
|
34253
34478
|
SELECT * FROM todos_project_registration_receipts
|
|
34254
34479
|
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
34255
|
-
AND
|
|
34256
|
-
AND
|
|
34480
|
+
AND route = $4 AND package_version = $5
|
|
34481
|
+
AND operation_id = $6 AND step_id = $7 AND resource_kind = $8
|
|
34482
|
+
AND direction = $9 AND idempotency_key = $10 AND target_selector = $11
|
|
34257
34483
|
ORDER BY CASE outcome
|
|
34258
34484
|
WHEN 'terminal_nonacceptance' THEN 0
|
|
34259
34485
|
WHEN 'duplicate_of_accepted' THEN 1
|
|
@@ -34264,6 +34490,8 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34264
34490
|
identity.authority_id,
|
|
34265
34491
|
identity.tenant_id,
|
|
34266
34492
|
identity.corpus_id,
|
|
34493
|
+
identity.route,
|
|
34494
|
+
identity.package_version,
|
|
34267
34495
|
identity.operation_id,
|
|
34268
34496
|
identity.step_id,
|
|
34269
34497
|
identity.resource_kind,
|
|
@@ -34463,6 +34691,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34463
34691
|
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
34464
34692
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
34465
34693
|
LIMIT 1
|
|
34694
|
+
FOR UPDATE
|
|
34466
34695
|
`, [this.service, path, taskListSlug]);
|
|
34467
34696
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
34468
34697
|
}
|
|
@@ -34473,6 +34702,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34473
34702
|
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
34474
34703
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
34475
34704
|
LIMIT 1
|
|
34705
|
+
FOR UPDATE
|
|
34476
34706
|
`, [this.service, projectId, slug]);
|
|
34477
34707
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
34478
34708
|
}
|
|
@@ -34483,10 +34713,24 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34483
34713
|
return await this.storage.taskLists.create(input);
|
|
34484
34714
|
}
|
|
34485
34715
|
async getProject(id) {
|
|
34486
|
-
|
|
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;
|
|
34487
34724
|
}
|
|
34488
34725
|
async getTaskList(id) {
|
|
34489
|
-
|
|
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;
|
|
34490
34734
|
}
|
|
34491
34735
|
async lockCompensationWrites() {
|
|
34492
34736
|
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
@@ -34566,11 +34810,102 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
34566
34810
|
async getTaskList(id) {
|
|
34567
34811
|
return (await this.direct()).getTaskList(id);
|
|
34568
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
|
+
}
|
|
34569
34903
|
}
|
|
34570
34904
|
|
|
34571
34905
|
// src/project-registration/sqlite.ts
|
|
34572
34906
|
init_database();
|
|
34573
34907
|
init_storage_tombstones();
|
|
34908
|
+
import { createHash as createHash14 } from "crypto";
|
|
34574
34909
|
var sqliteTransactionTails2 = new WeakMap;
|
|
34575
34910
|
var PROJECT_REFERENCE_COLUMNS = new Set([
|
|
34576
34911
|
"project_id",
|
|
@@ -34671,6 +35006,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
34671
35006
|
const row = this.db.query(`
|
|
34672
35007
|
SELECT * FROM todos_project_registration_receipts
|
|
34673
35008
|
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35009
|
+
AND route = ? AND package_version = ?
|
|
34674
35010
|
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
34675
35011
|
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
34676
35012
|
ORDER BY CASE outcome
|
|
@@ -34679,7 +35015,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
34679
35015
|
ELSE 2
|
|
34680
35016
|
END, created_at DESC, receipt_id DESC
|
|
34681
35017
|
LIMIT 1
|
|
34682
|
-
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
35018
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.route, identity.package_version, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
34683
35019
|
return row ? receiptFromRow2(row) : null;
|
|
34684
35020
|
}
|
|
34685
35021
|
async getReceiptById(receiptId) {
|
|
@@ -34833,7 +35169,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
34833
35169
|
}
|
|
34834
35170
|
async lockStep(_identity) {}
|
|
34835
35171
|
async getReceiptForLookup(identity) {
|
|
34836
|
-
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
35172
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.route === identity.route && receipt.package_version === identity.package_version && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
34837
35173
|
const stored = await this.direct.getReceiptForLookup(identity);
|
|
34838
35174
|
if (stored)
|
|
34839
35175
|
staged.push(stored);
|
|
@@ -35223,6 +35559,64 @@ class SqliteTodosProjectRegistrationBackend {
|
|
|
35223
35559
|
getTaskList(id) {
|
|
35224
35560
|
return this.direct.getTaskList(id);
|
|
35225
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
|
+
}
|
|
35226
35620
|
}
|
|
35227
35621
|
|
|
35228
35622
|
// src/project-registration/authority.ts
|
|
@@ -35230,8 +35624,12 @@ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-
|
|
|
35230
35624
|
var WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
|
|
35231
35625
|
var OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
35232
35626
|
var STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
35627
|
+
var AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
35628
|
+
var PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
35233
35629
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
35234
35630
|
var IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
35631
|
+
var PROJECT_RESOURCE_PAGE_LIMIT = 500;
|
|
35632
|
+
var PROJECT_RESOURCE_CURSOR_VERSION = 1;
|
|
35235
35633
|
|
|
35236
35634
|
class WriteBoundaryError extends Error {
|
|
35237
35635
|
point;
|
|
@@ -35259,7 +35657,7 @@ function canonicalize4(value) {
|
|
|
35259
35657
|
return out;
|
|
35260
35658
|
}
|
|
35261
35659
|
function digestProjectRegistrationValue(value) {
|
|
35262
|
-
return
|
|
35660
|
+
return createHash15("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
35263
35661
|
}
|
|
35264
35662
|
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
35265
35663
|
return `prk_${digestProjectRegistrationValue({
|
|
@@ -35371,35 +35769,68 @@ function projectRecord(project) {
|
|
|
35371
35769
|
return {
|
|
35372
35770
|
target_id: project.id,
|
|
35373
35771
|
revision: project.updated_at,
|
|
35374
|
-
digest:
|
|
35375
|
-
id: project.id,
|
|
35376
|
-
name: project.name,
|
|
35377
|
-
path: project.path,
|
|
35378
|
-
description: project.description,
|
|
35379
|
-
task_list_id: project.task_list_id,
|
|
35380
|
-
task_prefix: project.task_prefix,
|
|
35381
|
-
task_counter: project.task_counter,
|
|
35382
|
-
created_at: project.created_at,
|
|
35383
|
-
updated_at: project.updated_at
|
|
35384
|
-
})
|
|
35772
|
+
digest: projectRegistrationDigest(project)
|
|
35385
35773
|
};
|
|
35386
35774
|
}
|
|
35387
35775
|
function taskListRecord(taskList) {
|
|
35388
35776
|
return {
|
|
35389
35777
|
target_id: taskList.id,
|
|
35390
35778
|
revision: taskList.updated_at,
|
|
35391
|
-
digest:
|
|
35392
|
-
|
|
35393
|
-
|
|
35394
|
-
|
|
35395
|
-
|
|
35396
|
-
|
|
35397
|
-
|
|
35398
|
-
|
|
35399
|
-
|
|
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
|
|
35789
|
+
})
|
|
35790
|
+
};
|
|
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
|
|
35400
35799
|
})
|
|
35401
35800
|
};
|
|
35402
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
|
+
}
|
|
35403
35834
|
function receiptId(input) {
|
|
35404
35835
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
35405
35836
|
}
|
|
@@ -35419,6 +35850,29 @@ function assertCapabilityRequest(request, capability2) {
|
|
|
35419
35850
|
}
|
|
35420
35851
|
}
|
|
35421
35852
|
function normalizedCallDigest(request) {
|
|
35853
|
+
return digestProjectRegistrationValue({
|
|
35854
|
+
authority_route: request.authority_route,
|
|
35855
|
+
package_version: request.package_version,
|
|
35856
|
+
authority_id: request.authority_id,
|
|
35857
|
+
tenant_id: request.tenant_id,
|
|
35858
|
+
corpus_id: request.corpus_id,
|
|
35859
|
+
operation_id: request.operation_id,
|
|
35860
|
+
step_id: request.step_id,
|
|
35861
|
+
resource_kind: request.resource_kind,
|
|
35862
|
+
direction: request.direction,
|
|
35863
|
+
target_selector: request.target_selector,
|
|
35864
|
+
idempotency_key: request.idempotency_key,
|
|
35865
|
+
request_digest: request.request_digest,
|
|
35866
|
+
precondition_digest: request.precondition_digest,
|
|
35867
|
+
project_id: request.project_id,
|
|
35868
|
+
project_slug: request.project_slug,
|
|
35869
|
+
project_name: request.project_name,
|
|
35870
|
+
desired: request.desired,
|
|
35871
|
+
bind_existing: request.bind_existing === true,
|
|
35872
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
35873
|
+
});
|
|
35874
|
+
}
|
|
35875
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
35422
35876
|
return digestProjectRegistrationValue({
|
|
35423
35877
|
authority_route: request.authority_route,
|
|
35424
35878
|
package_version: request.package_version,
|
|
@@ -35440,6 +35894,11 @@ function normalizedCallDigest(request) {
|
|
|
35440
35894
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
35441
35895
|
});
|
|
35442
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
|
+
}
|
|
35443
35902
|
function assertCommonRequest(request, capability2) {
|
|
35444
35903
|
assertBounds(request);
|
|
35445
35904
|
assertResourceKind(request.resource_kind);
|
|
@@ -35481,6 +35940,9 @@ function assertCommonRequest(request, capability2) {
|
|
|
35481
35940
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
35482
35941
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
35483
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
|
+
}
|
|
35484
35946
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
35485
35947
|
operation_id: request.operation_id,
|
|
35486
35948
|
step_id: request.step_id,
|
|
@@ -35502,7 +35964,7 @@ function assertForwardRequest(request, capability2) {
|
|
|
35502
35964
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
35503
35965
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
35504
35966
|
target_selector: request.target_selector,
|
|
35505
|
-
expected: "absent"
|
|
35967
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
35506
35968
|
});
|
|
35507
35969
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
35508
35970
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -35581,7 +36043,7 @@ function receiptBase(request, callDigest, capability2) {
|
|
|
35581
36043
|
normalized_call_digest: callDigest
|
|
35582
36044
|
};
|
|
35583
36045
|
}
|
|
35584
|
-
function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt2) {
|
|
36046
|
+
function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt2, createdByOperation = true) {
|
|
35585
36047
|
return makeReceipt({
|
|
35586
36048
|
...receiptBase(request, callDigest, capability2),
|
|
35587
36049
|
outcome: "accepted",
|
|
@@ -35591,7 +36053,7 @@ function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt
|
|
|
35591
36053
|
result_digest: record.digest,
|
|
35592
36054
|
duplicate_of_receipt_id: null,
|
|
35593
36055
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
35594
|
-
created_by_operation:
|
|
36056
|
+
created_by_operation: createdByOperation
|
|
35595
36057
|
}, createdAt2);
|
|
35596
36058
|
}
|
|
35597
36059
|
function makeDuplicateReceipt(request, callDigest, capability2, accepted, createdAt2) {
|
|
@@ -35653,6 +36115,53 @@ function bindingFor(request, callDigest, timestamp3, capability2) {
|
|
|
35653
36115
|
updated_at: timestamp3
|
|
35654
36116
|
};
|
|
35655
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
|
+
}
|
|
35656
36165
|
|
|
35657
36166
|
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
35658
36167
|
backend;
|
|
@@ -35674,6 +36183,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35674
36183
|
immutable_receipts: true,
|
|
35675
36184
|
exact_terminal_lookup: true,
|
|
35676
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,
|
|
35677
36190
|
conditional_inverse: true,
|
|
35678
36191
|
ambiguous_outcome_reconciliation: true
|
|
35679
36192
|
};
|
|
@@ -35718,6 +36231,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35718
36231
|
async existingForwardResolution(transaction, request, callDigest) {
|
|
35719
36232
|
const exact = await transaction.getReceiptForLookup({
|
|
35720
36233
|
...authorityScope(this.capabilityValue),
|
|
36234
|
+
route: this.capabilityValue.route,
|
|
36235
|
+
package_version: this.capabilityValue.package_version,
|
|
35721
36236
|
operation_id: request.operation_id,
|
|
35722
36237
|
step_id: request.step_id,
|
|
35723
36238
|
resource_kind: request.resource_kind,
|
|
@@ -35732,7 +36247,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35732
36247
|
if (!accepted2) {
|
|
35733
36248
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
35734
36249
|
}
|
|
35735
|
-
if (accepted2
|
|
36250
|
+
if (!acceptedCallMatches(request, accepted2, callDigest)) {
|
|
35736
36251
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
35737
36252
|
}
|
|
35738
36253
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
@@ -35746,7 +36261,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35746
36261
|
});
|
|
35747
36262
|
if (!accepted)
|
|
35748
36263
|
return null;
|
|
35749
|
-
if (accepted
|
|
36264
|
+
if (acceptedCallMatches(request, accepted, callDigest)) {
|
|
35750
36265
|
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
35751
36266
|
}
|
|
35752
36267
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
@@ -35757,6 +36272,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35757
36272
|
const slug2 = taskListSlug(request.project_slug);
|
|
35758
36273
|
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
35759
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
|
+
}
|
|
35760
36281
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
35761
36282
|
}
|
|
35762
36283
|
await this.fault("before_object_write", request);
|
|
@@ -35768,7 +36289,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35768
36289
|
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
35769
36290
|
});
|
|
35770
36291
|
await this.fault("after_object_write", request);
|
|
35771
|
-
return
|
|
36292
|
+
return {
|
|
36293
|
+
record: projectRecord(project),
|
|
36294
|
+
created_by_operation: true
|
|
36295
|
+
};
|
|
35772
36296
|
}
|
|
35773
36297
|
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
35774
36298
|
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
@@ -35782,6 +36306,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35782
36306
|
const slug = taskListSlug(request.project_slug);
|
|
35783
36307
|
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
35784
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
|
+
}
|
|
35785
36315
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
35786
36316
|
}
|
|
35787
36317
|
await this.fault("before_object_write", request);
|
|
@@ -35798,7 +36328,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35798
36328
|
if (taskList.project_id !== todosProjectId) {
|
|
35799
36329
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
35800
36330
|
}
|
|
35801
|
-
return
|
|
36331
|
+
return {
|
|
36332
|
+
record: taskListRecord(taskList),
|
|
36333
|
+
created_by_operation: true
|
|
36334
|
+
};
|
|
35802
36335
|
}
|
|
35803
36336
|
async create(request) {
|
|
35804
36337
|
const startedAt = Date.now();
|
|
@@ -35820,9 +36353,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35820
36353
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
|
|
35821
36354
|
if (!claimed) {
|
|
35822
36355
|
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
35823
|
-
if (binding?.state === "accepted" && binding.
|
|
36356
|
+
if (binding?.state === "accepted" && binding.accepted_receipt_id) {
|
|
35824
36357
|
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
35825
|
-
if (accepted2) {
|
|
36358
|
+
if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
|
|
35826
36359
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
35827
36360
|
}
|
|
35828
36361
|
}
|
|
@@ -35833,15 +36366,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35833
36366
|
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
35834
36367
|
return recordOrTerminal;
|
|
35835
36368
|
}
|
|
35836
|
-
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);
|
|
35837
36370
|
await this.fault("before_receipt_write", request);
|
|
35838
36371
|
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
35839
36372
|
await this.fault("after_receipt_write", request);
|
|
35840
36373
|
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
35841
|
-
target_id: recordOrTerminal.target_id,
|
|
36374
|
+
target_id: recordOrTerminal.record.target_id,
|
|
35842
36375
|
accepted_receipt_id: stored.receipt_id,
|
|
35843
|
-
result_revision: recordOrTerminal.revision,
|
|
35844
|
-
result_digest: recordOrTerminal.digest,
|
|
36376
|
+
result_revision: recordOrTerminal.record.revision,
|
|
36377
|
+
result_digest: recordOrTerminal.record.digest,
|
|
35845
36378
|
updated_at: this.now()
|
|
35846
36379
|
});
|
|
35847
36380
|
return stored;
|
|
@@ -35870,6 +36403,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35870
36403
|
});
|
|
35871
36404
|
const exact = await transaction.getReceiptForLookup({
|
|
35872
36405
|
...authorityScope(this.capabilityValue),
|
|
36406
|
+
route: this.capabilityValue.route,
|
|
36407
|
+
package_version: this.capabilityValue.package_version,
|
|
35873
36408
|
operation_id: request.operation_id,
|
|
35874
36409
|
step_id: request.step_id,
|
|
35875
36410
|
resource_kind: request.resource_kind,
|
|
@@ -35887,7 +36422,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35887
36422
|
direction: request.direction
|
|
35888
36423
|
});
|
|
35889
36424
|
if (accepted) {
|
|
35890
|
-
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 });
|
|
35891
36426
|
}
|
|
35892
36427
|
const timestamp3 = this.now();
|
|
35893
36428
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
|
|
@@ -35920,9 +36455,18 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35920
36455
|
if (request.max_items !== 1) {
|
|
35921
36456
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
35922
36457
|
}
|
|
35923
|
-
if (request.authority !== "todos" || request.
|
|
36458
|
+
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
|
|
35924
36459
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
35925
36460
|
}
|
|
36461
|
+
requireString(request.authority_route, "authority_route", {
|
|
36462
|
+
min: 3,
|
|
36463
|
+
max: 128,
|
|
36464
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
36465
|
+
});
|
|
36466
|
+
requireString(request.package_version, "package_version", {
|
|
36467
|
+
max: 128,
|
|
36468
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
36469
|
+
});
|
|
35926
36470
|
requireString(request.operation_id, "operation_id", {
|
|
35927
36471
|
min: 8,
|
|
35928
36472
|
max: 128,
|
|
@@ -35944,6 +36488,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35944
36488
|
}
|
|
35945
36489
|
const receipt = await this.backend.getReceiptForLookup({
|
|
35946
36490
|
...authorityScope(this.capabilityValue),
|
|
36491
|
+
route: request.authority_route,
|
|
36492
|
+
package_version: request.package_version,
|
|
35947
36493
|
operation_id: request.operation_id,
|
|
35948
36494
|
step_id: request.step_id,
|
|
35949
36495
|
resource_kind: request.resource_kind,
|
|
@@ -35956,6 +36502,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35956
36502
|
}
|
|
35957
36503
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
35958
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
|
+
}
|
|
35959
36663
|
async storedAcceptedReceipt(request, supplied) {
|
|
35960
36664
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
35961
36665
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -35982,6 +36686,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
35982
36686
|
});
|
|
35983
36687
|
const exact = await transaction.getReceiptForLookup({
|
|
35984
36688
|
...authorityScope(this.capabilityValue),
|
|
36689
|
+
route: this.capabilityValue.route,
|
|
36690
|
+
package_version: this.capabilityValue.package_version,
|
|
35985
36691
|
operation_id: request.operation_id,
|
|
35986
36692
|
step_id: request.step_id,
|
|
35987
36693
|
resource_kind: request.resource_kind,
|
|
@@ -36076,6 +36782,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36076
36782
|
await this.storedAcceptedReceipt(request, accepted);
|
|
36077
36783
|
const receipt = await this.backend.getReceiptForLookup({
|
|
36078
36784
|
...authorityScope(this.capabilityValue),
|
|
36785
|
+
route: this.capabilityValue.route,
|
|
36786
|
+
package_version: this.capabilityValue.package_version,
|
|
36079
36787
|
operation_id: request.operation_id,
|
|
36080
36788
|
step_id: request.step_id,
|
|
36081
36789
|
resource_kind: request.resource_kind,
|
|
@@ -36123,6 +36831,65 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
36123
36831
|
cursorTableName
|
|
36124
36832
|
}), authorityOptions);
|
|
36125
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
|
+
|
|
36126
36893
|
// src/project-registration/http.ts
|
|
36127
36894
|
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
36128
36895
|
function json(body, status = 200) {
|
|
@@ -36169,6 +36936,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
36169
36936
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
36170
36937
|
return json({ capability: await authority.capability() });
|
|
36171
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
|
+
}
|
|
36172
36953
|
if (method !== "POST")
|
|
36173
36954
|
return json({ error: "method not allowed" }, 405);
|
|
36174
36955
|
const body = await readJson(req);
|
|
@@ -36191,6 +36972,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
36191
36972
|
record: await authority.readExact(body)
|
|
36192
36973
|
});
|
|
36193
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
|
+
}
|
|
36194
36981
|
if (action === "compensate") {
|
|
36195
36982
|
return json({
|
|
36196
36983
|
receipt: await authority.compensate(body)
|
|
@@ -36265,6 +37052,28 @@ class TodosProjectRegistrationHttpClient {
|
|
|
36265
37052
|
async lookupReceipt(request) {
|
|
36266
37053
|
return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
|
|
36267
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
|
+
}
|
|
36268
37077
|
async compensate(request) {
|
|
36269
37078
|
const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
36270
37079
|
return body.receipt;
|
|
@@ -40258,7 +41067,9 @@ init_types();
|
|
|
40258
41067
|
|
|
40259
41068
|
// src/task-manifest/types.ts
|
|
40260
41069
|
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
|
|
41070
|
+
var TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1";
|
|
40261
41071
|
var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
|
|
41072
|
+
var TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1";
|
|
40262
41073
|
function supportsIdempotentOutboxDelivery(capability2) {
|
|
40263
41074
|
return capability2 !== null && typeof capability2 === "object" && capability2["idempotent_outbox_delivery"] === true;
|
|
40264
41075
|
}
|
|
@@ -40288,6 +41099,8 @@ var TODOS_TASK_MANIFEST_BOUNDS = {
|
|
|
40288
41099
|
};
|
|
40289
41100
|
var key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
40290
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}$/);
|
|
40291
41104
|
var uuid2 = exports_external.string().uuid();
|
|
40292
41105
|
var scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
40293
41106
|
var boundedScalarRecord = (limit, field2) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
@@ -40330,7 +41143,9 @@ var effect = exports_external.object({
|
|
|
40330
41143
|
var schema = exports_external.object({
|
|
40331
41144
|
version: exports_external.literal(1),
|
|
40332
41145
|
operation_id: identifier,
|
|
40333
|
-
|
|
41146
|
+
step_id: identifier,
|
|
41147
|
+
idempotency_key: idempotencyKey,
|
|
41148
|
+
precondition_digest: digest,
|
|
40334
41149
|
project_id: uuid2,
|
|
40335
41150
|
task_list_id: uuid2.optional(),
|
|
40336
41151
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -40346,7 +41161,10 @@ var schema = exports_external.object({
|
|
|
40346
41161
|
}).strict();
|
|
40347
41162
|
var compensationSchema = exports_external.object({
|
|
40348
41163
|
receipt_id: uuid2,
|
|
40349
|
-
|
|
41164
|
+
operation_id: identifier,
|
|
41165
|
+
step_id: identifier,
|
|
41166
|
+
idempotency_key: idempotencyKey,
|
|
41167
|
+
precondition_digest: digest,
|
|
40350
41168
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
40351
41169
|
}).strict();
|
|
40352
41170
|
var bindingLookupSchema = exports_external.object({
|
|
@@ -40431,10 +41249,31 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
40431
41249
|
}
|
|
40432
41250
|
|
|
40433
41251
|
// src/task-manifest/plan-slug.ts
|
|
41252
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
40434
41253
|
function taskManifestPlanSlug(manifest, planId) {
|
|
40435
41254
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
40436
41255
|
return `${base}-${planId}`;
|
|
40437
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
|
+
}
|
|
40438
41277
|
|
|
40439
41278
|
// src/task-manifest/backend.ts
|
|
40440
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;
|
|
@@ -40448,11 +41287,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
40448
41287
|
const row = rows[0];
|
|
40449
41288
|
const bindingVersion = Number(row.binding_version);
|
|
40450
41289
|
const state = row.state;
|
|
40451
|
-
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") {
|
|
40452
41291
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
40453
41292
|
}
|
|
40454
41293
|
return {
|
|
40455
41294
|
plan_id: planId,
|
|
41295
|
+
operation_id: row.binding_operation_id,
|
|
41296
|
+
step_id: row.binding_step_id,
|
|
40456
41297
|
apply_receipt_id: row.apply_receipt_id,
|
|
40457
41298
|
binding_version: bindingVersion,
|
|
40458
41299
|
state
|
|
@@ -40560,9 +41401,15 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40560
41401
|
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
40561
41402
|
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
40562
41403
|
operation_id TEXT NOT NULL,
|
|
41404
|
+
step_id TEXT NOT NULL,
|
|
40563
41405
|
idempotency_key TEXT NOT NULL,
|
|
40564
41406
|
request_digest TEXT NOT NULL,
|
|
41407
|
+
precondition_digest TEXT NOT NULL,
|
|
40565
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,
|
|
40566
41413
|
binding_version INTEGER NOT NULL,
|
|
40567
41414
|
apply_receipt_id TEXT,
|
|
40568
41415
|
manifest_json TEXT,
|
|
@@ -40573,9 +41420,13 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40573
41420
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
40574
41421
|
operation_id TEXT PRIMARY KEY,
|
|
40575
41422
|
tenant_id TEXT NOT NULL,
|
|
41423
|
+
step_id TEXT NOT NULL,
|
|
40576
41424
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
40577
41425
|
request_digest TEXT NOT NULL,
|
|
41426
|
+
precondition_digest TEXT NOT NULL,
|
|
40578
41427
|
result_digest TEXT NOT NULL,
|
|
41428
|
+
slug_provenance TEXT,
|
|
41429
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
40579
41430
|
apply_receipt_id TEXT NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
40580
41431
|
manifest_json TEXT NOT NULL,
|
|
40581
41432
|
result_json TEXT NOT NULL,
|
|
@@ -40596,6 +41447,27 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40596
41447
|
created_at TEXT NOT NULL,
|
|
40597
41448
|
delivered_at TEXT
|
|
40598
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
|
+
);
|
|
40599
41471
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_outbox_receipt
|
|
40600
41472
|
ON todos_task_manifest_outbox(apply_receipt_id, status);
|
|
40601
41473
|
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_receipts_immutable_update
|
|
@@ -40606,6 +41478,14 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
40606
41478
|
BEFORE DELETE ON todos_task_manifest_receipts BEGIN
|
|
40607
41479
|
SELECT RAISE(ABORT, 'todos task manifest receipts are immutable');
|
|
40608
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;
|
|
40609
41489
|
`;
|
|
40610
41490
|
}
|
|
40611
41491
|
function sqliteTableHasColumn(db, tableName, columnName) {
|
|
@@ -40622,6 +41502,24 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
40622
41502
|
if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
|
|
40623
41503
|
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
|
|
40624
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
|
+
}
|
|
40625
41523
|
}
|
|
40626
41524
|
db.exec(`
|
|
40627
41525
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
|
|
@@ -40631,6 +41529,12 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
40631
41529
|
tenant_id,
|
|
40632
41530
|
json_extract(result_json, '$.graph.plan_id')
|
|
40633
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);
|
|
40634
41538
|
`);
|
|
40635
41539
|
}
|
|
40636
41540
|
function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
@@ -40644,9 +41548,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40644
41548
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
40645
41549
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
40646
41550
|
operation_id text NOT NULL,
|
|
41551
|
+
step_id text NOT NULL,
|
|
40647
41552
|
idempotency_key text NOT NULL,
|
|
40648
41553
|
request_digest text NOT NULL,
|
|
41554
|
+
precondition_digest text NOT NULL,
|
|
40649
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,
|
|
40650
41560
|
binding_version integer NOT NULL,
|
|
40651
41561
|
apply_receipt_id text,
|
|
40652
41562
|
manifest_json jsonb,
|
|
@@ -40658,12 +41568,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40658
41568
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
40659
41569
|
`ALTER TABLE todos_task_manifest_receipts
|
|
40660
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`,
|
|
40661
41583
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
40662
41584
|
operation_id text PRIMARY KEY,
|
|
40663
41585
|
tenant_id text NOT NULL,
|
|
41586
|
+
step_id text NOT NULL,
|
|
40664
41587
|
idempotency_key text NOT NULL UNIQUE,
|
|
40665
41588
|
request_digest text NOT NULL,
|
|
41589
|
+
precondition_digest text NOT NULL,
|
|
40666
41590
|
result_digest text NOT NULL,
|
|
41591
|
+
slug_provenance text,
|
|
41592
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
40667
41593
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
40668
41594
|
manifest_json jsonb NOT NULL,
|
|
40669
41595
|
result_json jsonb NOT NULL,
|
|
@@ -40677,6 +41603,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40677
41603
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
40678
41604
|
`ALTER TABLE todos_task_manifest_bindings
|
|
40679
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'`,
|
|
40680
41614
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
40681
41615
|
id text PRIMARY KEY,
|
|
40682
41616
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -40688,10 +41622,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40688
41622
|
created_at timestamptz NOT NULL,
|
|
40689
41623
|
delivered_at timestamptz
|
|
40690
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
|
+
)`,
|
|
40691
41646
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
40692
41647
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
40693
41648
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
40694
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)`,
|
|
40695
41656
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
40696
41657
|
ON todos_task_manifest_bindings(
|
|
40697
41658
|
tenant_id,
|
|
@@ -40704,6 +41665,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
40704
41665
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
40705
41666
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
40706
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
|
|
40707
41672
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
40708
41673
|
];
|
|
40709
41674
|
}
|
|
@@ -40715,7 +41680,76 @@ function fault(faults, point) {
|
|
|
40715
41680
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
40716
41681
|
}
|
|
40717
41682
|
function parseApplyResult(value, duplicate) {
|
|
40718
|
-
|
|
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
|
+
}
|
|
40719
41753
|
}
|
|
40720
41754
|
|
|
40721
41755
|
class SqliteTodosTaskManifestBackend {
|
|
@@ -40755,33 +41789,49 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40755
41789
|
async apply(input, faults) {
|
|
40756
41790
|
return this.serialized(() => {
|
|
40757
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);
|
|
40758
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);
|
|
40759
41802
|
if (binding) {
|
|
40760
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
40761
|
-
|
|
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 };
|
|
40762
41809
|
}
|
|
40763
41810
|
if (binding["state"] !== "applied") {
|
|
40764
|
-
|
|
41811
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
40765
41812
|
}
|
|
40766
41813
|
return parseApplyResult(String(binding["result_json"]), true);
|
|
40767
41814
|
}
|
|
40768
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);
|
|
40769
41816
|
if (idempotency)
|
|
40770
|
-
|
|
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
|
+
}
|
|
40771
41821
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
40772
|
-
|
|
41822
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
40773
41823
|
}
|
|
40774
41824
|
if (!this.db.query("SELECT 1 AS found FROM projects WHERE id = ? LIMIT 1").get(manifest.project_id)) {
|
|
40775
|
-
|
|
41825
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
40776
41826
|
}
|
|
40777
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)) {
|
|
40778
|
-
|
|
41828
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
40779
41829
|
}
|
|
40780
41830
|
const allIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids];
|
|
40781
41831
|
for (const id of allIds) {
|
|
40782
41832
|
for (const table of ["plans", "tasks", "task_comments", "task_verifications"]) {
|
|
40783
41833
|
if (this.db.query(`SELECT 1 AS found FROM ${table} WHERE id = ? LIMIT 1`).get(id)) {
|
|
40784
|
-
|
|
41834
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
40785
41835
|
}
|
|
40786
41836
|
}
|
|
40787
41837
|
}
|
|
@@ -40837,9 +41887,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40837
41887
|
schema_version: 1,
|
|
40838
41888
|
kind: "apply",
|
|
40839
41889
|
operation_id: manifest.operation_id,
|
|
41890
|
+
step_id: manifest.step_id,
|
|
40840
41891
|
idempotency_key: manifest.idempotency_key,
|
|
40841
41892
|
request_digest: input.request_digest,
|
|
41893
|
+
precondition_digest: manifest.precondition_digest,
|
|
40842
41894
|
result_digest: input.result_digest,
|
|
41895
|
+
outcome: "accepted",
|
|
41896
|
+
reason: null,
|
|
41897
|
+
duplicate_of_receipt_id: null,
|
|
40843
41898
|
binding_version: 1,
|
|
40844
41899
|
apply_receipt_id: null,
|
|
40845
41900
|
created_at: input.now
|
|
@@ -40855,9 +41910,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40855
41910
|
const resultJson = canonicalJson(result);
|
|
40856
41911
|
const manifestJson = canonicalJson(manifest);
|
|
40857
41912
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40858
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40859
|
-
request_digest,
|
|
40860
|
-
|
|
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);
|
|
40861
41917
|
for (const entry2 of input.outbox) {
|
|
40862
41918
|
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
40863
41919
|
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
@@ -40865,18 +41921,37 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40865
41921
|
}
|
|
40866
41922
|
fault(faults, "after_outbox_write");
|
|
40867
41923
|
this.db.query(`INSERT INTO todos_task_manifest_bindings (
|
|
40868
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
40869
|
-
manifest_json, result_json, state, version, created_at, updated_at
|
|
40870
|
-
) 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);
|
|
40871
41927
|
fault(faults, "after_receipt_write");
|
|
40872
41928
|
return result;
|
|
40873
41929
|
});
|
|
40874
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
|
+
}
|
|
40875
41947
|
async readExact(receiptId2) {
|
|
40876
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);
|
|
40877
|
-
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)
|
|
40878
41953
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
40879
|
-
return parseApplyResult(
|
|
41954
|
+
return parseApplyResult(terminal.result_json, false);
|
|
40880
41955
|
}
|
|
40881
41956
|
async lookupBindingByPlanId(planId) {
|
|
40882
41957
|
const rows = this.db.query(`
|
|
@@ -40886,6 +41961,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40886
41961
|
b.version AS binding_version,
|
|
40887
41962
|
b.tenant_id AS binding_tenant_id,
|
|
40888
41963
|
b.operation_id AS binding_operation_id,
|
|
41964
|
+
b.step_id AS binding_step_id,
|
|
40889
41965
|
json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
|
|
40890
41966
|
r.tenant_id AS receipt_tenant_id,
|
|
40891
41967
|
r.authority AS receipt_authority,
|
|
@@ -40893,6 +41969,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40893
41969
|
r.schema_version AS receipt_schema_version,
|
|
40894
41970
|
r.kind AS receipt_kind,
|
|
40895
41971
|
r.operation_id AS receipt_operation_id,
|
|
41972
|
+
r.step_id AS receipt_step_id,
|
|
40896
41973
|
json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
|
|
40897
41974
|
FROM todos_task_manifest_bindings b
|
|
40898
41975
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -40955,6 +42032,12 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40955
42032
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
40956
42033
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
40957
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
|
+
}
|
|
40958
42041
|
if (binding["state"] !== "applied")
|
|
40959
42042
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
40960
42043
|
const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -40965,10 +42048,16 @@ class SqliteTodosTaskManifestBackend {
|
|
|
40965
42048
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
40966
42049
|
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
40967
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;
|
|
40968
42053
|
const expectedEffects = [
|
|
40969
42054
|
{
|
|
40970
42055
|
topic: "todos.task-manifest.applied",
|
|
40971
|
-
payload: {
|
|
42056
|
+
payload: {
|
|
42057
|
+
operation_id: manifest.operation_id,
|
|
42058
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
42059
|
+
project_id: manifest.project_id
|
|
42060
|
+
}
|
|
40972
42061
|
},
|
|
40973
42062
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
40974
42063
|
];
|
|
@@ -41008,8 +42097,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41008
42097
|
if (canonicalJson(actualReadback) !== canonicalJson(applyResult.readback)) {
|
|
41009
42098
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: graph changed since apply", { actualReadback });
|
|
41010
42099
|
}
|
|
41011
|
-
const
|
|
41012
|
-
|
|
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)) {
|
|
41013
42104
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
41014
42105
|
}
|
|
41015
42106
|
for (const task3 of manifest.tasks) {
|
|
@@ -41086,9 +42177,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
41086
42177
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
41087
42178
|
const resultJson = canonicalJson(result);
|
|
41088
42179
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
41089
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
41090
|
-
request_digest,
|
|
41091
|
-
|
|
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);
|
|
41092
42184
|
const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
|
|
41093
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);
|
|
41094
42186
|
if (updated.changes !== 1) {
|
|
@@ -41122,6 +42214,39 @@ function safeIdentifier2(value, field2) {
|
|
|
41122
42214
|
function parseJson2(value) {
|
|
41123
42215
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
41124
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
|
+
}
|
|
41125
42250
|
function timestamp3(value) {
|
|
41126
42251
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
41127
42252
|
}
|
|
@@ -41129,6 +42254,41 @@ function fault2(faults, point) {
|
|
|
41129
42254
|
if (faults.points.has(point))
|
|
41130
42255
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
41131
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
|
+
}
|
|
41132
42292
|
function receiptFromRow3(row) {
|
|
41133
42293
|
return {
|
|
41134
42294
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -41137,9 +42297,14 @@ function receiptFromRow3(row) {
|
|
|
41137
42297
|
schema_version: 1,
|
|
41138
42298
|
kind: row["kind"],
|
|
41139
42299
|
operation_id: String(row["operation_id"]),
|
|
42300
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
41140
42301
|
idempotency_key: String(row["idempotency_key"]),
|
|
41141
42302
|
request_digest: String(row["request_digest"]),
|
|
42303
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
41142
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"]),
|
|
41143
42308
|
binding_version: Number(row["binding_version"]),
|
|
41144
42309
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
41145
42310
|
created_at: timestamp3(row["created_at"])
|
|
@@ -41257,46 +42422,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41257
42422
|
now4
|
|
41258
42423
|
]);
|
|
41259
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
|
+
}
|
|
41260
42456
|
async apply(input, faults) {
|
|
41261
42457
|
await this.ensureSchema();
|
|
41262
42458
|
return this.client.transaction(async (tx) => {
|
|
41263
42459
|
const { manifest } = input;
|
|
41264
42460
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
41265
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
|
+
}
|
|
41266
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]);
|
|
41267
42472
|
if (existing.rows[0]) {
|
|
41268
42473
|
const binding = existing.rows[0];
|
|
41269
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
41270
|
-
|
|
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");
|
|
41271
42476
|
}
|
|
41272
42477
|
if (binding["state"] !== "applied") {
|
|
41273
|
-
|
|
42478
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41274
42479
|
}
|
|
41275
|
-
return
|
|
42480
|
+
return parseApplyResult2(binding["result_json"], true);
|
|
41276
42481
|
}
|
|
41277
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]);
|
|
41278
42483
|
if (reused.rows[0])
|
|
41279
|
-
|
|
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
|
+
}
|
|
41280
42488
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
41281
|
-
|
|
42489
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
41282
42490
|
}
|
|
41283
42491
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
41284
42492
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
41285
42493
|
if (!project.rows[0])
|
|
41286
|
-
|
|
42494
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
41287
42495
|
if (manifest.task_list_id) {
|
|
41288
42496
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
41289
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]);
|
|
41290
42498
|
const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
|
|
41291
42499
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
41292
|
-
|
|
42500
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
41293
42501
|
}
|
|
41294
42502
|
}
|
|
41295
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];
|
|
41296
42504
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
41297
42505
|
WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
41298
42506
|
if (conflict.rows[0])
|
|
41299
|
-
|
|
42507
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
41300
42508
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
41301
42509
|
fault2(faults, "after_plan_write");
|
|
41302
42510
|
for (const task3 of manifest.tasks) {
|
|
@@ -41366,9 +42574,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41366
42574
|
schema_version: 1,
|
|
41367
42575
|
kind: "apply",
|
|
41368
42576
|
operation_id: manifest.operation_id,
|
|
42577
|
+
step_id: manifest.step_id,
|
|
41369
42578
|
idempotency_key: manifest.idempotency_key,
|
|
41370
42579
|
request_digest: input.request_digest,
|
|
42580
|
+
precondition_digest: manifest.precondition_digest,
|
|
41371
42581
|
result_digest: input.result_digest,
|
|
42582
|
+
outcome: "accepted",
|
|
42583
|
+
reason: null,
|
|
42584
|
+
duplicate_of_receipt_id: null,
|
|
41372
42585
|
binding_version: 1,
|
|
41373
42586
|
apply_receipt_id: null,
|
|
41374
42587
|
created_at: input.now
|
|
@@ -41385,14 +42598,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41385
42598
|
const resultJson = canonicalJson(result);
|
|
41386
42599
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
41387
42600
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
41388
|
-
|
|
41389
|
-
|
|
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)`, [
|
|
41390
42604
|
input.receipt_id,
|
|
41391
42605
|
this.tenantId,
|
|
41392
42606
|
manifest.operation_id,
|
|
41393
42607
|
manifest.idempotency_key,
|
|
42608
|
+
manifest.step_id,
|
|
41394
42609
|
input.request_digest,
|
|
42610
|
+
manifest.precondition_digest,
|
|
41395
42611
|
input.result_digest,
|
|
42612
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
41396
42613
|
manifestJson,
|
|
41397
42614
|
resultJson,
|
|
41398
42615
|
input.now
|
|
@@ -41411,14 +42628,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41411
42628
|
}
|
|
41412
42629
|
fault2(faults, "after_outbox_write");
|
|
41413
42630
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
41414
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
41415
|
-
|
|
41416
|
-
|
|
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)`, [
|
|
41417
42635
|
manifest.operation_id,
|
|
41418
42636
|
this.tenantId,
|
|
42637
|
+
manifest.step_id,
|
|
41419
42638
|
manifest.idempotency_key,
|
|
41420
42639
|
input.request_digest,
|
|
42640
|
+
manifest.precondition_digest,
|
|
41421
42641
|
input.result_digest,
|
|
42642
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
41422
42643
|
input.receipt_id,
|
|
41423
42644
|
manifestJson,
|
|
41424
42645
|
resultJson,
|
|
@@ -41431,9 +42652,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41431
42652
|
async readExact(receiptId2) {
|
|
41432
42653
|
await this.ensureSchema();
|
|
41433
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]);
|
|
41434
|
-
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])
|
|
41435
42659
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
41436
|
-
return
|
|
42660
|
+
return parseApplyResult2(terminal.rows[0]["result_json"], false);
|
|
41437
42661
|
}
|
|
41438
42662
|
async lookupBindingByPlanId(planId) {
|
|
41439
42663
|
await this.ensureSchema();
|
|
@@ -41444,6 +42668,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41444
42668
|
b.version AS binding_version,
|
|
41445
42669
|
b.tenant_id AS binding_tenant_id,
|
|
41446
42670
|
b.operation_id AS binding_operation_id,
|
|
42671
|
+
b.step_id AS binding_step_id,
|
|
41447
42672
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
41448
42673
|
r.tenant_id AS receipt_tenant_id,
|
|
41449
42674
|
r.authority AS receipt_authority,
|
|
@@ -41451,6 +42676,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41451
42676
|
r.schema_version AS receipt_schema_version,
|
|
41452
42677
|
r.kind AS receipt_kind,
|
|
41453
42678
|
r.operation_id AS receipt_operation_id,
|
|
42679
|
+
r.step_id AS receipt_step_id,
|
|
41454
42680
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
41455
42681
|
FROM todos_task_manifest_bindings b
|
|
41456
42682
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -41537,6 +42763,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41537
42763
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
41538
42764
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
41539
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
|
+
}
|
|
41540
42770
|
if (binding["state"] !== "applied")
|
|
41541
42771
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
41542
42772
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -41551,12 +42781,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41551
42781
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
41552
42782
|
if (delivered.rows[0])
|
|
41553
42783
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
41554
|
-
const applyResult =
|
|
42784
|
+
const applyResult = parseApplyResult2(applyRow["result_json"], false);
|
|
41555
42785
|
const manifest = parseJson2(applyRow["manifest_json"]);
|
|
42786
|
+
const manifestRecord = manifest;
|
|
42787
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
41556
42788
|
const expectedEffects = [
|
|
41557
42789
|
{
|
|
41558
42790
|
topic: "todos.task-manifest.applied",
|
|
41559
|
-
payload: {
|
|
42791
|
+
payload: {
|
|
42792
|
+
operation_id: manifest.operation_id,
|
|
42793
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
42794
|
+
project_id: manifest.project_id
|
|
42795
|
+
}
|
|
41560
42796
|
},
|
|
41561
42797
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
41562
42798
|
];
|
|
@@ -41604,9 +42840,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41604
42840
|
}
|
|
41605
42841
|
const appliedAt = receiptFromRow3(applyRow).created_at;
|
|
41606
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"]);
|
|
41607
42849
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
41608
42850
|
type: "plans",
|
|
41609
|
-
payload: canonicalJson(
|
|
42851
|
+
payload: canonicalJson(planExpected)
|
|
41610
42852
|
});
|
|
41611
42853
|
for (const task3 of manifest.tasks)
|
|
41612
42854
|
expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
|
|
@@ -41706,14 +42948,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
41706
42948
|
const readback = await this.readback(tx, applyResult.graph);
|
|
41707
42949
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
41708
42950
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
41709
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
41710
|
-
request_digest,
|
|
41711
|
-
|
|
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)`, [
|
|
41712
42955
|
compensationReceiptId,
|
|
41713
42956
|
this.tenantId,
|
|
41714
42957
|
receipt.operation_id,
|
|
42958
|
+
receipt.step_id,
|
|
41715
42959
|
input.idempotency_key,
|
|
41716
42960
|
requestDigest,
|
|
42961
|
+
input.precondition_digest,
|
|
41717
42962
|
receipt.result_digest,
|
|
41718
42963
|
receipt.binding_version,
|
|
41719
42964
|
input.receipt_id,
|
|
@@ -41773,36 +43018,83 @@ function resolveTenantId(value) {
|
|
|
41773
43018
|
}
|
|
41774
43019
|
return tenantId;
|
|
41775
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
|
+
}
|
|
41776
43055
|
function normalize(input, now4) {
|
|
41777
43056
|
const parsed = parseTodosTaskManifest(input);
|
|
41778
43057
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
41779
43058
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
41780
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 });
|
|
41781
43060
|
}
|
|
43061
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
43062
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
41782
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
|
+
});
|
|
41783
43076
|
const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
|
|
41784
43077
|
task3.key,
|
|
41785
|
-
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)
|
|
41786
43079
|
]));
|
|
41787
43080
|
const graph = {
|
|
41788
|
-
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),
|
|
41789
43082
|
task_ids,
|
|
41790
|
-
comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task3.key, String(index)))),
|
|
41791
|
-
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)))),
|
|
41792
43085
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
41793
43086
|
};
|
|
41794
|
-
const request_digest = canonicalDigest(parsed);
|
|
41795
43087
|
const effectInputs = [
|
|
41796
43088
|
{
|
|
41797
43089
|
topic: "todos.task-manifest.applied",
|
|
41798
|
-
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 }
|
|
41799
43091
|
},
|
|
41800
43092
|
...manifest.effects ?? []
|
|
41801
43093
|
];
|
|
41802
43094
|
const outbox = effectInputs.map((effect2, index) => {
|
|
41803
43095
|
const payload = { ...effect2.payload };
|
|
41804
43096
|
return {
|
|
41805
|
-
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)),
|
|
41806
43098
|
topic: effect2.topic,
|
|
41807
43099
|
payload,
|
|
41808
43100
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -41812,11 +43104,14 @@ function normalize(input, now4) {
|
|
|
41812
43104
|
return {
|
|
41813
43105
|
manifest,
|
|
41814
43106
|
request_digest,
|
|
43107
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
41815
43108
|
result_digest,
|
|
41816
|
-
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),
|
|
41817
43111
|
graph,
|
|
41818
43112
|
outbox,
|
|
41819
|
-
now: now4
|
|
43113
|
+
now: now4,
|
|
43114
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
41820
43115
|
};
|
|
41821
43116
|
}
|
|
41822
43117
|
function sanitizeManifest(manifest) {
|
|
@@ -41874,6 +43169,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41874
43169
|
tenant_id: this.tenantId,
|
|
41875
43170
|
backend: this.backend.kind,
|
|
41876
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,
|
|
41877
43176
|
immutable_receipts: true,
|
|
41878
43177
|
transactional_outbox: true,
|
|
41879
43178
|
idempotent_outbox_delivery: true,
|
|
@@ -41903,7 +43202,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41903
43202
|
async apply(input) {
|
|
41904
43203
|
const normalized = normalize(input, this.now());
|
|
41905
43204
|
const faults = await this.prepareFaults();
|
|
41906
|
-
|
|
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;
|
|
41907
43210
|
}
|
|
41908
43211
|
readExact(receiptId2) {
|
|
41909
43212
|
if (!receiptId2 || receiptId2.length > 200) {
|
|
@@ -41936,18 +43239,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
41936
43239
|
async compensate(input) {
|
|
41937
43240
|
const request = parseTodosTaskManifestCompensation(input);
|
|
41938
43241
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
41939
|
-
|
|
41940
|
-
|
|
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);
|
|
41941
43269
|
const receipt = {
|
|
41942
43270
|
receipt_id: compensationReceiptId,
|
|
41943
43271
|
authority: "todos",
|
|
41944
43272
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
41945
43273
|
schema_version: 1,
|
|
41946
43274
|
kind: "compensate",
|
|
41947
|
-
operation_id:
|
|
43275
|
+
operation_id: request.operation_id,
|
|
43276
|
+
step_id: request.step_id,
|
|
41948
43277
|
idempotency_key: request.idempotency_key,
|
|
41949
43278
|
request_digest: requestDigest,
|
|
43279
|
+
precondition_digest: request.precondition_digest,
|
|
41950
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,
|
|
41951
43284
|
binding_version: request.if_binding_version + 1,
|
|
41952
43285
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
41953
43286
|
created_at: this.now()
|
|
@@ -42129,7 +43462,7 @@ function createTodosTaskManifestHttpClient(options) {
|
|
|
42129
43462
|
return new TodosTaskManifestHttpClient(options);
|
|
42130
43463
|
}
|
|
42131
43464
|
// src/ai-tools.ts
|
|
42132
|
-
import { createHash as
|
|
43465
|
+
import { createHash as createHash16 } from "crypto";
|
|
42133
43466
|
// src/cli/cloud-router.ts
|
|
42134
43467
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
42135
43468
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
@@ -43437,7 +44770,7 @@ function deriveTodosAiUpdateTaskApprovalIdentity(input) {
|
|
|
43437
44770
|
expected_version: input.expected_version,
|
|
43438
44771
|
patch
|
|
43439
44772
|
});
|
|
43440
|
-
const payloadDigest =
|
|
44773
|
+
const payloadDigest = createHash16("sha256").update(canonical).digest("hex");
|
|
43441
44774
|
return {
|
|
43442
44775
|
ref: `todos-ai:update_task:${payloadDigest}`,
|
|
43443
44776
|
payload_digest: payloadDigest
|
|
@@ -43592,9 +44925,9 @@ function normalizeUpdateTaskInput(input) {
|
|
|
43592
44925
|
if (!Object.hasOwn(record, "expected_version")) {
|
|
43593
44926
|
throw new Error("expected_version is required");
|
|
43594
44927
|
}
|
|
43595
|
-
const
|
|
43596
|
-
const idempotencyBytes = ENCODER.encode(
|
|
43597
|
-
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)) {
|
|
43598
44931
|
throw new Error("idempotency_key must be a bounded stable identifier");
|
|
43599
44932
|
}
|
|
43600
44933
|
const patchValue = record["patch"];
|
|
@@ -43613,7 +44946,7 @@ function normalizeUpdateTaskInput(input) {
|
|
|
43613
44946
|
expected_version: expectedVersion,
|
|
43614
44947
|
patch,
|
|
43615
44948
|
changed_fields: changedFields,
|
|
43616
|
-
idempotency_key:
|
|
44949
|
+
idempotency_key: idempotencyKey2,
|
|
43617
44950
|
payload_digest: identity.payload_digest,
|
|
43618
44951
|
approval_ref: identity.ref
|
|
43619
44952
|
};
|
|
@@ -44294,7 +45627,7 @@ init_task_lifecycle();
|
|
|
44294
45627
|
init_task_crud();
|
|
44295
45628
|
init_redaction();
|
|
44296
45629
|
import { Database as Database3 } from "bun:sqlite";
|
|
44297
|
-
import { createHash as
|
|
45630
|
+
import { createHash as createHash17 } from "crypto";
|
|
44298
45631
|
import { existsSync as existsSync9, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
44299
45632
|
import { basename as basename2, dirname as dirname6, join as join9, resolve as resolve10 } from "path";
|
|
44300
45633
|
|
|
@@ -44577,8 +45910,8 @@ function normalizePath3(input) {
|
|
|
44577
45910
|
return resolve10(input);
|
|
44578
45911
|
}
|
|
44579
45912
|
function sourceStoreId(sourceDbPath) {
|
|
44580
|
-
const
|
|
44581
|
-
return `sqlite:${
|
|
45913
|
+
const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
45914
|
+
return `sqlite:${digest2}`;
|
|
44582
45915
|
}
|
|
44583
45916
|
function inferSourceRepoPath(sourceDbPath) {
|
|
44584
45917
|
const normalized = normalizePath3(sourceDbPath);
|
|
@@ -46539,7 +47872,7 @@ init_comments();
|
|
|
46539
47872
|
|
|
46540
47873
|
// src/db/api-keys.ts
|
|
46541
47874
|
init_database();
|
|
46542
|
-
import { createHash as
|
|
47875
|
+
import { createHash as createHash18, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
46543
47876
|
function rowToRecord(row) {
|
|
46544
47877
|
return {
|
|
46545
47878
|
id: row.id,
|
|
@@ -46553,7 +47886,7 @@ function rowToRecord(row) {
|
|
|
46553
47886
|
};
|
|
46554
47887
|
}
|
|
46555
47888
|
function hashApiKey(key2) {
|
|
46556
|
-
return
|
|
47889
|
+
return createHash18("sha256").update(key2).digest("hex");
|
|
46557
47890
|
}
|
|
46558
47891
|
function safeEqualHex(a, b) {
|
|
46559
47892
|
if (a.length !== b.length)
|
|
@@ -50454,7 +51787,7 @@ init_database();
|
|
|
50454
51787
|
init_tasks();
|
|
50455
51788
|
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
|
|
50456
51789
|
import { basename as basename5 } from "path";
|
|
50457
|
-
import { createHash as
|
|
51790
|
+
import { createHash as createHash19 } from "crypto";
|
|
50458
51791
|
init_secret_redaction();
|
|
50459
51792
|
var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
|
|
50460
51793
|
var INTAKE_SOURCE_TYPES = [
|
|
@@ -50467,7 +51800,7 @@ var INTAKE_SOURCE_TYPES = [
|
|
|
50467
51800
|
];
|
|
50468
51801
|
var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
|
|
50469
51802
|
function fingerprint2(text) {
|
|
50470
|
-
return
|
|
51803
|
+
return createHash19("sha256").update(text).digest("hex").slice(0, 16);
|
|
50471
51804
|
}
|
|
50472
51805
|
function loadRawContent(input) {
|
|
50473
51806
|
if (input.github_url) {
|
|
@@ -55927,7 +57260,7 @@ init_database();
|
|
|
55927
57260
|
init_tasks();
|
|
55928
57261
|
init_redaction();
|
|
55929
57262
|
init_sync_utils();
|
|
55930
|
-
import { createHash as
|
|
57263
|
+
import { createHash as createHash20 } from "crypto";
|
|
55931
57264
|
import { existsSync as existsSync22, readFileSync as readFileSync20, statSync as statSync9 } from "fs";
|
|
55932
57265
|
import { hostname as hostname3, platform, arch } from "os";
|
|
55933
57266
|
import { dirname as dirname15, join as join18, resolve as resolve17 } from "path";
|
|
@@ -55949,7 +57282,7 @@ var CONFIG_FILES = [
|
|
|
55949
57282
|
"dashboard/vite.config.ts"
|
|
55950
57283
|
];
|
|
55951
57284
|
function sha2566(value) {
|
|
55952
|
-
return
|
|
57285
|
+
return createHash20("sha256").update(value).digest("hex");
|
|
55953
57286
|
}
|
|
55954
57287
|
function fileRecord(root, relativePath) {
|
|
55955
57288
|
const path = join18(root, relativePath);
|
|
@@ -56064,8 +57397,8 @@ function defaultSnapshotDir() {
|
|
|
56064
57397
|
return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
|
|
56065
57398
|
}
|
|
56066
57399
|
function snapshotWithId(snapshot) {
|
|
56067
|
-
const
|
|
56068
|
-
return { id: `env_${
|
|
57400
|
+
const digest2 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
57401
|
+
return { id: `env_${digest2}`, ...snapshot };
|
|
56069
57402
|
}
|
|
56070
57403
|
function captureEnvironmentSnapshot(input = {}) {
|
|
56071
57404
|
const root = resolve17(input.root || process.cwd());
|
|
@@ -56201,7 +57534,7 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
|
|
|
56201
57534
|
init_database();
|
|
56202
57535
|
init_projects();
|
|
56203
57536
|
init_plans();
|
|
56204
|
-
import { createHash as
|
|
57537
|
+
import { createHash as createHash21 } from "crypto";
|
|
56205
57538
|
import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "fs";
|
|
56206
57539
|
import { dirname as dirname16, join as join19 } from "path";
|
|
56207
57540
|
var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
|
|
@@ -56257,7 +57590,7 @@ function rowToDecisionRecord(row) {
|
|
|
56257
57590
|
}
|
|
56258
57591
|
function stableSnapshotHash(payload) {
|
|
56259
57592
|
const { captured_at: _capturedAt, ...rest } = payload;
|
|
56260
|
-
return
|
|
57593
|
+
return createHash21("sha256").update(JSON.stringify(rest)).digest("hex");
|
|
56261
57594
|
}
|
|
56262
57595
|
function createDecisionRecord(input, db) {
|
|
56263
57596
|
const d = db || getDatabase();
|
|
@@ -60750,7 +62083,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
60750
62083
|
init_tasks();
|
|
60751
62084
|
init_task_files();
|
|
60752
62085
|
import { existsSync as existsSync27, readFileSync as readFileSync24, statSync as statSync10 } from "fs";
|
|
60753
|
-
import { createHash as
|
|
62086
|
+
import { createHash as createHash22 } from "crypto";
|
|
60754
62087
|
import { relative as relative6, resolve as resolve18, join as join25 } from "path";
|
|
60755
62088
|
var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
60756
62089
|
var DEFAULT_EXTENSIONS = new Set([
|
|
@@ -60815,7 +62148,7 @@ var SKIP_DIRS2 = new Set([
|
|
|
60815
62148
|
".parcel-cache"
|
|
60816
62149
|
]);
|
|
60817
62150
|
function stableHash(value) {
|
|
60818
|
-
return
|
|
62151
|
+
return createHash22("sha256").update(value).digest("hex");
|
|
60819
62152
|
}
|
|
60820
62153
|
function normalizePathForMatch(value) {
|
|
60821
62154
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -61713,7 +63046,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
|
|
|
61713
63046
|
}
|
|
61714
63047
|
// src/lib/agent-replay-simulator.ts
|
|
61715
63048
|
init_redaction();
|
|
61716
|
-
import { createHash as
|
|
63049
|
+
import { createHash as createHash23 } from "crypto";
|
|
61717
63050
|
import { readFileSync as readFileSync25 } from "fs";
|
|
61718
63051
|
function isObject(value) {
|
|
61719
63052
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -61735,7 +63068,7 @@ function stable2(value) {
|
|
|
61735
63068
|
return Object.fromEntries(Object.keys(value).sort().map((key2) => [key2, stable2(value[key2])]));
|
|
61736
63069
|
}
|
|
61737
63070
|
function fingerprint3(value) {
|
|
61738
|
-
return
|
|
63071
|
+
return createHash23("sha256").update(JSON.stringify(stable2(value))).digest("hex");
|
|
61739
63072
|
}
|
|
61740
63073
|
function unpackFixture(input) {
|
|
61741
63074
|
if (!isObject(input))
|
|
@@ -61974,7 +63307,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
|
|
|
61974
63307
|
}
|
|
61975
63308
|
// src/lib/local-extensions.ts
|
|
61976
63309
|
init_config2();
|
|
61977
|
-
import { createHash as
|
|
63310
|
+
import { createHash as createHash24, createVerify } from "crypto";
|
|
61978
63311
|
import { existsSync as existsSync28, readdirSync as readdirSync6, readFileSync as readFileSync26, statSync as statSync11 } from "fs";
|
|
61979
63312
|
import { basename as basename6, join as join26, resolve as resolve19 } from "path";
|
|
61980
63313
|
init_redaction();
|
|
@@ -62062,7 +63395,7 @@ function parseJson3(path) {
|
|
|
62062
63395
|
return JSON.parse(readFileSync26(path, "utf8"));
|
|
62063
63396
|
}
|
|
62064
63397
|
function sha2567(bytes) {
|
|
62065
|
-
return `sha256:${
|
|
63398
|
+
return `sha256:${createHash24("sha256").update(bytes).digest("hex")}`;
|
|
62066
63399
|
}
|
|
62067
63400
|
function compareVersions(a, b) {
|
|
62068
63401
|
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -64677,6 +66010,8 @@ export {
|
|
|
64677
66010
|
testTerminalNotificationRule,
|
|
64678
66011
|
testLocalEventHook,
|
|
64679
66012
|
tasksFromTemplate,
|
|
66013
|
+
taskManifestRequestDigest,
|
|
66014
|
+
taskManifestCompensationRequestDigest,
|
|
64680
66015
|
taskFromTemplate,
|
|
64681
66016
|
tagToPriority,
|
|
64682
66017
|
syncWithAgents,
|
|
@@ -65411,6 +66746,9 @@ export {
|
|
|
65411
66746
|
detectInboxSourceType,
|
|
65412
66747
|
detectCyclesFromEdges,
|
|
65413
66748
|
describeTerminalNotificationRule,
|
|
66749
|
+
deriveTodosTaskManifestIdempotencyKey,
|
|
66750
|
+
deriveTodosTaskManifestCompensationPreconditionDigest,
|
|
66751
|
+
deriveTodosTaskManifestApplyPreconditionDigest,
|
|
65414
66752
|
deriveTodosProjectRegistrationIdempotencyKey,
|
|
65415
66753
|
deriveTodosAiUpdateTaskApprovalIdentity,
|
|
65416
66754
|
deriveInboxTitle,
|
|
@@ -65655,6 +66993,8 @@ export {
|
|
|
65655
66993
|
TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
65656
66994
|
TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
65657
66995
|
TODOS_TASK_MANIFEST_ROUTE,
|
|
66996
|
+
TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
66997
|
+
TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
65658
66998
|
TODOS_TASK_MANIFEST_BOUNDS,
|
|
65659
66999
|
TODOS_STORAGE_TABLES,
|
|
65660
67000
|
TODOS_STORAGE_FALLBACK_ENV,
|