@hasna/todos 0.15.29 → 0.15.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cli/cloud-router.d.ts +13 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-registration-commands.d.ts +2 -0
  4. package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
  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 +2835 -334
  8. package/dist/contracts.js +52 -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 +1447 -144
  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 +2031 -135
  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 +24 -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/sqlite.d.ts +13 -1
  33. package/dist/project-registration/sqlite.d.ts.map +1 -1
  34. package/dist/project-registration/types.d.ts +68 -1
  35. package/dist/project-registration/types.d.ts.map +1 -1
  36. package/dist/project-registration.js +831 -58
  37. package/dist/registry.js +52 -2
  38. package/dist/release-provenance.json +5 -5
  39. package/dist/sdk/index.d.ts +1 -1
  40. package/dist/sdk/index.d.ts.map +1 -1
  41. package/dist/sdk/index.js +151 -0
  42. package/dist/sdk/v1.generated.d.ts +282 -0
  43. package/dist/sdk/v1.generated.d.ts.map +1 -1
  44. package/dist/server/index.js +2022 -126
  45. package/dist/server/openapi.d.ts +2856 -1357
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/v1.d.ts.map +1 -1
  48. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  49. package/dist/storage/postgres-sync.d.ts +8 -0
  50. package/dist/storage/postgres-sync.d.ts.map +1 -1
  51. package/dist/storage.js +232 -19
  52. package/dist/task-manifest/authority.d.ts +13 -1
  53. package/dist/task-manifest/authority.d.ts.map +1 -1
  54. package/dist/task-manifest/backend.d.ts +5 -0
  55. package/dist/task-manifest/backend.d.ts.map +1 -1
  56. package/dist/task-manifest/index.d.ts +2 -2
  57. package/dist/task-manifest/index.d.ts.map +1 -1
  58. package/dist/task-manifest/plan-slug.d.ts +20 -0
  59. package/dist/task-manifest/plan-slug.d.ts.map +1 -1
  60. package/dist/task-manifest/postgres.d.ts +1 -0
  61. package/dist/task-manifest/postgres.d.ts.map +1 -1
  62. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  63. package/dist/task-manifest/schema.d.ts.map +1 -1
  64. package/dist/task-manifest/sqlite.d.ts +1 -0
  65. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  66. package/dist/task-manifest/types.d.ts +21 -1
  67. package/dist/task-manifest/types.d.ts.map +1 -1
  68. package/dist/task-manifest.js +592 -61
  69. package/dist/types/index.d.ts +4 -2
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/package.json +1 -1
package/dist/mcp/index.js CHANGED
@@ -12492,6 +12492,46 @@ function guardPlanRowsSqlite(planIds, db) {
12492
12492
  }
12493
12493
  }
12494
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
+
12495
12535
  // src/lib/creator-identity.ts
12496
12536
  import { existsSync as existsSync6, rmSync } from "fs";
12497
12537
  import { join as join5 } from "path";
