@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.
Files changed (72) hide show
  1. package/dist/cli/cloud-router.d.ts +15 -0
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-registration-commands.d.ts +5 -0
  4. package/dist/cli/commands/project-registration-commands.d.ts.map +1 -0
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +13040 -10368
  8. package/dist/contracts.js +64 -2
  9. package/dist/db/task-crud.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1489 -149
  13. package/dist/lib/task-parent-integrity.d.ts +14 -0
  14. package/dist/lib/task-parent-integrity.d.ts.map +1 -0
  15. package/dist/mcp/index.js +2073 -140
  16. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/project-registration/adoption-validation.d.ts +3 -0
  19. package/dist/project-registration/adoption-validation.d.ts.map +1 -0
  20. package/dist/project-registration/authority.d.ts +3 -1
  21. package/dist/project-registration/authority.d.ts.map +1 -1
  22. package/dist/project-registration/backend.d.ts +26 -1
  23. package/dist/project-registration/backend.d.ts.map +1 -1
  24. package/dist/project-registration/http.d.ts +3 -1
  25. package/dist/project-registration/http.d.ts.map +1 -1
  26. package/dist/project-registration/index.d.ts +2 -1
  27. package/dist/project-registration/index.d.ts.map +1 -1
  28. package/dist/project-registration/page-validation.d.ts +5 -0
  29. package/dist/project-registration/page-validation.d.ts.map +1 -0
  30. package/dist/project-registration/postgres.d.ts +13 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration/schema.d.ts.map +1 -1
  33. package/dist/project-registration/sqlite.d.ts +13 -1
  34. package/dist/project-registration/sqlite.d.ts.map +1 -1
  35. package/dist/project-registration/types.d.ts +68 -1
  36. package/dist/project-registration/types.d.ts.map +1 -1
  37. package/dist/project-registration.js +873 -63
  38. package/dist/registry.js +64 -2
  39. package/dist/release-provenance.json +5 -5
  40. package/dist/sdk/index.d.ts +1 -1
  41. package/dist/sdk/index.d.ts.map +1 -1
  42. package/dist/sdk/index.js +151 -0
  43. package/dist/sdk/v1.generated.d.ts +282 -0
  44. package/dist/sdk/v1.generated.d.ts.map +1 -1
  45. package/dist/server/index.js +2064 -131
  46. package/dist/server/openapi.d.ts +2856 -1357
  47. package/dist/server/openapi.d.ts.map +1 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  50. package/dist/storage/postgres-sync.d.ts +8 -0
  51. package/dist/storage/postgres-sync.d.ts.map +1 -1
  52. package/dist/storage.js +244 -19
  53. package/dist/task-manifest/authority.d.ts +13 -1
  54. package/dist/task-manifest/authority.d.ts.map +1 -1
  55. package/dist/task-manifest/backend.d.ts +5 -0
  56. package/dist/task-manifest/backend.d.ts.map +1 -1
  57. package/dist/task-manifest/index.d.ts +2 -2
  58. package/dist/task-manifest/index.d.ts.map +1 -1
  59. package/dist/task-manifest/plan-slug.d.ts +20 -0
  60. package/dist/task-manifest/plan-slug.d.ts.map +1 -1
  61. package/dist/task-manifest/postgres.d.ts +1 -0
  62. package/dist/task-manifest/postgres.d.ts.map +1 -1
  63. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  64. package/dist/task-manifest/schema.d.ts.map +1 -1
  65. package/dist/task-manifest/sqlite.d.ts +1 -0
  66. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  67. package/dist/task-manifest/types.d.ts +21 -1
  68. package/dist/task-manifest/types.d.ts.map +1 -1
  69. package/dist/task-manifest.js +592 -61
  70. package/dist/types/index.d.ts +4 -2
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/package.json +1 -1
package/dist/mcp/index.js CHANGED
@@ -289,6 +289,12 @@ function sqliteTodosProjectRegistrationSchemaSql() {
289
289
  authority_id, tenant_id, corpus_id, operation_id, step_id,
290
290
  resource_kind, direction, idempotency_key
291
291
  );
292
+ CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_source_identity
293
+ ON todos_project_registration_receipts (
294
+ authority_id, tenant_id, corpus_id, route, package_version,
295
+ operation_id, step_id, resource_kind, direction, idempotency_key,
296
+ target_selector
297
+ );
292
298
  CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
293
299
  ON todos_project_registration_receipts (
294
300
  authority_id, tenant_id, corpus_id, operation_id, step_id,
@@ -388,6 +394,12 @@ function postgresTodosProjectRegistrationSchemaSql() {
388
394
  authority_id, tenant_id, corpus_id, operation_id, step_id,
389
395
  resource_kind, direction, idempotency_key
390
396
  )`,
397
+ `CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_source_identity_idx
398
+ ON todos_project_registration_receipts (
399
+ authority_id, tenant_id, corpus_id, route, package_version,
400
+ operation_id, step_id, resource_kind, direction, idempotency_key,
401
+ target_selector
402
+ )`,
391
403
  `CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
392
404
  ON todos_project_registration_receipts (
393
405
  authority_id, tenant_id, corpus_id, operation_id, step_id,
@@ -12480,6 +12492,46 @@ function guardPlanRowsSqlite(planIds, db) {
12480
12492
  }
12481
12493
  }
12482
12494
 
12495
+ // src/lib/task-parent-integrity.ts
12496
+ function parentCycleError(taskId, parentId) {
12497
+ return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
12498
+ }
12499
+ function assertTaskParentIntegrity(taskId, parentId, getTask) {
12500
+ if (parentId === undefined || parentId === null)
12501
+ return;
12502
+ const visited = new Set;
12503
+ let cursor = parentId;
12504
+ while (cursor) {
12505
+ if (cursor === taskId || visited.has(cursor)) {
12506
+ throw parentCycleError(taskId, parentId);
12507
+ }
12508
+ visited.add(cursor);
12509
+ const parent = getTask(cursor);
12510
+ if (!parent)
12511
+ throw new TaskNotFoundError(cursor);
12512
+ cursor = parent.parent_id;
12513
+ }
12514
+ }
12515
+ async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
12516
+ if (parentId === undefined || parentId === null)
12517
+ return;
12518
+ const visited = new Set;
12519
+ let cursor = parentId;
12520
+ while (cursor) {
12521
+ if (cursor === taskId || visited.has(cursor)) {
12522
+ throw parentCycleError(taskId, parentId);
12523
+ }
12524
+ visited.add(cursor);
12525
+ const parent = await getTask(cursor);
12526
+ if (!parent)
12527
+ throw new TaskNotFoundError(cursor);
12528
+ cursor = parent.parent_id;
12529
+ }
12530
+ }
12531
+ var init_task_parent_integrity = __esm(() => {
12532
+ init_types();
12533
+ });
12534
+
12483
12535
  // src/lib/creator-identity.ts
12484
12536
  import { existsSync as existsSync6, rmSync } from "fs";
12485
12537
  import { join as join5 } from "path";
