@hasna/todos 0.15.29 → 0.15.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +2861 -342
  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 +1466 -147
  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 +2050 -138
  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 +849 -60
  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 +2041 -129
  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 +233 -20
  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 +593 -62
  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";
@@ -21237,7 +21312,7 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
21237
21312
  return await request();
21238
21313
  } catch (error) {
21239
21314
  const status = error && typeof error === "object" ? error.status : undefined;
21240
- if (status === 404) {
21315
+ if (status === 404 || status === 405) {
21241
21316
  const body = error && typeof error === "object" ? error.body : undefined;
21242
21317
  const code = body && typeof body === "object" && !Array.isArray(body) ? body.code : undefined;
21243
21318
  if (typeof code === "string" && recognized404Codes.includes(code))
@@ -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.33",
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",
@@ -46124,7 +46248,7 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
46124
46248
  AS $$ SELECT unaccent('unaccent', $1) $$`,
46125
46249
  `CREATE OR REPLACE FUNCTION todos_try_timestamptz(text)
46126
46250
  RETURNS timestamptz
46127
- LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
46251
+ LANGUAGE plpgsql IMMUTABLE PARALLEL UNSAFE
46128
46252
  SET DateStyle TO 'ISO, YMD'
46129
46253
  AS $$
46130
46254
  BEGIN
@@ -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
  }
@@ -50855,9 +51266,37 @@ function normalizedCallDigest(request) {
50855
51266
  project_slug: request.project_slug,
50856
51267
  project_name: request.project_name,
50857
51268
  desired: request.desired,
51269
+ bind_existing: request.bind_existing === true,
50858
51270
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
50859
51271
  });
50860
51272
  }
51273
+ function legacyNormalizedCallDigestBeforeBindExisting(request) {
51274
+ return digestProjectRegistrationValue({
51275
+ authority_route: request.authority_route,
51276
+ package_version: request.package_version,
51277
+ authority_id: request.authority_id,
51278
+ tenant_id: request.tenant_id,
51279
+ corpus_id: request.corpus_id,
51280
+ operation_id: request.operation_id,
51281
+ step_id: request.step_id,
51282
+ resource_kind: request.resource_kind,
51283
+ direction: request.direction,
51284
+ target_selector: request.target_selector,
51285
+ idempotency_key: request.idempotency_key,
51286
+ request_digest: request.request_digest,
51287
+ precondition_digest: request.precondition_digest,
51288
+ project_id: request.project_id,
51289
+ project_slug: request.project_slug,
51290
+ project_name: request.project_name,
51291
+ desired: request.desired,
51292
+ accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
51293
+ });
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));
@@ -51342,9 +51853,14 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51342
51853
  if (request.max_items !== 1) {
51343
51854
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
51344
51855
  }
51345
- if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
51856
+ if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id) {
51346
51857
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
51347
51858
  }
51859
+ requireString(request.corpus_id, "corpus_id", {
51860
+ min: 3,
51861
+ max: 128,
51862
+ pattern: AUTHORITY_ROUTE_PATTERN
51863
+ });
51348
51864
  requireString(request.authority_route, "authority_route", {
51349
51865
  min: 3,
51350
51866
  max: 128,
@@ -51375,6 +51891,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51375
51891
  }
51376
51892
  const receipt = await this.backend.getReceiptForLookup({
51377
51893
  ...authorityScope(this.capabilityValue),
51894
+ corpus_id: request.corpus_id,
51378
51895
  route: request.authority_route,
51379
51896
  package_version: request.package_version,
51380
51897
  operation_id: request.operation_id,
@@ -51389,6 +51906,174 @@ class PackageOwnedTodosProjectRegistrationAuthority {
51389
51906
  }
51390
51907
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
51391
51908
  }
51909
+ async listProjectResources(request) {
51910
+ const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
51911
+ if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
51912
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
51913
+ }
51914
+ if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
51915
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
51916
+ }
51917
+ const includeAnchors = request.include_anchors === true;
51918
+ const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
51919
+ if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
51920
+ 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 });
51921
+ }
51922
+ const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
51923
+ if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
51924
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
51925
+ source_project_id: sourceProjectId,
51926
+ todos_project_id: projectBinding.target_id
51927
+ });
51928
+ }
51929
+ const cursor = decodeProjectResourceCursor(request.cursor, {
51930
+ source_project_id: sourceProjectId,
51931
+ include_anchors: includeAnchors
51932
+ });
51933
+ const collectionInput = {
51934
+ todos_project_id: projectBinding.target_id,
51935
+ task_list_id: taskListBinding.target_id,
51936
+ include_anchors: includeAnchors
51937
+ };
51938
+ const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
51939
+ if (cursor && cursor.collection_revision !== collectionRevision) {
51940
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
51941
+ source_project_id: sourceProjectId,
51942
+ expected_collection_revision: cursor.collection_revision,
51943
+ current_collection_revision: collectionRevision
51944
+ });
51945
+ }
51946
+ const candidates = await this.backend.listProjectResourceCandidates({
51947
+ ...collectionInput,
51948
+ after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
51949
+ limit: request.limit + 1
51950
+ });
51951
+ const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
51952
+ if (verifiedCollectionRevision !== collectionRevision) {
51953
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
51954
+ source_project_id: sourceProjectId,
51955
+ expected_collection_revision: collectionRevision,
51956
+ current_collection_revision: verifiedCollectionRevision
51957
+ });
51958
+ }
51959
+ const hasMore = candidates.length > request.limit;
51960
+ const pageCandidates = candidates.slice(0, request.limit);
51961
+ const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
51962
+ const last = pageCandidates.at(-1);
51963
+ return {
51964
+ authority: "todos",
51965
+ route: this.capabilityValue.route,
51966
+ package_version: this.capabilityValue.package_version,
51967
+ authority_id: this.capabilityValue.authority_id,
51968
+ tenant_id: this.capabilityValue.tenant_id,
51969
+ corpus_id: this.capabilityValue.corpus_id,
51970
+ source_project_id: sourceProjectId,
51971
+ todos_project_id: projectBinding.target_id,
51972
+ task_list_id: taskListBinding.target_id,
51973
+ include_anchors: includeAnchors,
51974
+ collection_revision: collectionRevision,
51975
+ limit: request.limit,
51976
+ count: resources.length,
51977
+ resources,
51978
+ has_more: hasMore,
51979
+ next_cursor: hasMore && last ? encodeProjectResourceCursor({
51980
+ source_project_id: sourceProjectId,
51981
+ include_anchors: includeAnchors,
51982
+ collection_revision: collectionRevision,
51983
+ kind_rank: last.kind_rank,
51984
+ target_id: last.target_id
51985
+ }) : null,
51986
+ complete: !hasMore,
51987
+ truncated: false
51988
+ };
51989
+ }
51990
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
51991
+ const startedAt = Date.now();
51992
+ if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
51993
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
51994
+ }
51995
+ requireString(sourceRequest.package_version, "package_version", {
51996
+ max: 128,
51997
+ pattern: PACKAGE_VERSION_PATTERN
51998
+ });
51999
+ requireString(sourceRequest.corpus_id, "corpus_id", {
52000
+ min: 3,
52001
+ max: 128,
52002
+ pattern: AUTHORITY_ROUTE_PATTERN
52003
+ });
52004
+ assertForwardRequest(sourceRequest, {
52005
+ ...this.capabilityValue,
52006
+ package_version: sourceRequest.package_version,
52007
+ corpus_id: sourceRequest.corpus_id
52008
+ });
52009
+ const validation = await this.backend.transaction(async (transaction) => {
52010
+ const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
52011
+ if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
52012
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
52013
+ }
52014
+ const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
52015
+ if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
52016
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
52017
+ }
52018
+ 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);
52019
+ 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)) {
52020
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
52021
+ }
52022
+ const binding = await transaction.getBinding({
52023
+ authority_id: sourceRequest.authority_id,
52024
+ tenant_id: sourceRequest.tenant_id,
52025
+ corpus_id: sourceRequest.corpus_id
52026
+ }, sourceRequest.resource_kind, sourceRequest.target_selector);
52027
+ 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) {
52028
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
52029
+ }
52030
+ const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
52031
+ if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
52032
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
52033
+ }
52034
+ let stableMatch = false;
52035
+ if (sourceRequest.resource_kind === "task_list") {
52036
+ stableMatch = taskListRegistrationDigest({
52037
+ ...current,
52038
+ updated_at: accepted.result_revision
52039
+ }) === accepted.result_digest;
52040
+ } else {
52041
+ const project = current;
52042
+ if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
52043
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
52044
+ }
52045
+ for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
52046
+ if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
52047
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
52048
+ }
52049
+ if (projectRegistrationDigest({
52050
+ ...project,
52051
+ task_counter: priorTaskCounter,
52052
+ updated_at: accepted.result_revision
52053
+ }) === accepted.result_digest) {
52054
+ stableMatch = true;
52055
+ break;
52056
+ }
52057
+ }
52058
+ }
52059
+ if (!stableMatch) {
52060
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
52061
+ }
52062
+ return {
52063
+ valid: true,
52064
+ resource_kind: sourceRequest.resource_kind,
52065
+ target_id: accepted.target_id,
52066
+ source_receipt_id: storedSource.receipt_id,
52067
+ accepted_receipt_id: accepted.receipt_id,
52068
+ source_outcome: storedSource.outcome,
52069
+ created_at: current.created_at,
52070
+ current_revision: current.updated_at,
52071
+ accepted_result_digest: accepted.result_digest
52072
+ };
52073
+ });
52074
+ assertWithinBounds(validation, sourceRequest, startedAt);
52075
+ return validation;
52076
+ }
51392
52077
  async storedAcceptedReceipt(request, supplied) {
51393
52078
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
51394
52079
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -51557,7 +52242,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
51557
52242
  cursorTableName
51558
52243
  }), authorityOptions);