@@ -14071,6 +14111,7 @@ function createTaskStored(input, d) {
14071
14111
  let id = uuid();
14072
14112
  for (let attempt = 0;attempt < 3; attempt++) {
14073
14113
  try {
14114
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
14074
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)
14075
14116
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
14076
14117
  id,
@@ -14449,6 +14490,7 @@ function updateTaskStored(id, input, db) {
14449
14490
  throw new VersionConflictError(id, input.version, task.version);
14450
14491
  }
14451
14492
  input = sanitizeUpdateTaskInput(input);
14493
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
14452
14494
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
14453
14495
  const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
14454
14496
  if (linkedProjectId) {
@@ -14502,6 +14544,10 @@ function updateTaskStored(id, input, db) {
14502
14544
  sets.push("project_id = ?");
14503
14545
  params.push(input.project_id);
14504
14546
  }
14547
+ if (input.parent_id !== undefined) {
14548
+ sets.push("parent_id = ?");
14549
+ params.push(input.parent_id);
14550
+ }
14505
14551
  if (input.assigned_to !== undefined) {
14506
14552
  sets.push("assigned_to = ?");
14507
14553
  params.push(input.assigned_to);
@@ -14617,6 +14663,8 @@ function updateTaskStored(id, input, db) {
14617
14663
  logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
14618
14664
  if (input.title !== undefined && input.title !== task.title)
14619
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);
14620
14668
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
14621
14669
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
14622
14670
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
@@ -14674,7 +14722,8 @@ function updateTask(id, input, db) {
14674
14722
  if (!before)
14675
14723
  throw new TaskNotFoundError(id);
14676
14724
  const guardedPlanIds = [before.plan_id, input.plan_id];
14677
- if (!guardedPlanIds.some(Boolean))
14725
+ const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
14726
+ if (!needsSerializedWrite)
14678
14727
  return updateTaskStored(id, input, d);
14679
14728
  return d.transaction(() => {
14680
14729
  guardPlanRowsSqlite(guardedPlanIds, d);
@@ -14710,6 +14759,7 @@ var init_task_crud = __esm(() => {
14710
14759
  init_checklists();
14711
14760
  init_storage_tombstones();
14712
14761
  init_prewrite_secrets();
14762
+ init_task_parent_integrity();
14713
14763
  });
14714
14764
 
14715
14765
  // src/db/task-status.ts
@@ -21016,6 +21066,31 @@ var init_http_client = __esm(() => {
21016
21066
  ]);
21017
21067
  });
21018
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
+
21019
21094
  // src/cli/cloud-router.ts
21020
21095
  import { resolveStorageClient } from "@hasna/contracts/client/storage";
21021
21096
  import { normalizeStorageMode } from "@hasna/contracts/mode";
@@ -21416,12 +21491,39 @@ async function cloudListTasks(client, filter = {}) {
21416
21491
  union.sort(compareCloudTaskOrder);
21417
21492
  return union.slice(start, windowEnd);
21418
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
+ }
21419
21520
  async function cloudGetTask(client, id) {
21420
21521
  const raw = await client.get("tasks", id);
21421
21522
  return raw == null ? null : unwrapTask(raw);
21422
21523
  }
21423
21524
  async function cloudCreateTask(client, input, verification = {}) {
21424
21525
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
21526
+ const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
21425
21527
  const expectedCreatedBy = typeof verification.expectedCreatedBy === "string" && verification.expectedCreatedBy.trim() ? verification.expectedCreatedBy : null;
21426
21528
  if (expectedCreatedBy !== null) {
21427
21529
  await requireTaskCreatorCapability(client);
@@ -21434,9 +21536,9 @@ async function cloudCreateTask(client, input, verification = {}) {
21434
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");
21435
21537
  }
21436
21538
  const persisted = await cloudGetTask(client, created.id);
21437
- 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) {
21438
21540
  const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
21439
- 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`);
21440
21542
  }
21441
21543
  return persisted;
21442
21544
  }
@@ -21703,6 +21805,8 @@ var init_cloud_router = __esm(() => {
21703
21805
  init_redaction();
21704
21806
  init_plan_project_link_contract();
21705
21807
  init_http_client();
21808
+ init_adoption_validation();
21809
+ init_page_validation();
21706
21810
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21707
21811
  TRANSPORT_TOKENS = {
21708
21812
  sqlite: "sqlite",
@@ -21739,6 +21843,7 @@ function registerTaskCrudTools(server, ctx) {
21739
21843
  compact["version"] = task.version;
21740
21844
  compact["created_at"] = task.created_at;
21741
21845
  compact["task_list_id"] = task.task_list_id;
21846
+ compact["parent_id"] = task.parent_id;
21742
21847
  return compactJson(compact);
21743
21848
  }
21744
21849
  function versionFor(taskId, version) {
@@ -22008,6 +22113,7 @@ ${task.description}` : null
22008
22113
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
22009
22114
  assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
22010
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"),
22011
22117
  task_list_id: exports_external.string().nullable().optional(),
22012
22118
  depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
22013
22119
  tags: exports_external.array(exports_external.string()).optional(),
@@ -22039,6 +22145,9 @@ ${task.description}` : null
22039
22145
  if (typeof patch.project_id === "string" && patch.project_id) {
22040
22146
  patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
22041
22147
  }
22148
+ if (typeof patch.parent_id === "string" && patch.parent_id) {
22149
+ patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
22150
+ }
22042
22151
  if (typeof patch.task_list_id === "string" && patch.task_list_id) {
22043
22152
  let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
22044
22153
  if (!scope) {
@@ -22047,9 +22156,22 @@ ${task.description}` : null
22047
22156
  }
22048
22157
  patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
22049
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
+ }
22050
22165
  if (version2 !== undefined)
22051
22166
  patch.version = version2;
22052
- 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
+ }
22053
22175
  return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
22054
22176
  }
22055
22177
  const resolvedId = resolveId(params.task_id);
@@ -22061,6 +22183,8 @@ ${task.description}` : null
22061
22183
  resolved.assigned_to = resolveAssignee(resolved.assigned_to);
22062
22184
  if (resolved.project_id && typeof resolved.project_id === "string")
22063
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);
22064
22188
  if (resolved.task_list_id && typeof resolved.task_list_id === "string")
22065
22189
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
22066
22190
  if (resolved.depends_on && Array.isArray(resolved.depends_on))
@@ -32760,7 +32884,7 @@ function registerTaskMetaTools(server, ctx) {
32760
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",
32761
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",
32762
32886
  get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
32763
- 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",
32764
32888
  delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
32765
32889
  start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
32766
32890
  complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
@@ -35815,7 +35939,7 @@ var package_default;
35815
35939
  var init_package = __esm(() => {
35816
35940
  package_default = {
35817
35941
  name: "@hasna/todos",
35818
- version: "0.15.29",
35942
+ version: "0.15.32",
35819
35943
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
35820
35944
  type: "module",
35821
35945
  main: "dist/index.js",
@@ -46947,8 +47071,8 @@ function createPostgresTodosStorageAdapter(options) {
46947
47071
  resolveRef: (ref) => store.resolveTaskRef(ref),
46948
47072
  list: (filter = {}) => store.listTasks(filter),
46949
47073
  count: (filter = {}) => store.countTasks(filter),
46950
- update: (id, input) => updateTask2(id, input, store),
46951
- 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),
46952
47076
  start: (id, agentId) => startTask2(id, agentId, store),
46953
47077
  complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
46954
47078
  fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
@@ -47481,22 +47605,70 @@ class PostgresJsonRecordStore {
47481
47605
  return "identical";
47482
47606
  throw new Error(divergentAuditHistoryReplayError(value.id));
47483
47607
  }
47484
- 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) {
47485
47618
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
47486
- if (planIds.length === 0)
47619
+ if (planIds.length === 0 && !parentGuard)
47487
47620
  return this.upsert("tasks", value, context);
47488
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;
47489
47626
  const updatedAt = value.updated_at;
47490
47627
  const targetPlanId = value.plan_id;
47491
- 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
+ ),
47492
47634
  locked_plans AS MATERIALIZED (
47493
47635
  SELECT object_id, payload FROM ${this.tableName}
47494
47636
  WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
47495
47637
  AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
47496
47638
  ORDER BY object_id
47497
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
47498
47661
  ), validation AS (
47499
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,
47500
47672
  (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
47501
47673
  ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
47502
47674
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
@@ -47517,7 +47689,13 @@ class PostgresJsonRecordStore {
47517
47689
  )
47518
47690
  SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
47519
47691
  FROM guarded
47520
- 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
47521
47699
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
47522
47700
  payload = EXCLUDED.payload,
47523
47701
  updated_at = EXCLUDED.updated_at,
@@ -47530,8 +47708,10 @@ class PostgresJsonRecordStore {
47530
47708
  AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
47531
47709
  RETURNING payload
47532
47710
  )
47533
- SELECT guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
47534
- (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
47535
47715
  FROM guarded`, [
47536
47716
  this.service,
47537
47717
  value.id,
@@ -47541,9 +47721,26 @@ class PostgresJsonRecordStore {
47541
47721
  numberValue3(value.version),
47542
47722
  jsonbParam(planIds),
47543
47723
  targetPlanId,
47544
- explicitProject
47724
+ explicitProject,
47725
+ Boolean(parentGuard),
47726
+ parentGuard?.parentId ?? null,
47727
+ parentGuard?.expectedVersion ?? null,
47728
+ parentGuard?.operation === "update"
47545
47729
  ]);
47546
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
+ }
47547
47744
  if (!row?.all_plans_found || !row.target_plan_found) {
47548
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 });
47549
47746
  }
@@ -47551,6 +47748,8 @@ class PostgresJsonRecordStore {
47551
47748
  throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
47552
47749
  }
47553
47750
  if (!row.payload) {
47751
+ if (row.current_payload)
47752
+ return payloadRecord2(row.current_payload);
47554
47753
  return await requireRecord("tasks", value.id, this);
47555
47754
  }
47556
47755
  return payloadRecord2(row.payload);
@@ -47876,6 +48075,80 @@ class PostgresJsonRecordStore {
47876
48075
  version: numberValue3(existing["version"])
47877
48076
  }, context);