@@ -14059,6 +14111,7 @@ function createTaskStored(input, d) {
14059
14111
  let id = uuid();
14060
14112
  for (let attempt = 0;attempt < 3; attempt++) {
14061
14113
  try {
14114
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
14062
14115
  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)
14063
14116
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
14064
14117
  id,
@@ -14437,6 +14490,7 @@ function updateTaskStored(id, input, db) {
14437
14490
  throw new VersionConflictError(id, input.version, task.version);
14438
14491
  }
14439
14492
  input = sanitizeUpdateTaskInput(input);
14493
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
14440
14494
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
14441
14495
  const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
14442
14496
  if (linkedProjectId) {
@@ -14490,6 +14544,10 @@ function updateTaskStored(id, input, db) {
14490
14544
  sets.push("project_id = ?");
14491
14545
  params.push(input.project_id);
14492
14546
  }
14547
+ if (input.parent_id !== undefined) {
14548
+ sets.push("parent_id = ?");
14549
+ params.push(input.parent_id);
14550
+ }
14493
14551
  if (input.assigned_to !== undefined) {
14494
14552
  sets.push("assigned_to = ?");
14495
14553
  params.push(input.assigned_to);
@@ -14605,6 +14663,8 @@ function updateTaskStored(id, input, db) {
14605
14663
  logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
14606
14664
  if (input.title !== undefined && input.title !== task.title)
14607
14665
  logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
14666
+ if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
14667
+ logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
14608
14668
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
14609
14669
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
14610
14670
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
@@ -14662,7 +14722,8 @@ function updateTask(id, input, db) {
14662
14722
  if (!before)
14663
14723
  throw new TaskNotFoundError(id);
14664
14724
  const guardedPlanIds = [before.plan_id, input.plan_id];
14665
- if (!guardedPlanIds.some(Boolean))
14725
+ const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
14726
+ if (!needsSerializedWrite)
14666
14727
  return updateTaskStored(id, input, d);
14667
14728
  return d.transaction(() => {
14668
14729
  guardPlanRowsSqlite(guardedPlanIds, d);
@@ -14698,6 +14759,7 @@ var init_task_crud = __esm(() => {
14698
14759
  init_checklists();
14699
14760
  init_storage_tombstones();
14700
14761
  init_prewrite_secrets();
14762
+ init_task_parent_integrity();
14701
14763
  });
14702
14764
 
14703
14765
  // src/db/task-status.ts
@@ -21004,6 +21066,31 @@ var init_http_client = __esm(() => {
21004
21066
  ]);
21005
21067
  });
21006
21068
 
21069
+ // src/project-registration/types.ts
21070
+ var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1", TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1", TodosProjectRegistrationError;
21071
+ var init_types4 = __esm(() => {
21072
+ TodosProjectRegistrationError = class TodosProjectRegistrationError extends Error {
21073
+ code;
21074
+ details;
21075
+ constructor(code, message, details = {}) {
21076
+ super(message);
21077
+ this.code = code;
21078
+ this.details = details;
21079
+ this.name = "TodosProjectRegistrationError";
21080
+ }
21081
+ };
21082
+ });
21083
+
21084
+ // src/project-registration/adoption-validation.ts
21085
+ var init_adoption_validation = __esm(() => {
21086
+ init_types4();
21087
+ });
21088
+
21089
+ // src/project-registration/page-validation.ts
21090
+ var init_page_validation = __esm(() => {
21091
+ init_types4();
21092
+ });
21093
+
21007
21094
  // src/cli/cloud-router.ts
21008
21095
  import { resolveStorageClient } from "@hasna/contracts/client/storage";
21009
21096
  import { normalizeStorageMode } from "@hasna/contracts/mode";
@@ -21404,12 +21491,39 @@ async function cloudListTasks(client, filter = {}) {
21404
21491
  union.sort(compareCloudTaskOrder);
21405
21492
  return union.slice(start, windowEnd);
21406
21493
  }
21494
+ async function cloudResolveTaskRef(client, ref) {
21495
+ const input = ref.trim().toLowerCase();
21496
+ if (!input)
21497
+ throw new Error("Task reference must not be empty");
21498
+ if (UUID_RE.test(input))
21499
+ return input;
21500
+ let task;
21501
+ try {
21502
+ task = await cloudGetTask(client, input);
21503
+ } catch (error) {
21504
+ const status = error && typeof error === "object" ? error.status : undefined;
21505
+ if (status === 409) {
21506
+ const body = error && typeof error === "object" ? error.body : undefined;
21507
+ const authorityMessage = body && typeof body === "object" && !Array.isArray(body) ? body.error : undefined;
21508
+ throw new Error(typeof authorityMessage === "string" && authorityMessage.trim() ? authorityMessage : `Task reference is ambiguous: "${ref}"`);
21509
+ }
21510
+ throw error;
21511
+ }
21512
+ if (task && typeof task.id === "string" && (task.short_id?.toLowerCase() === input || task.id.toLowerCase().startsWith(input))) {
21513
+ return task.id;
21514
+ }
21515
+ if (task) {
21516
+ throw new Error(`Task not found: ${ref} \u2014 the authority returned a task carrying neither this short id ` + "nor this id prefix, so the reference was not resolved.");
21517
+ }
21518
+ throw new Error(`Task not found: ${ref} \u2014 the authority resolved no task for this short reference. ` + "That is not proof the task is absent: an authority predating server-side " + "short-reference resolution answers EVERY short id and id prefix with the same 404. " + "Retry with the full task UUID to tell the two apart, or deploy the current " + "@hasna/todos /v1 server.");
21519
+ }
21407
21520
  async function cloudGetTask(client, id) {
21408
21521
  const raw = await client.get("tasks", id);
21409
21522
  return raw == null ? null : unwrapTask(raw);
21410
21523
  }
21411
21524
  async function cloudCreateTask(client, input, verification = {}) {
21412
21525
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
21526
+ const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
21413
21527
  const expectedCreatedBy = typeof verification.expectedCreatedBy === "string" && verification.expectedCreatedBy.trim() ? verification.expectedCreatedBy : null;
21414
21528
  if (expectedCreatedBy !== null) {
21415
21529
  await requireTaskCreatorCapability(client);
@@ -21422,9 +21536,9 @@ async function cloudCreateTask(client, input, verification = {}) {
21422
21536
  throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
21423
21537
  }
21424
21538
  const persisted = await cloudGetTask(client, created.id);
21425
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
21539
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
21426
21540
  const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
21427
- throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id and parent_id${creatorDetail}; no success row or local SQLite fallback is permitted`);
21541
+ throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id, parent_id, and plan_id ` + `(requested plan_id=${JSON.stringify(expectedPlanId)}, readback=${JSON.stringify(persisted?.plan_id ?? null)})` + `${creatorDetail}; no success row or local SQLite fallback is permitted`);
21428
21542
  }
21429
21543
  return persisted;
21430
21544
  }
@@ -21691,6 +21805,8 @@ var init_cloud_router = __esm(() => {
21691
21805
  init_redaction();
21692
21806
  init_plan_project_link_contract();
21693
21807
  init_http_client();
21808
+ init_adoption_validation();
21809
+ init_page_validation();
21694
21810
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21695
21811
  TRANSPORT_TOKENS = {
21696
21812
  sqlite: "sqlite",
@@ -21727,6 +21843,7 @@ function registerTaskCrudTools(server, ctx) {
21727
21843
  compact["version"] = task.version;
21728
21844
  compact["created_at"] = task.created_at;
21729
21845
  compact["task_list_id"] = task.task_list_id;
21846
+ compact["parent_id"] = task.parent_id;
21730
21847
  return compactJson(compact);
21731
21848
  }
21732
21849
  function versionFor(taskId, version) {
@@ -21996,6 +22113,7 @@ ${task.description}` : null
21996
22113
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
21997
22114
  assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
21998
22115
  project_id: exports_external.string().nullable().optional(),
22116
+ parent_id: exports_external.string().nullable().optional().describe("Existing parent task ID/reference, null to detach"),
21999
22117
  task_list_id: exports_external.string().nullable().optional(),
22000
22118
  depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
22001
22119
  tags: exports_external.array(exports_external.string()).optional(),
@@ -22027,6 +22145,9 @@ ${task.description}` : null
22027
22145
  if (typeof patch.project_id === "string" && patch.project_id) {
22028
22146
  patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
22029
22147
  }
22148
+ if (typeof patch.parent_id === "string" && patch.parent_id) {
22149
+ patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
22150
+ }
22030
22151
  if (typeof patch.task_list_id === "string" && patch.task_list_id) {
22031
22152
  let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
22032
22153
  if (!scope) {
@@ -22035,9 +22156,22 @@ ${task.description}` : null
22035
22156
  }
22036
22157
  patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
22037
22158
  }
22159
+ if (patch.parent_id !== undefined && version2 === undefined) {
22160
+ const current = await cloudGetTask(cloud, task_id2);
22161
+ if (!current)
22162
+ throw new TaskNotFoundError(task_id2);
22163
+ patch.version = current.version;
22164
+ }
22038
22165
  if (version2 !== undefined)
22039
22166
  patch.version = version2;
22040
- const updated = await cloudUpdateTask(cloud, task_id2, patch);
22167
+ let updated = await cloudUpdateTask(cloud, task_id2, patch);
22168
+ if (patch.parent_id !== undefined) {
22169
+ const persisted = await cloudGetTask(cloud, task_id2);
22170
+ if (!persisted || (persisted.parent_id ?? null) !== patch.parent_id) {
22171
+ throw new Error(`TASK_REPARENT_PERSISTENCE_UNVERIFIED: parent_id expected ${patch.parent_id ?? "null"}, ` + `received ${persisted?.parent_id ?? "missing task"}`);
22172
+ }
22173
+ updated = persisted;
22174
+ }
22041
22175
  return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
22042
22176
  }
22043
22177
  const resolvedId = resolveId(params.task_id);
@@ -22049,6 +22183,8 @@ ${task.description}` : null
22049
22183
  resolved.assigned_to = resolveAssignee(resolved.assigned_to);
22050
22184
  if (resolved.project_id && typeof resolved.project_id === "string")
22051
22185
  resolved.project_id = resolveId(resolved.project_id, "projects");
22186
+ if (resolved.parent_id && typeof resolved.parent_id === "string")
22187
+ resolved.parent_id = resolveId(resolved.parent_id);
22052
22188
  if (resolved.task_list_id && typeof resolved.task_list_id === "string")
22053
22189
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
22054
22190
  if (resolved.depends_on && Array.isArray(resolved.depends_on))
@@ -32748,7 +32884,7 @@ function registerTaskMetaTools(server, ctx) {
32748
32884
  create_task: "create_task \u2014 Create a new task. Params: title (required), description, status, priority, project_id, task_list_id, assigned_to, depends_on, short_id (null to disable), tags, estimate (minutes), confidence (0.0-1.0), deadline (ISO), retry_count",
32749
32885
  list_tasks: "list_tasks \u2014 List tasks with filters. Params: status, priority, project_id, task_list_id, assigned_to, tags[], created_after, created_before, limit, offset",
32750
32886
  get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
32751
- update_task: "update_task \u2014 Update task fields (optimistic locking). Params: task_id (required), title, description, status, priority, assigned_to (null to unassign), project_id, task_list_id, depends_on[], tags[], estimate, actual_minutes, confidence, approved_by, completed_at, deadline, retry_count, version",
32887
+ update_task: "update_task \u2014 Update task fields (optimistic locking). Params: task_id (required), title, description, status, priority, assigned_to (null to unassign), project_id, parent_id (null to detach), task_list_id, depends_on[], tags[], estimate, actual_minutes, confidence, approved_by, completed_at, deadline, retry_count, version",
32752
32888
  delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
32753
32889
  start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
32754
32890
  complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
@@ -35803,7 +35939,7 @@ var package_default;
35803
35939
  var init_package = __esm(() => {
35804
35940
  package_default = {
35805
35941
  name: "@hasna/todos",
35806
- version: "0.15.28",
35942
+ version: "0.15.32",
35807
35943
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
35808
35944
  type: "module",
35809
35945
  main: "dist/index.js",
@@ -46935,8 +47071,8 @@ function createPostgresTodosStorageAdapter(options) {
46935
47071
  resolveRef: (ref) => store.resolveTaskRef(ref),
46936
47072
  list: (filter = {}) => store.listTasks(filter),
46937
47073
  count: (filter = {}) => store.countTasks(filter),
46938
- update: (id, input) => updateTask2(id, input, store),
46939
- delete: (id, context) => store.delete("tasks", id, context),
47074
+ update: (id, input, context) => updateTask2(id, input, store, context),
47075
+ delete: (id, context) => store.deleteTaskHierarchy(id, context),
46940
47076
  start: (id, agentId) => startTask2(id, agentId, store),
46941
47077
  complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
46942
47078
  fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
@@ -47469,22 +47605,70 @@ class PostgresJsonRecordStore {
47469
47605
  return "identical";
47470
47606
  throw new Error(divergentAuditHistoryReplayError(value.id));
47471
47607
  }
47472
- async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
47608
+ async withTaskParentIntegrityTransaction(fn) {
47609
+ if (typeof this.options.client.transaction !== "function") {
47610
+ throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
47611
+ }
47612
+ return this.options.client.transaction(async (client) => {
47613
+ await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
47614
+ return fn(client);
47615
+ });
47616
+ }
47617
+ async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
47473
47618
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
47474
- if (planIds.length === 0)
47619
+ if (planIds.length === 0 && !parentGuard)
47475
47620
  return this.upsert("tasks", value, context);
47476
47621
  await this.ensureSchema();
47622
+ if (parentGuard && !queryClient) {
47623
+ return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
47624
+ }
47625
+ const client = queryClient ?? this.options.client;
47477
47626
  const updatedAt = value.updated_at;
47478
47627
  const targetPlanId = value.plan_id;
47479
- const result = await this.options.client.query(`/* todos:task-plan-membership-guard */ WITH
47628
+ const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
47629
+ locked_task AS MATERIALIZED (
47630
+ SELECT payload FROM ${this.tableName}
47631
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
47632
+ FOR UPDATE
47633
+ ),
47480
47634
  locked_plans AS MATERIALIZED (
47481
47635
  SELECT object_id, payload FROM ${this.tableName}
47482
47636
  WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
47483
47637
  AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
47484
47638
  ORDER BY object_id
47485
47639
  FOR UPDATE
47640
+ ), parent_chain(object_id, payload, path, cycle) AS (
47641
+ SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
47642
+ FROM ${this.tableName} AS parent
47643
+ WHERE $10::boolean
47644
+ AND $11::text IS NOT NULL
47645
+ AND parent.service = $1
47646
+ AND parent.object_type = 'tasks'
47647
+ AND parent.object_id = $11
47648
+ AND parent.deleted_at IS NULL
47649
+ UNION ALL
47650
+ SELECT ancestor.object_id,
47651
+ ancestor.payload,
47652
+ chain.path || ancestor.object_id,
47653
+ ancestor.object_id = ANY(chain.path)
47654
+ FROM parent_chain AS chain
47655
+ JOIN ${this.tableName} AS ancestor
47656
+ ON ancestor.service = $1
47657
+ AND ancestor.object_type = 'tasks'
47658
+ AND ancestor.object_id = chain.payload->>'parent_id'
47659
+ AND ancestor.deleted_at IS NULL
47660
+ WHERE NOT chain.cycle
47486
47661
  ), validation AS (
47487
47662
  SELECT
47663
+ (NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
47664
+ (NOT $10::boolean OR NOT $13::boolean
47665
+ OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
47666
+ (NOT $10::boolean OR $11::text IS NULL
47667
+ OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
47668
+ (NOT $10::boolean OR $11::text IS NULL
47669
+ OR ($11::text <> $2
47670
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
47671
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
47488
47672
  (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
47489
47673
  ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
47490
47674
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
@@ -47505,7 +47689,13 @@ class PostgresJsonRecordStore {
47505
47689
  )
47506
47690
  SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
47507
47691
  FROM guarded
47508
- WHERE guarded.all_plans_found AND guarded.target_plan_found AND NOT guarded.project_conflict
47692
+ WHERE guarded.task_found
47693
+ AND guarded.version_matches
47694
+ AND guarded.parent_found
47695
+ AND guarded.parent_acyclic
47696
+ AND guarded.all_plans_found
47697
+ AND guarded.target_plan_found
47698
+ AND NOT guarded.project_conflict
47509
47699
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
47510
47700
  payload = EXCLUDED.payload,
47511
47701
  updated_at = EXCLUDED.updated_at,
@@ -47518,8 +47708,10 @@ class PostgresJsonRecordStore {
47518
47708
  AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
47519
47709
  RETURNING payload
47520
47710
  )
47521
- SELECT guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
47522
- (SELECT payload FROM stored) AS payload
47711
+ SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
47712
+ guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
47713
+ (SELECT payload FROM stored) AS payload,
47714
+ (SELECT payload FROM locked_task) AS current_payload
47523
47715
  FROM guarded`, [
47524
47716
  this.service,
47525
47717
  value.id,
@@ -47529,9 +47721,26 @@ class PostgresJsonRecordStore {
47529
47721
  numberValue3(value.version),
47530
47722
  jsonbParam(planIds),
47531
47723
  targetPlanId,
47532
- explicitProject
47724
+ explicitProject,
47725
+ Boolean(parentGuard),
47726
+ parentGuard?.parentId ?? null,
47727
+ parentGuard?.expectedVersion ?? null,
47728
+ parentGuard?.operation === "update"
47533
47729
  ]);
47534
47730
  const row = result.rows[0];
47731
+ if (parentGuard && !row?.task_found) {
47732
+ throw new TaskNotFoundError(value.id);
47733
+ }
47734
+ if (parentGuard && !row?.version_matches) {
47735
+ const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
47736
+ throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
47737
+ }
47738
+ if (parentGuard && !row?.parent_found && parentGuard.parentId) {
47739
+ throw new TaskNotFoundError(parentGuard.parentId);
47740
+ }
47741
+ if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
47742
+ throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
47743
+ }
47535
47744
  if (!row?.all_plans_found || !row.target_plan_found) {
47536
47745
  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 });
47537
47746
  }
@@ -47539,6 +47748,8 @@ class PostgresJsonRecordStore {
47539
47748
  throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
47540
47749
  }
47541
47750
  if (!row.payload) {
47751
+ if (row.current_payload)
47752
+ return payloadRecord2(row.current_payload);
47542
47753
  return await requireRecord("tasks", value.id, this);
47543
47754
  }
47544
47755
  return payloadRecord2(row.payload);
@@ -47864,6 +48075,80 @@ class PostgresJsonRecordStore {
47864
48075
  version: numberValue3(existing["version"])
47865
48076
  }, context);
47866
48077
  }
48078
+ async deleteTaskHierarchy(id, context = {}) {
48079
+ await this.ensureSchema();
48080
+ return this.withTaskParentIntegrityTransaction(async (client) => {
48081
+ const timestamp4 = new Date().toISOString();
48082
+ const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
48083
+ task_tree(object_id, path, cycle) AS (
48084
+ SELECT task.object_id, ARRAY[task.object_id], false
48085
+ FROM ${this.tableName} AS task
48086
+ WHERE task.service = $1
48087
+ AND task.object_type = 'tasks'
48088
+ AND task.object_id = $2
48089
+ AND task.deleted_at IS NULL
48090
+ UNION ALL
48091
+ SELECT child.object_id,
48092
+ tree.path || child.object_id,
48093
+ child.object_id = ANY(tree.path)
48094
+ FROM task_tree AS tree
48095
+ JOIN ${this.tableName} AS child
48096
+ ON child.service = $1
48097
+ AND child.object_type = 'tasks'
48098
+ AND child.payload->>'parent_id' = tree.object_id
48099
+ AND child.deleted_at IS NULL
48100
+ WHERE NOT tree.cycle
48101
+ ), tombstoned AS (
48102
+ UPDATE ${this.tableName} AS task
48103
+ SET deleted_at = $3::timestamptz,
48104
+ updated_at = $3::timestamptz,
48105
+ source_machine_id = $4
48106
+ WHERE task.service = $1
48107
+ AND task.object_type = 'tasks'
48108
+ AND task.deleted_at IS NULL
48109
+ AND task.object_id IN (
48110
+ SELECT object_id FROM task_tree WHERE NOT cycle
48111
+ )
48112
+ RETURNING task.object_id
48113
+ ), tombstoned_related AS (
48114
+ UPDATE ${this.tableName} AS related
48115
+ SET deleted_at = $3::timestamptz,
48116
+ updated_at = $3::timestamptz,
48117
+ source_machine_id = $4
48118
+ WHERE related.service = $1
48119
+ AND related.deleted_at IS NULL
48120
+ AND (
48121
+ (
48122
+ related.object_type = 'dependencies'
48123
+ AND (
48124
+ related.payload->>'task_id' IN (
48125
+ SELECT object_id FROM task_tree WHERE NOT cycle
48126
+ )
48127
+ OR related.payload->>'depends_on' IN (
48128
+ SELECT object_id FROM task_tree WHERE NOT cycle
48129
+ )
48130
+ )
48131
+ )
48132
+ OR (
48133
+ related.object_type IN ('comments', 'verifications', 'commits', 'refs')
48134
+ AND related.payload->>'task_id' IN (
48135
+ SELECT object_id FROM task_tree WHERE NOT cycle
48136
+ )
48137
+ )
48138
+ )
48139
+ RETURNING related.object_id
48140
+ )
48141
+ SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
48142
+ (SELECT count(*) FROM tombstoned) AS deleted_count,
48143
+ (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
48144
+ this.service,
48145
+ id,
48146
+ timestamp4,
48147
+ context.requestId ?? this.sourceMachineId ?? null
48148
+ ]);
48149
+ return Boolean(result.rows[0]?.found);
48150
+ });
48151
+ }
47867
48152
  async getPlanProjectLinkReceipt(receiptId) {
47868
48153
  const value = await this.get("plan_project_link_receipts", receiptId);
47869
48154
  return value ? assertPlanProjectLinkReceipt(value) : null;
@@ -48284,9 +48569,8 @@ class PostgresJsonRecordStore {
48284
48569
  }
48285
48570
  async function createTask3(input, store, context) {
48286
48571
  const timestamp4 = new Date().toISOString();
48287
- if (input.parent_id && !await store.get("tasks", input.parent_id)) {
48288
- throw new TaskNotFoundError(input.parent_id);
48289
- }
48572
+ const taskId = randomUUID4();
48573
+ await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
48290
48574
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
48291
48575
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
48292
48576
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -48295,7 +48579,7 @@ async function createTask3(input, store, context) {
48295
48579
  const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
48296
48580
  const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
48297
48581
  const task2 = {
48298
- id: randomUUID4(),
48582
+ id: taskId,
48299
48583
  short_id: shortId,
48300
48584
  project_id: effectiveProjectId,
48301
48585
  parent_id: input.parent_id ?? null,
@@ -48351,15 +48635,16 @@ async function createTask3(input, store, context) {
48351
48635
  synced_at: null,
48352
48636
  archived_at: null
48353
48637
  };
48354
- const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, task2.plan_id ? [task2.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
48638
+ 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);
48355
48639
  await logTaskChange2(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
48356
48640
  return storedTask;
48357
48641
  }
48358
- async function updateTask2(id, input, store) {
48642
+ async function updateTask2(id, input, store, context) {
48359
48643
  const existing = await requireRecord("tasks", id, store);
48360
48644
  if (existing.version !== input.version) {
48361
- throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
48645
+ throw new VersionConflictError(id, input.version, existing.version);
48362
48646
  }
48647
+ await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
48363
48648
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
48364
48649
  const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
48365
48650
  if (linkedPlan?.project_id) {
@@ -48384,10 +48669,19 @@ async function updateTask2(id, input, store) {
48384
48669
  metadata: input.metadata ?? existing.metadata,
48385
48670
  requires_approval: input.requires_approval ?? existing.requires_approval,
48386
48671
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
48672
+ parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
48387
48673
  created_by: existing.created_by,
48388
48674
  completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
48389
48675
  };
48390
- return store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined);
48676
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
48677
+ operation: "update",
48678
+ expectedVersion: input.version,
48679
+ parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
48680
+ });
48681
+ if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
48682
+ await logTaskChange2(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
48683
+ }
48684
+ return storedTask;
48391
48685
  }
48392
48686
  async function startTask2(id, agentId, store) {
48393
48687
  const task2 = await requireRecord("tasks", id, store);
@@ -48451,7 +48745,11 @@ async function patchTask(task2, patch, store) {
48451
48745
  version: task2.version + 1,
48452
48746
  updated_at: new Date().toISOString()
48453
48747
  };
48454
- return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"));
48748
+ return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
48749
+ operation: "update",
48750
+ expectedVersion: task2.version,
48751
+ parentId: updated.parent_id
48752
+ });
48455
48753
  }
48456
48754
  function sameCloudLockHolder(stored, incoming) {
48457
48755
  if (!stored || !incoming)
@@ -49154,6 +49452,7 @@ var init_postgres_adapter = __esm(() => {
49154
49452
  init_postgres_sync();
49155
49453
  init_integrity();
49156
49454
  init_redaction();
49455
+ init_task_parent_integrity();
49157
49456
  init_audit_history_import();
49158
49457
  init_canonical();
49159
49458
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
@@ -49639,21 +49938,6 @@ var init_postgres = __esm(() => {
49639
49938
  init_types3();
49640
49939
  });
49641
49940
 
49642
- // src/project-registration/types.ts
49643
- var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1", TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1", TodosProjectRegistrationError;
49644
- var init_types4 = __esm(() => {
49645
- TodosProjectRegistrationError = class TodosProjectRegistrationError extends Error {
49646
- code;
49647
- details;
49648
- constructor(code, message, details = {}) {
49649
- super(message);
49650
- this.code = code;
49651
- this.details = details;
49652
- this.name = "TodosProjectRegistrationError";
49653
- }
49654
- };
49655
- });
49656
-
49657
49941
  // src/project-registration/postgres.ts
49658
49942
  function safeIdentifier(value, field) {
49659
49943
  if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
@@ -49717,8 +50001,9 @@ class PostgresTodosProjectRegistrationTransaction {
49717
50001
  const result = await this.client.query(`
49718
50002
  SELECT * FROM todos_project_registration_receipts
49719
50003
  WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
49720
- AND operation_id = $4 AND step_id = $5 AND resource_kind = $6
49721
- AND direction = $7 AND idempotency_key = $8 AND target_selector = $9
50004
+ AND route = $4 AND package_version = $5
50005
+ AND operation_id = $6 AND step_id = $7 AND resource_kind = $8
50006
+ AND direction = $9 AND idempotency_key = $10 AND target_selector = $11
49722
50007
  ORDER BY CASE outcome
49723
50008
  WHEN 'terminal_nonacceptance' THEN 0
49724
50009
  WHEN 'duplicate_of_accepted' THEN 1
@@ -49729,6 +50014,8 @@ class PostgresTodosProjectRegistrationTransaction {
49729
50014
  identity.authority_id,
49730
50015
  identity.tenant_id,
49731
50016
  identity.corpus_id,
50017
+ identity.route,
50018
+ identity.package_version,
49732
50019
  identity.operation_id,
49733
50020
  identity.step_id,
49734
50021
  identity.resource_kind,
@@ -49928,6 +50215,7 @@ class PostgresTodosProjectRegistrationTransaction {
49928
50215
  AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
49929
50216
  ORDER BY payload->>'created_at' ASC, object_id ASC
49930
50217
  LIMIT 1
50218
+ FOR UPDATE
49931
50219
  `, [this.service, path, taskListSlug]);
49932
50220
  return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
49933
50221
  }
@@ -49938,6 +50226,7 @@ class PostgresTodosProjectRegistrationTransaction {
49938
50226
  AND payload->>'project_id' = $2 AND payload->>'slug' = $3
49939
50227
  ORDER BY payload->>'created_at' ASC, object_id ASC
49940
50228
  LIMIT 1
50229
+ FOR UPDATE
49941
50230
  `, [this.service, projectId, slug]);
49942
50231
  return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
49943
50232
  }
@@ -49948,10 +50237,24 @@ class PostgresTodosProjectRegistrationTransaction {
49948
50237
  return await this.storage.taskLists.create(input);
49949
50238
  }
49950
50239
  async getProject(id) {
49951
- return await this.storage.projects.get(id);
50240
+ const result = await this.client.query(`
50241
+ SELECT payload FROM ${this.tableName}
50242
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2
50243
+ AND deleted_at IS NULL
50244
+ LIMIT 1
50245
+ FOR SHARE
50246
+ `, [this.service, id]);
50247
+ return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
49952
50248
  }
49953
50249
  async getTaskList(id) {
49954
- return await this.storage.taskLists.get(id);
50250
+ const result = await this.client.query(`
50251
+ SELECT payload FROM ${this.tableName}
50252
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
50253
+ AND deleted_at IS NULL
50254
+ LIMIT 1
50255
+ FOR SHARE
50256
+ `, [this.service, id]);
50257
+ return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
49955
50258
  }
49956
50259
  async lockCompensationWrites() {
49957
50260
  await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
@@ -50031,6 +50334,96 @@ class PostgresTodosProjectRegistrationBackend {
50031
50334
  async getTaskList(id) {
50032
50335
  return (await this.direct()).getTaskList(id);
50033
50336
  }
50337
+ async getProjectResourceCollectionRevision(input) {
50338
+ await this.ensureSchema();
50339
+ const result = await this.client.query(`
50340
+ WITH resources(kind_rank, target_id, revision) AS (
50341
+ SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
50342
+ FROM ${this.tableName}
50343
+ WHERE service = $1 AND object_type = 'projects'
50344
+ AND deleted_at IS NULL AND object_id = $2
50345
+ UNION ALL
50346
+ SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
50347
+ FROM ${this.tableName}
50348
+ WHERE service = $1 AND object_type = 'task_lists'
50349
+ AND deleted_at IS NULL AND object_id = $3
50350
+ AND payload->>'project_id' = $2
50351
+ UNION ALL
50352
+ SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
50353
+ FROM ${this.tableName}
50354
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
50355
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
50356
+ UNION ALL
50357
+ SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
50358
+ FROM ${this.tableName}
50359
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
50360
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
50361
+ )
50362
+ SELECT 'md5:' || md5(COALESCE(string_agg(
50363
+ kind_rank::text || chr(31) || target_id || chr(31) || revision,
50364
+ chr(30) ORDER BY kind_rank ASC, target_id ASC
50365
+ ), '')) AS revision
50366
+ FROM resources
50367
+ `, [
50368
+ this.service,
50369
+ input.todos_project_id,
50370
+ input.task_list_id,
50371
+ input.include_anchors
50372
+ ]);
50373
+ const revision = result.rows[0]?.revision;
50374
+ if (!revision) {
50375
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
50376
+ }
50377
+ return revision;
50378
+ }
50379
+ async listProjectResourceCandidates(input) {
50380
+ await this.ensureSchema();
50381
+ const afterRank = input.after?.kind_rank ?? -1;
50382
+ const afterId = input.after?.target_id ?? "";
50383
+ const result = await this.client.query(`
50384
+ WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
50385
+ SELECT 'project'::text, 0, object_id, NULL::text,
50386
+ COALESCE(payload->>'updated_at', updated_at::text)
50387
+ FROM ${this.tableName}
50388
+ WHERE service = $1 AND object_type = 'projects'
50389
+ AND deleted_at IS NULL AND object_id = $2
50390
+ UNION ALL
50391
+ SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
50392
+ COALESCE(payload->>'updated_at', updated_at::text)
50393
+ FROM ${this.tableName}
50394
+ WHERE service = $1 AND object_type = 'task_lists'
50395
+ AND deleted_at IS NULL AND object_id = $3
50396
+ AND payload->>'project_id' = $2
50397
+ UNION ALL
50398
+ SELECT 'plan'::text, 2, object_id, payload->>'project_id',
50399
+ COALESCE(payload->>'updated_at', updated_at::text)
50400
+ FROM ${this.tableName}
50401
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
50402
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
50403
+ UNION ALL
50404
+ SELECT 'task'::text, 3, object_id,
50405
+ COALESCE(payload->>'plan_id', payload->>'project_id'),
50406
+ COALESCE(payload->>'updated_at', updated_at::text)
50407
+ FROM ${this.tableName}
50408
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
50409
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
50410
+ )
50411
+ SELECT kind, kind_rank, target_id, parent_id, revision
50412
+ FROM resources
50413
+ WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
50414
+ ORDER BY kind_rank ASC, target_id ASC
50415
+ LIMIT $7
50416
+ `, [
50417
+ this.service,
50418
+ input.todos_project_id,
50419
+ input.task_list_id,
50420
+ input.include_anchors,
50421
+ afterRank,
50422
+ afterId,
50423
+ input.limit
50424
+ ]);
50425
+ return result.rows;
50426
+ }
50034
50427
  }
50035
50428
  var init_postgres2 = __esm(() => {
50036
50429
  init_postgres_adapter();
@@ -50123,6 +50516,7 @@ class SqliteTodosProjectRegistrationTransaction {
50123
50516
  const row = this.db.query(`
50124
50517
  SELECT * FROM todos_project_registration_receipts
50125
50518
  WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
50519
+ AND route = ? AND package_version = ?
50126
50520
  AND operation_id = ? AND step_id = ? AND resource_kind = ?
50127
50521
  AND direction = ? AND idempotency_key = ? AND target_selector = ?
50128
50522
  ORDER BY CASE outcome
@@ -50131,7 +50525,7 @@ class SqliteTodosProjectRegistrationTransaction {
50131
50525
  ELSE 2
50132
50526
  END, created_at DESC, receipt_id DESC
50133
50527
  LIMIT 1
50134
- `).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);
50528
+ `).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);
50135
50529
  return row ? receiptFromRow2(row) : null;
50136
50530
  }
50137
50531
  async getReceiptById(receiptId) {
@@ -50285,7 +50679,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
50285
50679
  }
50286
50680
  async lockStep(_identity) {}
50287
50681
  async getReceiptForLookup(identity) {
50288
- 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);
50682
+ 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);
50289
50683
  const stored = await this.direct.getReceiptForLookup(identity);
50290
50684
  if (stored)
50291
50685
  staged.push(stored);
@@ -50773,35 +51167,68 @@ function projectRecord(project) {
50773
51167
  return {
50774
51168
  target_id: project.id,
50775
51169
  revision: project.updated_at,
50776
- digest: digestProjectRegistrationValue({
50777
- id: project.id,
50778
- name: project.name,
50779
- path: project.path,
50780
- description: project.description,
50781
- task_list_id: project.task_list_id,
50782
- task_prefix: project.task_prefix,
50783
- task_counter: project.task_counter,
50784
- created_at: project.created_at,
50785
- updated_at: project.updated_at
50786
- })
51170
+ digest: projectRegistrationDigest(project)
50787
51171
  };
50788
51172
  }
50789
51173
  function taskListRecord(taskList) {
50790
51174
  return {
50791
51175
  target_id: taskList.id,
50792
51176
  revision: taskList.updated_at,
50793
- digest: digestProjectRegistrationValue({
50794
- id: taskList.id,
50795
- project_id: taskList.project_id,
50796
- slug: taskList.slug,
50797
- name: taskList.name,
50798
- description: taskList.description,
50799
- metadata: taskList.metadata,
50800
- created_at: taskList.created_at,
50801
- updated_at: taskList.updated_at
51177
+ digest: taskListRegistrationDigest(taskList)
51178
+ };
51179
+ }
51180
+ function boundExistingProjectRecord(project) {
51181
+ return {
51182
+ target_id: project.id,
51183
+ revision: project.created_at,
51184
+ digest: projectRegistrationDigest({
51185
+ ...project,
51186
+ updated_at: project.created_at
51187
+ })
51188
+ };
51189
+ }
51190
+ function boundExistingTaskListRecord(taskList) {
51191
+ return {
51192
+ target_id: taskList.id,
51193
+ revision: taskList.created_at,
51194
+ digest: taskListRegistrationDigest({
51195
+ ...taskList,
51196
+ updated_at: taskList.created_at
50802
51197
  })
50803
51198
  };
50804
51199
  }
51200
+ function projectRegistrationDigest(project) {
51201
+ return digestProjectRegistrationValue({
51202
+ id: project.id,
51203
+ name: project.name,
51204
+ path: project.path,
51205
+ description: project.description,
51206
+ task_list_id: project.task_list_id,
51207
+ task_prefix: project.task_prefix,
51208
+ task_counter: project.task_counter,
51209
+ created_at: project.created_at,
51210
+ updated_at: project.updated_at
51211
+ });
51212
+ }
51213
+ function taskListRegistrationDigest(taskList) {
51214
+ return digestProjectRegistrationValue({
51215
+ id: taskList.id,
51216
+ project_id: taskList.project_id,
51217
+ slug: taskList.slug,
51218
+ name: taskList.name,
51219
+ description: taskList.description,
51220
+ metadata: taskList.metadata,
51221
+ created_at: taskList.created_at,
51222
+ updated_at: taskList.updated_at
51223
+ });
51224
+ }
51225
+ function canonicalValuesEqual(left, right) {
51226
+ try {
51227
+ return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
51228
+ } catch {
51229
+ return false;
51230
+ }
51231
+ }
50805
51232
  function receiptId(input) {
50806
51233
  return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
50807
51234
  }
@@ -50821,6 +51248,29 @@ function assertCapabilityRequest(request, capability) {
50821
51248
  }
50822
51249
  }
50823
51250
  function normalizedCallDigest(request) {
51251
+ return digestProjectRegistrationValue({
51252
+ authority_route: request.authority_route,
51253
+ package_version: request.package_version,
51254
+ authority_id: request.authority_id,
51255
+ tenant_id: request.tenant_id,
51256
+ corpus_id: request.corpus_id,
51257
+ operation_id: request.operation_id,
51258
+ step_id: request.step_id,
51259
+ resource_kind: request.resource_kind,
51260
+ direction: request.direction,
51261
+ target_selector: request.target_selector,
51262
+ idempotency_key: request.idempotency_key,
51263
+ request_digest: request.request_digest,
51264
+ precondition_digest: request.precondition_digest,
51265
+ project_id: request.project_id,
51266
+ project_slug: request.project_slug,
51267
+ project_name: request.project_name,
51268
+ desired: request.desired,
51269
+ bind_existing: request.bind_existing === true,
51270
+ accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
51271
+ });
51272
+ }
51273
+ function legacyNormalizedCallDigestBeforeBindExisting(request) {
50824
51274
  return digestProjectRegistrationValue({
50825
51275
  authority_route: request.authority_route,
50826
51276
  package_version: request.package_version,
@@ -50842,6 +51292,11 @@ function normalizedCallDigest(request) {
50842
51292
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
50843
51293
  });
50844
51294
  }
51295
+ function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
51296
+ if (accepted.normalized_call_digest === callDigest)
51297
+ return true;
51298
+ return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
51299
+ }
50845
51300
  function assertCommonRequest(request, capability) {
50846
51301
  assertBounds(request);
50847
51302
  assertResourceKind(request.resource_kind);
@@ -50883,6 +51338,9 @@ function assertCommonRequest(request, capability) {
50883
51338
  if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
50884
51339
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
50885
51340
  }
51341
+ if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
51342
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
51343
+ }
50886
51344
  const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
50887
51345
  operation_id: request.operation_id,
50888
51346
  step_id: request.step_id,
@@ -50904,7 +51362,7 @@ function assertForwardRequest(request, capability) {
50904
51362
  const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
50905
51363
  const expectedPreconditionDigest = digestProjectRegistrationValue({
50906
51364
  target_selector: request.target_selector,
50907
- expected: "absent"
51365
+ expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
50908
51366
  });
50909
51367
  if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
50910
51368
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
@@ -50983,7 +51441,7 @@ function receiptBase(request, callDigest, capability) {
50983
51441
  normalized_call_digest: callDigest
50984
51442
  };
50985
51443
  }
50986
- function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2) {
51444
+ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2, createdByOperation = true) {
50987
51445
  return makeReceipt({
50988
51446
  ...receiptBase(request, callDigest, capability),
50989
51447
  outcome: "accepted",
@@ -50993,7 +51451,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2
50993
51451
  result_digest: record.digest,
50994
51452
  duplicate_of_receipt_id: null,
50995
51453
  accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
50996
- created_by_operation: true
51454
+ created_by_operation: createdByOperation
50997
51455
  }, createdAt2);
50998
51456
  }
50999
51457
  function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt2) {
@@ -51055,6 +51513,53 @@ function bindingFor(request, callDigest, timestamp4, capability) {
51055
51513
  updated_at: timestamp4
51056
51514
  };
51057
51515
  }
51516
+ function encodeProjectResourceCursor(input) {
51517
+ return Buffer.from(JSON.stringify({
51518
+ version: PROJECT_RESOURCE_CURSOR_VERSION,
51519
+ ...input
51520
+ }), "utf8").toString("base64url");
51521
+ }
51522
+ function decodeProjectResourceCursor(cursor, expected) {
51523
+ if (!cursor)
51524
+ return null;
51525
+ let parsed;
51526
+ try {
51527
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
51528
+ } catch {
51529
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
51530
+ }
51531
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
51532
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
51533
+ }
51534
+ const value = parsed;
51535
+ 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) {
51536
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
51537
+ }
51538
+ return {
51539
+ kind_rank: Number(value["kind_rank"]),
51540
+ target_id: value["target_id"],
51541
+ collection_revision: value["collection_revision"]
51542
+ };
51543
+ }
51544
+ function projectResourceFromCandidate(sourceProjectId, candidate) {
51545
+ const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
51546
+ return {
51547
+ source_project_id: sourceProjectId,
51548
+ kind: candidate.kind,
51549
+ scope,
51550
+ target_id: candidate.target_id,
51551
+ parent_id: candidate.parent_id,
51552
+ revision: candidate.revision,
51553
+ digest: digestProjectRegistrationValue({
51554
+ source_project_id: sourceProjectId,
51555
+ kind: candidate.kind,
51556
+ scope,
51557
+ target_id: candidate.target_id,
51558
+ parent_id: candidate.parent_id,
51559
+ revision: candidate.revision
51560
+ })
51561
+ };
51562
+ }
51058
51563
 
51059
51564
  class PackageOwnedTodosProjectRegistrationAuthority {
51060
51565
  backend;
@@ -51076,6 +51581,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51076
51581
  immutable_receipts: true,
51077
51582
  exact_terminal_lookup: true,
51078
51583
  exact_readback: true,
51584
+ bind_existing_adoption: true,
51585
+ prior_registration_adoption_validation: true,
51586
+ project_resource_enumeration: true,
51587
+ project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
51079
51588
  conditional_inverse: true,
51080
51589
  ambiguous_outcome_reconciliation: true
51081
51590
  };
@@ -51120,6 +51629,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51120
51629
  async existingForwardResolution(transaction, request, callDigest) {
51121
51630
  const exact = await transaction.getReceiptForLookup({
51122
51631
  ...authorityScope(this.capabilityValue),
51632
+ route: this.capabilityValue.route,
51633
+ package_version: this.capabilityValue.package_version,
51123
51634
  operation_id: request.operation_id,
51124
51635
  step_id: request.step_id,
51125
51636
  resource_kind: request.resource_kind,
@@ -51134,7 +51645,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51134
51645
  if (!accepted2) {
51135
51646
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
51136
51647
  }
51137
- if (accepted2.normalized_call_digest !== callDigest) {
51648
+ if (!acceptedCallMatches(request, accepted2, callDigest)) {
51138
51649
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
51139
51650
  }
51140
51651
  return this.duplicateFor(transaction, request, callDigest, accepted2);
@@ -51148,7 +51659,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51148
51659
  });
51149
51660
  if (!accepted)
51150
51661
  return null;
51151
- if (accepted.normalized_call_digest === callDigest) {
51662
+ if (acceptedCallMatches(request, accepted, callDigest)) {
51152
51663
  return this.duplicateFor(transaction, request, callDigest, accepted);
51153
51664
  }
51154
51665
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
@@ -51159,6 +51670,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51159
51670
  const slug2 = taskListSlug(request.project_slug);
51160
51671
  const conflict2 = await transaction.findProjectConflict(path, slug2);
51161
51672
  if (conflict2) {
51673
+ if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
51674
+ return {
51675
+ record: boundExistingProjectRecord(conflict2),
51676
+ created_by_operation: false
51677
+ };
51678
+ }
51162
51679
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
51163
51680
  }
51164
51681
  await this.fault("before_object_write", request);
@@ -51170,7 +51687,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51170
51687
  task_prefix: deterministicTaskPrefix(request.project_slug)
51171
51688
  });
51172
51689
  await this.fault("after_object_write", request);
51173
- return projectRecord(project);
51690
+ return {
51691
+ record: projectRecord(project),
51692
+ created_by_operation: true
51693
+ };
51174
51694
  }
51175
51695
  const todosProjectId = String(request.desired["todos_project_id"]);
51176
51696
  const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
@@ -51184,6 +51704,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51184
51704
  const slug = taskListSlug(request.project_slug);
51185
51705
  const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
51186
51706
  if (conflict) {
51707
+ if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
51708
+ return {
51709
+ record: boundExistingTaskListRecord(conflict),
51710
+ created_by_operation: false
51711
+ };
51712
+ }
51187
51713
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
51188
51714
  }
51189
51715
  await this.fault("before_object_write", request);
@@ -51200,7 +51726,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51200
51726
  if (taskList.project_id !== todosProjectId) {
51201
51727
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
51202
51728
  }
51203
- return taskListRecord(taskList);
51729
+ return {
51730
+ record: taskListRecord(taskList),
51731
+ created_by_operation: true
51732
+ };
51204
51733
  }
51205
51734
  async create(request) {
51206
51735
  const startedAt = Date.now();
@@ -51222,9 +51751,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51222
51751
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
51223
51752
  if (!claimed) {
51224
51753
  const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
51225
- if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
51754
+ if (binding?.state === "accepted" && binding.accepted_receipt_id) {
51226
51755
  const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
51227
- if (accepted2) {
51756
+ if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
51228
51757
  return this.duplicateFor(transaction, request, callDigest, accepted2);
51229
51758
  }
51230
51759
  }
@@ -51235,15 +51764,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51235
51764
  await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
51236
51765
  return recordOrTerminal;
51237
51766
  }
51238
- const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
51767
+ const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
51239
51768
  await this.fault("before_receipt_write", request);
51240
51769
  const stored = await insertDeterministicReceipt(transaction, accepted);
51241
51770
  await this.fault("after_receipt_write", request);
51242
51771
  await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
51243
- target_id: recordOrTerminal.target_id,
51772
+ target_id: recordOrTerminal.record.target_id,
51244
51773
  accepted_receipt_id: stored.receipt_id,
51245
- result_revision: recordOrTerminal.revision,
51246
- result_digest: recordOrTerminal.digest,
51774
+ result_revision: recordOrTerminal.record.revision,
51775
+ result_digest: recordOrTerminal.record.digest,
51247
51776
  updated_at: this.now()
51248
51777
  });
51249
51778
  return stored;
@@ -51272,6 +51801,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51272
51801
  });
51273
51802
  const exact = await transaction.getReceiptForLookup({
51274
51803
  ...authorityScope(this.capabilityValue),
51804
+ route: this.capabilityValue.route,
51805
+ package_version: this.capabilityValue.package_version,
51275
51806
  operation_id: request.operation_id,
51276
51807
  step_id: request.step_id,
51277
51808
  resource_kind: request.resource_kind,
@@ -51289,7 +51820,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51289
51820
  direction: request.direction
51290
51821
  });
51291
51822
  if (accepted) {
51292
- return accepted.normalized_call_digest === callDigest ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
51823
+ return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
51293
51824
  }
51294
51825
  const timestamp4 = this.now();
51295
51826
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
@@ -51322,9 +51853,18 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51322
51853
  if (request.max_items !== 1) {
51323
51854
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
51324
51855
  }
51325
- if (request.authority !== "todos" || request.authority_route !== this.capabilityValue.route || request.package_version !== this.capabilityValue.package_version || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
51856
+ 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) {
51326
51857
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
51327
51858
  }
51859
+ requireString(request.authority_route, "authority_route", {
51860
+ min: 3,
51861
+ max: 128,
51862
+ pattern: AUTHORITY_ROUTE_PATTERN
51863
+ });
51864
+ requireString(request.package_version, "package_version", {
51865
+ max: 128,
51866
+ pattern: PACKAGE_VERSION_PATTERN
51867
+ });
51328
51868
  requireString(request.operation_id, "operation_id", {
51329
51869
  min: 8,
51330
51870
  max: 128,
@@ -51346,6 +51886,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51346
51886
  }
51347
51887
  const receipt = await this.backend.getReceiptForLookup({
51348
51888
  ...authorityScope(this.capabilityValue),
51889
+ route: request.authority_route,
51890
+ package_version: request.package_version,
51349
51891
  operation_id: request.operation_id,
51350
51892
  step_id: request.step_id,
51351
51893
  resource_kind: request.resource_kind,
@@ -51358,6 +51900,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51358
51900
  }
51359
51901
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
51360
51902
  }
51903
+ async listProjectResources(request) {
51904
+ const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
51905
+ if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
51906
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
51907
+ }
51908
+ if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
51909
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
51910
+ }
51911
+ const includeAnchors = request.include_anchors === true;
51912
+ const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
51913
+ if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
51914
+ 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 });
51915
+ }
51916
+ const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
51917
+ if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
51918
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
51919
+ source_project_id: sourceProjectId,
51920
+ todos_project_id: projectBinding.target_id
51921
+ });
51922
+ }
51923
+ const cursor = decodeProjectResourceCursor(request.cursor, {
51924
+ source_project_id: sourceProjectId,
51925
+ include_anchors: includeAnchors
51926
+ });
51927
+ const collectionInput = {
51928
+ todos_project_id: projectBinding.target_id,
51929
+ task_list_id: taskListBinding.target_id,
51930
+ include_anchors: includeAnchors
51931
+ };
51932
+ const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
51933
+ if (cursor && cursor.collection_revision !== collectionRevision) {
51934
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
51935
+ source_project_id: sourceProjectId,
51936
+ expected_collection_revision: cursor.collection_revision,
51937
+ current_collection_revision: collectionRevision
51938
+ });
51939
+ }
51940
+ const candidates = await this.backend.listProjectResourceCandidates({
51941
+ ...collectionInput,
51942
+ after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
51943
+ limit: request.limit + 1
51944
+ });
51945
+ const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
51946
+ if (verifiedCollectionRevision !== collectionRevision) {
51947
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
51948
+ source_project_id: sourceProjectId,
51949
+ expected_collection_revision: collectionRevision,
51950
+ current_collection_revision: verifiedCollectionRevision
51951
+ });
51952
+ }
51953
+ const hasMore = candidates.length > request.limit;
51954
+ const pageCandidates = candidates.slice(0, request.limit);
51955
+ const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
51956
+ const last = pageCandidates.at(-1);
51957
+ return {
51958
+ authority: "todos",
51959
+ route: this.capabilityValue.route,
51960
+ package_version: this.capabilityValue.package_version,
51961
+ authority_id: this.capabilityValue.authority_id,
51962
+ tenant_id: this.capabilityValue.tenant_id,
51963
+ corpus_id: this.capabilityValue.corpus_id,
51964
+ source_project_id: sourceProjectId,
51965
+ todos_project_id: projectBinding.target_id,
51966
+ task_list_id: taskListBinding.target_id,
51967
+ include_anchors: includeAnchors,
51968
+ collection_revision: collectionRevision,
51969
+ limit: request.limit,
51970
+ count: resources.length,
51971
+ resources,
51972
+ has_more: hasMore,
51973
+ next_cursor: hasMore && last ? encodeProjectResourceCursor({
51974
+ source_project_id: sourceProjectId,
51975
+ include_anchors: includeAnchors,
51976
+ collection_revision: collectionRevision,
51977
+ kind_rank: last.kind_rank,
51978
+ target_id: last.target_id
51979
+ }) : null,
51980
+ complete: !hasMore,
51981
+ truncated: false
51982
+ };
51983
+ }
51984
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
51985
+ const startedAt = Date.now();
51986
+ if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
51987
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
51988
+ }
51989
+ requireString(sourceRequest.package_version, "package_version", {
51990
+ max: 128,
51991
+ pattern: PACKAGE_VERSION_PATTERN
51992
+ });
51993
+ assertForwardRequest(sourceRequest, {
51994
+ ...this.capabilityValue,
51995
+ package_version: sourceRequest.package_version
51996
+ });
51997
+ const validation = await this.backend.transaction(async (transaction) => {
51998
+ const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
51999
+ if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
52000
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
52001
+ }
52002
+ const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
52003
+ if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
52004
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
52005
+ }
52006
+ 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);
52007
+ 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)) {
52008
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
52009
+ }
52010
+ const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
52011
+ 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) {
52012
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
52013
+ }
52014
+ const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
52015
+ if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
52016
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
52017
+ }
52018
+ let stableMatch = false;
52019
+ if (sourceRequest.resource_kind === "task_list") {
52020
+ stableMatch = taskListRegistrationDigest({
52021
+ ...current,
52022
+ updated_at: accepted.result_revision
52023
+ }) === accepted.result_digest;
52024
+ } else {
52025
+ const project = current;
52026
+ if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
52027
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
52028
+ }
52029
+ for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
52030
+ if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
52031
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
52032
+ }
52033
+ if (projectRegistrationDigest({
52034
+ ...project,
52035
+ task_counter: priorTaskCounter,
52036
+ updated_at: accepted.result_revision
52037
+ }) === accepted.result_digest) {
52038
+ stableMatch = true;
52039
+ break;
52040
+ }
52041
+ }
52042
+ }
52043
+ if (!stableMatch) {
52044
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
52045
+ }
52046
+ return {
52047
+ valid: true,
52048
+ resource_kind: sourceRequest.resource_kind,
52049
+ target_id: accepted.target_id,
52050
+ source_receipt_id: storedSource.receipt_id,
52051
+ accepted_receipt_id: accepted.receipt_id,
52052
+ source_outcome: storedSource.outcome,
52053
+ created_at: current.created_at,
52054
+ current_revision: current.updated_at,
52055
+ accepted_result_digest: accepted.result_digest
52056
+ };
52057
+ });
52058
+ assertWithinBounds(validation, sourceRequest, startedAt);
52059
+ return validation;
52060
+ }
51361
52061
  async storedAcceptedReceipt(request, supplied) {
51362
52062
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
51363
52063
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -51384,6 +52084,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51384
52084
  });