51559
52244
  }
51560
- var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
52245
+ 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
52246
  var init_authority = __esm(() => {
51562
52247
  init_package_version();
51563
52248
  init_postgres2();
@@ -51627,6 +52312,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51627
52312
  if ((action === "" || action === "capability") && method === "GET") {
51628
52313
  return json({ capability: await authority.capability() });
51629
52314
  }
52315
+ if (action === "resources" && method === "GET") {
52316
+ const sourceProjectId = url.searchParams.get("source_project_id");
52317
+ const limit = Number(url.searchParams.get("limit") ?? "100");
52318
+ const includeAnchorsRaw = url.searchParams.get("include_anchors");
52319
+ const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
52320
+ return json({
52321
+ page: await authority.listProjectResources({
52322
+ source_project_id: sourceProjectId,
52323
+ limit,
52324
+ include_anchors: includeAnchors,
52325
+ cursor: url.searchParams.get("cursor") ?? undefined
52326
+ })
52327
+ });
52328
+ }
51630
52329
  if (method !== "POST")
51631
52330
  return json({ error: "method not allowed" }, 405);
51632
52331
  const body = await readJson(req);
@@ -51649,6 +52348,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51649
52348
  record: await authority.readExact(body)
51650
52349
  });
51651
52350
  }
52351
+ if (action === "validate-prior-adoption") {
52352
+ const input = body;
52353
+ return json({
52354
+ validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
52355
+ });
52356
+ }
51652
52357
  if (action === "compensate") {
51653
52358
  return json({
51654
52359
  receipt: await authority.compensate(body)
@@ -51681,6 +52386,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
51681
52386
  var JSON_HEADERS;
51682
52387
  var init_http2 = __esm(() => {
51683
52388
  init_types4();
52389
+ init_adoption_validation();
51684
52390
  JSON_HEADERS = { "Content-Type": "application/json" };
51685
52391
  });
51686
52392
 
@@ -51688,13 +52394,14 @@ var init_http2 = __esm(() => {
51688
52394
  var init_project_registration = __esm(() => {
51689
52395
  init_authority();
51690
52396
  init_http2();
52397
+ init_adoption_validation();
51691
52398
  init_postgres2();
51692
52399
  init_sqlite();
51693
52400
  init_types4();
51694
52401
  });
51695
52402
 
51696
52403
  // src/task-manifest/types.ts
51697
- var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
52404
+ 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
52405
  var init_types5 = __esm(() => {
51699
52406
  TodosTaskManifestError = class TodosTaskManifestError extends Error {
51700
52407
  code;
@@ -51781,7 +52488,7 @@ function parseTodosTaskManifestBindingLookup(input) {
51781
52488
  }
51782
52489
  return parsed.data;
51783
52490
  }
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) => {
52491
+ 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
52492
  if (Object.keys(value).length > limit) {
51786
52493
  context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
51787
52494
  }
@@ -51803,6 +52510,8 @@ var init_schema2 = __esm(() => {
51803
52510
  };
51804
52511
  key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
51805
52512
  identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
52513
+ digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
52514
+ idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
51806
52515
  uuid2 = exports_external.string().uuid();
51807
52516
  scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
51808
52517
  comment = exports_external.object({
@@ -51840,7 +52549,9 @@ var init_schema2 = __esm(() => {
51840
52549
  schema = exports_external.object({
51841
52550
  version: exports_external.literal(1),
51842
52551
  operation_id: identifier,
51843
- idempotency_key: identifier,
52552
+ step_id: identifier,
52553
+ idempotency_key: idempotencyKey,
52554
+ precondition_digest: digest,
51844
52555
  project_id: uuid2,
51845
52556
  task_list_id: uuid2.optional(),
51846
52557
  if_binding_version: exports_external.number().int().min(0).optional(),
@@ -51856,7 +52567,10 @@ var init_schema2 = __esm(() => {
51856
52567
  }).strict();
51857
52568
  compensationSchema = exports_external.object({
51858
52569
  receipt_id: uuid2,
51859
- idempotency_key: identifier,
52570
+ operation_id: identifier,
52571
+ step_id: identifier,
52572
+ idempotency_key: idempotencyKey,
52573
+ precondition_digest: digest,
51860
52574
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
51861
52575
  }).strict();
51862
52576
  bindingLookupSchema = exports_external.object({
@@ -51874,6 +52588,7 @@ function taskManifestPlanSlug(manifest, planId) {
51874
52588
  const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
51875
52589
  return `${base}-${planId}`;
51876
52590
  }
52591
+ var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
51877
52592
  var init_plan_slug = () => {};
51878
52593
 
51879
52594
  // src/task-manifest/backend.ts
@@ -51887,11 +52602,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
51887
52602
  const row = rows[0];
51888
52603
  const bindingVersion = Number(row.binding_version);
51889
52604
  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") {
52605
+ 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
52606
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
51892
52607
  }
51893
52608
  return {
51894
52609
  plan_id: planId,
52610
+ operation_id: row.binding_operation_id,
52611
+ step_id: row.binding_step_id,
51895
52612
  apply_receipt_id: row.apply_receipt_id,
51896
52613
  binding_version: bindingVersion,
51897
52614
  state
@@ -51944,9 +52661,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51944
52661
  schema_version integer NOT NULL CHECK(schema_version = 1),
51945
52662
  kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
51946
52663
  operation_id text NOT NULL,
52664
+ step_id text NOT NULL,
51947
52665
  idempotency_key text NOT NULL,
51948
52666
  request_digest text NOT NULL,
52667
+ precondition_digest text NOT NULL,
51949
52668
  result_digest text NOT NULL,
52669
+ slug_provenance text,
52670
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
52671
+ reason text,
52672
+ duplicate_of_receipt_id text,
51950
52673
  binding_version integer NOT NULL,
51951
52674
  apply_receipt_id text,
51952
52675
  manifest_json jsonb,
@@ -51958,12 +52681,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51958
52681
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
51959
52682
  `ALTER TABLE todos_task_manifest_receipts
51960
52683
  ALTER COLUMN tenant_id DROP DEFAULT`,
52684
+ `ALTER TABLE todos_task_manifest_receipts
52685
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
52686
+ `ALTER TABLE todos_task_manifest_receipts
52687
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
52688
+ `ALTER TABLE todos_task_manifest_receipts
52689
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
52690
+ `ALTER TABLE todos_task_manifest_receipts
52691
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
52692
+ `ALTER TABLE todos_task_manifest_receipts
52693
+ ADD COLUMN IF NOT EXISTS reason text`,
52694
+ `ALTER TABLE todos_task_manifest_receipts
52695
+ ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
51961
52696
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
51962
52697
  operation_id text PRIMARY KEY,
51963
52698
  tenant_id text NOT NULL,
52699
+ step_id text NOT NULL,
51964
52700
  idempotency_key text NOT NULL UNIQUE,
51965
52701
  request_digest text NOT NULL,
52702
+ precondition_digest text NOT NULL,
51966
52703
  result_digest text NOT NULL,
52704
+ slug_provenance text,
52705
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
51967
52706
  apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
51968
52707
  manifest_json jsonb NOT NULL,
51969
52708
  result_json jsonb NOT NULL,
@@ -51977,6 +52716,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51977
52716
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
51978
52717
  `ALTER TABLE todos_task_manifest_bindings
51979
52718
  ALTER COLUMN tenant_id DROP DEFAULT`,
52719
+ `ALTER TABLE todos_task_manifest_bindings
52720
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
52721
+ `ALTER TABLE todos_task_manifest_bindings
52722
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
52723
+ `ALTER TABLE todos_task_manifest_bindings
52724
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
52725
+ `ALTER TABLE todos_task_manifest_bindings
52726
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
51980
52727
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
51981
52728
  id text PRIMARY KEY,
51982
52729
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -51988,10 +52735,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
51988
52735
  created_at timestamptz NOT NULL,
51989
52736
  delivered_at timestamptz
51990
52737
  )`,
52738
+ `CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
52739
+ receipt_id text PRIMARY KEY,
52740
+ tenant_id text NOT NULL,
52741
+ authority text NOT NULL CHECK(authority = 'todos'),
52742
+ route text NOT NULL,
52743
+ schema_version integer NOT NULL CHECK(schema_version = 1),
52744
+ kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
52745
+ operation_id text NOT NULL,
52746
+ step_id text NOT NULL,
52747
+ idempotency_key text NOT NULL,
52748
+ request_digest text NOT NULL,
52749
+ precondition_digest text NOT NULL,
52750
+ result_digest text NOT NULL,
52751
+ outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
52752
+ reason text NOT NULL,
52753
+ binding_version integer NOT NULL,
52754
+ apply_receipt_id text,
52755
+ manifest_json jsonb,
52756
+ result_json jsonb NOT NULL,
52757
+ created_at timestamptz NOT NULL
52758
+ )`,
51991
52759
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
51992
52760
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
51993
52761
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
51994
52762
  ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
52763
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
52764
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
52765
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
52766
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
52767
+ `CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
52768
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
51995
52769
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
51996
52770
  ON todos_task_manifest_bindings(
51997
52771
  tenant_id,
@@ -52004,6 +52778,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
52004
52778
  `DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
52005
52779
  `CREATE TRIGGER todos_task_manifest_receipts_immutable
52006
52780
  BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
52781
+ FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
52782
+ `DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
52783
+ `CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
52784
+ BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
52007
52785
  FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
52008
52786
  ];
52009
52787
  }
@@ -52028,6 +52806,39 @@ function safeIdentifier2(value, field) {
52028
52806
  function parseJson2(value) {
52029
52807
  return typeof value === "string" ? JSON.parse(value) : value;
52030
52808
  }
52809
+ function parseApplyResult(value, duplicate) {
52810
+ const parsed = parseJson2(value);
52811
+ return {
52812
+ ...parsed,
52813
+ duplicate,
52814
+ receipt: {
52815
+ ...parsed.receipt,
52816
+ step_id: parsed.receipt.step_id ?? "legacy-apply",
52817
+ precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
52818
+ outcome: parsed.receipt.outcome ?? "accepted",
52819
+ reason: parsed.receipt.reason ?? null,
52820
+ duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
52821
+ }
52822
+ };
52823
+ }
52824
+ function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
52825
+ if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
52826
+ const expected = taskManifestPlanSlug(manifest, planId);
52827
+ if (slug !== expected) {
52828
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
52829
+ }
52830
+ return expected;
52831
+ }
52832
+ if (provenance !== null && provenance !== undefined) {
52833
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
52834
+ }
52835
+ if (slug === null || slug === undefined)
52836
+ return null;
52837
+ if (slug !== null && slug !== undefined) {
52838
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
52839
+ }
52840
+ return null;
52841
+ }
52031
52842
  function timestamp4(value) {
52032
52843
  return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
52033
52844
  }
@@ -52035,6 +52846,41 @@ function fault(faults, point) {
52035
52846
  if (faults.points.has(point))
52036
52847
  throw new Error(`Injected task-manifest fault at ${point}`);
52037
52848
  }
52849
+ function terminalApplyResult(input, reason) {
52850
+ const receipt = {
52851
+ receipt_id: input.terminal_receipt_id,
52852
+ authority: "todos",
52853
+ route: "todos.task-manifest.v1",
52854
+ schema_version: 1,
52855
+ kind: "apply",
52856
+ operation_id: input.manifest.operation_id,
52857
+ step_id: input.manifest.step_id,
52858
+ idempotency_key: input.manifest.idempotency_key,
52859
+ request_digest: input.request_digest,
52860
+ precondition_digest: input.manifest.precondition_digest,
52861
+ result_digest: canonicalDigest({
52862
+ outcome: "terminal_nonacceptance",
52863
+ reason,
52864
+ operation_id: input.manifest.operation_id,
52865
+ step_id: input.manifest.step_id,
52866
+ request_digest: input.request_digest
52867
+ }),
52868
+ outcome: "terminal_nonacceptance",
52869
+ reason,
52870
+ duplicate_of_receipt_id: null,
52871
+ binding_version: 0,
52872
+ apply_receipt_id: null,
52873
+ created_at: input.now
52874
+ };
52875
+ return {
52876
+ duplicate: false,
52877
+ receipt,
52878
+ graph: input.graph,
52879
+ readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
52880
+ outbox_ids: [],
52881
+ result_digest: receipt.result_digest
52882
+ };
52883
+ }
52038
52884
  function receiptFromRow3(row) {
52039
52885
  return {
52040
52886
  receipt_id: String(row["receipt_id"]),
@@ -52043,9 +52889,14 @@ function receiptFromRow3(row) {
52043
52889
  schema_version: 1,
52044
52890
  kind: row["kind"],
52045
52891
  operation_id: String(row["operation_id"]),
52892
+ step_id: String(row["step_id"] ?? "legacy-apply"),
52046
52893
  idempotency_key: String(row["idempotency_key"]),
52047
52894
  request_digest: String(row["request_digest"]),
52895
+ precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
52048
52896
  result_digest: String(row["result_digest"]),
52897
+ outcome: row["outcome"] ?? "accepted",
52898
+ reason: row["reason"] == null ? null : row["reason"],
52899
+ duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
52049
52900
  binding_version: Number(row["binding_version"]),
52050
52901
  apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
52051
52902
  created_at: timestamp4(row["created_at"])
@@ -52163,46 +53014,89 @@ class PostgresTodosTaskManifestBackend {
52163
53014
  now4
52164
53015
  ]);
52165
53016
  }
53017
+ async persistTerminal(tx, input, reason) {
53018
+ const result = terminalApplyResult(input, reason);
53019
+ const resultJson = canonicalJson(result);
53020
+ await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
53021
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
53022
+ idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
53023
+ binding_version, apply_receipt_id, manifest_json, result_json, created_at
53024
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
53025
+ 'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
53026
+ ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
53027
+ result.receipt.receipt_id,
53028
+ this.tenantId,
53029
+ input.manifest.operation_id,
53030
+ input.manifest.step_id,
53031
+ input.manifest.idempotency_key,
53032
+ input.request_digest,
53033
+ input.manifest.precondition_digest,
53034
+ result.receipt.result_digest,
53035
+ reason,
53036
+ canonicalJson(input.manifest),
53037
+ resultJson,
53038
+ input.now
53039
+ ]);
53040
+ const stored = await tx.query(`SELECT receipt_id, result_json
53041
+ FROM todos_task_manifest_terminal_receipts
53042
+ WHERE tenant_id = $1 AND kind = 'apply'
53043
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
53044
+ ORDER BY created_at ASC, receipt_id ASC
53045
+ LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
53046
+ return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
53047
+ }
52166
53048
  async apply(input, faults) {
52167
53049
  await this.ensureSchema();
52168
53050
  return this.client.transaction(async (tx) => {
52169
53051
  const { manifest } = input;
52170
53052
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
52171
53053
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
53054
+ const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
53055
+ WHERE tenant_id = $1
53056
+ AND kind = 'apply'
53057
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
53058
+ ORDER BY created_at ASC, receipt_id ASC
53059
+ LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
53060
+ if (terminal.rows[0]) {
53061
+ return parseApplyResult(terminal.rows[0]["result_json"], true);
53062
+ }
52172
53063
  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
53064
  if (existing.rows[0]) {
52174
53065
  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");
53066
+ 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) {
53067
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
52177
53068
  }
52178
53069
  if (binding["state"] !== "applied") {
52179
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
53070
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
52180
53071
  }
52181
- return { ...parseJson2(binding["result_json"]), duplicate: true };
53072
+ return parseApplyResult(binding["result_json"], true);
52182
53073
  }
52183
53074
  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
53075
  if (reused.rows[0])
52185
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
53076
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
53077
+ if (manifest.idempotency_key !== input.expected_idempotency_key) {
53078
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
53079
+ }
52186
53080
  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");
53081
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
52188
53082
  }
52189
53083
  const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
52190
53084
  WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
52191
53085
  if (!project.rows[0])
52192
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
53086
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
52193
53087
  if (manifest.task_list_id) {
52194
53088
  const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
52195
53089
  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
53090
  const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
52197
53091
  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");
53092
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
52199
53093
  }
52200
53094
  }
52201
53095
  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
53096
  const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
52203
53097
  WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
52204
53098
  if (conflict.rows[0])
52205
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "A deterministic graph id already exists");
53099
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
52206
53100
  await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
52207
53101
  fault(faults, "after_plan_write");
52208
53102
  for (const task3 of manifest.tasks) {
@@ -52272,9 +53166,14 @@ class PostgresTodosTaskManifestBackend {
52272
53166
  schema_version: 1,
52273
53167
  kind: "apply",
52274
53168
  operation_id: manifest.operation_id,
53169
+ step_id: manifest.step_id,
52275
53170
  idempotency_key: manifest.idempotency_key,
52276
53171
  request_digest: input.request_digest,
53172
+ precondition_digest: manifest.precondition_digest,
52277
53173
  result_digest: input.result_digest,
53174
+ outcome: "accepted",
53175
+ reason: null,
53176
+ duplicate_of_receipt_id: null,
52278
53177
  binding_version: 1,
52279
53178
  apply_receipt_id: null,
52280
53179
  created_at: input.now
@@ -52291,14 +53190,18 @@ class PostgresTodosTaskManifestBackend {
52291
53190
  const resultJson = canonicalJson(result);
52292
53191
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
52293
53192
  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)`, [
53193
+ step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
53194
+ reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
53195
+ ) 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
53196
  input.receipt_id,
52297
53197
  this.tenantId,
52298
53198
  manifest.operation_id,
52299
53199
  manifest.idempotency_key,
53200
+ manifest.step_id,
52300
53201
  input.request_digest,
53202
+ manifest.precondition_digest,
52301
53203
  input.result_digest,
53204
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
52302
53205
  manifestJson,
52303
53206
  resultJson,
52304
53207
  input.now
@@ -52317,14 +53220,18 @@ class PostgresTodosTaskManifestBackend {
52317
53220
  }
52318
53221
  fault(faults, "after_outbox_write");
52319
53222
  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)`, [
53223
+ operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
53224
+ result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
53225
+ state, version, created_at, updated_at
53226
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
52323
53227
  manifest.operation_id,
52324
53228
  this.tenantId,
53229
+ manifest.step_id,
52325
53230
  manifest.idempotency_key,
52326
53231
  input.request_digest,
53232
+ manifest.precondition_digest,
52327
53233
  input.result_digest,
53234
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
52328
53235
  input.receipt_id,
52329
53236
  manifestJson,
52330
53237
  resultJson,
@@ -52337,9 +53244,12 @@ class PostgresTodosTaskManifestBackend {
52337
53244
  async readExact(receiptId2) {
52338
53245
  await this.ensureSchema();
52339
53246
  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])
53247
+ if (result.rows[0])
53248
+ return parseApplyResult(result.rows[0]["result_json"], false);
53249
+ 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]);
53250
+ if (!terminal.rows[0])
52341
53251
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
52342
- return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
53252
+ return parseApplyResult(terminal.rows[0]["result_json"], false);
52343
53253
  }
52344
53254
  async lookupBindingByPlanId(planId) {
52345
53255
  await this.ensureSchema();
@@ -52350,6 +53260,7 @@ class PostgresTodosTaskManifestBackend {
52350
53260
  b.version AS binding_version,
52351
53261
  b.tenant_id AS binding_tenant_id,
52352
53262
  b.operation_id AS binding_operation_id,
53263
+ b.step_id AS binding_step_id,
52353
53264
  b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
52354
53265
  r.tenant_id AS receipt_tenant_id,
52355
53266
  r.authority AS receipt_authority,
@@ -52357,6 +53268,7 @@ class PostgresTodosTaskManifestBackend {
52357
53268
  r.schema_version AS receipt_schema_version,
52358
53269
  r.kind AS receipt_kind,
52359
53270
  r.operation_id AS receipt_operation_id,
53271
+ r.step_id AS receipt_step_id,
52360
53272
  r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
52361
53273
  FROM todos_task_manifest_bindings b
52362
53274
  LEFT JOIN todos_task_manifest_receipts r
@@ -52443,6 +53355,10 @@ class PostgresTodosTaskManifestBackend {
52443
53355
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
52444
53356
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
52445
53357
  }
53358
+ const appliedReceipt = receiptFromRow3(applyRow);
53359
+ 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"]) {
53360
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
53361
+ }
52446
53362
  if (binding["state"] !== "applied")
52447
53363
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
52448
53364
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -52457,12 +53373,18 @@ class PostgresTodosTaskManifestBackend {
52457
53373
  LIMIT 1`, [this.tenantId, input.receipt_id]);
52458
53374
  if (delivered.rows[0])
52459
53375
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
52460
- const applyResult = parseJson2(applyRow["result_json"]);
53376
+ const applyResult = parseApplyResult(applyRow["result_json"], false);
52461
53377
  const manifest = parseJson2(applyRow["manifest_json"]);
53378
+ const manifestRecord2 = manifest;
53379
+ const applyStepId = typeof manifestRecord2["step_id"] === "string" ? String(manifestRecord2["step_id"]) : null;
52462
53380
  const expectedEffects = [
52463
53381
  {
52464
53382
  topic: "todos.task-manifest.applied",
52465
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
53383
+ payload: {
53384
+ operation_id: manifest.operation_id,
53385
+ ...applyStepId ? { step_id: applyStepId } : {},
53386
+ project_id: manifest.project_id
53387
+ }
52466
53388
  },
52467
53389
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
52468
53390
  ];
@@ -52510,9 +53432,15 @@ class PostgresTodosTaskManifestBackend {
52510
53432
  }
52511
53433
  const appliedAt = receiptFromRow3(applyRow).created_at;
52512
53434
  const expectedPayloads = new Map;
53435
+ const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
53436
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
53437
+ LIMIT 1`, [this.service, applyResult.graph.plan_id]);
53438
+ const actualPlan = planRow.rows[0] ? parseJson2(planRow.rows[0]["payload"]) : null;
53439
+ const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
53440
+ planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
52513
53441
  expectedPayloads.set(applyResult.graph.plan_id, {
52514
53442
  type: "plans",
52515
- payload: canonicalJson(planPayload({ manifest, graph: applyResult.graph, now: appliedAt }))
53443
+ payload: canonicalJson(planExpected)
52516
53444
  });
52517
53445
  for (const task3 of manifest.tasks)
52518
53446
  expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
@@ -52612,14 +53540,17 @@ class PostgresTodosTaskManifestBackend {
52612
53540
  const readback = await this.readback(tx, applyResult.graph);
52613
53541
  const result = { duplicate: false, receipt, absent: true, readback };
52614
53542
  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)`, [
53543
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
53544
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
53545
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
53546
+ ) 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
53547
  compensationReceiptId,
52619
53548
  this.tenantId,
52620
53549
  receipt.operation_id,
53550
+ receipt.step_id,
52621
53551
  input.idempotency_key,
52622
53552
  requestDigest,
53553
+ input.precondition_digest,
52623
53554
  receipt.result_digest,
52624
53555
  receipt.binding_version,
52625
53556
  input.receipt_id,
@@ -52676,36 +53607,83 @@ function resolveTenantId(value) {
52676
53607
  }
52677
53608
  return tenantId;
52678
53609
  }
53610
+ function taskManifestRequestDigest(manifest) {
53611
+ const { idempotency_key: _idempotencyKey, ...request } = manifest;
53612
+ return canonicalDigest(request);
53613
+ }
53614
+ function taskManifestCompensationRequestDigest(request) {
53615
+ return canonicalDigest(request);
53616
+ }
53617
+ function deriveTodosTaskManifestApplyPreconditionDigest(input) {
53618
+ return canonicalDigest({
53619
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
53620
+ direction: "apply",
53621
+ operation_id: input.operation_id,
53622
+ step_id: input.step_id,
53623
+ project_id: input.project_id,
53624
+ task_list_id: input.task_list_id ?? null,
53625
+ expected_binding_version: input.if_binding_version ?? 0
53626
+ });
53627
+ }
53628
+ function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
53629
+ return canonicalDigest({
53630
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
53631
+ direction: "compensate",
53632
+ operation_id: input.operation_id,
53633
+ step_id: input.step_id,
53634
+ apply_receipt_id: input.receipt_id,
53635
+ expected_binding_version: input.if_binding_version
53636
+ });
53637
+ }
53638
+ function deriveTodosTaskManifestIdempotencyKey(input) {
53639
+ return `tmk_${canonicalDigest({
53640
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
53641
+ ...input
53642
+ }).slice(0, 48)}`;
53643
+ }
52679
53644
  function normalize(input, now4) {
52680
53645
  const parsed = parseTodosTaskManifest(input);
52681
53646
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
52682
53647
  if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
52683
53648
  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
53649
  }
53650
+ const { idempotency_key: _idempotencyKey, ...request } = parsed;
53651
+ const request_digest = taskManifestRequestDigest(request);
52685
53652
  const manifest = sanitizeManifest(parsed);
53653
+ const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
53654
+ if (manifest.precondition_digest !== expectedPreconditionDigest) {
53655
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
53656
+ }
53657
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
53658
+ operation_id: manifest.operation_id,
53659
+ step_id: manifest.step_id,
53660
+ direction: "apply",
53661
+ target_selector: manifest.project_id,
53662
+ request_digest,
53663
+ precondition_digest: manifest.precondition_digest
53664
+ });
52686
53665
  const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
52687
53666
  task3.key,
52688
- deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task3.key)
53667
+ deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task3.key)
52689
53668
  ]));
52690
53669
  const graph = {
52691
- plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
53670
+ plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
52692
53671
  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)))),
53672
+ 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)))),
53673
+ 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
53674
  dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
52696
53675
  };
52697
- const request_digest = canonicalDigest(parsed);
52698
53676
  const effectInputs = [
52699
53677
  {
52700
53678
  topic: "todos.task-manifest.applied",
52701
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
53679
+ payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
52702
53680
  },
52703
53681
  ...manifest.effects ?? []
52704
53682
  ];
52705
53683
  const outbox = effectInputs.map((effect2, index) => {
52706
53684
  const payload = { ...effect2.payload };
52707
53685
  return {
52708
- id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
53686
+ id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
52709
53687
  topic: effect2.topic,
52710
53688
  payload,
52711
53689
  digest: canonicalDigest({ topic: effect2.topic, payload })
@@ -52715,11 +53693,14 @@ function normalize(input, now4) {
52715
53693
  return {
52716
53694
  manifest,
52717
53695
  request_digest,
53696
+ expected_idempotency_key: expectedIdempotencyKey,
52718
53697
  result_digest,
52719
- receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
53698
+ receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
53699
+ terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
52720
53700
  graph,
52721
53701
  outbox,
52722
- now: now4
53702
+ now: now4,
53703
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
52723
53704
  };
52724
53705
  }
52725
53706
  function sanitizeManifest(manifest) {
@@ -52777,6 +53758,10 @@ class PackageOwnedTodosTaskManifestAuthority {
52777
53758
  tenant_id: this.tenantId,
52778
53759
  backend: this.backend.kind,
52779
53760
  deterministic_ids: true,
53761
+ operation_step_identity: true,
53762
+ deterministic_idempotency_keys: true,
53763
+ terminal_nonacceptance_receipts: true,
53764
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
52780
53765
  immutable_receipts: true,
52781
53766
  transactional_outbox: true,
52782
53767
  idempotent_outbox_delivery: true,
@@ -52806,7 +53791,11 @@ class PackageOwnedTodosTaskManifestAuthority {
52806
53791
  async apply(input) {
52807
53792
  const normalized = normalize(input, this.now());
52808
53793
  const faults = await this.prepareFaults();
52809
- return this.bounded(await this.backend.apply(normalized, faults));
53794
+ const result = this.bounded(await this.backend.apply(normalized, faults));
53795
+ if (result.receipt.outcome === "terminal_nonacceptance") {
53796
+ throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
53797
+ }
53798
+ return result;
52810
53799
  }
52811
53800
  readExact(receiptId2) {
52812
53801
  if (!receiptId2 || receiptId2.length > 200) {
@@ -52839,18 +53828,48 @@ class PackageOwnedTodosTaskManifestAuthority {
52839
53828
  async compensate(input) {
52840
53829
  const request = parseTodosTaskManifestCompensation(input);
52841
53830
  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);
53831
+ if (applied.receipt.outcome !== "accepted") {
53832
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
53833
+ }
53834
+ if (request.operation_id !== applied.receipt.operation_id) {
53835
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
53836
+ }
53837
+ if (request.step_id === applied.receipt.step_id) {
53838
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
53839
+ }
53840
+ const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
53841
+ if (request.precondition_digest !== expectedPreconditionDigest) {
53842
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
53843
+ }
53844
+ const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
53845
+ const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
53846
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
53847
+ operation_id: request.operation_id,
53848
+ step_id: request.step_id,
53849
+ direction: "compensate",
53850
+ target_selector: request.receipt_id,
53851
+ request_digest: requestDigest,
53852
+ precondition_digest: request.precondition_digest
53853
+ });
53854
+ if (request.idempotency_key !== expectedIdempotencyKey) {
53855
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
53856
+ }
53857
+ const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
52844
53858
  const receipt = {
52845
53859
  receipt_id: compensationReceiptId,
52846
53860
  authority: "todos",
52847
53861
  route: TODOS_TASK_MANIFEST_ROUTE,
52848
53862
  schema_version: 1,
52849
53863
  kind: "compensate",
52850
- operation_id: applied.receipt.operation_id,
53864
+ operation_id: request.operation_id,
53865
+ step_id: request.step_id,
52851
53866
  idempotency_key: request.idempotency_key,
52852
53867
  request_digest: requestDigest,
53868
+ precondition_digest: request.precondition_digest,
52853
53869
  result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
53870
+ outcome: "accepted",
53871
+ reason: null,
53872
+ duplicate_of_receipt_id: null,
52854
53873
  binding_version: request.if_binding_version + 1,
52855
53874
  apply_receipt_id: applied.receipt.receipt_id,
52856
53875
  created_at: this.now()
@@ -54516,9 +55535,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
54516
55535
  TaskManifestBounds: taskManifestBoundsSchema,
54517
55536
  TaskManifestCapability: taskManifestCapabilitySchema,
54518
55537
  TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
55538
+ TaskManifest: taskManifestSchema,
55539
+ TaskManifestReceipt: taskManifestReceiptSchema,
55540
+ TaskManifestApplyResult: taskManifestApplyResultSchema,
55541
+ TaskManifestApplyResponse: taskManifestApplyResponseSchema,
55542
+ TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
55543
+ TaskManifestCompensationResult: taskManifestCompensationResultSchema,
55544
+ TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
55545
+ TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
54519
55546
  TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
54520
55547
  TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
54521
55548
  TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
55549
+ ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
55550
+ ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
55551
+ ProjectRegistrationRequest: projectRegistrationRequestSchema,
55552
+ ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
55553
+ PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
55554
+ ProjectResource: projectResourceSchema,
55555
+ ProjectResourcePage: projectResourcePageSchema,
54522
55556
  TaskList: taskListSchema,
54523
55557
  ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
54524
55558
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
@@ -54561,6 +55595,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
54561
55595
  priority: { type: "string", enum: [...TASK_PRIORITIES] },
54562
55596
  assigned_to: { type: "string" },
54563
55597
  project_id: { type: "string", nullable: true },
55598
+ parent_id: { type: "string", nullable: true },
54564
55599
  plan_id: { type: "string", nullable: true },
54565
55600
  task_list_id: { type: "string", nullable: true },
54566
55601
  version: { type: "number" }
@@ -55515,6 +56550,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55515
56550
  },
55516
56551
  security: [{ apiKey: [] }],
55517
56552
  paths: {
56553
+ "/v1/project-registration/capability": {
56554
+ get: {
56555
+ operationId: "getProjectRegistrationCapability",
56556
+ summary: "Read the live package-owned Projects to Todos registration capability",
56557
+ responses: {
56558
+ "200": {
56559
+ content: {
56560
+ "application/json": {
56561
+ schema: {
56562
+ type: "object",
56563
+ additionalProperties: false,
56564
+ required: ["capability"],
56565
+ properties: {
56566
+ capability: {
56567
+ $ref: "#/components/schemas/ProjectRegistrationCapability"
56568
+ }
56569
+ }
56570
+ }
56571
+ }
56572
+ }
56573
+ }
56574
+ }
56575
+ }
56576
+ },
56577
+ "/v1/project-registration/resources": {
56578
+ get: {
56579
+ operationId: "listProjectRegistrationResources",
56580
+ summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
56581
+ parameters: [
56582
+ {
56583
+ name: "source_project_id",
56584
+ in: "query",
56585
+ required: true,
56586
+ schema: { type: "string" }
56587
+ },
56588
+ {
56589
+ name: "include_anchors",
56590
+ in: "query",
56591
+ schema: { type: "boolean", default: false }
56592
+ },
56593
+ {
56594
+ name: "limit",
56595
+ in: "query",
56596
+ schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
56597
+ },
56598
+ {
56599
+ name: "cursor",
56600
+ in: "query",
56601
+ schema: { type: "string" }
56602
+ }
56603
+ ],
56604
+ responses: {
56605
+ "200": {
56606
+ content: {
56607
+ "application/json": {
56608
+ schema: {
56609
+ type: "object",
56610
+ additionalProperties: false,
56611
+ required: ["page"],
56612
+ properties: {
56613
+ page: { $ref: "#/components/schemas/ProjectResourcePage" }
56614
+ }
56615
+ }
56616
+ }
56617
+ }
56618
+ },
56619
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56620
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56621
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56622
+ }
56623
+ }
56624
+ },
56625
+ "/v1/project-registration/create": {
56626
+ post: {
56627
+ operationId: "createProjectRegistrationResource",
56628
+ summary: "Create or deterministically bind one Projects to Todos resource",
56629
+ requestBody: {
56630
+ required: true,
56631
+ content: {
56632
+ "application/json": {
56633
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
56634
+ }
56635
+ }
56636
+ },
56637
+ responses: {
56638
+ "201": {
56639
+ content: {
56640
+ "application/json": {
56641
+ schema: {
56642
+ type: "object",
56643
+ additionalProperties: false,
56644
+ required: ["receipt"],
56645
+ properties: {
56646
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
56647
+ }
56648
+ }
56649
+ }
56650
+ }
56651
+ },
56652
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56653
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56654
+ }
56655
+ }
56656
+ },
56657
+ "/v1/project-registration/read-exact": {
56658
+ post: {
56659
+ operationId: "readExactProjectRegistrationResource",
56660
+ summary: "Read one registered project or task list by exact full UUID",
56661
+ requestBody: {
56662
+ required: true,
56663
+ content: {
56664
+ "application/json": {
56665
+ schema: {
56666
+ type: "object",
56667
+ additionalProperties: false,
56668
+ required: [
56669
+ "resource_kind",
56670
+ "target_id",
56671
+ "response_byte_limit",
56672
+ "time_budget_ms"
56673
+ ],
56674
+ properties: {
56675
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
56676
+ target_id: { type: "string", format: "uuid" },
56677
+ ...projectRegistrationBoundsProperties
56678
+ }
56679
+ }
56680
+ }
56681
+ }
56682
+ },
56683
+ responses: {
56684
+ "200": {
56685
+ content: {
56686
+ "application/json": {
56687
+ schema: {
56688
+ type: "object",
56689
+ additionalProperties: false,
56690
+ required: ["record"],
56691
+ properties: {
56692
+ record: {
56693
+ type: "object",
56694
+ additionalProperties: false,
56695
+ required: ["target_id", "revision", "digest"],
56696
+ properties: {
56697
+ target_id: { type: "string", format: "uuid" },
56698
+ revision: { type: "string" },
56699
+ digest: { type: "string" }
56700
+ }
56701
+ }
56702
+ }
56703
+ }
56704
+ }
56705
+ }
56706
+ },
56707
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56708
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56709
+ }
56710
+ }
56711
+ },
56712
+ "/v1/project-registration/receipts/lookup": {
56713
+ post: {
56714
+ operationId: "lookupProjectRegistrationReceipt",
56715
+ summary: "Recover one exact immutable terminal registration receipt",
56716
+ requestBody: {
56717
+ required: true,
56718
+ content: {
56719
+ "application/json": {
56720
+ schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
56721
+ }
56722
+ }
56723
+ },
56724
+ responses: {
56725
+ "200": {
56726
+ content: {
56727
+ "application/json": {
56728
+ schema: {
56729
+ type: "object",
56730
+ additionalProperties: false,
56731
+ required: ["receipt", "response_control"],
56732
+ properties: {
56733
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
56734
+ response_control: {
56735
+ type: "object",
56736
+ additionalProperties: false,
56737
+ required: [
56738
+ "response_byte_limit",
56739
+ "time_budget_ms",
56740
+ "response_bytes",
56741
+ "elapsed_ms",
56742
+ "complete",
56743
+ "truncated"
56744
+ ],
56745
+ properties: {
56746
+ ...projectRegistrationBoundsProperties,
56747
+ response_bytes: { type: "integer", minimum: 0 },
56748
+ elapsed_ms: { type: "integer", minimum: 0 },
56749
+ complete: { type: "boolean", enum: [true] },
56750
+ truncated: { type: "boolean", enum: [false] }
56751
+ }
56752
+ }
56753
+ }
56754
+ }
56755
+ }
56756
+ }
56757
+ },
56758
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56759
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56760
+ }
56761
+ }
56762
+ },
56763
+ "/v1/project-registration/validate-prior-adoption": {
56764
+ post: {
56765
+ operationId: "validatePriorRegistrationAdoption",
56766
+ summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
56767
+ requestBody: {
56768
+ required: true,
56769
+ content: {
56770
+ "application/json": {
56771
+ schema: {
56772
+ type: "object",
56773
+ additionalProperties: false,
56774
+ required: ["source_request", "source_receipt", "current_record"],
56775
+ properties: {
56776
+ source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
56777
+ source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
56778
+ current_record: {
56779
+ oneOf: [
56780
+ { $ref: "#/components/schemas/Project" },
56781
+ { $ref: "#/components/schemas/TaskList" }
56782
+ ]
56783
+ }
56784
+ }
56785
+ }
56786
+ }
56787
+ }
56788
+ },
56789
+ responses: {
56790
+ "200": {
56791
+ content: {
56792
+ "application/json": {
56793
+ schema: {
56794
+ type: "object",
56795
+ additionalProperties: false,
56796
+ required: ["validation"],
56797
+ properties: {
56798
+ validation: {
56799
+ $ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
56800
+ }
56801
+ }
56802
+ }
56803
+ }
56804
+ }
56805
+ },
56806
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56807
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56808
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56809
+ }
56810
+ }
56811
+ },
56812
+ "/v1/project-registration/compensate": {
56813
+ post: {
56814
+ operationId: "compensateProjectRegistrationResource",
56815
+ summary: "Conditionally remove an unchanged receipt-owned registration resource",
56816
+ requestBody: {
56817
+ required: true,
56818
+ content: {
56819
+ "application/json": {
56820
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
56821
+ }
56822
+ }
56823
+ },
56824
+ responses: {
56825
+ "201": {
56826
+ content: {
56827
+ "application/json": {
56828
+ schema: {
56829
+ type: "object",
56830
+ additionalProperties: false,
56831
+ required: ["receipt"],
56832
+ properties: {
56833
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
56834
+ }
56835
+ }
56836
+ }
56837
+ }
56838
+ },
56839
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56840
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56841
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56842
+ }
56843
+ }
56844
+ },
56845
+ "/v1/project-registration/verify-inverse": {
56846
+ post: {
56847
+ operationId: "verifyInverseProjectRegistrationResource",
56848
+ summary: "Verify exact absence after conditional registration compensation",
56849
+ requestBody: {
56850
+ required: true,
56851
+ content: {
56852
+ "application/json": {
56853
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
56854
+ }
56855
+ }
56856
+ },
56857
+ responses: {
56858
+ "200": {
56859
+ content: {
56860
+ "application/json": {
56861
+ schema: {
56862
+ type: "object",
56863
+ additionalProperties: false,
56864
+ required: ["verification"],
56865
+ properties: {
56866
+ verification: {
56867
+ type: "object",
56868
+ additionalProperties: false,
56869
+ required: ["target_id", "accepted_receipt_id", "absent", "digest"],
56870
+ properties: {
56871
+ target_id: { type: "string", format: "uuid" },
56872
+ accepted_receipt_id: { type: "string" },
56873
+ absent: { type: "boolean", enum: [true] },
56874
+ digest: { type: "string" }
56875
+ }
56876
+ }
56877
+ }
56878
+ }
56879
+ }
56880
+ }
56881
+ },
56882
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56883
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56884
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56885
+ }
56886
+ }
56887
+ },
55518
56888
  "/v1/task-manifest/capability": {
55519
56889
  get: {
55520
56890
  operationId: "getTaskManifestCapability",
@@ -55532,6 +56902,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55532
56902
  }
55533
56903
  }
55534
56904
  },
56905
+ "/v1/task-manifest/apply": {
56906
+ post: {
56907
+ operationId: "applyTaskManifest",
56908
+ summary: "Apply one exact task-manifest graph through the Todos authority",
56909
+ requestBody: {
56910
+ required: true,
56911
+ content: {
56912
+ "application/json": {
56913
+ schema: { $ref: "#/components/schemas/TaskManifest" }
56914
+ }
56915
+ }
56916
+ },
56917
+ responses: {
56918
+ "201": {
56919
+ content: {
56920
+ "application/json": {
56921
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
56922
+ }
56923
+ }
56924
+ },
56925
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56926
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56927
+ "503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56928
+ }
56929
+ }
56930
+ },
56931
+ "/v1/task-manifest/read-exact": {
56932
+ post: {
56933
+ operationId: "readExactTaskManifest",
56934
+ summary: "Read one exact immutable task-manifest apply receipt",
56935
+ requestBody: {
56936
+ required: true,
56937
+ content: {
56938
+ "application/json": {
56939
+ schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
56940
+ }
56941
+ }
56942
+ },
56943
+ responses: {
56944
+ "200": {
56945
+ content: {
56946
+ "application/json": {
56947
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
56948
+ }
56949
+ }
56950
+ },
56951
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56952
+ }
56953
+ }
56954
+ },
56955
+ "/v1/task-manifest/compensate": {
56956
+ post: {
56957
+ operationId: "compensateTaskManifest",
56958
+ summary: "Compensate one exact untouched task-manifest graph with CAS protection",
56959
+ requestBody: {
56960
+ required: true,
56961
+ content: {
56962
+ "application/json": {
56963
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
56964
+ }
56965
+ }
56966
+ },
56967
+ responses: {
56968
+ "201": {
56969
+ content: {
56970
+ "application/json": {
56971
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
56972
+ }
56973
+ }
56974
+ },
56975
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56976
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
56977
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
56978
+ }
56979
+ }
56980
+ },
55535
56981
  "/v1/task-manifest/bindings/lookup": {
55536
56982
  post: {
55537
56983
  operationId: "lookupTaskManifestBinding",
@@ -56392,7 +57838,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
56392
57838
  }
56393
57839
  });
56394
57840
  }
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;
57841
+ 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
57842
  var init_openapi = __esm(() => {
56397
57843
  init_package_version();
56398
57844
  init_types();
@@ -56454,6 +57900,10 @@ var init_openapi = __esm(() => {
56454
57900
  "tenant_id",
56455
57901
  "backend",
56456
57902
  "deterministic_ids",
57903
+ "operation_step_identity",
57904
+ "deterministic_idempotency_keys",
57905
+ "terminal_nonacceptance_receipts",
57906
+ "plan_slug_provenance",
56457
57907
  "immutable_receipts",
56458
57908
  "transactional_outbox",
56459
57909
  "idempotent_outbox_delivery",
@@ -56469,6 +57919,10 @@ var init_openapi = __esm(() => {
56469
57919
  tenant_id: { type: "string", minLength: 1, maxLength: 200 },
56470
57920
  backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
56471
57921
  deterministic_ids: { type: "boolean", enum: [true] },
57922
+ operation_step_identity: { type: "boolean", enum: [true] },
57923
+ deterministic_idempotency_keys: { type: "boolean", enum: [true] },
57924
+ terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
57925
+ plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
56472
57926
  immutable_receipts: { type: "boolean", enum: [true] },
56473
57927
  transactional_outbox: { type: "boolean", enum: [true] },
56474
57928
  idempotent_outbox_delivery: { type: "boolean", enum: [true] },
@@ -56522,6 +57976,8 @@ var init_openapi = __esm(() => {
56522
57976
  "schema_version",
56523
57977
  "tenant_id",
56524
57978
  "plan_id",
57979
+ "operation_id",
57980
+ "step_id",
56525
57981
  "apply_receipt_id",
56526
57982
  "binding_version",
56527
57983
  "state"
@@ -56532,11 +57988,169 @@ var init_openapi = __esm(() => {
56532
57988
  schema_version: { type: "integer", enum: [1] },
56533
57989
  tenant_id: { type: "string" },
56534
57990
  plan_id: { type: "string", format: "uuid" },
57991
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
57992
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
56535
57993
  apply_receipt_id: { type: "string", format: "uuid" },
56536
57994
  binding_version: { type: "integer", minimum: 1 },
56537
57995
  state: { type: "string", enum: ["applied", "compensated"] }
56538
57996
  }
56539
57997
  };
57998
+ taskManifestSchema = {
57999
+ type: "object",
58000
+ additionalProperties: false,
58001
+ required: [
58002
+ "version",
58003
+ "operation_id",
58004
+ "step_id",
58005
+ "idempotency_key",
58006
+ "precondition_digest",
58007
+ "project_id",
58008
+ "plan",
58009
+ "tasks"
58010
+ ],
58011
+ properties: {
58012
+ version: { type: "integer", enum: [1] },
58013
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
58014
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
58015
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
58016
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58017
+ project_id: { type: "string", format: "uuid" },
58018
+ task_list_id: { type: "string", format: "uuid" },
58019
+ if_binding_version: { type: "integer", minimum: 0 },
58020
+ plan: {
58021
+ type: "object",
58022
+ additionalProperties: false,
58023
+ required: ["key", "name"],
58024
+ properties: {
58025
+ key: { type: "string", minLength: 1, maxLength: 200 },
58026
+ name: { type: "string", minLength: 1, maxLength: 200 },
58027
+ description: { type: "string" },
58028
+ status: { type: "string", enum: ["active", "completed", "archived"] }
58029
+ }
58030
+ },
58031
+ tasks: {
58032
+ type: "array",
58033
+ minItems: 1,
58034
+ items: {
58035
+ type: "object",
58036
+ additionalProperties: false,
58037
+ required: ["key", "title"],
58038
+ properties: {
58039
+ key: { type: "string", minLength: 1, maxLength: 200 },
58040
+ title: { type: "string", minLength: 1, maxLength: 200 },
58041
+ description: { type: "string" },
58042
+ status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
58043
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
58044
+ assigned_to: { type: "string" },
58045
+ created_by: { type: "string" },
58046
+ tags: { type: "array", items: { type: "string" } },
58047
+ metadata: { type: "object", additionalProperties: true },
58048
+ comments: { type: "array", items: { type: "object", additionalProperties: true } },
58049
+ verifications: { type: "array", items: { type: "object", additionalProperties: true } }
58050
+ }
58051
+ }
58052
+ },
58053
+ dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
58054
+ effects: { type: "array", items: { type: "object", additionalProperties: true } }
58055
+ }
58056
+ };
58057
+ taskManifestReceiptSchema = {
58058
+ type: "object",
58059
+ additionalProperties: false,
58060
+ required: [
58061
+ "receipt_id",
58062
+ "authority",
58063
+ "route",
58064
+ "schema_version",
58065
+ "kind",
58066
+ "operation_id",
58067
+ "step_id",
58068
+ "idempotency_key",
58069
+ "request_digest",
58070
+ "precondition_digest",
58071
+ "result_digest",
58072
+ "outcome",
58073
+ "reason",
58074
+ "duplicate_of_receipt_id",
58075
+ "binding_version",
58076
+ "apply_receipt_id",
58077
+ "created_at"
58078
+ ],
58079
+ properties: {
58080
+ receipt_id: { type: "string", format: "uuid" },
58081
+ authority: { type: "string", enum: ["todos"] },
58082
+ route: { type: "string", enum: ["todos.task-manifest.v1"] },
58083
+ schema_version: { type: "integer", enum: [1] },
58084
+ kind: { type: "string", enum: ["apply", "compensate"] },
58085
+ operation_id: { type: "string" },
58086
+ step_id: { type: "string" },
58087
+ idempotency_key: { type: "string" },
58088
+ request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58089
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58090
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58091
+ outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
58092
+ reason: { type: "string", nullable: true },
58093
+ duplicate_of_receipt_id: { type: "string", nullable: true },
58094
+ binding_version: { type: "integer", minimum: 0 },
58095
+ apply_receipt_id: { type: "string", nullable: true },
58096
+ created_at: { type: "string", format: "date-time" }
58097
+ }
58098
+ };
58099
+ taskManifestApplyResultSchema = {
58100
+ type: "object",
58101
+ additionalProperties: false,
58102
+ required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
58103
+ properties: {
58104
+ duplicate: { type: "boolean" },
58105
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
58106
+ graph: { type: "object", additionalProperties: true },
58107
+ readback: { type: "object", additionalProperties: true },
58108
+ outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
58109
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
58110
+ }
58111
+ };
58112
+ taskManifestApplyResponseSchema = {
58113
+ type: "object",
58114
+ additionalProperties: false,
58115
+ required: ["result"],
58116
+ properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
58117
+ };
58118
+ taskManifestCompensateRequestSchema = {
58119
+ type: "object",
58120
+ additionalProperties: false,
58121
+ required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
58122
+ properties: {
58123
+ receipt_id: { type: "string", format: "uuid" },
58124
+ operation_id: { type: "string" },
58125
+ step_id: { type: "string" },
58126
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
58127
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
58128
+ if_binding_version: { type: "integer", minimum: 1 }
58129
+ }
58130
+ };
58131
+ taskManifestCompensationResultSchema = {
58132
+ type: "object",
58133
+ additionalProperties: false,
58134
+ required: ["duplicate", "receipt", "absent", "readback"],
58135
+ properties: {
58136
+ duplicate: { type: "boolean" },
58137
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
58138
+ absent: { type: "boolean", enum: [true] },
58139
+ readback: { type: "object", additionalProperties: true }
58140
+ }
58141
+ };
58142
+ taskManifestCompensateResponseSchema = {
58143
+ type: "object",
58144
+ additionalProperties: false,
58145
+ required: ["result"],
58146
+ properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
58147
+ };
58148
+ taskManifestReadExactRequestSchema = {
58149
+ type: "object",
58150
+ additionalProperties: false,
58151
+ required: ["receipt_id"],
58152
+ properties: { receipt_id: { type: "string", format: "uuid" } }
58153
+ };
56540
58154
  taskManifestBindingLookupResponseSchema = {
56541
58155
  type: "object",
56542
58156
  additionalProperties: false,
@@ -56886,6 +58500,292 @@ var init_openapi = __esm(() => {
56886
58500
  metadata: { type: "object", additionalProperties: true }
56887
58501
  }
56888
58502
  };
58503
+ projectRegistrationBoundsProperties = {
58504
+ response_byte_limit: { type: "integer", minimum: 1 },
58505
+ time_budget_ms: { type: "integer", minimum: 1 }
58506
+ };
58507
+ projectRegistrationReceiptSchema = {
58508
+ type: "object",
58509
+ additionalProperties: false,
58510
+ required: [
58511
+ "receipt_id",
58512
+ "authority",
58513
+ "route",
58514
+ "package_version",
58515
+ "authority_id",
58516
+ "tenant_id",
58517
+ "corpus_id",
58518
+ "operation_id",
58519
+ "step_id",
58520
+ "resource_kind",
58521
+ "direction",
58522
+ "idempotency_key",
58523
+ "request_digest",
58524
+ "precondition_digest",
58525
+ "outcome",
58526
+ "reason",
58527
+ "target_id",
58528
+ "result_revision",
58529
+ "result_digest",
58530
+ "duplicate_of_receipt_id",
58531
+ "accepted_receipt_id",
58532
+ "created_by_operation",
58533
+ "created_at"
58534
+ ],
58535
+ properties: {
58536
+ receipt_id: { type: "string" },
58537
+ authority: { type: "string", enum: ["todos"] },
58538
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
58539
+ package_version: { type: "string" },
58540
+ authority_id: { type: "string" },
58541
+ tenant_id: { type: "string" },
58542
+ corpus_id: { type: "string" },
58543
+ operation_id: { type: "string" },
58544
+ step_id: { type: "string" },
58545
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58546
+ direction: { type: "string", enum: ["forward", "inverse"] },
58547
+ idempotency_key: { type: "string" },
58548
+ request_digest: { type: "string" },
58549
+ precondition_digest: { type: "string" },
58550
+ outcome: {
58551
+ type: "string",
58552
+ enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
58553
+ },
58554
+ reason: { type: "string", nullable: true },
58555
+ target_id: { type: "string", format: "uuid", nullable: true },
58556
+ result_revision: { type: "string", nullable: true },
58557
+ result_digest: { type: "string", nullable: true },
58558
+ duplicate_of_receipt_id: { type: "string", nullable: true },
58559
+ accepted_receipt_id: { type: "string", nullable: true },
58560
+ created_by_operation: { type: "boolean" },
58561
+ created_at: { type: "string", format: "date-time" }
58562
+ }
58563
+ };
58564
+ projectRegistrationCapabilitySchema = {
58565
+ type: "object",
58566
+ additionalProperties: false,
58567
+ required: [
58568
+ "authority",
58569
+ "route",
58570
+ "package_version",
58571
+ "authority_id",
58572
+ "tenant_id",
58573
+ "corpus_id",
58574
+ "supported_resources",
58575
+ "conditional_create",
58576
+ "immutable_receipts",
58577
+ "exact_terminal_lookup",
58578
+ "exact_readback",
58579
+ "bind_existing_adoption",
58580
+ "prior_registration_adoption_validation",
58581
+ "project_resource_enumeration",
58582
+ "project_resource_page_limit",
58583
+ "conditional_inverse",
58584
+ "ambiguous_outcome_reconciliation"
58585
+ ],
58586
+ properties: {
58587
+ authority: { type: "string", enum: ["todos"] },
58588
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
58589
+ package_version: { type: "string" },
58590
+ authority_id: { type: "string" },
58591
+ tenant_id: { type: "string" },
58592
+ corpus_id: { type: "string" },
58593
+ supported_resources: {
58594
+ type: "array",
58595
+ items: { type: "string", enum: ["project", "task_list"] }
58596
+ },
58597
+ conditional_create: { type: "boolean", enum: [true] },
58598
+ immutable_receipts: { type: "boolean", enum: [true] },
58599
+ exact_terminal_lookup: { type: "boolean", enum: [true] },
58600
+ exact_readback: { type: "boolean", enum: [true] },
58601
+ bind_existing_adoption: { type: "boolean", enum: [true] },
58602
+ prior_registration_adoption_validation: { type: "boolean", enum: [true] },
58603
+ project_resource_enumeration: { type: "boolean", enum: [true] },
58604
+ project_resource_page_limit: { type: "integer", minimum: 1 },
58605
+ conditional_inverse: { type: "boolean", enum: [true] },
58606
+ ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
58607
+ }
58608
+ };
58609
+ projectRegistrationRequestSchema = {
58610
+ type: "object",
58611
+ additionalProperties: false,
58612
+ required: [
58613
+ "operation_id",
58614
+ "step_id",
58615
+ "resource_kind",
58616
+ "direction",
58617
+ "authority_route",
58618
+ "package_version",
58619
+ "authority_id",
58620
+ "tenant_id",
58621
+ "corpus_id",
58622
+ "target_selector",
58623
+ "idempotency_key",
58624
+ "request_digest",
58625
+ "precondition_digest",
58626
+ "project_id",
58627
+ "project_slug",
58628
+ "project_name",
58629
+ "desired",
58630
+ "response_byte_limit",
58631
+ "time_budget_ms"
58632
+ ],
58633
+ properties: {
58634
+ operation_id: { type: "string" },
58635
+ step_id: { type: "string" },
58636
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58637
+ direction: { type: "string", enum: ["forward", "inverse"] },
58638
+ authority_route: { type: "string" },
58639
+ package_version: { type: "string" },
58640
+ authority_id: { type: "string" },
58641
+ tenant_id: { type: "string" },
58642
+ corpus_id: { type: "string" },
58643
+ target_selector: { type: "string" },
58644
+ idempotency_key: { type: "string" },
58645
+ request_digest: { type: "string" },
58646
+ precondition_digest: { type: "string" },
58647
+ project_id: { type: "string" },
58648
+ project_slug: { type: "string" },
58649
+ project_name: { type: "string" },
58650
+ desired: { type: "object", additionalProperties: true },
58651
+ bind_existing: { type: "boolean" },
58652
+ accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
58653
+ ...projectRegistrationBoundsProperties
58654
+ }
58655
+ };
58656
+ projectRegistrationLookupRequestSchema = {
58657
+ type: "object",
58658
+ additionalProperties: false,
58659
+ required: [
58660
+ "operation_id",
58661
+ "step_id",
58662
+ "resource_kind",
58663
+ "direction",
58664
+ "authority",
58665
+ "authority_route",
58666
+ "package_version",
58667
+ "authority_id",
58668
+ "tenant_id",
58669
+ "corpus_id",
58670
+ "target_selector",
58671
+ "idempotency_key",
58672
+ "max_items",
58673
+ "response_byte_limit",
58674
+ "time_budget_ms"
58675
+ ],
58676
+ properties: {
58677
+ operation_id: { type: "string" },
58678
+ step_id: { type: "string" },
58679
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58680
+ direction: { type: "string", enum: ["forward", "inverse"] },
58681
+ authority: { type: "string", enum: ["todos"] },
58682
+ authority_route: { type: "string" },
58683
+ package_version: { type: "string" },
58684
+ authority_id: { type: "string" },
58685
+ tenant_id: { type: "string" },
58686
+ corpus_id: { type: "string" },
58687
+ target_selector: { type: "string" },
58688
+ idempotency_key: { type: "string" },
58689
+ target_id: { type: "string", format: "uuid" },
58690
+ max_items: { type: "integer", enum: [1] },
58691
+ ...projectRegistrationBoundsProperties
58692
+ }
58693
+ };
58694
+ priorRegistrationAdoptionValidationSchema = {
58695
+ type: "object",
58696
+ additionalProperties: false,
58697
+ required: [
58698
+ "valid",
58699
+ "resource_kind",
58700
+ "target_id",
58701
+ "source_receipt_id",
58702
+ "accepted_receipt_id",
58703
+ "source_outcome",
58704
+ "created_at",
58705
+ "current_revision",
58706
+ "accepted_result_digest"
58707
+ ],
58708
+ properties: {
58709
+ valid: { type: "boolean", enum: [true] },
58710
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
58711
+ target_id: { type: "string", format: "uuid" },
58712
+ source_receipt_id: { type: "string" },
58713
+ accepted_receipt_id: { type: "string" },
58714
+ source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
58715
+ created_at: { type: "string", format: "date-time" },
58716
+ current_revision: { type: "string", format: "date-time" },
58717
+ accepted_result_digest: { type: "string" }
58718
+ }
58719
+ };
58720
+ projectResourceSchema = {
58721
+ type: "object",
58722
+ additionalProperties: false,
58723
+ required: [
58724
+ "source_project_id",
58725
+ "kind",
58726
+ "scope",
58727
+ "target_id",
58728
+ "parent_id",
58729
+ "revision",
58730
+ "digest"
58731
+ ],
58732
+ properties: {
58733
+ source_project_id: { type: "string" },
58734
+ kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
58735
+ scope: { type: "string", enum: ["collection", "resource"] },
58736
+ target_id: { type: "string", format: "uuid" },
58737
+ parent_id: { type: "string", format: "uuid", nullable: true },
58738
+ revision: { type: "string" },
58739
+ digest: { type: "string" }
58740
+ }
58741
+ };
58742
+ projectResourcePageSchema = {
58743
+ type: "object",
58744
+ additionalProperties: false,
58745
+ required: [
58746
+ "authority",
58747
+ "route",
58748
+ "package_version",
58749
+ "authority_id",
58750
+ "tenant_id",
58751
+ "corpus_id",
58752
+ "source_project_id",
58753
+ "todos_project_id",
58754
+ "task_list_id",
58755
+ "include_anchors",
58756
+ "collection_revision",
58757
+ "limit",
58758
+ "count",
58759
+ "resources",
58760
+ "has_more",
58761
+ "next_cursor",
58762
+ "complete",
58763
+ "truncated"
58764
+ ],
58765
+ properties: {
58766
+ authority: { type: "string", enum: ["todos"] },
58767
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
58768
+ package_version: { type: "string" },
58769
+ authority_id: { type: "string" },
58770
+ tenant_id: { type: "string" },
58771
+ corpus_id: { type: "string" },
58772
+ source_project_id: { type: "string" },
58773
+ todos_project_id: { type: "string", format: "uuid" },
58774
+ task_list_id: { type: "string", format: "uuid" },
58775
+ include_anchors: { type: "boolean" },
58776
+ collection_revision: { type: "string" },
58777
+ limit: { type: "integer", minimum: 1, maximum: 500 },
58778
+ count: { type: "integer", minimum: 0 },
58779
+ resources: {
58780
+ type: "array",
58781
+ items: { $ref: "#/components/schemas/ProjectResource" }
58782
+ },
58783
+ has_more: { type: "boolean" },
58784
+ next_cursor: { type: "string", nullable: true },
58785
+ complete: { type: "boolean" },
58786
+ truncated: { type: "boolean", enum: [false] }
58787
+ }
58788
+ };
56889
58789
  });
56890
58790
 
56891
58791
  // src/server/pr-groups.ts
@@ -57032,11 +58932,11 @@ function canonicalJson2(value) {
57032
58932
  return `[${value.map(canonicalJson2).join(",")}]`;
57033
58933
  return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
57034
58934
  }
57035
- function digest(value) {
58935
+ function digest2(value) {
57036
58936
  return createHash17("sha256").update(canonicalJson2(value)).digest("hex");
57037
58937
  }
57038
58938
  function deriveIdempotencyKey(projectId, slug) {
57039
- return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
58939
+ return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
57040
58940
  }
57041
58941
  function normalizeIdempotencyKey(value, projectId, slug) {
57042
58942
  const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
@@ -57045,13 +58945,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
57045
58945
  }
57046
58946
  return key2;
57047
58947
  }
57048
- function receiptId2(projectId, slug, idempotencyKey) {
57049
- return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
58948
+ function receiptId2(projectId, slug, idempotencyKey2) {
58949
+ return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
57050
58950
  }
57051
58951
  function semanticListDigest(list) {
57052
58952
  const metadata = { ...list.metadata ?? {} };
57053
58953
  delete metadata[RECEIPT_METADATA_KEY];
57054
- return digest({
58954
+ return digest2({
57055
58955
  project_id: list.project_id,
57056
58956
  slug: list.slug,
57057
58957
  name: list.name,
@@ -57068,10 +58968,10 @@ function storedMarker(list) {
57068
58968
  return null;
57069
58969
  return marker;
57070
58970
  }
57071
- function receiptFor(store, project, list, idempotencyKey) {
58971
+ function receiptFor(store, project, list, idempotencyKey2) {
57072
58972
  const marker = storedMarker(list);
57073
58973
  const owned = marker?.project_id === project.id && marker.slug === list.slug;
57074
- if (owned && marker.idempotency_key !== idempotencyKey) {
58974
+ if (owned && marker.idempotency_key !== idempotencyKey2) {
57075
58975
  throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
57076
58976
  project_id: project.id,
57077
58977
  task_list_id: list.id,
@@ -57080,8 +58980,8 @@ function receiptFor(store, project, list, idempotencyKey) {
57080
58980
  }
57081
58981
  return {
57082
58982
  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,
58983
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
58984
+ idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
57085
58985
  project_id: project.id,
57086
58986
  task_list_id: list.id,
57087
58987
  slug: list.slug,
@@ -57133,20 +59033,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57133
59033
  });
57134
59034
  }
57135
59035
  const slug = project.task_list_id;
57136
- const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
59036
+ const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
57137
59037
  if (state.scoped) {
57138
59038
  return {
57139
59039
  mode: "apply",
57140
59040
  action: "already_present",
57141
59041
  project,
57142
59042
  task_list: state.scoped,
57143
- receipt: receiptFor(store, project, state.scoped, idempotencyKey)
59043
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
57144
59044
  };
57145
59045
  }
57146
59046
  const marker = {
57147
59047
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
57148
- receipt_id: receiptId2(project.id, slug, idempotencyKey),
57149
- idempotency_key: idempotencyKey,
59048
+ receipt_id: receiptId2(project.id, slug, idempotencyKey2),
59049
+ idempotency_key: idempotencyKey2,
57150
59050
  project_id: project.id,
57151
59051
  slug,
57152
59052
  result_digest: semanticListDigest({
@@ -57184,7 +59084,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57184
59084
  action: "already_present",
57185
59085
  project: raced.project,
57186
59086
  task_list: raced.scoped,
57187
- receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
59087
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
57188
59088
  };
57189
59089
  }
57190
59090
  const projectReadback = await store.projects.get(project.id);
@@ -57214,7 +59114,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
57214
59114
  action: "created",
57215
59115
  project: projectReadback,
57216
59116
  task_list: readback,
57217
- receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
59117
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
57218
59118
  };
57219
59119
  }
57220
59120
  async function rollbackProjectTaskListEnsure(store, projectId, options) {
@@ -57259,7 +59159,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
57259
59159
  project_id: project.id,
57260
59160
  task_list_id: list.id,
57261
59161
  accepted_receipt_id: options.receipt_id,
57262
- rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
59162
+ rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
57263
59163
  removed_at: new Date().toISOString()
57264
59164
  };
57265
59165
  }
@@ -57458,6 +59358,9 @@ function validateTaskPatchVocabulary(value) {
57458
59358
  if (!parsed.ok)
57459
59359
  return { ok: false, message: parsed.message };
57460
59360
  }
59361
+ if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
59362
+ return { ok: false, message: "parent_id must be a non-empty task id or null" };
59363
+ }
57461
59364
  return { ok: true, patch: body2 };
57462
59365
  }
57463
59366
  function validateProjectPatch(value) {
@@ -57942,8 +59845,8 @@ async function handleV1Request(req, url, dependencies = {}) {
57942
59845
  }
57943
59846
  const created = await store.tasks.create(body2, storageContext);
57944
59847
  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" });
59848
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null) || body2.created_by !== undefined && persisted.created_by !== body2.created_by) {
59849
+ return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, plan_id, and explicit created_by", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
57947
59850
  }
57948
59851
  return json5({ task: persisted }, 201);
57949
59852
  }
@@ -58817,6 +60720,15 @@ async function handleV1Request(req, url, dependencies = {}) {
58817
60720
  if (e instanceof TaskNotFoundError) {
58818
60721
  return error(404, e.message, { code: TaskNotFoundError.code });
58819
60722
  }
60723
+ if (e instanceof VersionConflictError) {
60724
+ return error(409, e.message, {
60725
+ code: VersionConflictError.code,
60726
+ conflict: true,
60727
+ task_id: e.taskId,
60728
+ expected_version: e.expectedVersion,
60729
+ current_version: e.actualVersion
60730
+ });
60731
+ }
58820
60732
  if (e instanceof StaleLockHandoffError) {
58821
60733
  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
60734
  return error(status2, e.message, {