47878
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
+ }
47879
48152
  async getPlanProjectLinkReceipt(receiptId) {
47880
48153
  const value = await this.get("plan_project_link_receipts", receiptId);
47881
48154
  return value ? assertPlanProjectLinkReceipt(value) : null;
@@ -48296,9 +48569,8 @@ class PostgresJsonRecordStore {
48296
48569
  }
48297
48570
  async function createTask3(input, store, context) {
48298
48571
  const timestamp4 = new Date().toISOString();
48299
- if (input.parent_id && !await store.get("tasks", input.parent_id)) {
48300
- throw new TaskNotFoundError(input.parent_id);
48301
- }
48572
+ const taskId = randomUUID4();
48573
+ await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
48302
48574
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
48303
48575
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
48304
48576
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -48307,7 +48579,7 @@ async function createTask3(input, store, context) {
48307
48579
  const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
48308
48580
  const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
48309
48581
  const task2 = {
48310
- id: randomUUID4(),
48582
+ id: taskId,
48311
48583
  short_id: shortId,
48312
48584
  project_id: effectiveProjectId,
48313
48585
  parent_id: input.parent_id ?? null,
@@ -48363,15 +48635,16 @@ async function createTask3(input, store, context) {
48363
48635
  synced_at: null,
48364
48636
  archived_at: null
48365
48637
  };
48366
- 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);
48367
48639
  await logTaskChange2(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
48368
48640
  return storedTask;
48369
48641
  }
48370
- async function updateTask2(id, input, store) {
48642
+ async function updateTask2(id, input, store, context) {
48371
48643
  const existing = await requireRecord("tasks", id, store);
48372
48644
  if (existing.version !== input.version) {
48373
- throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
48645
+ throw new VersionConflictError(id, input.version, existing.version);
48374
48646
  }
48647
+ await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
48375
48648
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
48376
48649
  const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
48377
48650
  if (linkedPlan?.project_id) {
@@ -48396,10 +48669,19 @@ async function updateTask2(id, input, store) {
48396
48669
  metadata: input.metadata ?? existing.metadata,
48397
48670
  requires_approval: input.requires_approval ?? existing.requires_approval,
48398
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,
48399
48673
  created_by: existing.created_by,
48400
48674
  completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
48401
48675
  };
48402
- 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;
48403
48685
  }
48404
48686
  async function startTask2(id, agentId, store) {
48405
48687
  const task2 = await requireRecord("tasks", id, store);
@@ -48463,7 +48745,11 @@ async function patchTask(task2, patch, store) {
48463
48745
  version: task2.version + 1,
48464
48746
  updated_at: new Date().toISOString()
48465
48747
  };
48466
- 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
+ });
48467
48753
  }
48468
48754
  function sameCloudLockHolder(stored, incoming) {
48469
48755
  if (!stored || !incoming)
@@ -49166,6 +49452,7 @@ var init_postgres_adapter = __esm(() => {
49166
49452
  init_postgres_sync();
49167
49453
  init_integrity();
49168
49454
  init_redaction();
49455
+ init_task_parent_integrity();
49169
49456
  init_audit_history_import();
49170
49457
  init_canonical();
49171
49458
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
@@ -49651,21 +49938,6 @@ var init_postgres = __esm(() => {
49651
49938
  init_types3();
49652
49939
  });
49653
49940
 
49654
- // src/project-registration/types.ts
49655
- var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1", TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1", TodosProjectRegistrationError;
49656
- var init_types4 = __esm(() => {
49657
- TodosProjectRegistrationError = class TodosProjectRegistrationError extends Error {
49658
- code;
49659
- details;
49660
- constructor(code, message, details = {}) {
49661
- super(message);
49662
- this.code = code;
49663
- this.details = details;
49664
- this.name = "TodosProjectRegistrationError";
49665
- }
49666
- };
49667
- });
49668
-
49669
49941
  // src/project-registration/postgres.ts
49670
49942
  function safeIdentifier(value, field) {
49671
49943
  if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
@@ -49943,6 +50215,7 @@ class PostgresTodosProjectRegistrationTransaction {
49943
50215
  AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
49944
50216
  ORDER BY payload->>'created_at' ASC, object_id ASC
49945
50217
  LIMIT 1
50218
+ FOR UPDATE
49946
50219
  `, [this.service, path, taskListSlug]);
49947
50220
  return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
49948
50221
  }
@@ -49953,6 +50226,7 @@ class PostgresTodosProjectRegistrationTransaction {
49953
50226
  AND payload->>'project_id' = $2 AND payload->>'slug' = $3
49954
50227
  ORDER BY payload->>'created_at' ASC, object_id ASC
49955
50228
  LIMIT 1
50229
+ FOR UPDATE
49956
50230
  `, [this.service, projectId, slug]);
49957
50231
  return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
49958
50232
  }
@@ -49963,10 +50237,24 @@ class PostgresTodosProjectRegistrationTransaction {
49963
50237
  return await this.storage.taskLists.create(input);
49964
50238
  }
49965
50239
  async getProject(id) {
49966
- 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;
49967
50248
  }
49968
50249
  async getTaskList(id) {
49969
- 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;
49970
50258
  }
49971
50259
  async lockCompensationWrites() {
49972
50260
  await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
@@ -50046,6 +50334,96 @@ class PostgresTodosProjectRegistrationBackend {
50046
50334
  async getTaskList(id) {
50047
50335
  return (await this.direct()).getTaskList(id);
50048
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
+ }
50049
50427
  }
50050
50428
  var init_postgres2 = __esm(() => {
50051
50429
  init_postgres_adapter();
@@ -50789,35 +51167,68 @@ function projectRecord(project) {
50789
51167
  return {
50790
51168
  target_id: project.id,
50791
51169
  revision: project.updated_at,
50792
- digest: digestProjectRegistrationValue({
50793
- id: project.id,
50794
- name: project.name,
50795
- path: project.path,
50796
- description: project.description,
50797
- task_list_id: project.task_list_id,
50798
- task_prefix: project.task_prefix,
50799
- task_counter: project.task_counter,
50800
- created_at: project.created_at,
50801
- updated_at: project.updated_at
50802
- })
51170
+ digest: projectRegistrationDigest(project)
50803
51171
  };
50804
51172
  }
50805
51173
  function taskListRecord(taskList) {
50806
51174
  return {
50807
51175
  target_id: taskList.id,
50808
51176
  revision: taskList.updated_at,
50809
- digest: digestProjectRegistrationValue({
50810
- id: taskList.id,
50811
- project_id: taskList.project_id,
50812
- slug: taskList.slug,
50813
- name: taskList.name,
50814
- description: taskList.description,
50815
- metadata: taskList.metadata,
50816
- created_at: taskList.created_at,
50817
- 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
50818
51197
  })
50819
51198
  };
50820
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
+ }
50821
51232
  function receiptId(input) {
50822
51233
  return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
50823
51234
  }
@@ -50837,6 +51248,29 @@ function assertCapabilityRequest(request, capability) {
50837
51248
  }
50838
51249
  }
50839
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) {
50840
51274
  return digestProjectRegistrationValue({
50841
51275
  authority_route: request.authority_route,
50842
51276
  package_version: request.package_version,
@@ -50858,6 +51292,11 @@ function normalizedCallDigest(request) {
50858
51292
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
50859
51293
  });
50860
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
+ }
50861
51300
  function assertCommonRequest(request, capability) {
50862
51301
  assertBounds(request);
50863
51302
  assertResourceKind(request.resource_kind);
@@ -50899,6 +51338,9 @@ function assertCommonRequest(request, capability) {
50899
51338
  if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
50900
51339
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
50901
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
+ }
50902
51344
  const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
50903
51345
  operation_id: request.operation_id,
50904
51346
  step_id: request.step_id,
@@ -50920,7 +51362,7 @@ function assertForwardRequest(request, capability) {
50920
51362
  const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
50921
51363
  const expectedPreconditionDigest = digestProjectRegistrationValue({
50922
51364
  target_selector: request.target_selector,
50923
- expected: "absent"
51365
+ expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
50924
51366
  });
50925
51367
  if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
50926
51368
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
@@ -50999,7 +51441,7 @@ function receiptBase(request, callDigest, capability) {
50999
51441
  normalized_call_digest: callDigest
51000
51442
  };
51001
51443
  }
51002
- function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2) {
51444
+ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2, createdByOperation = true) {
51003
51445
  return makeReceipt({
51004
51446
  ...receiptBase(request, callDigest, capability),
51005
51447
  outcome: "accepted",
@@ -51009,7 +51451,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2
51009
51451
  result_digest: record.digest,
51010
51452
  duplicate_of_receipt_id: null,
51011
51453
  accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
51012
- created_by_operation: true
51454
+ created_by_operation: createdByOperation
51013
51455
  }, createdAt2);
51014
51456
  }
51015
51457
  function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt2) {
@@ -51071,6 +51513,53 @@ function bindingFor(request, callDigest, timestamp4, capability) {
51071
51513
  updated_at: timestamp4
51072
51514
  };
51073
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
+ }
51074
51563
 
51075
51564
  class PackageOwnedTodosProjectRegistrationAuthority {
51076
51565
  backend;
@@ -51092,6 +51581,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51092
51581
  immutable_receipts: true,
51093
51582
  exact_terminal_lookup: true,
51094
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,
51095
51588
  conditional_inverse: true,
51096
51589
  ambiguous_outcome_reconciliation: true
51097
51590
  };
@@ -51152,7 +51645,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51152
51645
  if (!accepted2) {
51153
51646
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
51154
51647
  }
51155
- if (accepted2.normalized_call_digest !== callDigest) {
51648
+ if (!acceptedCallMatches(request, accepted2, callDigest)) {
51156
51649
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
51157
51650
  }
51158
51651
  return this.duplicateFor(transaction, request, callDigest, accepted2);
@@ -51166,7 +51659,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51166
51659
  });
51167
51660
  if (!accepted)
51168
51661
  return null;
51169
- if (accepted.normalized_call_digest === callDigest) {
51662
+ if (acceptedCallMatches(request, accepted, callDigest)) {
51170
51663
  return this.duplicateFor(transaction, request, callDigest, accepted);
51171
51664
  }
51172
51665
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
@@ -51177,6 +51670,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51177
51670
  const slug2 = taskListSlug(request.project_slug);
51178
51671
  const conflict2 = await transaction.findProjectConflict(path, slug2);
51179
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
+ }
51180
51679
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
51181
51680
  }
51182
51681
  await this.fault("before_object_write", request);
@@ -51188,7 +51687,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51188
51687
  task_prefix: deterministicTaskPrefix(request.project_slug)
51189
51688
  });
51190
51689
  await this.fault("after_object_write", request);
51191
- return projectRecord(project);
51690
+ return {
51691
+ record: projectRecord(project),
51692
+ created_by_operation: true
51693
+ };
51192
51694
  }
51193
51695
  const todosProjectId = String(request.desired["todos_project_id"]);
51194
51696
  const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
@@ -51202,6 +51704,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51202
51704
  const slug = taskListSlug(request.project_slug);
51203
51705
  const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
51204
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
+ }
51205
51713
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
51206
51714
  }