51385
52085
  const exact = await transaction.getReceiptForLookup({
51386
52086
  ...authorityScope(this.capabilityValue),
52087
+ route: this.capabilityValue.route,
52088
+ package_version: this.capabilityValue.package_version,
51387
52089
  operation_id: request.operation_id,
51388
52090
  step_id: request.step_id,
51389
52091
  resource_kind: request.resource_kind,
@@ -51478,6 +52180,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51478
52180
  await this.storedAcceptedReceipt(request, accepted);
51479
52181
  const receipt = await this.backend.getReceiptForLookup({
51480
52182
  ...authorityScope(this.capabilityValue),
52183
+ route: this.capabilityValue.route,
52184
+ package_version: this.capabilityValue.package_version,
51481
52185
  operation_id: request.operation_id,
51482
52186
  step_id: request.step_id,
51483
52187
  resource_kind: request.resource_kind,
@@ -51522,7 +52226,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
51522
52226
  cursorTableName
51523
52227
  }), authorityOptions);
51524
52228
  }
51525
- var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
52229
+ var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, PROJECT_RESOURCE_PAGE_LIMIT = 500, PROJECT_RESOURCE_CURSOR_VERSION = 1, WriteBoundaryError;
51526
52230
  var init_authority = __esm(() => {
51527
52231
  init_package_version();
51528
52232
  init_postgres2();
@@ -51532,6 +52236,8 @@ var init_authority = __esm(() => {
51532
52236
  WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
51533
52237
  OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
51534
52238
  STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
52239
+ AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
52240
+ PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
51535
52241
  SHA256_PATTERN = /^[0-9a-f]{64}$/;
51536
52242
  IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
51537
52243
  WriteBoundaryError = class WriteBoundaryError extends Error {
@@ -51590,6 +52296,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51590
52296
  if ((action === "" || action === "capability") && method === "GET") {
51591
52297
  return json({ capability: await authority.capability() });
51592
52298
  }
52299
+ if (action === "resources" && method === "GET") {
52300
+ const sourceProjectId = url.searchParams.get("source_project_id");
52301
+ const limit = Number(url.searchParams.get("limit") ?? "100");
52302
+ const includeAnchorsRaw = url.searchParams.get("include_anchors");
52303
+ const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
52304
+ return json({
52305
+ page: await authority.listProjectResources({
52306
+ source_project_id: sourceProjectId,
52307
+ limit,
52308
+ include_anchors: includeAnchors,
52309
+ cursor: url.searchParams.get("cursor") ?? undefined
52310
+ })
52311
+ });
52312
+ }
51593
52313
  if (method !== "POST")
51594
52314
  return json({ error: "method not allowed" }, 405);
51595
52315
  const body = await readJson(req);
@@ -51612,6 +52332,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51612
52332
  record: await authority.readExact(body)
51613
52333
  });
51614
52334
  }
52335
+ if (action === "validate-prior-adoption") {
52336
+ const input = body;
52337
+ return json({
52338
+ validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
52339
+ });
52340
+ }
51615
52341
  if (action === "compensate") {
51616
52342
  return json({
51617
52343
  receipt: await authority.compensate(body)
@@ -51644,6 +52370,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51644
52370
  var JSON_HEADERS;
51645
52371
  var init_http2 = __esm(() => {
51646
52372
  init_types4();
52373
+ init_adoption_validation();
51647
52374
  JSON_HEADERS = { "Content-Type": "application/json" };
51648
52375
  });
51649
52376
 
@@ -51651,13 +52378,14 @@ var init_http2 = __esm(() => {
51651
52378
  var init_project_registration = __esm(() => {
51652
52379
  init_authority();
51653
52380
  init_http2();
52381
+ init_adoption_validation();
51654
52382
  init_postgres2();
51655
52383
  init_sqlite();
51656
52384
  init_types4();
51657
52385
  });
51658
52386
 
51659
52387
  // src/task-manifest/types.ts
51660
- var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
52388
+ var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1", TodosTaskManifestError;
51661
52389
  var init_types5 = __esm(() => {
51662
52390
  TodosTaskManifestError = class TodosTaskManifestError extends Error {
51663
52391
  code;
@@ -51744,7 +52472,7 @@ function parseTodosTaskManifestBindingLookup(input) {
51744
52472
  }
51745
52473
  return parsed.data;
51746
52474
  }
51747
- var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
52475
+ var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, digest, idempotencyKey, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
51748
52476
  if (Object.keys(value).length > limit) {
51749
52477
  context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
51750
52478
  }
@@ -51766,6 +52494,8 @@ var init_schema2 = __esm(() => {
51766
52494
  };
51767
52495
  key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
51768
52496
  identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
52497
+ digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
52498
+ idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
51769
52499
  uuid2 = exports_external.string().uuid();
51770
52500
  scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
51771
52501
  comment = exports_external.object({
@@ -51803,7 +52533,9 @@ var init_schema2 = __esm(() => {
51803
52533
  schema = exports_external.object({
51804
52534
  version: exports_external.literal(1),
51805
52535
  operation_id: identifier,
51806
- idempotency_key: identifier,
52536
+ step_id: identifier,
52537
+ idempotency_key: idempotencyKey,
52538
+ precondition_digest: digest,
51807
52539
  project_id: uuid2,
51808
52540
  task_list_id: uuid2.optional(),
51809
52541
  if_binding_version: exports_external.number().int().min(0).optional(),
@@ -51819,7 +52551,10 @@ var init_schema2 = __esm(() => {
51819
52551
  }).strict();
51820
52552
  compensationSchema = exports_external.object({
51821
52553
  receipt_id: uuid2,
51822
- idempotency_key: identifier,
52554
+ operation_id: identifier,
52555
+ step_id: identifier,
52556
+ idempotency_key: idempotencyKey,
52557
+ precondition_digest: digest,
51823
52558
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
51824
52559
  }).strict();
51825
52560
  bindingLookupSchema = exports_external.object({
@@ -51837,6 +52572,7 @@ function taskManifestPlanSlug(manifest, planId) {
51837
52572
  const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
51838
52573
  return `${base}-${planId}`;
51839
52574
  }
52575
+ var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
51840
52576
  var init_plan_slug = () => {};
51841
52577
 
51842
52578
  // src/task-manifest/backend.ts
@@ -51850,11 +52586,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
51850
52586
  const row = rows[0];
51851
52587
  const bindingVersion = Number(row.binding_version);
51852
52588
  const state = row.state;
51853
- 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") {
52589
+ 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") {
51854
52590
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
51855
52591
  }
51856
52592
  return {
51857
52593
  plan_id: planId,
52594
+ operation_id: row.binding_operation_id,
52595
+ step_id: row.binding_step_id,
51858
52596
  apply_receipt_id: row.apply_receipt_id,
51859
52597
  binding_version: bindingVersion,
51860
52598
  state
@@ -51907,9 +52645,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51907
52645
  schema_version integer NOT NULL CHECK(schema_version = 1),
51908
52646
  kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
51909
52647
  operation_id text NOT NULL,
52648
+ step_id text NOT NULL,
51910
52649
  idempotency_key text NOT NULL,
51911
52650
  request_digest text NOT NULL,
52651
+ precondition_digest text NOT NULL,
51912
52652
  result_digest text NOT NULL,
52653
+ slug_provenance text,
52654
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
52655
+ reason text,
52656
+ duplicate_of_receipt_id text,
51913
52657
  binding_version integer NOT NULL,
51914
52658
  apply_receipt_id text,
51915
52659
  manifest_json jsonb,
@@ -51921,12 +52665,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51921
52665
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
51922
52666
  `ALTER TABLE todos_task_manifest_receipts
51923
52667
  ALTER COLUMN tenant_id DROP DEFAULT`,
52668
+ `ALTER TABLE todos_task_manifest_receipts
52669
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
52670
+ `ALTER TABLE todos_task_manifest_receipts
52671
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
52672
+ `ALTER TABLE todos_task_manifest_receipts
52673
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
52674
+ `ALTER TABLE todos_task_manifest_receipts
52675
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
52676
+ `ALTER TABLE todos_task_manifest_receipts
52677
+ ADD COLUMN IF NOT EXISTS reason text`,
52678
+ `ALTER TABLE todos_task_manifest_receipts
52679
+ ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
51924
52680
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
51925
52681
  operation_id text PRIMARY KEY,
51926
52682
  tenant_id text NOT NULL,
52683
+ step_id text NOT NULL,
51927
52684
  idempotency_key text NOT NULL UNIQUE,
51928
52685
  request_digest text NOT NULL,
52686
+ precondition_digest text NOT NULL,
51929
52687
  result_digest text NOT NULL,
52688
+ slug_provenance text,
52689
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
51930
52690
  apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
51931
52691
  manifest_json jsonb NOT NULL,
51932
52692
  result_json jsonb NOT NULL,
@@ -51940,6 +52700,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51940
52700
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
51941
52701
  `ALTER TABLE todos_task_manifest_bindings
51942
52702
  ALTER COLUMN tenant_id DROP DEFAULT`,
52703
+ `ALTER TABLE todos_task_manifest_bindings
52704
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
52705
+ `ALTER TABLE todos_task_manifest_bindings
52706
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
52707
+ `ALTER TABLE todos_task_manifest_bindings
52708
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
52709
+ `ALTER TABLE todos_task_manifest_bindings
52710
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
51943
52711
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
51944
52712
  id text PRIMARY KEY,
51945
52713
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -51951,10 +52719,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51951
52719
  created_at timestamptz NOT NULL,
51952
52720
  delivered_at timestamptz
51953
52721
  )`,
52722
+ `CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
52723
+ receipt_id text PRIMARY KEY,
52724
+ tenant_id text NOT NULL,
52725
+ authority text NOT NULL CHECK(authority = 'todos'),
52726
+ route text NOT NULL,
52727
+ schema_version integer NOT NULL CHECK(schema_version = 1),
52728
+ kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
52729
+ operation_id text NOT NULL,
52730
+ step_id text NOT NULL,
52731
+ idempotency_key text NOT NULL,
52732
+ request_digest text NOT NULL,
52733
+ precondition_digest text NOT NULL,
52734
+ result_digest text NOT NULL,
52735
+ outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
52736
+ reason text NOT NULL,
52737
+ binding_version integer NOT NULL,
52738
+ apply_receipt_id text,
52739
+ manifest_json jsonb,
52740
+ result_json jsonb NOT NULL,
52741
+ created_at timestamptz NOT NULL
52742
+ )`,
51954
52743
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
51955
52744
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
51956
52745
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
51957
52746
  ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
52747
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
52748
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
52749
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
52750
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
52751
+ `CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
52752
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
51958
52753
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
51959
52754
  ON todos_task_manifest_bindings(
51960
52755
  tenant_id,
@@ -51967,6 +52762,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51967
52762
  `DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
51968
52763
  `CREATE TRIGGER todos_task_manifest_receipts_immutable
51969
52764
  BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
52765
+ FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
52766
+ `DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
52767
+ `CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
52768
+ BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
51970
52769
  FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
51971
52770
  ];
51972
52771
  }
@@ -51991,6 +52790,39 @@ function safeIdentifier2(value, field) {
51991
52790
  function parseJson2(value) {
51992
52791
  return typeof value === "string" ? JSON.parse(value) : value;
51993
52792
  }
52793
+ function parseApplyResult(value, duplicate) {
52794
+ const parsed = parseJson2(value);
52795
+ return {
52796
+ ...parsed,
52797
+ duplicate,
52798
+ receipt: {
52799
+ ...parsed.receipt,
52800
+ step_id: parsed.receipt.step_id ?? "legacy-apply",
52801
+ precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
52802
+ outcome: parsed.receipt.outcome ?? "accepted",
52803
+ reason: parsed.receipt.reason ?? null,
52804
+ duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
52805
+ }
52806
+ };
52807
+ }
52808
+ function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
52809
+ if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
52810
+ const expected = taskManifestPlanSlug(manifest, planId);
52811
+ if (slug !== expected) {
52812
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
52813
+ }
52814
+ return expected;
52815
+ }
52816
+ if (provenance !== null && provenance !== undefined) {
52817
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
52818
+ }
52819
+ if (slug === null || slug === undefined)
52820
+ return null;
52821
+ if (slug !== null && slug !== undefined) {
52822
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
52823
+ }
52824
+ return null;
52825
+ }
51994
52826
  function timestamp4(value) {
51995
52827
  return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
51996
52828
  }
@@ -51998,6 +52830,41 @@ function fault(faults, point) {
51998
52830
  if (faults.points.has(point))
51999
52831
  throw new Error(`Injected task-manifest fault at ${point}`);
52000
52832
  }
52833
+ function terminalApplyResult(input, reason) {
52834
+ const receipt = {
52835
+ receipt_id: input.terminal_receipt_id,
52836
+ authority: "todos",
52837
+ route: "todos.task-manifest.v1",
52838
+ schema_version: 1,
52839
+ kind: "apply",
52840
+ operation_id: input.manifest.operation_id,
52841
+ step_id: input.manifest.step_id,
52842
+ idempotency_key: input.manifest.idempotency_key,
52843
+ request_digest: input.request_digest,
52844
+ precondition_digest: input.manifest.precondition_digest,
52845
+ result_digest: canonicalDigest({
52846
+ outcome: "terminal_nonacceptance",
52847
+ reason,
52848
+ operation_id: input.manifest.operation_id,
52849
+ step_id: input.manifest.step_id,
52850
+ request_digest: input.request_digest
52851
+ }),
52852
+ outcome: "terminal_nonacceptance",
52853
+ reason,
52854
+ duplicate_of_receipt_id: null,
52855
+ binding_version: 0,
52856
+ apply_receipt_id: null,
52857
+ created_at: input.now
52858
+ };
52859
+ return {
52860
+ duplicate: false,
52861
+ receipt,
52862
+ graph: input.graph,
52863
+ readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
52864
+ outbox_ids: [],
52865
+ result_digest: receipt.result_digest
52866
+ };
52867
+ }
52001
52868
  function receiptFromRow3(row) {
52002
52869
  return {
52003
52870
  receipt_id: String(row["receipt_id"]),
@@ -52006,9 +52873,14 @@ function receiptFromRow3(row) {
52006
52873
  schema_version: 1,
52007
52874
  kind: row["kind"],
52008
52875
  operation_id: String(row["operation_id"]),
52876
+ step_id: String(row["step_id"] ?? "legacy-apply"),
52009
52877
  idempotency_key: String(row["idempotency_key"]),
52010
52878
  request_digest: String(row["request_digest"]),
52879
+ precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
52011
52880
  result_digest: String(row["result_digest"]),
52881
+ outcome: row["outcome"] ?? "accepted",
52882
+ reason: row["reason"] == null ? null : row["reason"],
52883
+ duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
52012
52884
  binding_version: Number(row["binding_version"]),
52013
52885
  apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
52014
52886
  created_at: timestamp4(row["created_at"])
@@ -52126,46 +52998,89 @@ class PostgresTodosTaskManifestBackend {
52126
52998
  now4
52127
52999
  ]);
52128
53000
  }
53001
+ async persistTerminal(tx, input, reason) {
53002
+ const result = terminalApplyResult(input, reason);
53003
+ const resultJson = canonicalJson(result);
53004
+ await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
53005
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
53006
+ idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
53007
+ binding_version, apply_receipt_id, manifest_json, result_json, created_at
53008
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
53009
+ 'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
53010
+ ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
53011
+ result.receipt.receipt_id,
53012
+ this.tenantId,
53013
+ input.manifest.operation_id,
53014
+ input.manifest.step_id,
53015
+ input.manifest.idempotency_key,
53016
+ input.request_digest,
53017
+ input.manifest.precondition_digest,
53018
+ result.receipt.result_digest,
53019
+ reason,
53020
+ canonicalJson(input.manifest),
53021
+ resultJson,
53022
+ input.now
53023
+ ]);
53024
+ const stored = await tx.query(`SELECT receipt_id, result_json
53025
+ FROM todos_task_manifest_terminal_receipts
53026
+ WHERE tenant_id = $1 AND kind = 'apply'
53027
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
53028
+ ORDER BY created_at ASC, receipt_id ASC
53029
+ LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
53030
+ return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
53031
+ }
52129
53032
  async apply(input, faults) {
52130
53033
  await this.ensureSchema();
52131
53034
  return this.client.transaction(async (tx) => {
52132
53035
  const { manifest } = input;
52133
53036
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
52134
53037
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
53038
+ const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
53039
+ WHERE tenant_id = $1
53040
+ AND kind = 'apply'
53041
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
53042
+ ORDER BY created_at ASC, receipt_id ASC
53043
+ LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
53044
+ if (terminal.rows[0]) {
53045
+ return parseApplyResult(terminal.rows[0]["result_json"], true);
53046
+ }
52135
53047
  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]);
52136
53048
  if (existing.rows[0]) {
52137
53049
  const binding = existing.rows[0];
52138
- if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
52139
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
53050
+ 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) {
53051
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
52140
53052
  }
52141
53053
  if (binding["state"] !== "applied") {
52142
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
53054
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
52143
53055
  }
52144
- return { ...parseJson2(binding["result_json"]), duplicate: true };
53056
+ return parseApplyResult(binding["result_json"], true);
52145
53057
  }
52146
53058
  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]);
52147
53059
  if (reused.rows[0])
52148
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
53060
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
53061
+ if (manifest.idempotency_key !== input.expected_idempotency_key) {
53062
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
53063
+ }
52149
53064
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
52150
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "New manifest binding version must be 0");
53065
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
52151
53066
  }
52152
53067
  const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
52153
53068
  WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
52154
53069
  if (!project.rows[0])
52155
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
53070
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
52156
53071
  if (manifest.task_list_id) {
52157
53072
  const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
52158
53073
  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]);
52159
53074
  const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
52160
53075
  if (!payload || payload["project_id"] !== manifest.project_id) {
52161
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Task list does not belong to the project");
53076
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
52162
53077
  }
52163
53078
  }
52164
53079
  const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
52165
53080
  const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
52166
53081
  WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
52167
53082
  if (conflict.rows[0])
52168
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "A deterministic graph id already exists");
53083
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
52169
53084
  await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
52170
53085
  fault(faults, "after_plan_write");
52171
53086
  for (const task3 of manifest.tasks) {
@@ -52235,9 +53150,14 @@ class PostgresTodosTaskManifestBackend {
52235
53150
  schema_version: 1,
52236
53151
  kind: "apply",
52237
53152
  operation_id: manifest.operation_id,
53153
+ step_id: manifest.step_id,
52238
53154
  idempotency_key: manifest.idempotency_key,
52239
53155
  request_digest: input.request_digest,
53156
+ precondition_digest: manifest.precondition_digest,
52240
53157
  result_digest: input.result_digest,
53158
+ outcome: "accepted",
53159
+ reason: null,
53160
+ duplicate_of_receipt_id: null,
52241
53161
  binding_version: 1,
52242
53162
  apply_receipt_id: null,
52243
53163
  created_at: input.now
@@ -52254,14 +53174,18 @@ class PostgresTodosTaskManifestBackend {
52254
53174
  const resultJson = canonicalJson(result);
52255
53175
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
52256
53176
  receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
52257
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
52258
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
53177
+ step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
53178
+ reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
53179
+ ) 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)`, [
52259
53180
  input.receipt_id,
52260
53181
  this.tenantId,
52261
53182
  manifest.operation_id,
52262
53183
  manifest.idempotency_key,
53184
+ manifest.step_id,
52263
53185
  input.request_digest,
53186
+ manifest.precondition_digest,
52264
53187
  input.result_digest,
53188
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
52265
53189
  manifestJson,
52266
53190
  resultJson,
52267
53191
  input.now
@@ -52280,14 +53204,18 @@ class PostgresTodosTaskManifestBackend {
52280
53204
  }
52281
53205
  fault(faults, "after_outbox_write");
52282
53206
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
52283
- operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
52284
- manifest_json, result_json, state, version, created_at, updated_at
52285
- ) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
53207
+ operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
53208
+ result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
53209
+ state, version, created_at, updated_at
53210
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
52286
53211
  manifest.operation_id,
52287
53212
  this.tenantId,
53213
+ manifest.step_id,
52288
53214
  manifest.idempotency_key,
52289
53215
  input.request_digest,
53216
+ manifest.precondition_digest,
52290
53217
  input.result_digest,
53218
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
52291
53219
  input.receipt_id,
52292
53220
  manifestJson,
52293
53221
  resultJson,
@@ -52300,9 +53228,12 @@ class PostgresTodosTaskManifestBackend {
52300
53228
  async readExact(receiptId2) {
52301
53229
  await this.ensureSchema();
52302
53230
  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]);
52303
- if (!result.rows[0])
53231
+ if (result.rows[0])
53232
+ return parseApplyResult(result.rows[0]["result_json"], false);
53233
+ 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]);
53234
+ if (!terminal.rows[0])
52304
53235
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
52305
- return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
53236
+ return parseApplyResult(terminal.rows[0]["result_json"], false);
52306
53237
  }
52307
53238
  async lookupBindingByPlanId(planId) {
52308
53239
  await this.ensureSchema();
@@ -52313,6 +53244,7 @@ class PostgresTodosTaskManifestBackend {
52313
53244
  b.version AS binding_version,
52314
53245
  b.tenant_id AS binding_tenant_id,
52315
53246
  b.operation_id AS binding_operation_id,
53247
+ b.step_id AS binding_step_id,
52316
53248
  b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
52317
53249
  r.tenant_id AS receipt_tenant_id,
52318
53250
  r.authority AS receipt_authority,
@@ -52320,6 +53252,7 @@ class PostgresTodosTaskManifestBackend {
52320
53252
  r.schema_version AS receipt_schema_version,
52321
53253
  r.kind AS receipt_kind,
52322
53254
  r.operation_id AS receipt_operation_id,
53255
+ r.step_id AS receipt_step_id,
52323
53256
  r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
52324
53257
  FROM todos_task_manifest_bindings b
52325
53258
  LEFT JOIN todos_task_manifest_receipts r
@@ -52406,6 +53339,10 @@ class PostgresTodosTaskManifestBackend {
52406
53339
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
52407
53340
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
52408
53341
  }
53342
+ const appliedReceipt = receiptFromRow3(applyRow);
53343
+ 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"]) {
53344
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
53345
+ }
52409
53346
  if (binding["state"] !== "applied")
52410
53347
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
52411
53348
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -52420,12 +53357,18 @@ class PostgresTodosTaskManifestBackend {
52420
53357
  LIMIT 1`, [this.tenantId, input.receipt_id]);
52421
53358
  if (delivered.rows[0])
52422
53359
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
52423
- const applyResult = parseJson2(applyRow["result_json"]);
53360
+ const applyResult = parseApplyResult(applyRow["result_json"], false);
52424
53361
  const manifest = parseJson2(applyRow["manifest_json"]);
53362
+ const manifestRecord2 = manifest;
53363
+ const applyStepId = typeof manifestRecord2["step_id"] === "string" ? String(manifestRecord2["step_id"]) : null;
52425
53364
  const expectedEffects = [
52426
53365
  {
52427
53366
  topic: "todos.task-manifest.applied",
52428
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
53367
+ payload: {
53368
+ operation_id: manifest.operation_id,
53369
+ ...applyStepId ? { step_id: applyStepId } : {},
53370
+ project_id: manifest.project_id
53371
+ }
52429
53372
  },
52430
53373
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
52431
53374
  ];
@@ -52473,9 +53416,15 @@ class PostgresTodosTaskManifestBackend {
52473
53416
  }
52474
53417
  const appliedAt = receiptFromRow3(applyRow).created_at;
52475
53418
  const expectedPayloads = new Map;
53419
+ const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
53420
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
53421
+ LIMIT 1`, [this.service, applyResult.graph.plan_id]);
53422
+ const actualPlan = planRow.rows[0] ? parseJson2(planRow.rows[0]["payload"]) : null;
53423
+ const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
53424
+ planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
52476
53425
  expectedPayloads.set(applyResult.graph.plan_id, {
52477
53426
  type: "plans",
52478
- payload: canonicalJson(planPayload({ manifest, graph: applyResult.graph, now: appliedAt }))
53427
+ payload: canonicalJson(planExpected)
52479
53428
  });
52480
53429
  for (const task3 of manifest.tasks)
52481
53430
  expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
@@ -52575,14 +53524,17 @@ class PostgresTodosTaskManifestBackend {
52575
53524
  const readback = await this.readback(tx, applyResult.graph);
52576
53525
  const result = { duplicate: false, receipt, absent: true, readback };
52577
53526
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
52578
- receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
52579
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
52580
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
53527
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
53528
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
53529
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
53530
+ ) 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)`, [
52581
53531
  compensationReceiptId,
52582
53532
  this.tenantId,
52583
53533
  receipt.operation_id,
53534
+ receipt.step_id,
52584
53535
  input.idempotency_key,
52585
53536
  requestDigest,
53537
+ input.precondition_digest,
52586
53538
  receipt.result_digest,
52587
53539
  receipt.binding_version,
52588
53540
  input.receipt_id,
@@ -52639,36 +53591,83 @@ function resolveTenantId(value) {
52639
53591
  }
52640
53592
  return tenantId;
52641
53593
  }
53594
+ function taskManifestRequestDigest(manifest) {
53595
+ const { idempotency_key: _idempotencyKey, ...request } = manifest;
53596
+ return canonicalDigest(request);
53597
+ }
53598
+ function taskManifestCompensationRequestDigest(request) {
53599
+ return canonicalDigest(request);
53600
+ }
53601
+ function deriveTodosTaskManifestApplyPreconditionDigest(input) {
53602
+ return canonicalDigest({
53603
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
53604
+ direction: "apply",
53605
+ operation_id: input.operation_id,
53606
+ step_id: input.step_id,
53607
+ project_id: input.project_id,
53608
+ task_list_id: input.task_list_id ?? null,
53609
+ expected_binding_version: input.if_binding_version ?? 0
53610
+ });
53611
+ }
53612
+ function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
53613
+ return canonicalDigest({
53614
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
53615
+ direction: "compensate",
53616
+ operation_id: input.operation_id,
53617
+ step_id: input.step_id,
53618
+ apply_receipt_id: input.receipt_id,
53619
+ expected_binding_version: input.if_binding_version
53620
+ });
53621
+ }
53622
+ function deriveTodosTaskManifestIdempotencyKey(input) {
53623
+ return `tmk_${canonicalDigest({
53624
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
53625
+ ...input
53626
+ }).slice(0, 48)}`;
53627
+ }
52642
53628
  function normalize(input, now4) {
52643
53629
  const parsed = parseTodosTaskManifest(input);
52644
53630
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
52645
53631
  if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
52646
53632
  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 });
52647
53633
  }
53634
+ const { idempotency_key: _idempotencyKey, ...request } = parsed;
53635
+ const request_digest = taskManifestRequestDigest(request);
52648
53636
  const manifest = sanitizeManifest(parsed);
53637
+ const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
53638
+ if (manifest.precondition_digest !== expectedPreconditionDigest) {
53639
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
53640
+ }
53641
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
53642
+ operation_id: manifest.operation_id,
53643
+ step_id: manifest.step_id,
53644
+ direction: "apply",
53645
+ target_selector: manifest.project_id,
53646
+ request_digest,
53647
+ precondition_digest: manifest.precondition_digest
53648
+ });
52649
53649
  const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
52650
53650
  task3.key,
52651
- deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task3.key)
53651
+ deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task3.key)
52652
53652
  ]));