51207
51715
  await this.fault("before_object_write", request);
@@ -51218,7 +51726,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51218
51726
  if (taskList.project_id !== todosProjectId) {
51219
51727
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
51220
51728
  }
51221
- return taskListRecord(taskList);
51729
+ return {
51730
+ record: taskListRecord(taskList),
51731
+ created_by_operation: true
51732
+ };
51222
51733
  }
51223
51734
  async create(request) {
51224
51735
  const startedAt = Date.now();
@@ -51240,9 +51751,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51240
51751
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
51241
51752
  if (!claimed) {
51242
51753
  const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
51243
- if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
51754
+ if (binding?.state === "accepted" && binding.accepted_receipt_id) {
51244
51755
  const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
51245
- if (accepted2) {
51756
+ if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
51246
51757
  return this.duplicateFor(transaction, request, callDigest, accepted2);
51247
51758
  }
51248
51759
  }
@@ -51253,15 +51764,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51253
51764
  await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
51254
51765
  return recordOrTerminal;
51255
51766
  }
51256
- 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);
51257
51768
  await this.fault("before_receipt_write", request);
51258
51769
  const stored = await insertDeterministicReceipt(transaction, accepted);
51259
51770
  await this.fault("after_receipt_write", request);
51260
51771
  await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
51261
- target_id: recordOrTerminal.target_id,
51772
+ target_id: recordOrTerminal.record.target_id,
51262
51773
  accepted_receipt_id: stored.receipt_id,
51263
- result_revision: recordOrTerminal.revision,
51264
- result_digest: recordOrTerminal.digest,
51774
+ result_revision: recordOrTerminal.record.revision,
51775
+ result_digest: recordOrTerminal.record.digest,
51265
51776
  updated_at: this.now()
51266
51777
  });
51267
51778
  return stored;
@@ -51309,7 +51820,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51309
51820
  direction: request.direction
51310
51821
  });
51311
51822
  if (accepted) {
51312
- 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 });
51313
51824
  }
51314
51825
  const timestamp4 = this.now();
51315
51826
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
@@ -51389,6 +51900,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51389
51900
  }
51390
51901
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
51391
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
+ }
51392
52061
  async storedAcceptedReceipt(request, supplied) {
51393
52062
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
51394
52063
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -51557,7 +52226,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
51557
52226
  cursorTableName
51558
52227
  }), authorityOptions);
51559
52228
  }
51560
- var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_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;
51561
52230
  var init_authority = __esm(() => {
51562
52231
  init_package_version();
51563
52232
  init_postgres2();
@@ -51627,6 +52296,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51627
52296
  if ((action === "" || action === "capability") && method === "GET") {
51628
52297
  return json({ capability: await authority.capability() });
51629
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
+ }
51630
52313
  if (method !== "POST")
51631
52314
  return json({ error: "method not allowed" }, 405);
51632
52315
  const body = await readJson(req);
@@ -51649,6 +52332,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51649
52332
  record: await authority.readExact(body)
51650
52333
  });
51651
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
+ }
51652
52341
  if (action === "compensate") {
51653
52342
  return json({
51654
52343
  receipt: await authority.compensate(body)
@@ -51681,6 +52370,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51681
52370
  var JSON_HEADERS;
51682
52371
  var init_http2 = __esm(() => {
51683
52372
  init_types4();
52373
+ init_adoption_validation();
51684
52374
  JSON_HEADERS = { "Content-Type": "application/json" };
51685
52375
  });
51686
52376
 
@@ -51688,13 +52378,14 @@ var init_http2 = __esm(() => {
51688
52378
  var init_project_registration = __esm(() => {
51689
52379
  init_authority();
51690
52380
  init_http2();
52381
+ init_adoption_validation();
51691
52382
  init_postgres2();
51692
52383
  init_sqlite();
51693
52384
  init_types4();
51694
52385
  });
51695
52386
 
51696
52387
  // src/task-manifest/types.ts
51697
- 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;
51698
52389
  var init_types5 = __esm(() => {
51699
52390
  TodosTaskManifestError = class TodosTaskManifestError extends Error {
51700
52391
  code;
@@ -51781,7 +52472,7 @@ function parseTodosTaskManifestBindingLookup(input) {
51781
52472
  }
51782
52473
  return parsed.data;
51783
52474
  }
51784
- 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) => {
51785
52476
  if (Object.keys(value).length > limit) {
51786
52477
  context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
51787
52478
  }
@@ -51803,6 +52494,8 @@ var init_schema2 = __esm(() => {
51803
52494
  };
51804
52495
  key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
51805
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}$/);
51806
52499
  uuid2 = exports_external.string().uuid();
51807
52500
  scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
51808
52501
  comment = exports_external.object({
@@ -51840,7 +52533,9 @@ var init_schema2 = __esm(() => {
51840
52533
  schema = exports_external.object({
51841
52534
  version: exports_external.literal(1),
51842
52535
  operation_id: identifier,
51843
- idempotency_key: identifier,
52536
+ step_id: identifier,
52537
+ idempotency_key: idempotencyKey,
52538
+ precondition_digest: digest,
51844
52539
  project_id: uuid2,
51845
52540
  task_list_id: uuid2.optional(),
51846
52541
  if_binding_version: exports_external.number().int().min(0).optional(),
@@ -51856,7 +52551,10 @@ var init_schema2 = __esm(() => {
51856
52551
  }).strict();
51857
52552
  compensationSchema = exports_external.object({
51858
52553
  receipt_id: uuid2,
51859
- idempotency_key: identifier,
52554
+ operation_id: identifier,
52555
+ step_id: identifier,
52556
+ idempotency_key: idempotencyKey,
52557
+ precondition_digest: digest,
51860
52558
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
51861
52559
  }).strict();
51862
52560
  bindingLookupSchema = exports_external.object({
@@ -51874,6 +52572,7 @@ function taskManifestPlanSlug(manifest, planId) {
51874
52572
  const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
51875
52573
  return `${base}-${planId}`;
51876
52574
  }
52575
+ var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
51877
52576
  var init_plan_slug = () => {};
51878
52577
 
51879
52578
  // src/task-manifest/backend.ts
@@ -51887,11 +52586,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
51887
52586
  const row = rows[0];
51888
52587
  const bindingVersion = Number(row.binding_version);
51889
52588
  const state = row.state;
51890
- 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") {
51891
52590
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
51892
52591
  }
51893
52592
  return {
51894
52593
  plan_id: planId,
52594
+ operation_id: row.binding_operation_id,
52595
+ step_id: row.binding_step_id,
51895
52596
  apply_receipt_id: row.apply_receipt_id,
51896
52597
  binding_version: bindingVersion,
51897
52598
  state
@@ -51944,9 +52645,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51944
52645
  schema_version integer NOT NULL CHECK(schema_version = 1),
51945
52646
  kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
51946
52647
  operation_id text NOT NULL,
52648
+ step_id text NOT NULL,
51947
52649
  idempotency_key text NOT NULL,
51948
52650
  request_digest text NOT NULL,
52651
+ precondition_digest text NOT NULL,
51949
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,
51950
52657
  binding_version integer NOT NULL,
51951
52658
  apply_receipt_id text,
51952
52659
  manifest_json jsonb,
@@ -51958,12 +52665,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51958
52665
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
51959
52666
  `ALTER TABLE todos_task_manifest_receipts
51960
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`,
51961
52680
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
51962
52681
  operation_id text PRIMARY KEY,
51963
52682
  tenant_id text NOT NULL,
52683
+ step_id text NOT NULL,
51964
52684
  idempotency_key text NOT NULL UNIQUE,
51965
52685
  request_digest text NOT NULL,
52686
+ precondition_digest text NOT NULL,
51966
52687
  result_digest text NOT NULL,
52688
+ slug_provenance text,
52689
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
51967
52690
  apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
51968
52691
  manifest_json jsonb NOT NULL,
51969
52692
  result_json jsonb NOT NULL,
@@ -51977,6 +52700,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51977
52700
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
51978
52701
  `ALTER TABLE todos_task_manifest_bindings
51979
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'`,
51980
52711
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
51981
52712
  id text PRIMARY KEY,
51982
52713
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -51988,10 +52719,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51988
52719
  created_at timestamptz NOT NULL,
51989
52720
  delivered_at timestamptz
51990
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
+ )`,
51991
52743
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
51992
52744
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
51993
52745
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
51994
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)`,
51995
52753
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
51996
52754
  ON todos_task_manifest_bindings(
51997
52755
  tenant_id,
@@ -52004,6 +52762,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
52004
52762
  `DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
52005
52763
  `CREATE TRIGGER todos_task_manifest_receipts_immutable
52006
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
52007
52769
  FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
52008
52770
  ];
52009
52771
  }
@@ -52028,6 +52790,39 @@ function safeIdentifier2(value, field) {
52028
52790
  function parseJson2(value) {
52029
52791
  return typeof value === "string" ? JSON.parse(value) : value;
52030
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
+ }
52031
52826
  function timestamp4(value) {
52032
52827
  return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
52033
52828
  }
@@ -52035,6 +52830,41 @@ function fault(faults, point) {
52035
52830
  if (faults.points.has(point))
52036
52831
  throw new Error(`Injected task-manifest fault at ${point}`);
52037
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
+ }
52038
52868
  function receiptFromRow3(row) {
52039
52869
  return {
52040
52870
  receipt_id: String(row["receipt_id"]),
@@ -52043,9 +52873,14 @@ function receiptFromRow3(row) {
52043
52873
  schema_version: 1,
52044
52874
  kind: row["kind"],
52045
52875
  operation_id: String(row["operation_id"]),
52876
+ step_id: String(row["step_id"] ?? "legacy-apply"),
52046
52877
  idempotency_key: String(row["idempotency_key"]),
52047
52878
  request_digest: String(row["request_digest"]),
52879
+ precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
52048
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"]),
52049
52884
  binding_version: Number(row["binding_version"]),
52050
52885
  apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
52051
52886
  created_at: timestamp4(row["created_at"])
@@ -52163,46 +52998,89 @@ class PostgresTodosTaskManifestBackend {
52163
52998
  now4
52164
52999
  ]);
52165
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
+ }
52166
53032
  async apply(input, faults) {
52167
53033
  await this.ensureSchema();
52168
53034
  return this.client.transaction(async (tx) => {
52169
53035
  const { manifest } = input;
52170
53036
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
52171
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
+ }
52172
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]);
52173
53048
  if (existing.rows[0]) {
52174
53049
  const binding = existing.rows[0];
52175
- if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
52176
- 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");
52177
53052
  }
52178
53053
  if (binding["state"] !== "applied") {
52179
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
53054
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
52180
53055
  }
52181
- return { ...parseJson2(binding["result_json"]), duplicate: true };
53056
+ return parseApplyResult(binding["result_json"], true);
52182
53057
  }
52183
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]);
52184
53059
  if (reused.rows[0])
52185
- 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
+ }
52186
53064
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
52187
- 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");
52188
53066
  }
52189
53067
  const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
52190
53068
  WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
52191
53069
  if (!project.rows[0])
52192
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
53070
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
52193
53071
  if (manifest.task_list_id) {
52194
53072
  const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
52195
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]);
52196
53074
  const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
52197
53075
  if (!payload || payload["project_id"] !== manifest.project_id) {
52198
- 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");
52199
53077
  }
52200
53078
  }
52201
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];
52202
53080
  const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
52203
53081
  WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
52204
53082
  if (conflict.rows[0])
52205
- 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");
52206
53084
  await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
52207
53085
  fault(faults, "after_plan_write");
52208
53086
  for (const task3 of manifest.tasks) {
@@ -52272,9 +53150,14 @@ class PostgresTodosTaskManifestBackend {
52272
53150
  schema_version: 1,
52273
53151
  kind: "apply",
52274
53152
  operation_id: manifest.operation_id,
53153
+ step_id: manifest.step_id,
52275
53154
  idempotency_key: manifest.idempotency_key,
52276
53155
  request_digest: input.request_digest,
53156
+ precondition_digest: manifest.precondition_digest,
52277
53157
  result_digest: input.result_digest,
53158
+ outcome: "accepted",
53159
+ reason: null,
53160
+ duplicate_of_receipt_id: null,
52278
53161
  binding_version: 1,
52279
53162
  apply_receipt_id: null,
52280
53163
  created_at: input.now
@@ -52291,14 +53174,18 @@ class PostgresTodosTaskManifestBackend {
52291
53174
  const resultJson = canonicalJson(result);
52292
53175
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
52293
53176
  receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
52294
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
52295
- ) 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)`, [
52296
53180
  input.receipt_id,
52297
53181
  this.tenantId,
52298
53182
  manifest.operation_id,
52299
53183
  manifest.idempotency_key,
53184
+ manifest.step_id,
52300
53185
  input.request_digest,
53186
+ manifest.precondition_digest,
52301
53187
  input.result_digest,
53188
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
52302
53189
  manifestJson,
52303
53190
  resultJson,
52304
53191
  input.now
@@ -52317,14 +53204,18 @@ class PostgresTodosTaskManifestBackend {
52317
53204
  }
52318
53205
  fault(faults, "after_outbox_write");
52319
53206
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
52320
- operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
52321
- manifest_json, result_json, state, version, created_at, updated_at
52322
- ) 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)`, [
52323
53211
  manifest.operation_id,
52324
53212
  this.tenantId,
53213
+ manifest.step_id,
52325
53214
  manifest.idempotency_key,
52326
53215
  input.request_digest,
53216
+ manifest.precondition_digest,
52327
53217
  input.result_digest,
53218
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
52328
53219
  input.receipt_id,
52329
53220
  manifestJson,
52330
53221
  resultJson,
@@ -52337,9 +53228,12 @@ class PostgresTodosTaskManifestBackend {
52337
53228
  async readExact(receiptId2) {
52338
53229
  await this.ensureSchema();
52339
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]);
52340
- 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])
52341
53235
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
52342
- return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
53236
+ return parseApplyResult(terminal.rows[0]["result_json"], false);
52343
53237
  }
52344
53238
  async lookupBindingByPlanId(planId) {
52345
53239
  await this.ensureSchema();
@@ -52350,6 +53244,7 @@ class PostgresTodosTaskManifestBackend {
52350
53244
  b.version AS binding_version,
52351
53245
  b.tenant_id AS binding_tenant_id,
52352
53246
  b.operation_id AS binding_operation_id,
53247
+ b.step_id AS binding_step_id,
52353
53248
  b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
52354
53249
  r.tenant_id AS receipt_tenant_id,
52355
53250
  r.authority AS receipt_authority,
@@ -52357,6 +53252,7 @@ class PostgresTodosTaskManifestBackend {
52357
53252
  r.schema_version AS receipt_schema_version,
52358
53253
  r.kind AS receipt_kind,
52359
53254
  r.operation_id AS receipt_operation_id,
53255
+ r.step_id AS receipt_step_id,
52360
53256
  r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
52361
53257
  FROM todos_task_manifest_bindings b
52362
53258
  LEFT JOIN todos_task_manifest_receipts r
@@ -52443,6 +53339,10 @@ class PostgresTodosTaskManifestBackend {
52443
53339
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
52444
53340
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
52445
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
+ }
52446
53346
  if (binding["state"] !== "applied")
52447
53347
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
52448
53348
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -52457,12 +53357,18 @@ class PostgresTodosTaskManifestBackend {
52457
53357
  LIMIT 1`, [this.tenantId, input.receipt_id]);
52458
53358
  if (delivered.rows[0])
52459
53359
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
52460
- const applyResult = parseJson2(applyRow["result_json"]);
53360
+ const applyResult = parseApplyResult(applyRow["result_json"], false);
52461
53361
  const manifest = parseJson2(applyRow["manifest_json"]);
53362
+ const manifestRecord2 = manifest;
53363
+ const applyStepId = typeof manifestRecord2["step_id"] === "string" ? String(manifestRecord2["step_id"]) : null;
52462
53364
  const expectedEffects = [
52463
53365
  {
52464
53366
  topic: "todos.task-manifest.applied",
52465
- 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
+ }
52466
53372
  },
52467
53373
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
52468
53374
  ];
@@ -52510,9 +53416,15 @@ class PostgresTodosTaskManifestBackend {
52510
53416
  }
52511
53417
  const appliedAt = receiptFromRow3(applyRow).created_at;
52512
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"]);
52513
53425
  expectedPayloads.set(applyResult.graph.plan_id, {
52514
53426
  type: "plans",
52515
- payload: canonicalJson(planPayload({ manifest, graph: applyResult.graph, now: appliedAt }))
53427
+ payload: canonicalJson(planExpected)
52516
53428
  });
52517
53429
  for (const task3 of manifest.tasks)
52518
53430
  expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
@@ -52612,14 +53524,17 @@ class PostgresTodosTaskManifestBackend {
52612
53524
  const readback = await this.readback(tx, applyResult.graph);
52613
53525
  const result = { duplicate: false, receipt, absent: true, readback };
52614
53526
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
52615
- receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
52616
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
52617
- ) 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)`, [
52618
53531
  compensationReceiptId,
52619
53532
  this.tenantId,
52620
53533
  receipt.operation_id,
53534
+ receipt.step_id,
52621
53535
  input.idempotency_key,
52622
53536
  requestDigest,
53537
+ input.precondition_digest,
52623
53538
  receipt.result_digest,
52624
53539
  receipt.binding_version,
52625
53540
  input.receipt_id,
@@ -52676,36 +53591,83 @@ function resolveTenantId(value) {
52676
53591
  }
52677
53592
  return tenantId;
52678
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
+ }
52679
53628
  function normalize(input, now4) {
52680
53629
  const parsed = parseTodosTaskManifest(input);
52681
53630
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
52682
53631
  if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
52683
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 });
52684
53633
  }
53634
+ const { idempotency_key: _idempotencyKey, ...request } = parsed;
53635
+ const request_digest = taskManifestRequestDigest(request);
52685
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
+ });
52686
53649
  const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
52687
53650
  task3.key,
52688
- 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)
52689
53652
  ]));
52690
53653
  const graph = {
52691
- 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),
52692
53655
  task_ids,
52693
- comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task3.key, String(index)))),
52694
- 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)))),
52695
53658
  dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