52653
53653
  const graph = {
52654
- plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
53654
+ plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
52655
53655
  task_ids,
52656
- comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task3.key, String(index)))),
52657
- verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task3.key, String(index)))),
53656
+ 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)))),
53657
+ 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)))),
52658
53658
  dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
52659
53659
  };
52660
- const request_digest = canonicalDigest(parsed);
52661
53660
  const effectInputs = [
52662
53661
  {
52663
53662
  topic: "todos.task-manifest.applied",
52664
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
53663
+ payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
52665
53664
  },
52666
53665
  ...manifest.effects ?? []
52667
53666
  ];
52668
53667
  const outbox = effectInputs.map((effect2, index) => {
52669
53668
  const payload = { ...effect2.payload };
52670
53669
  return {
52671
- id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
53670
+ id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
52672
53671
  topic: effect2.topic,
52673
53672
  payload,
52674
53673
  digest: canonicalDigest({ topic: effect2.topic, payload })
@@ -52678,11 +53677,14 @@ function normalize(input, now4) {
52678
53677
  return {
52679
53678
  manifest,
52680
53679
  request_digest,
53680
+ expected_idempotency_key: expectedIdempotencyKey,
52681
53681
  result_digest,
52682
- receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
53682
+ receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
53683
+ terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
52683
53684
  graph,
52684
53685
  outbox,
52685
- now: now4
53686
+ now: now4,
53687
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
52686
53688
  };
52687
53689
  }
52688
53690
  function sanitizeManifest(manifest) {
@@ -52740,6 +53742,10 @@ class PackageOwnedTodosTaskManifestAuthority {
52740
53742
  tenant_id: this.tenantId,
52741
53743
  backend: this.backend.kind,
52742
53744
  deterministic_ids: true,
53745
+ operation_step_identity: true,
53746
+ deterministic_idempotency_keys: true,
53747
+ terminal_nonacceptance_receipts: true,
53748
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
52743
53749
  immutable_receipts: true,
52744
53750
  transactional_outbox: true,
52745
53751
  idempotent_outbox_delivery: true,
@@ -52769,7 +53775,11 @@ class PackageOwnedTodosTaskManifestAuthority {
52769
53775
  async apply(input) {
52770
53776
  const normalized = normalize(input, this.now());
52771
53777
  const faults = await this.prepareFaults();
52772
- return this.bounded(await this.backend.apply(normalized, faults));
53778
+ const result = this.bounded(await this.backend.apply(normalized, faults));
53779
+ if (result.receipt.outcome === "terminal_nonacceptance") {
53780
+ throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
53781
+ }
53782
+ return result;
52773
53783
  }
52774
53784
  readExact(receiptId2) {
52775
53785
  if (!receiptId2 || receiptId2.length > 200) {
@@ -52802,18 +53812,48 @@ class PackageOwnedTodosTaskManifestAuthority {
52802
53812
  async compensate(input) {
52803
53813
  const request = parseTodosTaskManifestCompensation(input);
52804
53814
  const applied = await this.backend.readExact(request.receipt_id);
52805
- const requestDigest = canonicalDigest(request);
52806
- const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", applied.receipt.operation_id, request.idempotency_key, requestDigest);
53815
+ if (applied.receipt.outcome !== "accepted") {
53816
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
53817
+ }
53818
+ if (request.operation_id !== applied.receipt.operation_id) {
53819
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
53820
+ }
53821
+ if (request.step_id === applied.receipt.step_id) {
53822
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
53823
+ }
53824
+ const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
53825
+ if (request.precondition_digest !== expectedPreconditionDigest) {
53826
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
53827
+ }
53828
+ const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
53829
+ const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
53830
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
53831
+ operation_id: request.operation_id,
53832
+ step_id: request.step_id,
53833
+ direction: "compensate",
53834
+ target_selector: request.receipt_id,
53835
+ request_digest: requestDigest,
53836
+ precondition_digest: request.precondition_digest
53837
+ });
53838
+ if (request.idempotency_key !== expectedIdempotencyKey) {
53839
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
53840
+ }
53841
+ const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
52807
53842
  const receipt = {
52808
53843
  receipt_id: compensationReceiptId,
52809
53844
  authority: "todos",
52810
53845
  route: TODOS_TASK_MANIFEST_ROUTE,
52811
53846
  schema_version: 1,
52812
53847
  kind: "compensate",
52813
- operation_id: applied.receipt.operation_id,
53848
+ operation_id: request.operation_id,
53849
+ step_id: request.step_id,
52814
53850
  idempotency_key: request.idempotency_key,
52815
53851
  request_digest: requestDigest,
53852
+ precondition_digest: request.precondition_digest,
52816
53853
  result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
53854
+ outcome: "accepted",
53855
+ reason: null,
53856
+ duplicate_of_receipt_id: null,
52817
53857
  binding_version: request.if_binding_version + 1,
52818
53858
  apply_receipt_id: applied.receipt.receipt_id,
52819
53859
  created_at: this.now()
@@ -54479,9 +55519,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
54479
55519
  TaskManifestBounds: taskManifestBoundsSchema,
54480
55520
  TaskManifestCapability: taskManifestCapabilitySchema,
54481
55521
  TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
55522
+ TaskManifest: taskManifestSchema,
55523
+ TaskManifestReceipt: taskManifestReceiptSchema,
55524
+ TaskManifestApplyResult: taskManifestApplyResultSchema,
55525
+ TaskManifestApplyResponse: taskManifestApplyResponseSchema,
55526
+ TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
55527
+ TaskManifestCompensationResult: taskManifestCompensationResultSchema,
55528
+ TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
55529
+ TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
54482
55530
  TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
54483
55531
  TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
54484
55532
  TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
55533
+ ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
55534
+ ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
55535
+ ProjectRegistrationRequest: projectRegistrationRequestSchema,
55536
+ ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
55537
+ PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
55538
+ ProjectResource: projectResourceSchema,
55539
+ ProjectResourcePage: projectResourcePageSchema,
54485
55540
  TaskList: taskListSchema,
54486
55541
  ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
54487
55542
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
@@ -54524,6 +55579,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
54524
55579
  priority: { type: "string", enum: [...TASK_PRIORITIES] },
54525
55580
  assigned_to: { type: "string" },
54526
55581
  project_id: { type: "string", nullable: true },
55582
+ parent_id: { type: "string", nullable: true },
54527
55583
  plan_id: { type: "string", nullable: true },
54528
55584
  task_list_id: { type: "string", nullable: true },
54529
55585
  version: { type: "number" }
@@ -55478,6 +56534,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55478
56534
  },
55479
56535
  security: [{ apiKey: [] }],
55480
56536
  paths: {
56537
+ "/v1/project-registration/capability": {
56538
+ get: {
56539
+ operationId: "getProjectRegistrationCapability",
56540
+ summary: "Read the live package-owned Projects to Todos registration capability",
56541
+ responses: {
56542
+ "200": {
56543
+ content: {
56544
+ "application/json": {
56545
+ schema: {
56546
+ type: "object",
56547
+ additionalProperties: false,
56548
+ required: ["capability"],
56549
+ properties: {
56550
+ capability: {
56551
+ $ref: "#/components/schemas/ProjectRegistrationCapability"
56552
+ }
56553
+ }
56554
+ }
56555
+ }
56556
+ }
56557
+ }
56558
+ }
56559
+ }
56560
+ },
56561
+ "/v1/project-registration/resources": {
56562
+ get: {
56563
+ operationId: "listProjectRegistrationResources",
56564
+ summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
56565
+ parameters: [
56566
+ {
56567
+ name: "source_project_id",
56568
+ in: "query",
56569
+ required: true,
56570
+ schema: { type: "string" }
56571
+ },
56572
+ {
56573
+ name: "include_anchors",
56574
+ in: "query",
56575
+ schema: { type: "boolean", default: false }
56576
+ },
56577
+ {
56578
+ name: "limit",
56579
+ in: "query",
56580
+ schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
56581
+ },
56582
+ {
56583
+ name: "cursor",
56584
+ in: "query",
56585
+ schema: { type: "string" }
56586
+ }
56587
+ ],
56588
+ responses: {
56589
+ "200": {
56590
+ content: {
56591
+ "application/json": {
56592
+ schema: {
56593
+ type: "object",
56594
+ additionalProperties: false,
56595
+ required: ["page"],
56596
+ properties: {
56597
+ page: { $ref: "#/components/schemas/ProjectResourcePage" }
56598
+ }
56599
+ }
56600
+ }
56601
+ }
56602
+ },
56603
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56604
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56605
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56606
+ }
56607
+ }
56608
+ },
56609
+ "/v1/project-registration/create": {
56610
+ post: {
56611
+ operationId: "createProjectRegistrationResource",
56612
+ summary: "Create or deterministically bind one Projects to Todos resource",
56613
+ requestBody: {
56614
+ required: true,
56615
+ content: {
56616
+ "application/json": {
56617
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
56618
+ }
56619
+ }
56620
+ },
56621
+ responses: {
56622
+ "201": {
56623
+ content: {
56624
+ "application/json": {
56625
+ schema: {
56626
+ type: "object",
56627
+ additionalProperties: false,
56628
+ required: ["receipt"],
56629
+ properties: {
56630
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
56631
+ }
56632
+ }
56633
+ }
56634
+ }
56635
+ },
56636
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56637
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56638
+ }
56639
+ }
56640
+ },
56641
+ "/v1/project-registration/read-exact": {
56642
+ post: {
56643
+ operationId: "readExactProjectRegistrationResource",
56644
+ summary: "Read one registered project or task list by exact full UUID",
56645
+ requestBody: {
56646
+ required: true,
56647
+ content: {
56648
+ "application/json": {
56649
+ schema: {
56650
+ type: "object",
56651
+ additionalProperties: false,
56652
+ required: [
56653
+ "resource_kind",
56654
+ "target_id",
56655
+ "response_byte_limit",
56656
+ "time_budget_ms"
56657
+ ],
56658
+ properties: {
56659
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
56660
+ target_id: { type: "string", format: "uuid" },
56661
+ ...projectRegistrationBoundsProperties
56662
+ }
56663
+ }
56664
+ }
56665
+ }
56666
+ },
56667
+ responses: {
56668
+ "200": {
56669
+ content: {
56670
+ "application/json": {
56671
+ schema: {
56672
+ type: "object",
56673
+ additionalProperties: false,
56674
+ required: ["record"],
56675
+ properties: {
56676
+ record: {
56677
+ type: "object",
56678
+ additionalProperties: false,
56679
+ required: ["target_id", "revision", "digest"],
56680
+ properties: {
56681
+ target_id: { type: "string", format: "uuid" },
56682
+ revision: { type: "string" },
56683
+ digest: { type: "string" }
56684
+ }
56685
+ }
56686
+ }
56687
+ }
56688
+ }
56689
+ }
56690
+ },
56691
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56692
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56693
+ }
56694
+ }
56695
+ },
56696
+ "/v1/project-registration/receipts/lookup": {
56697
+ post: {
56698
+ operationId: "lookupProjectRegistrationReceipt",
56699
+ summary: "Recover one exact immutable terminal registration receipt",
56700
+ requestBody: {
56701
+ required: true,
56702
+ content: {
56703
+ "application/json": {
56704
+ schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
56705
+ }
56706
+ }
56707
+ },
56708
+ responses: {
56709
+ "200": {
56710
+ content: {
56711
+ "application/json": {
56712
+ schema: {
56713
+ type: "object",
56714
+ additionalProperties: false,
56715
+ required: ["receipt", "response_control"],
56716
+ properties: {
56717
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
56718
+ response_control: {
56719
+ type: "object",
56720
+ additionalProperties: false,
56721
+ required: [
56722
+ "response_byte_limit",
56723
+ "time_budget_ms",
56724
+ "response_bytes",
56725
+ "elapsed_ms",
56726
+ "complete",
56727
+ "truncated"
56728
+ ],
56729
+ properties: {
56730
+ ...projectRegistrationBoundsProperties,
56731
+ response_bytes: { type: "integer", minimum: 0 },
56732
+ elapsed_ms: { type: "integer", minimum: 0 },
56733
+ complete: { type: "boolean", enum: [true] },
56734
+ truncated: { type: "boolean", enum: [false] }
56735
+ }
56736
+ }
56737
+ }
56738
+ }
56739
+ }
56740
+ }
56741
+ },
56742
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56743
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56744
+ }
56745
+ }
56746
+ },
56747
+ "/v1/project-registration/validate-prior-adoption": {
56748
+ post: {
56749
+ operationId: "validatePriorRegistrationAdoption",
56750
+ summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
56751
+ requestBody: {
56752
+ required: true,
56753
+ content: {
56754
+ "application/json": {
56755
+ schema: {
56756
+ type: "object",
56757
+ additionalProperties: false,
56758
+ required: ["source_request", "source_receipt", "current_record"],
56759
+ properties: {
56760
+ source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
56761
+ source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
56762
+ current_record: {
56763
+ oneOf: [
56764
+ { $ref: "#/components/schemas/Project" },
56765
+ { $ref: "#/components/schemas/TaskList" }
56766
+ ]
56767
+ }
56768
+ }
56769
+ }
56770
+ }
56771
+ }
56772
+ },
56773
+ responses: {
56774
+ "200": {
56775
+ content: {
56776
+ "application/json": {
56777
+ schema: {
56778
+ type: "object",
56779
+ additionalProperties: false,
56780
+ required: ["validation"],
56781
+ properties: {
56782
+ validation: {
56783
+ $ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
56784
+ }
56785
+ }
56786
+ }
56787
+ }
56788
+ }
56789
+ },
56790
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56791
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56792
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56793
+ }
56794
+ }
56795
+ },
56796
+ "/v1/project-registration/compensate": {
56797
+ post: {
56798
+ operationId: "compensateProjectRegistrationResource",
56799
+ summary: "Conditionally remove an unchanged receipt-owned registration resource",
56800
+ requestBody: {
56801
+ required: true,
56802
+ content: {
56803
+ "application/json": {
56804
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
56805
+ }
56806
+ }
56807
+ },
56808
+ responses: {
56809
+ "201": {
56810
+ content: {
56811
+ "application/json": {
56812
+ schema: {
56813
+ type: "object",
56814
+ additionalProperties: false,
56815
+ required: ["receipt"],
56816
+ properties: {
56817
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
56818
+ }
56819
+ }
56820
+ }
56821
+ }
56822
+ },
56823
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56824
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56825
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56826
+ }
56827
+ }
56828
+ },
56829
+ "/v1/project-registration/verify-inverse": {
56830
+ post: {
56831
+ operationId: "verifyInverseProjectRegistrationResource",
56832
+ summary: "Verify exact absence after conditional registration compensation",
56833
+ requestBody: {
56834
+ required: true,
56835
+ content: {
56836
+ "application/json": {
56837
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
56838
+ }
56839
+ }
56840
+ },
56841
+ responses: {
56842
+ "200": {
56843
+ content: {
56844
+ "application/json": {
56845
+ schema: {
56846
+ type: "object",
56847
+ additionalProperties: false,
56848
+ required: ["verification"],
56849
+ properties: {
56850
+ verification: {
56851
+ type: "object",
56852
+ additionalProperties: false,
56853
+ required: ["target_id", "accepted_receipt_id", "absent", "digest"],
56854
+ properties: {
56855
+ target_id: { type: "string", format: "uuid" },
56856
+ accepted_receipt_id: { type: "string" },
56857
+ absent: { type: "boolean", enum: [true] },
56858
+ digest: { type: "string" }
56859
+ }
56860
+ }
56861
+ }
56862
+ }
56863
+ }
56864
+ }
56865
+ },
56866
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56867
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56868
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56869
+ }
56870
+ }
56871
+ },
55481
56872
  "/v1/task-manifest/capability": {
55482
56873
  get: {
55483
56874
  operationId: "getTaskManifestCapability",
@@ -55495,6 +56886,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55495
56886
  }
55496
56887
  }
55497
56888
  },
56889
+ "/v1/task-manifest/apply": {
56890
+ post: {
56891
+ operationId: "applyTaskManifest",
56892
+ summary: "Apply one exact task-manifest graph through the Todos authority",
56893
+ requestBody: {
56894
+ required: true,
56895
+ content: {
56896
+ "application/json": {
56897
+ schema: { $ref: "#/components/schemas/TaskManifest" }
56898
+ }
56899
+ }
56900
+ },
56901
+ responses: {
56902
+ "201": {
56903
+ content: {
56904
+ "application/json": {
56905
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
56906
+ }
56907
+ }
56908
+ },
56909
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56910
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56911
+ "503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56912
+ }
56913
+ }
56914
+ },
56915
+ "/v1/task-manifest/read-exact": {
56916
+ post: {
56917
+ operationId: "readExactTaskManifest",
56918
+ summary: "Read one exact immutable task-manifest apply receipt",
56919
+ requestBody: {
56920
+ required: true,
56921
+ content: {
56922
+ "application/json": {
56923
+ schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
56924
+ }
56925
+ }
56926
+ },
56927
+ responses: {
56928
+ "200": {
56929
+ content: {
56930
+ "application/json": {
56931
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
56932
+ }
56933
+ }
56934
+ },
56935
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56936
+ }
56937
+ }
56938
+ },
56939
+ "/v1/task-manifest/compensate": {
56940
+ post: {
56941
+ operationId: "compensateTaskManifest",
56942
+ summary: "Compensate one exact untouched task-manifest graph with CAS protection",
56943
+ requestBody: {
56944
+ required: true,
56945
+ content: {
56946
+ "application/json": {
56947
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
56948
+ }
56949
+ }
56950
+ },
56951
+ responses: {
56952
+ "201": {
56953
+ content: {
56954
+ "application/json": {
56955
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
56956
+ }
56957
+ }
56958
+ },
56959
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56960
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56961
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56962
+ }
56963
+ }
56964
+ },
55498
56965
  "/v1/task-manifest/bindings/lookup": {
55499
56966
  post: {
55500
56967
  operationId: "lookupTaskManifestBinding",
@@ -56355,7 +57822,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
56355
57822
  }
56356
57823
  });
56357
57824
  }
56358
- var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
57825
+ var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
56359
57826
  var init_openapi = __esm(() => {
56360
57827
  init_package_version();
56361
57828
  init_types();
@@ -56417,6 +57884,10 @@ var init_openapi = __esm(() => {
56417
57884
  "tenant_id",
56418
57885
  "backend",
56419
57886
  "deterministic_ids",
57887
+ "operation_step_identity",
57888
+ "deterministic_idempotency_keys",
57889
+ "terminal_nonacceptance_receipts",
57890
+ "plan_slug_provenance",
56420
57891
  "immutable_receipts",
56421
57892
  "transactional_outbox",
56422
57893
  "idempotent_outbox_delivery",
@@ -56432,6 +57903,10 @@ var init_openapi = __esm(() => {
56432
57903
  tenant_id: { type: "string", minLength: 1, maxLength: 200 },
56433
57904
  backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
56434
57905
  deterministic_ids: { type: "boolean", enum: [true] },
57906
+ operation_step_identity: { type: "boolean", enum: [true] },
57907
+ deterministic_idempotency_keys: { type: "boolean", enum: [true] },
57908
+ terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
57909
+ plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
56435
57910
  immutable_receipts: { type: "boolean", enum: [true] },
56436
57911
  transactional_outbox: { type: "boolean", enum: [true] },
56437
57912
  idempotent_outbox_delivery: { type: "boolean", enum: [true] },
@@ -56485,6 +57960,8 @@ var init_openapi = __esm(() => {
56485
57960
  "schema_version",
56486
57961
  "tenant_id",
56487
57962
  "plan_id",
57963
+ "operation_id",
57964
+ "step_id",
56488
57965
  "apply_receipt_id",
56489
57966
  "binding_version",
56490
57967
  "state"
@@ -56495,11 +57972,169 @@ var init_openapi = __esm(() => {
56495
57972
  schema_version: { type: "integer", enum: [1] },
56496
57973
  tenant_id: { type: "string" },
56497
57974
  plan_id: { type: "string", format: "uuid" },
57975
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
57976
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
56498
57977
  apply_receipt_id: { type: "string", format: "uuid" },
56499
57978
  binding_version: { type: "integer", minimum: 1 },
56500
57979
  state: { type: "string", enum: ["applied", "compensated"] }
56501
57980
  }
56502
57981
  };
57982
+ taskManifestSchema = {
57983
+ type: "object",
57984
+ additionalProperties: false,
57985
+ required: [
57986
+ "version",
57987
+ "operation_id",
57988
+ "step_id",
57989
+ "idempotency_key",
57990
+ "precondition_digest",
57991
+ "project_id",
57992
+ "plan",
57993
+ "tasks"
57994
+ ],
57995
+ properties: {
57996
+ version: { type: "integer", enum: [1] },
57997
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
57998
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
57999
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
58000
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58001
+ project_id: { type: "string", format: "uuid" },
58002
+ task_list_id: { type: "string", format: "uuid" },
58003
+ if_binding_version: { type: "integer", minimum: 0 },
58004
+ plan: {
58005
+ type: "object",
58006
+ additionalProperties: false,
58007
+ required: ["key", "name"],
58008
+ properties: {
58009
+ key: { type: "string", minLength: 1, maxLength: 200 },
58010
+ name: { type: "string", minLength: 1, maxLength: 200 },
58011
+ description: { type: "string" },
58012
+ status: { type: "string", enum: ["active", "completed", "archived"] }
58013
+ }
58014
+ },
58015
+ tasks: {
58016
+ type: "array",
58017
+ minItems: 1,
58018
+ items: {
58019
+ type: "object",
58020
+ additionalProperties: false,
58021
+ required: ["key", "title"],
58022
+ properties: {
58023
+ key: { type: "string", minLength: 1, maxLength: 200 },
58024
+ title: { type: "string", minLength: 1, maxLength: 200 },
58025
+ description: { type: "string" },
58026
+ status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
58027
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
58028
+ assigned_to: { type: "string" },
58029
+ created_by: { type: "string" },
58030
+ tags: { type: "array", items: { type: "string" } },
58031
+ metadata: { type: "object", additionalProperties: true },
58032
+ comments: { type: "array", items: { type: "object", additionalProperties: true } },
58033
+ verifications: { type: "array", items: { type: "object", additionalProperties: true } }
58034
+ }
58035
+ }
58036
+ },
58037
+ dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
58038
+ effects: { type: "array", items: { type: "object", additionalProperties: true } }
58039
+ }
58040
+ };
58041
+ taskManifestReceiptSchema = {
58042
+ type: "object",
58043
+ additionalProperties: false,
58044
+ required: [
58045
+ "receipt_id",
58046
+ "authority",
58047
+ "route",
58048
+ "schema_version",
58049
+ "kind",
58050
+ "operation_id",
58051
+ "step_id",
58052
+ "idempotency_key",
58053
+ "request_digest",
58054
+ "precondition_digest",
58055
+ "result_digest",
58056
+ "outcome",
58057
+ "reason",
58058
+ "duplicate_of_receipt_id",
58059
+ "binding_version",
58060
+ "apply_receipt_id",
58061
+ "created_at"
58062
+ ],
58063
+ properties: {
58064
+ receipt_id: { type: "string", format: "uuid" },
58065
+ authority: { type: "string", enum: ["todos"] },
58066
+ route: { type: "string", enum: ["todos.task-manifest.v1"] },
58067
+ schema_version: { type: "integer", enum: [1] },
58068
+ kind: { type: "string", enum: ["apply", "compensate"] },
58069
+ operation_id: { type: "string" },
58070
+ step_id: { type: "string" },
58071
+ idempotency_key: { type: "string" },
58072
+ request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58073
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58074
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58075
+ outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
58076
+ reason: { type: "string", nullable: true },
58077
+ duplicate_of_receipt_id: { type: "string", nullable: true },
58078
+ binding_version: { type: "integer", minimum: 0 },
58079
+ apply_receipt_id: { type: "string", nullable: true },
58080
+ created_at: { type: "string", format: "date-time" }
58081
+ }
58082
+ };
58083
+ taskManifestApplyResultSchema = {
58084
+ type: "object",
58085
+ additionalProperties: false,
58086
+ required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
58087
+ properties: {
58088
+ duplicate: { type: "boolean" },
58089
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
58090
+ graph: { type: "object", additionalProperties: true },
58091
+ readback: { type: "object", additionalProperties: true },
58092
+ outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
58093
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
58094
+ }
58095
+ };
58096
+ taskManifestApplyResponseSchema = {
58097
+ type: "object",
58098
+ additionalProperties: false,
58099
+ required: ["result"],
58100
+ properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
58101
+ };
58102
+ taskManifestCompensateRequestSchema = {
58103
+ type: "object",
58104
+ additionalProperties: false,
58105
+ required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
58106
+ properties: {
58107
+ receipt_id: { type: "string", format: "uuid" },
58108
+ operation_id: { type: "string" },
58109
+ step_id: { type: "string" },
58110
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
58111
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58112
+ if_binding_version: { type: "integer", minimum: 1 }
58113
+ }
58114
+ };
58115
+ taskManifestCompensationResultSchema = {
58116
+ type: "object",
58117
+ additionalProperties: false,
58118
+ required: ["duplicate", "receipt", "absent", "readback"],
58119
+ properties: {
58120
+ duplicate: { type: "boolean" },
58121
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
58122
+ absent: { type: "boolean", enum: [true] },
58123
+ readback: { type: "object", additionalProperties: true }
58124
+ }
58125
+ };
58126
+ taskManifestCompensateResponseSchema = {
58127
+ type: "object",
58128
+ additionalProperties: false,
58129
+ required: ["result"],
58130
+ properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
58131
+ };
58132
+ taskManifestReadExactRequestSchema = {
58133
+ type: "object",
58134
+ additionalProperties: false,
58135
+ required: ["receipt_id"],
58136
+ properties: { receipt_id: { type: "string", format: "uuid" } }
58137
+ };
56503
58138
  taskManifestBindingLookupResponseSchema = {
56504
58139
  type: "object",
56505
58140
  additionalProperties: false,
@@ -56849,6 +58484,292 @@ var init_openapi = __esm(() => {
56849
58484
  metadata: { type: "object", additionalProperties: true }
56850
58485
  }
56851
58486
  };
58487
+ projectRegistrationBoundsProperties = {
58488
+ response_byte_limit: { type: "integer", minimum: 1 },
58489
+ time_budget_ms: { type: "integer", minimum: 1 }
58490
+ };
58491
+ projectRegistrationReceiptSchema = {
58492
+ type: "object",
58493
+ additionalProperties: false,
58494
+ required: [
58495
+ "receipt_id",
58496
+ "authority",
58497
+ "route",
58498
+ "package_version",
58499
+ "authority_id",
58500
+ "tenant_id",
58501
+ "corpus_id",
58502
+ "operation_id",
58503
+ "step_id",
58504
+ "resource_kind",
58505
+ "direction",
58506
+ "idempotency_key",
58507
+ "request_digest",
58508
+ "precondition_digest",
58509
+ "outcome",
58510
+ "reason",
58511
+ "target_id",
58512
+ "result_revision",
58513
+ "result_digest",
58514
+ "duplicate_of_receipt_id",
58515
+ "accepted_receipt_id",
58516
+ "created_by_operation",
58517
+ "created_at"
58518
+ ],
58519
+ properties: {
58520
+ receipt_id: { type: "string" },
58521
+ authority: { type: "string", enum: ["todos"] },
58522
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
58523
+ package_version: { type: "string" },
58524
+ authority_id: { type: "string" },
58525
+ tenant_id: { type: "string" },
58526
+ corpus_id: { type: "string" },
58527
+ operation_id: { type: "string" },
58528
+ step_id: { type: "string" },
58529
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58530
+ direction: { type: "string", enum: ["forward", "inverse"] },
58531
+ idempotency_key: { type: "string" },
58532
+ request_digest: { type: "string" },
58533
+ precondition_digest: { type: "string" },
58534
+ outcome: {
58535
+ type: "string",
58536
+ enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
58537
+ },
58538
+ reason: { type: "string", nullable: true },
58539
+ target_id: { type: "string", format: "uuid", nullable: true },
58540
+ result_revision: { type: "string", nullable: true },
58541
+ result_digest: { type: "string", nullable: true },
58542
+ duplicate_of_receipt_id: { type: "string", nullable: true },
58543
+ accepted_receipt_id: { type: "string", nullable: true },
58544
+ created_by_operation: { type: "boolean" },
58545
+ created_at: { type: "string", format: "date-time" }
58546
+ }
58547
+ };
58548
+ projectRegistrationCapabilitySchema = {
58549
+ type: "object",
58550
+ additionalProperties: false,
58551
+ required: [
58552
+ "authority",
58553
+ "route",
58554
+ "package_version",
58555
+ "authority_id",
58556
+ "tenant_id",
58557
+ "corpus_id",
58558
+ "supported_resources",
58559
+ "conditional_create",
58560
+ "immutable_receipts",
58561
+ "exact_terminal_lookup",
58562
+ "exact_readback",
58563
+ "bind_existing_adoption",
58564
+ "prior_registration_adoption_validation",
58565
+ "project_resource_enumeration",
58566
+ "project_resource_page_limit",
58567
+ "conditional_inverse",
58568
+ "ambiguous_outcome_reconciliation"
58569
+ ],
58570
+ properties: {
58571
+ authority: { type: "string", enum: ["todos"] },
58572
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
58573
+ package_version: { type: "string" },
58574
+ authority_id: { type: "string" },
58575
+ tenant_id: { type: "string" },
58576
+ corpus_id: { type: "string" },
58577
+ supported_resources: {
58578
+ type: "array",
58579
+ items: { type: "string", enum: ["project", "task_list"] }
58580
+ },
58581
+ conditional_create: { type: "boolean", enum: [true] },
58582
+ immutable_receipts: { type: "boolean", enum: [true] },
58583
+ exact_terminal_lookup: { type: "boolean", enum: [true] },
58584
+ exact_readback: { type: "boolean", enum: [true] },
58585
+ bind_existing_adoption: { type: "boolean", enum: [true] },
58586
+ prior_registration_adoption_validation: { type: "boolean", enum: [true] },
58587
+ project_resource_enumeration: { type: "boolean", enum: [true] },
58588
+ project_resource_page_limit: { type: "integer", minimum: 1 },
58589
+ conditional_inverse: { type: "boolean", enum: [true] },
58590
+ ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
58591
+ }
58592
+ };
58593
+ projectRegistrationRequestSchema = {
58594
+ type: "object",
58595
+ additionalProperties: false,
58596
+ required: [
58597
+ "operation_id",
58598
+ "step_id",
58599
+ "resource_kind",
58600
+ "direction",
58601
+ "authority_route",
58602
+ "package_version",
58603
+ "authority_id",
58604
+ "tenant_id",
58605
+ "corpus_id",
58606
+ "target_selector",
58607
+ "idempotency_key",
58608
+ "request_digest",
58609
+ "precondition_digest",
58610
+ "project_id",
58611
+ "project_slug",
58612
+ "project_name",
58613
+ "desired",
58614
+ "response_byte_limit",
58615
+ "time_budget_ms"
58616
+ ],
58617
+ properties: {
58618
+ operation_id: { type: "string" },
58619
+ step_id: { type: "string" },
58620
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58621
+ direction: { type: "string", enum: ["forward", "inverse"] },
58622
+ authority_route: { type: "string" },
58623
+ package_version: { type: "string" },
58624
+ authority_id: { type: "string" },
58625
+ tenant_id: { type: "string" },
58626
+ corpus_id: { type: "string" },
58627
+ target_selector: { type: "string" },
58628
+ idempotency_key: { type: "string" },
58629
+ request_digest: { type: "string" },
58630
+ precondition_digest: { type: "string" },
58631
+ project_id: { type: "string" },
58632
+ project_slug: { type: "string" },
58633
+ project_name: { type: "string" },
58634
+ desired: { type: "object", additionalProperties: true },
58635
+ bind_existing: { type: "boolean" },
58636
+ accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
58637
+ ...projectRegistrationBoundsProperties
58638
+ }
58639
+ };
58640
+ projectRegistrationLookupRequestSchema = {
58641
+ type: "object",
58642
+ additionalProperties: false,
58643
+ required: [
58644
+ "operation_id",
58645
+ "step_id",
58646
+ "resource_kind",
58647
+ "direction",
58648
+ "authority",
58649
+ "authority_route",
58650
+ "package_version",
58651
+ "authority_id",
58652
+ "tenant_id",
58653
+ "corpus_id",
58654
+ "target_selector",
58655
+ "idempotency_key",
58656
+ "max_items",
58657
+ "response_byte_limit",
58658
+ "time_budget_ms"
58659
+ ],
58660
+ properties: {
58661
+ operation_id: { type: "string" },
58662
+ step_id: { type: "string" },
58663
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58664
+ direction: { type: "string", enum: ["forward", "inverse"] },
58665
+ authority: { type: "string", enum: ["todos"] },
58666
+ authority_route: { type: "string" },
58667
+ package_version: { type: "string" },
58668
+ authority_id: { type: "string" },
58669
+ tenant_id: { type: "string" },
58670
+ corpus_id: { type: "string" },
58671
+ target_selector: { type: "string" },
58672
+ idempotency_key: { type: "string" },
58673
+ target_id: { type: "string", format: "uuid" },
58674
+ max_items: { type: "integer", enum: [1] },
58675
+ ...projectRegistrationBoundsProperties
58676
+ }
58677
+ };
58678
+ priorRegistrationAdoptionValidationSchema = {
58679
+ type: "object",
58680
+ additionalProperties: false,
58681
+ required: [
58682
+ "valid",
58683
+ "resource_kind",
58684
+ "target_id",
58685
+ "source_receipt_id",
58686
+ "accepted_receipt_id",
58687
+ "source_outcome",
58688
+ "created_at",
58689
+ "current_revision",
58690
+ "accepted_result_digest"
58691
+ ],
58692
+ properties: {
58693
+ valid: { type: "boolean", enum: [true] },
58694
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58695
+ target_id: { type: "string", format: "uuid" },
58696
+ source_receipt_id: { type: "string" },
58697
+ accepted_receipt_id: { type: "string" },
58698
+ source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
58699
+ created_at: { type: "string", format: "date-time" },
58700
+ current_revision: { type: "string", format: "date-time" },
58701
+ accepted_result_digest: { type: "string" }
58702
+ }
58703
+ };
58704
+ projectResourceSchema = {
58705
+ type: "object",
58706
+ additionalProperties: false,
58707
+ required: [
58708
+ "source_project_id",
58709
+ "kind",
58710
+ "scope",
58711
+ "target_id",
58712
+ "parent_id",
58713
+ "revision",
58714
+ "digest"
58715
+ ],
58716
+ properties: {
58717
+ source_project_id: { type: "string" },
58718
+ kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
58719
+ scope: { type: "string", enum: ["collection", "resource"] },
58720
+ target_id: { type: "string", format: "uuid" },
58721
+ parent_id: { type: "string", format: "uuid", nullable: true },
58722
+ revision: { type: "string" },
58723
+ digest: { type: "string" }
58724
+ }
58725
+ };
58726
+ projectResourcePageSchema = {
58727
+ type: "object",
58728
+ additionalProperties: false,
58729
+ required: [
58730
+ "authority",
58731
+ "route",
58732
+ "package_version",
58733
+ "authority_id",
58734
+ "tenant_id",
58735
+ "corpus_id",
58736
+ "source_project_id",
58737
+ "todos_project_id",
58738
+ "task_list_id",
58739
+ "include_anchors",
58740
+ "collection_revision",
58741
+ "limit",
58742
+ "count",
58743
+ "resources",
58744
+ "has_more",
58745
+ "next_cursor",
58746
+ "complete",
58747
+ "truncated"
58748
+ ],
58749
+ properties: {
58750
+ authority: { type: "string", enum: ["todos"] },
58751
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
58752
+ package_version: { type: "string" },
58753
+ authority_id: { type: "string" },
58754
+ tenant_id: { type: "string" },
58755
+ corpus_id: { type: "string" },
58756
+ source_project_id: { type: "string" },
58757
+ todos_project_id: { type: "string", format: "uuid" },
58758
+ task_list_id: { type: "string", format: "uuid" },
58759
+ include_anchors: { type: "boolean" },
58760
+ collection_revision: { type: "string" },
58761
+ limit: { type: "integer", minimum: 1, maximum: 500 },
58762
+ count: { type: "integer", minimum: 0 },
58763
+ resources: {
58764
+ type: "array",
58765
+ items: { $ref: "#/components/schemas/ProjectResource" }
58766
+ },
58767
+ has_more: { type: "boolean" },
58768
+ next_cursor: { type: "string", nullable: true },
58769
+ complete: { type: "boolean" },
58770
+ truncated: { type: "boolean", enum: [false] }
58771
+ }
58772
+ };
56852
58773
  });
56853
58774
 
56854
58775
  // src/server/pr-groups.ts
@@ -56995,11 +58916,11 @@ function canonicalJson2(value) {
56995
58916
  return `[${value.map(canonicalJson2).join(",")}]`;
56996
58917
  return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
56997
58918
  }
56998
- function digest(value) {
58919
+ function digest2(value) {
56999
58920
  return createHash17("sha256").update(canonicalJson2(value)).digest("hex");
57000
58921
  }
57001
58922
  function deriveIdempotencyKey(projectId, slug) {
57002
- return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
58923
+ return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
57003
58924
  }
57004
58925
  function normalizeIdempotencyKey(value, projectId, slug) {
57005
58926
  const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
@@ -57008,13 +58929,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
57008
58929
  }
57009
58930
  return key2;
57010
58931
  }
57011
- function receiptId2(projectId, slug, idempotencyKey) {
57012
- return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
58932
+ function receiptId2(projectId, slug, idempotencyKey2) {
58933
+ return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
57013
58934
  }
57014
58935
  function semanticListDigest(list) {
57015
58936
  const metadata = { ...list.metadata ?? {} };
57016
58937
  delete metadata[RECEIPT_METADATA_KEY];
57017
- return digest({
58938
+ return digest2({
57018
58939
  project_id: list.project_id,
57019
58940
  slug: list.slug,
57020
58941
  name: list.name,
@@ -57031,10 +58952,10 @@ function storedMarker(list) {
57031
58952
  return null;
57032
58953
  return marker;
57033
58954
  }
57034
- function receiptFor(store, project, list, idempotencyKey) {
58955
+ function receiptFor(store, project, list, idempotencyKey2) {
57035
58956
  const marker = storedMarker(list);
57036
58957
  const owned = marker?.project_id === project.id && marker.slug === list.slug;
57037
- if (owned && marker.idempotency_key !== idempotencyKey) {
58958
+ if (owned && marker.idempotency_key !== idempotencyKey2) {
57038
58959
  throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
57039
58960
  project_id: project.id,
57040
58961
  task_list_id: list.id,
@@ -57043,8 +58964,8 @@ function receiptFor(store, project, list, idempotencyKey) {
57043
58964
  }
57044
58965
  return {
57045
58966
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
57046
- receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
57047
- idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
58967
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
58968
+ idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
57048
58969
  project_id: project.id,
57049
58970
  task_list_id: list.id,
57050
58971
  slug: list.slug,
@@ -57096,20 +59017,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57096
59017
  });
57097
59018
  }
57098
59019
  const slug = project.task_list_id;
57099
- const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
59020
+ const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
57100
59021
  if (state.scoped) {
57101
59022
  return {
57102
59023
  mode: "apply",
57103
59024
  action: "already_present",
57104
59025
  project,
57105
59026
  task_list: state.scoped,
57106
- receipt: receiptFor(store, project, state.scoped, idempotencyKey)
59027
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
57107
59028
  };
57108
59029
  }
57109
59030
  const marker = {
57110
59031
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
57111
- receipt_id: receiptId2(project.id, slug, idempotencyKey),
57112
- idempotency_key: idempotencyKey,
59032
+ receipt_id: receiptId2(project.id, slug, idempotencyKey2),
59033
+ idempotency_key: idempotencyKey2,
57113
59034
  project_id: project.id,
57114
59035
  slug,
57115
59036
  result_digest: semanticListDigest({
@@ -57147,7 +59068,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57147
59068
  action: "already_present",
57148
59069
  project: raced.project,
57149
59070
  task_list: raced.scoped,
57150
- receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
59071
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
57151
59072
  };
57152
59073
  }
57153
59074
  const projectReadback = await store.projects.get(project.id);
@@ -57177,7 +59098,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57177
59098
  action: "created",
57178
59099
  project: projectReadback,
57179
59100
  task_list: readback,
57180
- receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
59101
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
57181
59102
  };
57182
59103
  }
57183
59104
  async function rollbackProjectTaskListEnsure(store, projectId, options) {
@@ -57222,7 +59143,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
57222
59143
  project_id: project.id,
57223
59144
  task_list_id: list.id,
57224
59145
  accepted_receipt_id: options.receipt_id,
57225
- rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
59146
+ rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
57226
59147
  removed_at: new Date().toISOString()
57227
59148
  };
57228
59149
  }
@@ -57421,6 +59342,9 @@ function validateTaskPatchVocabulary(value) {
57421
59342
  if (!parsed.ok)
57422
59343
  return { ok: false, message: parsed.message };
57423
59344
  }
59345
+ if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
59346
+ return { ok: false, message: "parent_id must be a non-empty task id or null" };
59347
+ }
57424
59348
  return { ok: true, patch: body2 };
57425
59349
  }
57426
59350
  function validateProjectPatch(value) {
@@ -57905,8 +59829,8 @@ async function handleV1Request(req, url, dependencies = {}) {
57905
59829
  }
57906
59830
  const created = await store.tasks.create(body2, storageContext);
57907
59831
  const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
57908
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
57909
- return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id and parent_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
59832
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null)) {
59833
+ return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, and plan_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
57910
59834
  }
57911
59835
  return json5({ task: persisted }, 201);
57912
59836
  }
@@ -58780,6 +60704,15 @@ async function handleV1Request(req, url, dependencies = {}) {
58780
60704
  if (e instanceof TaskNotFoundError) {
58781
60705
  return error(404, e.message, { code: TaskNotFoundError.code });
58782
60706
  }
60707
+ if (e instanceof VersionConflictError) {
60708
+ return error(409, e.message, {
60709
+ code: VersionConflictError.code,
60710
+ conflict: true,
60711
+ task_id: e.taskId,
60712
+ expected_version: e.expectedVersion,
60713
+ current_version: e.actualVersion
60714
+ });
60715
+ }
58783
60716
  if (e instanceof StaleLockHandoffError) {
58784
60717
  const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
58785
60718
  return error(status2, e.message, {