52696
53659
  };
52697
- const request_digest = canonicalDigest(parsed);
52698
53660
  const effectInputs = [
52699
53661
  {
52700
53662
  topic: "todos.task-manifest.applied",
52701
- 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 }
52702
53664
  },
52703
53665
  ...manifest.effects ?? []
52704
53666
  ];
52705
53667
  const outbox = effectInputs.map((effect2, index) => {
52706
53668
  const payload = { ...effect2.payload };
52707
53669
  return {
52708
- 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)),
52709
53671
  topic: effect2.topic,
52710
53672
  payload,
52711
53673
  digest: canonicalDigest({ topic: effect2.topic, payload })
@@ -52715,11 +53677,14 @@ function normalize(input, now4) {
52715
53677
  return {
52716
53678
  manifest,
52717
53679
  request_digest,
53680
+ expected_idempotency_key: expectedIdempotencyKey,
52718
53681
  result_digest,
52719
- 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),
52720
53684
  graph,
52721
53685
  outbox,
52722
- now: now4
53686
+ now: now4,
53687
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
52723
53688
  };
52724
53689
  }
52725
53690
  function sanitizeManifest(manifest) {
@@ -52777,6 +53742,10 @@ class PackageOwnedTodosTaskManifestAuthority {
52777
53742
  tenant_id: this.tenantId,
52778
53743
  backend: this.backend.kind,
52779
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,
52780
53749
  immutable_receipts: true,
52781
53750
  transactional_outbox: true,
52782
53751
  idempotent_outbox_delivery: true,
@@ -52806,7 +53775,11 @@ class PackageOwnedTodosTaskManifestAuthority {
52806
53775
  async apply(input) {
52807
53776
  const normalized = normalize(input, this.now());
52808
53777
  const faults = await this.prepareFaults();
52809
- 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;
52810
53783
  }
52811
53784
  readExact(receiptId2) {
52812
53785
  if (!receiptId2 || receiptId2.length > 200) {
@@ -52839,18 +53812,48 @@ class PackageOwnedTodosTaskManifestAuthority {
52839
53812
  async compensate(input) {
52840
53813
  const request = parseTodosTaskManifestCompensation(input);
52841
53814
  const applied = await this.backend.readExact(request.receipt_id);
52842
- const requestDigest = canonicalDigest(request);
52843
- 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);
52844
53842
  const receipt = {
52845
53843
  receipt_id: compensationReceiptId,
52846
53844
  authority: "todos",
52847
53845
  route: TODOS_TASK_MANIFEST_ROUTE,
52848
53846
  schema_version: 1,
52849
53847
  kind: "compensate",
52850
- operation_id: applied.receipt.operation_id,
53848
+ operation_id: request.operation_id,
53849
+ step_id: request.step_id,
52851
53850
  idempotency_key: request.idempotency_key,
52852
53851
  request_digest: requestDigest,
53852
+ precondition_digest: request.precondition_digest,
52853
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,
52854
53857
  binding_version: request.if_binding_version + 1,
52855
53858
  apply_receipt_id: applied.receipt.receipt_id,
52856
53859
  created_at: this.now()
@@ -54516,9 +55519,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
54516
55519
  TaskManifestBounds: taskManifestBoundsSchema,
54517
55520
  TaskManifestCapability: taskManifestCapabilitySchema,
54518
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,
54519
55530
  TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
54520
55531
  TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
54521
55532
  TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
55533
+ ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
55534
+ ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
55535
+ ProjectRegistrationRequest: projectRegistrationRequestSchema,
55536
+ ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
55537
+ PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
55538
+ ProjectResource: projectResourceSchema,
55539
+ ProjectResourcePage: projectResourcePageSchema,
54522
55540
  TaskList: taskListSchema,
54523
55541
  ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
54524
55542
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
@@ -54561,6 +55579,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
54561
55579
  priority: { type: "string", enum: [...TASK_PRIORITIES] },
54562
55580
  assigned_to: { type: "string" },
54563
55581
  project_id: { type: "string", nullable: true },
55582
+ parent_id: { type: "string", nullable: true },
54564
55583
  plan_id: { type: "string", nullable: true },
54565
55584
  task_list_id: { type: "string", nullable: true },
54566
55585
  version: { type: "number" }
@@ -55515,6 +56534,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55515
56534
  },
55516
56535
  security: [{ apiKey: [] }],
55517
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
+ },
55518
56872
  "/v1/task-manifest/capability": {
55519
56873
  get: {
55520
56874
  operationId: "getTaskManifestCapability",
@@ -55532,6 +56886,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55532
56886
  }
55533
56887
  }
55534
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
+ },
55535
56965
  "/v1/task-manifest/bindings/lookup": {
55536
56966
  post: {
55537
56967
  operationId: "lookupTaskManifestBinding",
@@ -56392,7 +57822,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
56392
57822
  }
56393
57823
  });
56394
57824
  }
56395
- 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;
56396
57826
  var init_openapi = __esm(() => {
56397
57827
  init_package_version();
56398
57828
  init_types();
@@ -56454,6 +57884,10 @@ var init_openapi = __esm(() => {
56454
57884
  "tenant_id",
56455
57885
  "backend",
56456
57886
  "deterministic_ids",
57887
+ "operation_step_identity",
57888
+ "deterministic_idempotency_keys",
57889
+ "terminal_nonacceptance_receipts",
57890
+ "plan_slug_provenance",
56457
57891
  "immutable_receipts",
56458
57892
  "transactional_outbox",
56459
57893
  "idempotent_outbox_delivery",
@@ -56469,6 +57903,10 @@ var init_openapi = __esm(() => {
56469
57903
  tenant_id: { type: "string", minLength: 1, maxLength: 200 },
56470
57904
  backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
56471
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"] },
56472
57910
  immutable_receipts: { type: "boolean", enum: [true] },
56473
57911
  transactional_outbox: { type: "boolean", enum: [true] },
56474
57912
  idempotent_outbox_delivery: { type: "boolean", enum: [true] },
@@ -56522,6 +57960,8 @@ var init_openapi = __esm(() => {
56522
57960
  "schema_version",
56523
57961
  "tenant_id",
56524
57962
  "plan_id",
57963
+ "operation_id",
57964
+ "step_id",
56525
57965
  "apply_receipt_id",
56526
57966
  "binding_version",
56527
57967
  "state"
@@ -56532,11 +57972,169 @@ var init_openapi = __esm(() => {
56532
57972
  schema_version: { type: "integer", enum: [1] },
56533
57973
  tenant_id: { type: "string" },
56534
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 },
56535
57977
  apply_receipt_id: { type: "string", format: "uuid" },
56536
57978
  binding_version: { type: "integer", minimum: 1 },
56537
57979
  state: { type: "string", enum: ["applied", "compensated"] }
56538
57980
  }
56539
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
+ };
56540
58138
  taskManifestBindingLookupResponseSchema = {
56541
58139
  type: "object",
56542
58140
  additionalProperties: false,
@@ -56886,6 +58484,292 @@ var init_openapi = __esm(() => {
56886
58484
  metadata: { type: "object", additionalProperties: true }
56887
58485
  }
56888
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
+ };
56889
58773
  });
56890
58774
 
56891
58775
  // src/server/pr-groups.ts
@@ -57032,11 +58916,11 @@ function canonicalJson2(value) {
57032
58916
  return `[${value.map(canonicalJson2).join(",")}]`;
57033
58917
  return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
57034
58918
  }
57035
- function digest(value) {
58919
+ function digest2(value) {
57036
58920
  return createHash17("sha256").update(canonicalJson2(value)).digest("hex");
57037
58921
  }
57038
58922
  function deriveIdempotencyKey(projectId, slug) {
57039
- return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
58923
+ return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
57040
58924
  }
57041
58925
  function normalizeIdempotencyKey(value, projectId, slug) {
57042
58926
  const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
@@ -57045,13 +58929,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
57045
58929
  }
57046
58930
  return key2;
57047
58931
  }
57048
- function receiptId2(projectId, slug, idempotencyKey) {
57049
- 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)}`;
57050
58934
  }
57051
58935
  function semanticListDigest(list) {
57052
58936
  const metadata = { ...list.metadata ?? {} };
57053
58937
  delete metadata[RECEIPT_METADATA_KEY];
57054
- return digest({
58938
+ return digest2({
57055
58939
  project_id: list.project_id,
57056
58940
  slug: list.slug,
57057
58941
  name: list.name,
@@ -57068,10 +58952,10 @@ function storedMarker(list) {
57068
58952
  return null;
57069
58953
  return marker;
57070
58954
  }
57071
- function receiptFor(store, project, list, idempotencyKey) {
58955
+ function receiptFor(store, project, list, idempotencyKey2) {
57072
58956
  const marker = storedMarker(list);
57073
58957
  const owned = marker?.project_id === project.id && marker.slug === list.slug;
57074
- if (owned && marker.idempotency_key !== idempotencyKey) {
58958
+ if (owned && marker.idempotency_key !== idempotencyKey2) {
57075
58959
  throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
57076
58960
  project_id: project.id,
57077
58961
  task_list_id: list.id,
@@ -57080,8 +58964,8 @@ function receiptFor(store, project, list, idempotencyKey) {
57080
58964
  }
57081
58965
  return {
57082
58966
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
57083
- receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
57084
- 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,
57085
58969
  project_id: project.id,
57086
58970
  task_list_id: list.id,
57087
58971
  slug: list.slug,
@@ -57133,20 +59017,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57133
59017
  });
57134
59018
  }
57135
59019
  const slug = project.task_list_id;
57136
- const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
59020
+ const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
57137
59021
  if (state.scoped) {
57138
59022
  return {
57139
59023
  mode: "apply",
57140
59024
  action: "already_present",
57141
59025
  project,
57142
59026
  task_list: state.scoped,
57143
- receipt: receiptFor(store, project, state.scoped, idempotencyKey)
59027
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
57144
59028
  };
57145
59029
  }
57146
59030
  const marker = {
57147
59031
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
57148
- receipt_id: receiptId2(project.id, slug, idempotencyKey),
57149
- idempotency_key: idempotencyKey,
59032
+ receipt_id: receiptId2(project.id, slug, idempotencyKey2),
59033
+ idempotency_key: idempotencyKey2,
57150
59034
  project_id: project.id,
57151
59035
  slug,
57152
59036
  result_digest: semanticListDigest({
@@ -57184,7 +59068,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57184
59068
  action: "already_present",
57185
59069
  project: raced.project,
57186
59070
  task_list: raced.scoped,
57187
- receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
59071
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
57188
59072
  };
57189
59073
  }
57190
59074
  const projectReadback = await store.projects.get(project.id);
@@ -57214,7 +59098,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57214
59098
  action: "created",
57215
59099
  project: projectReadback,
57216
59100
  task_list: readback,
57217
- receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
59101
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
57218
59102
  };
57219
59103
  }
57220
59104
  async function rollbackProjectTaskListEnsure(store, projectId, options) {
@@ -57259,7 +59143,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
57259
59143
  project_id: project.id,
57260
59144
  task_list_id: list.id,
57261
59145
  accepted_receipt_id: options.receipt_id,
57262
- 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)}`,
57263
59147
  removed_at: new Date().toISOString()
57264
59148
  };
57265
59149
  }
@@ -57458,6 +59342,9 @@ function validateTaskPatchVocabulary(value) {
57458
59342
  if (!parsed.ok)
57459
59343
  return { ok: false, message: parsed.message };
57460
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
+ }
57461
59348
  return { ok: true, patch: body2 };
57462
59349
  }
57463
59350
  function validateProjectPatch(value) {
@@ -57942,8 +59829,8 @@ async function handleV1Request(req, url, dependencies = {}) {
57942
59829
  }
57943
59830
  const created = await store.tasks.create(body2, storageContext);
57944
59831
  const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
57945
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
57946
- 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" });
57947
59834
  }
57948
59835
  return json5({ task: persisted }, 201);
57949
59836
  }
@@ -58817,6 +60704,15 @@ async function handleV1Request(req, url, dependencies = {}) {
58817
60704
  if (e instanceof TaskNotFoundError) {
58818
60705
  return error(404, e.message, { code: TaskNotFoundError.code });
58819
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
+ }
58820
60716
  if (e instanceof StaleLockHandoffError) {
58821
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;
58822
60718
  return error(status2, e.message, {