@hasna/todos 0.15.29 → 0.15.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cli/cloud-router.d.ts +13 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-registration-commands.d.ts +2 -0
  4. package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +2835 -334
  8. package/dist/contracts.js +52 -2
  9. package/dist/db/task-crud.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1447 -144
  13. package/dist/lib/task-parent-integrity.d.ts +14 -0
  14. package/dist/lib/task-parent-integrity.d.ts.map +1 -0
  15. package/dist/mcp/index.js +2031 -135
  16. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/project-registration/adoption-validation.d.ts +3 -0
  19. package/dist/project-registration/adoption-validation.d.ts.map +1 -0
  20. package/dist/project-registration/authority.d.ts +3 -1
  21. package/dist/project-registration/authority.d.ts.map +1 -1
  22. package/dist/project-registration/backend.d.ts +24 -1
  23. package/dist/project-registration/backend.d.ts.map +1 -1
  24. package/dist/project-registration/http.d.ts +3 -1
  25. package/dist/project-registration/http.d.ts.map +1 -1
  26. package/dist/project-registration/index.d.ts +2 -1
  27. package/dist/project-registration/index.d.ts.map +1 -1
  28. package/dist/project-registration/page-validation.d.ts +5 -0
  29. package/dist/project-registration/page-validation.d.ts.map +1 -0
  30. package/dist/project-registration/postgres.d.ts +13 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration/sqlite.d.ts +13 -1
  33. package/dist/project-registration/sqlite.d.ts.map +1 -1
  34. package/dist/project-registration/types.d.ts +68 -1
  35. package/dist/project-registration/types.d.ts.map +1 -1
  36. package/dist/project-registration.js +831 -58
  37. package/dist/registry.js +52 -2
  38. package/dist/release-provenance.json +5 -5
  39. package/dist/sdk/index.d.ts +1 -1
  40. package/dist/sdk/index.d.ts.map +1 -1
  41. package/dist/sdk/index.js +151 -0
  42. package/dist/sdk/v1.generated.d.ts +282 -0
  43. package/dist/sdk/v1.generated.d.ts.map +1 -1
  44. package/dist/server/index.js +2022 -126
  45. package/dist/server/openapi.d.ts +2856 -1357
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/v1.d.ts.map +1 -1
  48. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  49. package/dist/storage/postgres-sync.d.ts +8 -0
  50. package/dist/storage/postgres-sync.d.ts.map +1 -1
  51. package/dist/storage.js +232 -19
  52. package/dist/task-manifest/authority.d.ts +13 -1
  53. package/dist/task-manifest/authority.d.ts.map +1 -1
  54. package/dist/task-manifest/backend.d.ts +5 -0
  55. package/dist/task-manifest/backend.d.ts.map +1 -1
  56. package/dist/task-manifest/index.d.ts +2 -2
  57. package/dist/task-manifest/index.d.ts.map +1 -1
  58. package/dist/task-manifest/plan-slug.d.ts +20 -0
  59. package/dist/task-manifest/plan-slug.d.ts.map +1 -1
  60. package/dist/task-manifest/postgres.d.ts +1 -0
  61. package/dist/task-manifest/postgres.d.ts.map +1 -1
  62. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  63. package/dist/task-manifest/schema.d.ts.map +1 -1
  64. package/dist/task-manifest/sqlite.d.ts +1 -0
  65. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  66. package/dist/task-manifest/types.d.ts +21 -1
  67. package/dist/task-manifest/types.d.ts.map +1 -1
  68. package/dist/task-manifest.js +592 -61
  69. package/dist/types/index.d.ts +4 -2
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/package.json +1 -1
@@ -70,7 +70,7 @@ var package_default;
70
70
  var init_package = __esm(() => {
71
71
  package_default = {
72
72
  name: "@hasna/todos",
73
- version: "0.15.29",
73
+ version: "0.15.32",
74
74
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
75
75
  type: "module",
76
76
  main: "dist/index.js",
@@ -2373,6 +2373,46 @@ var init_integrity = __esm(() => {
2373
2373
  };
2374
2374
  });
2375
2375
 
2376
+ // src/lib/task-parent-integrity.ts
2377
+ function parentCycleError(taskId, parentId) {
2378
+ return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
2379
+ }
2380
+ function assertTaskParentIntegrity(taskId, parentId, getTask) {
2381
+ if (parentId === undefined || parentId === null)
2382
+ return;
2383
+ const visited = new Set;
2384
+ let cursor = parentId;
2385
+ while (cursor) {
2386
+ if (cursor === taskId || visited.has(cursor)) {
2387
+ throw parentCycleError(taskId, parentId);
2388
+ }
2389
+ visited.add(cursor);
2390
+ const parent = getTask(cursor);
2391
+ if (!parent)
2392
+ throw new TaskNotFoundError(cursor);
2393
+ cursor = parent.parent_id;
2394
+ }
2395
+ }
2396
+ async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
2397
+ if (parentId === undefined || parentId === null)
2398
+ return;
2399
+ const visited = new Set;
2400
+ let cursor = parentId;
2401
+ while (cursor) {
2402
+ if (cursor === taskId || visited.has(cursor)) {
2403
+ throw parentCycleError(taskId, parentId);
2404
+ }
2405
+ visited.add(cursor);
2406
+ const parent = await getTask(cursor);
2407
+ if (!parent)
2408
+ throw new TaskNotFoundError(cursor);
2409
+ cursor = parent.parent_id;
2410
+ }
2411
+ }
2412
+ var init_task_parent_integrity = __esm(() => {
2413
+ init_types();
2414
+ });
2415
+
2376
2416
  // src/storage/audit-history-import.ts
2377
2417
  function auditHistoryRowsAreFieldIdentical(left, right) {
2378
2418
  return AUDIT_HISTORY_FIELDS.every((field) => {
@@ -2469,8 +2509,8 @@ function createPostgresTodosStorageAdapter(options) {
2469
2509
  resolveRef: (ref) => store.resolveTaskRef(ref),
2470
2510
  list: (filter = {}) => store.listTasks(filter),
2471
2511
  count: (filter = {}) => store.countTasks(filter),
2472
- update: (id, input) => updateTask(id, input, store),
2473
- delete: (id, context) => store.delete("tasks", id, context),
2512
+ update: (id, input, context) => updateTask(id, input, store, context),
2513
+ delete: (id, context) => store.deleteTaskHierarchy(id, context),
2474
2514
  start: (id, agentId) => startTask(id, agentId, store),
2475
2515
  complete: (id, agentId, options2) => completeTask(id, agentId, options2, store),
2476
2516
  fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, store),
@@ -3003,22 +3043,70 @@ class PostgresJsonRecordStore {
3003
3043
  return "identical";
3004
3044
  throw new Error(divergentAuditHistoryReplayError(value.id));
3005
3045
  }
3006
- async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
3046
+ async withTaskParentIntegrityTransaction(fn) {
3047
+ if (typeof this.options.client.transaction !== "function") {
3048
+ throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
3049
+ }
3050
+ return this.options.client.transaction(async (client) => {
3051
+ await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
3052
+ return fn(client);
3053
+ });
3054
+ }
3055
+ async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
3007
3056
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
3008
- if (planIds.length === 0)
3057
+ if (planIds.length === 0 && !parentGuard)
3009
3058
  return this.upsert("tasks", value, context);
3010
3059
  await this.ensureSchema();
3060
+ if (parentGuard && !queryClient) {
3061
+ return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
3062
+ }
3063
+ const client = queryClient ?? this.options.client;
3011
3064
  const updatedAt = value.updated_at;
3012
3065
  const targetPlanId = value.plan_id;
3013
- const result = await this.options.client.query(`/* todos:task-plan-membership-guard */ WITH
3066
+ const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
3067
+ locked_task AS MATERIALIZED (
3068
+ SELECT payload FROM ${this.tableName}
3069
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
3070
+ FOR UPDATE
3071
+ ),
3014
3072
  locked_plans AS MATERIALIZED (
3015
3073
  SELECT object_id, payload FROM ${this.tableName}
3016
3074
  WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
3017
3075
  AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
3018
3076
  ORDER BY object_id
3019
3077
  FOR UPDATE
3078
+ ), parent_chain(object_id, payload, path, cycle) AS (
3079
+ SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
3080
+ FROM ${this.tableName} AS parent
3081
+ WHERE $10::boolean
3082
+ AND $11::text IS NOT NULL
3083
+ AND parent.service = $1
3084
+ AND parent.object_type = 'tasks'
3085
+ AND parent.object_id = $11
3086
+ AND parent.deleted_at IS NULL
3087
+ UNION ALL
3088
+ SELECT ancestor.object_id,
3089
+ ancestor.payload,
3090
+ chain.path || ancestor.object_id,
3091
+ ancestor.object_id = ANY(chain.path)
3092
+ FROM parent_chain AS chain
3093
+ JOIN ${this.tableName} AS ancestor
3094
+ ON ancestor.service = $1
3095
+ AND ancestor.object_type = 'tasks'
3096
+ AND ancestor.object_id = chain.payload->>'parent_id'
3097
+ AND ancestor.deleted_at IS NULL
3098
+ WHERE NOT chain.cycle
3020
3099
  ), validation AS (
3021
3100
  SELECT
3101
+ (NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
3102
+ (NOT $10::boolean OR NOT $13::boolean
3103
+ OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
3104
+ (NOT $10::boolean OR $11::text IS NULL
3105
+ OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
3106
+ (NOT $10::boolean OR $11::text IS NULL
3107
+ OR ($11::text <> $2
3108
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
3109
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
3022
3110
  (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
3023
3111
  ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
3024
3112
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
@@ -3039,7 +3127,13 @@ class PostgresJsonRecordStore {
3039
3127
  )
3040
3128
  SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
3041
3129
  FROM guarded
3042
- WHERE guarded.all_plans_found AND guarded.target_plan_found AND NOT guarded.project_conflict
3130
+ WHERE guarded.task_found
3131
+ AND guarded.version_matches
3132
+ AND guarded.parent_found
3133
+ AND guarded.parent_acyclic
3134
+ AND guarded.all_plans_found
3135
+ AND guarded.target_plan_found
3136
+ AND NOT guarded.project_conflict
3043
3137
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
3044
3138
  payload = EXCLUDED.payload,
3045
3139
  updated_at = EXCLUDED.updated_at,
@@ -3052,8 +3146,10 @@ class PostgresJsonRecordStore {
3052
3146
  AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
3053
3147
  RETURNING payload
3054
3148
  )
3055
- SELECT guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
3056
- (SELECT payload FROM stored) AS payload
3149
+ SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
3150
+ guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
3151
+ (SELECT payload FROM stored) AS payload,
3152
+ (SELECT payload FROM locked_task) AS current_payload
3057
3153
  FROM guarded`, [
3058
3154
  this.service,
3059
3155
  value.id,
@@ -3063,9 +3159,26 @@ class PostgresJsonRecordStore {
3063
3159
  numberValue2(value.version),
3064
3160
  jsonbParam(planIds),
3065
3161
  targetPlanId,
3066
- explicitProject
3162
+ explicitProject,
3163
+ Boolean(parentGuard),
3164
+ parentGuard?.parentId ?? null,
3165
+ parentGuard?.expectedVersion ?? null,
3166
+ parentGuard?.operation === "update"
3067
3167
  ]);
3068
3168
  const row = result.rows[0];
3169
+ if (parentGuard && !row?.task_found) {
3170
+ throw new TaskNotFoundError(value.id);
3171
+ }
3172
+ if (parentGuard && !row?.version_matches) {
3173
+ const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
3174
+ throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
3175
+ }
3176
+ if (parentGuard && !row?.parent_found && parentGuard.parentId) {
3177
+ throw new TaskNotFoundError(parentGuard.parentId);
3178
+ }
3179
+ if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
3180
+ throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
3181
+ }
3069
3182
  if (!row?.all_plans_found || !row.target_plan_found) {
3070
3183
  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 });
3071
3184
  }
@@ -3073,6 +3186,8 @@ class PostgresJsonRecordStore {
3073
3186
  throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
3074
3187
  }
3075
3188
  if (!row.payload) {
3189
+ if (row.current_payload)
3190
+ return payloadRecord2(row.current_payload);
3076
3191
  return await requireRecord("tasks", value.id, this);
3077
3192
  }
3078
3193
  return payloadRecord2(row.payload);
@@ -3398,6 +3513,80 @@ class PostgresJsonRecordStore {
3398
3513
  version: numberValue2(existing["version"])
3399
3514
  }, context);
3400
3515
  }
3516
+ async deleteTaskHierarchy(id, context = {}) {
3517
+ await this.ensureSchema();
3518
+ return this.withTaskParentIntegrityTransaction(async (client) => {
3519
+ const timestamp = new Date().toISOString();
3520
+ const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
3521
+ task_tree(object_id, path, cycle) AS (
3522
+ SELECT task.object_id, ARRAY[task.object_id], false
3523
+ FROM ${this.tableName} AS task
3524
+ WHERE task.service = $1
3525
+ AND task.object_type = 'tasks'
3526
+ AND task.object_id = $2
3527
+ AND task.deleted_at IS NULL
3528
+ UNION ALL
3529
+ SELECT child.object_id,
3530
+ tree.path || child.object_id,
3531
+ child.object_id = ANY(tree.path)
3532
+ FROM task_tree AS tree
3533
+ JOIN ${this.tableName} AS child
3534
+ ON child.service = $1
3535
+ AND child.object_type = 'tasks'
3536
+ AND child.payload->>'parent_id' = tree.object_id
3537
+ AND child.deleted_at IS NULL
3538
+ WHERE NOT tree.cycle
3539
+ ), tombstoned AS (
3540
+ UPDATE ${this.tableName} AS task
3541
+ SET deleted_at = $3::timestamptz,
3542
+ updated_at = $3::timestamptz,
3543
+ source_machine_id = $4
3544
+ WHERE task.service = $1
3545
+ AND task.object_type = 'tasks'
3546
+ AND task.deleted_at IS NULL
3547
+ AND task.object_id IN (
3548
+ SELECT object_id FROM task_tree WHERE NOT cycle
3549
+ )
3550
+ RETURNING task.object_id
3551
+ ), tombstoned_related AS (
3552
+ UPDATE ${this.tableName} AS related
3553
+ SET deleted_at = $3::timestamptz,
3554
+ updated_at = $3::timestamptz,
3555
+ source_machine_id = $4
3556
+ WHERE related.service = $1
3557
+ AND related.deleted_at IS NULL
3558
+ AND (
3559
+ (
3560
+ related.object_type = 'dependencies'
3561
+ AND (
3562
+ related.payload->>'task_id' IN (
3563
+ SELECT object_id FROM task_tree WHERE NOT cycle
3564
+ )
3565
+ OR related.payload->>'depends_on' IN (
3566
+ SELECT object_id FROM task_tree WHERE NOT cycle
3567
+ )
3568
+ )
3569
+ )
3570
+ OR (
3571
+ related.object_type IN ('comments', 'verifications', 'commits', 'refs')
3572
+ AND related.payload->>'task_id' IN (
3573
+ SELECT object_id FROM task_tree WHERE NOT cycle
3574
+ )
3575
+ )
3576
+ )
3577
+ RETURNING related.object_id
3578
+ )
3579
+ SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
3580
+ (SELECT count(*) FROM tombstoned) AS deleted_count,
3581
+ (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
3582
+ this.service,
3583
+ id,
3584
+ timestamp,
3585
+ context.requestId ?? this.sourceMachineId ?? null
3586
+ ]);
3587
+ return Boolean(result.rows[0]?.found);
3588
+ });
3589
+ }
3401
3590
  async getPlanProjectLinkReceipt(receiptId) {
3402
3591
  const value = await this.get("plan_project_link_receipts", receiptId);
3403
3592
  return value ? assertPlanProjectLinkReceipt(value) : null;
@@ -3818,9 +4007,8 @@ class PostgresJsonRecordStore {
3818
4007
  }
3819
4008
  async function createTask(input, store, context) {
3820
4009
  const timestamp = new Date().toISOString();
3821
- if (input.parent_id && !await store.get("tasks", input.parent_id)) {
3822
- throw new TaskNotFoundError(input.parent_id);
3823
- }
4010
+ const taskId = randomUUID();
4011
+ await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
3824
4012
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
3825
4013
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
3826
4014
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -3829,7 +4017,7 @@ async function createTask(input, store, context) {
3829
4017
  const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
3830
4018
  const shortId = effectiveProjectId ? await nextTaskShortId(effectiveProjectId, store, context) : null;
3831
4019
  const task = {
3832
- id: randomUUID(),
4020
+ id: taskId,
3833
4021
  short_id: shortId,
3834
4022
  project_id: effectiveProjectId,
3835
4023
  parent_id: input.parent_id ?? null,
@@ -3885,15 +4073,16 @@ async function createTask(input, store, context) {
3885
4073
  synced_at: null,
3886
4074
  archived_at: null
3887
4075
  };
3888
- const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, task.plan_id ? [task.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
4076
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, task.plan_id ? [task.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context, input.parent_id ? { operation: "create", expectedVersion: 0, parentId: input.parent_id } : undefined);
3889
4077
  await logTaskChange(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
3890
4078
  return storedTask;
3891
4079
  }
3892
- async function updateTask(id, input, store) {
4080
+ async function updateTask(id, input, store, context) {
3893
4081
  const existing = await requireRecord("tasks", id, store);
3894
4082
  if (existing.version !== input.version) {
3895
- throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
4083
+ throw new VersionConflictError(id, input.version, existing.version);
3896
4084
  }
4085
+ await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
3897
4086
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
3898
4087
  const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
3899
4088
  if (linkedPlan?.project_id) {
@@ -3918,10 +4107,19 @@ async function updateTask(id, input, store) {
3918
4107
  metadata: input.metadata ?? existing.metadata,
3919
4108
  requires_approval: input.requires_approval ?? existing.requires_approval,
3920
4109
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
4110
+ parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
3921
4111
  created_by: existing.created_by,
3922
4112
  completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
3923
4113
  };
3924
- return store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined);
4114
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
4115
+ operation: "update",
4116
+ expectedVersion: input.version,
4117
+ parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
4118
+ });
4119
+ if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
4120
+ await logTaskChange(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
4121
+ }
4122
+ return storedTask;
3925
4123
  }
3926
4124
  async function startTask(id, agentId, store) {
3927
4125
  const task = await requireRecord("tasks", id, store);
@@ -3985,7 +4183,11 @@ async function patchTask(task, patch, store) {
3985
4183
  version: task.version + 1,
3986
4184
  updated_at: new Date().toISOString()
3987
4185
  };
3988
- return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"));
4186
+ return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
4187
+ operation: "update",
4188
+ expectedVersion: task.version,
4189
+ parentId: updated.parent_id
4190
+ });
3989
4191
  }
3990
4192
  function sameCloudLockHolder(stored, incoming) {
3991
4193
  if (!stored || !incoming)
@@ -4688,6 +4890,7 @@ var init_postgres_adapter = __esm(() => {
4688
4890
  init_postgres_sync();
4689
4891
  init_integrity();
4690
4892
  init_redaction();
4893
+ init_task_parent_integrity();
4691
4894
  init_audit_history_import();
4692
4895
  init_canonical();
4693
4896
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
@@ -7078,6 +7281,7 @@ class PostgresTodosProjectRegistrationTransaction {
7078
7281
  AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
7079
7282
  ORDER BY payload->>'created_at' ASC, object_id ASC
7080
7283
  LIMIT 1
7284
+ FOR UPDATE
7081
7285
  `, [this.service, path, taskListSlug]);
7082
7286
  return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7083
7287
  }
@@ -7088,6 +7292,7 @@ class PostgresTodosProjectRegistrationTransaction {
7088
7292
  AND payload->>'project_id' = $2 AND payload->>'slug' = $3
7089
7293
  ORDER BY payload->>'created_at' ASC, object_id ASC
7090
7294
  LIMIT 1
7295
+ FOR UPDATE
7091
7296
  `, [this.service, projectId, slug]);
7092
7297
  return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7093
7298
  }
@@ -7098,10 +7303,24 @@ class PostgresTodosProjectRegistrationTransaction {
7098
7303
  return await this.storage.taskLists.create(input);
7099
7304
  }
7100
7305
  async getProject(id) {
7101
- return await this.storage.projects.get(id);
7306
+ const result = await this.client.query(`
7307
+ SELECT payload FROM ${this.tableName}
7308
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2
7309
+ AND deleted_at IS NULL
7310
+ LIMIT 1
7311
+ FOR SHARE
7312
+ `, [this.service, id]);
7313
+ return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7102
7314
  }
7103
7315
  async getTaskList(id) {
7104
- return await this.storage.taskLists.get(id);
7316
+ const result = await this.client.query(`
7317
+ SELECT payload FROM ${this.tableName}
7318
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
7319
+ AND deleted_at IS NULL
7320
+ LIMIT 1
7321
+ FOR SHARE
7322
+ `, [this.service, id]);
7323
+ return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7105
7324
  }
7106
7325
  async lockCompensationWrites() {
7107
7326
  await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
@@ -7181,6 +7400,96 @@ class PostgresTodosProjectRegistrationBackend {
7181
7400
  async getTaskList(id) {
7182
7401
  return (await this.direct()).getTaskList(id);
7183
7402
  }
7403
+ async getProjectResourceCollectionRevision(input) {
7404
+ await this.ensureSchema();
7405
+ const result = await this.client.query(`
7406
+ WITH resources(kind_rank, target_id, revision) AS (
7407
+ SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7408
+ FROM ${this.tableName}
7409
+ WHERE service = $1 AND object_type = 'projects'
7410
+ AND deleted_at IS NULL AND object_id = $2
7411
+ UNION ALL
7412
+ SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7413
+ FROM ${this.tableName}
7414
+ WHERE service = $1 AND object_type = 'task_lists'
7415
+ AND deleted_at IS NULL AND object_id = $3
7416
+ AND payload->>'project_id' = $2
7417
+ UNION ALL
7418
+ SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7419
+ FROM ${this.tableName}
7420
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
7421
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7422
+ UNION ALL
7423
+ SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7424
+ FROM ${this.tableName}
7425
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
7426
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7427
+ )
7428
+ SELECT 'md5:' || md5(COALESCE(string_agg(
7429
+ kind_rank::text || chr(31) || target_id || chr(31) || revision,
7430
+ chr(30) ORDER BY kind_rank ASC, target_id ASC
7431
+ ), '')) AS revision
7432
+ FROM resources
7433
+ `, [
7434
+ this.service,
7435
+ input.todos_project_id,
7436
+ input.task_list_id,
7437
+ input.include_anchors
7438
+ ]);
7439
+ const revision = result.rows[0]?.revision;
7440
+ if (!revision) {
7441
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
7442
+ }
7443
+ return revision;
7444
+ }
7445
+ async listProjectResourceCandidates(input) {
7446
+ await this.ensureSchema();
7447
+ const afterRank = input.after?.kind_rank ?? -1;
7448
+ const afterId = input.after?.target_id ?? "";
7449
+ const result = await this.client.query(`
7450
+ WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
7451
+ SELECT 'project'::text, 0, object_id, NULL::text,
7452
+ COALESCE(payload->>'updated_at', updated_at::text)
7453
+ FROM ${this.tableName}
7454
+ WHERE service = $1 AND object_type = 'projects'
7455
+ AND deleted_at IS NULL AND object_id = $2
7456
+ UNION ALL
7457
+ SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
7458
+ COALESCE(payload->>'updated_at', updated_at::text)
7459
+ FROM ${this.tableName}
7460
+ WHERE service = $1 AND object_type = 'task_lists'
7461
+ AND deleted_at IS NULL AND object_id = $3
7462
+ AND payload->>'project_id' = $2
7463
+ UNION ALL
7464
+ SELECT 'plan'::text, 2, object_id, payload->>'project_id',
7465
+ COALESCE(payload->>'updated_at', updated_at::text)
7466
+ FROM ${this.tableName}
7467
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
7468
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7469
+ UNION ALL
7470
+ SELECT 'task'::text, 3, object_id,
7471
+ COALESCE(payload->>'plan_id', payload->>'project_id'),
7472
+ COALESCE(payload->>'updated_at', updated_at::text)
7473
+ FROM ${this.tableName}
7474
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
7475
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7476
+ )
7477
+ SELECT kind, kind_rank, target_id, parent_id, revision
7478
+ FROM resources
7479
+ WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
7480
+ ORDER BY kind_rank ASC, target_id ASC
7481
+ LIMIT $7
7482
+ `, [
7483
+ this.service,
7484
+ input.todos_project_id,
7485
+ input.task_list_id,
7486
+ input.include_anchors,
7487
+ afterRank,
7488
+ afterId,
7489
+ input.limit
7490
+ ]);
7491
+ return result.rows;
7492
+ }
7184
7493
  }
7185
7494
  var init_postgres2 = __esm(() => {
7186
7495
  init_postgres_adapter();
@@ -15304,6 +15613,7 @@ function createTaskStored(input, d) {
15304
15613
  let id = uuid();
15305
15614
  for (let attempt = 0;attempt < 3; attempt++) {
15306
15615
  try {
15616
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
15307
15617
  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)
15308
15618
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
15309
15619
  id,
@@ -15682,6 +15992,7 @@ function updateTaskStored(id, input, db) {
15682
15992
  throw new VersionConflictError(id, input.version, task.version);
15683
15993
  }
15684
15994
  input = sanitizeUpdateTaskInput(input);
15995
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
15685
15996
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
15686
15997
  const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
15687
15998
  if (linkedProjectId) {
@@ -15735,6 +16046,10 @@ function updateTaskStored(id, input, db) {
15735
16046
  sets.push("project_id = ?");
15736
16047
  params.push(input.project_id);
15737
16048
  }
16049
+ if (input.parent_id !== undefined) {
16050
+ sets.push("parent_id = ?");
16051
+ params.push(input.parent_id);
16052
+ }
15738
16053
  if (input.assigned_to !== undefined) {
15739
16054
  sets.push("assigned_to = ?");
15740
16055
  params.push(input.assigned_to);
@@ -15850,6 +16165,8 @@ function updateTaskStored(id, input, db) {
15850
16165
  logTaskChange2(id, "update", "priority", task.priority, input.priority, agentId, d);
15851
16166
  if (input.title !== undefined && input.title !== task.title)
15852
16167
  logTaskChange2(id, "update", "title", task.title, input.title, agentId, d);
16168
+ if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
16169
+ logTaskChange2(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
15853
16170
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
15854
16171
  logTaskChange2(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
15855
16172
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
@@ -15907,7 +16224,8 @@ function updateTask2(id, input, db) {
15907
16224
  if (!before)
15908
16225
  throw new TaskNotFoundError(id);
15909
16226
  const guardedPlanIds = [before.plan_id, input.plan_id];
15910
- if (!guardedPlanIds.some(Boolean))
16227
+ const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
16228
+ if (!needsSerializedWrite)
15911
16229
  return updateTaskStored(id, input, d);
15912
16230
  return d.transaction(() => {
15913
16231
  guardPlanRowsSqlite(guardedPlanIds, d);
@@ -15943,6 +16261,7 @@ var init_task_crud = __esm(() => {
15943
16261
  init_checklists();
15944
16262
  init_storage_tombstones();
15945
16263
  init_prewrite_secrets();
16264
+ init_task_parent_integrity();
15946
16265
  });
15947
16266
 
15948
16267
  // src/db/task-status.ts
@@ -21129,35 +21448,68 @@ function projectRecord(project) {
21129
21448
  return {
21130
21449
  target_id: project.id,
21131
21450
  revision: project.updated_at,
21132
- digest: digestProjectRegistrationValue({
21133
- id: project.id,
21134
- name: project.name,
21135
- path: project.path,
21136
- description: project.description,
21137
- task_list_id: project.task_list_id,
21138
- task_prefix: project.task_prefix,
21139
- task_counter: project.task_counter,
21140
- created_at: project.created_at,
21141
- updated_at: project.updated_at
21142
- })
21451
+ digest: projectRegistrationDigest(project)
21143
21452
  };
21144
21453
  }
21145
21454
  function taskListRecord(taskList) {
21146
21455
  return {
21147
21456
  target_id: taskList.id,
21148
21457
  revision: taskList.updated_at,
21149
- digest: digestProjectRegistrationValue({
21150
- id: taskList.id,
21151
- project_id: taskList.project_id,
21152
- slug: taskList.slug,
21153
- name: taskList.name,
21154
- description: taskList.description,
21155
- metadata: taskList.metadata,
21156
- created_at: taskList.created_at,
21157
- updated_at: taskList.updated_at
21458
+ digest: taskListRegistrationDigest(taskList)
21459
+ };
21460
+ }
21461
+ function boundExistingProjectRecord(project) {
21462
+ return {
21463
+ target_id: project.id,
21464
+ revision: project.created_at,
21465
+ digest: projectRegistrationDigest({
21466
+ ...project,
21467
+ updated_at: project.created_at
21158
21468
  })
21159
21469
  };
21160
21470
  }
21471
+ function boundExistingTaskListRecord(taskList) {
21472
+ return {
21473
+ target_id: taskList.id,
21474
+ revision: taskList.created_at,
21475
+ digest: taskListRegistrationDigest({
21476
+ ...taskList,
21477
+ updated_at: taskList.created_at
21478
+ })
21479
+ };
21480
+ }
21481
+ function projectRegistrationDigest(project) {
21482
+ return digestProjectRegistrationValue({
21483
+ id: project.id,
21484
+ name: project.name,
21485
+ path: project.path,
21486
+ description: project.description,
21487
+ task_list_id: project.task_list_id,
21488
+ task_prefix: project.task_prefix,
21489
+ task_counter: project.task_counter,
21490
+ created_at: project.created_at,
21491
+ updated_at: project.updated_at
21492
+ });
21493
+ }
21494
+ function taskListRegistrationDigest(taskList) {
21495
+ return digestProjectRegistrationValue({
21496
+ id: taskList.id,
21497
+ project_id: taskList.project_id,
21498
+ slug: taskList.slug,
21499
+ name: taskList.name,
21500
+ description: taskList.description,
21501
+ metadata: taskList.metadata,
21502
+ created_at: taskList.created_at,
21503
+ updated_at: taskList.updated_at
21504
+ });
21505
+ }
21506
+ function canonicalValuesEqual(left, right) {
21507
+ try {
21508
+ return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
21509
+ } catch {
21510
+ return false;
21511
+ }
21512
+ }
21161
21513
  function receiptId(input) {
21162
21514
  return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
21163
21515
  }
@@ -21177,6 +21529,29 @@ function assertCapabilityRequest(request, capability) {
21177
21529
  }
21178
21530
  }
21179
21531
  function normalizedCallDigest(request) {
21532
+ return digestProjectRegistrationValue({
21533
+ authority_route: request.authority_route,
21534
+ package_version: request.package_version,
21535
+ authority_id: request.authority_id,
21536
+ tenant_id: request.tenant_id,
21537
+ corpus_id: request.corpus_id,
21538
+ operation_id: request.operation_id,
21539
+ step_id: request.step_id,
21540
+ resource_kind: request.resource_kind,
21541
+ direction: request.direction,
21542
+ target_selector: request.target_selector,
21543
+ idempotency_key: request.idempotency_key,
21544
+ request_digest: request.request_digest,
21545
+ precondition_digest: request.precondition_digest,
21546
+ project_id: request.project_id,
21547
+ project_slug: request.project_slug,
21548
+ project_name: request.project_name,
21549
+ desired: request.desired,
21550
+ bind_existing: request.bind_existing === true,
21551
+ accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
21552
+ });
21553
+ }
21554
+ function legacyNormalizedCallDigestBeforeBindExisting(request) {
21180
21555
  return digestProjectRegistrationValue({
21181
21556
  authority_route: request.authority_route,
21182
21557
  package_version: request.package_version,
@@ -21198,6 +21573,11 @@ function normalizedCallDigest(request) {
21198
21573
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
21199
21574
  });
21200
21575
  }
21576
+ function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
21577
+ if (accepted.normalized_call_digest === callDigest)
21578
+ return true;
21579
+ return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
21580
+ }
21201
21581
  function assertCommonRequest(request, capability) {
21202
21582
  assertBounds(request);
21203
21583
  assertResourceKind(request.resource_kind);
@@ -21239,6 +21619,9 @@ function assertCommonRequest(request, capability) {
21239
21619
  if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
21240
21620
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
21241
21621
  }
21622
+ if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
21623
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
21624
+ }
21242
21625
  const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
21243
21626
  operation_id: request.operation_id,
21244
21627
  step_id: request.step_id,
@@ -21260,7 +21643,7 @@ function assertForwardRequest(request, capability) {
21260
21643
  const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
21261
21644
  const expectedPreconditionDigest = digestProjectRegistrationValue({
21262
21645
  target_selector: request.target_selector,
21263
- expected: "absent"
21646
+ expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
21264
21647
  });
21265
21648
  if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
21266
21649
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
@@ -21339,7 +21722,7 @@ function receiptBase(request, callDigest, capability) {
21339
21722
  normalized_call_digest: callDigest
21340
21723
  };
21341
21724
  }
21342
- function makeAcceptedReceipt(request, callDigest, capability, record, createdAt) {
21725
+ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt, createdByOperation = true) {
21343
21726
  return makeReceipt({
21344
21727
  ...receiptBase(request, callDigest, capability),
21345
21728
  outcome: "accepted",
@@ -21349,7 +21732,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt)
21349
21732
  result_digest: record.digest,
21350
21733
  duplicate_of_receipt_id: null,
21351
21734
  accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
21352
- created_by_operation: true
21735
+ created_by_operation: createdByOperation
21353
21736
  }, createdAt);
21354
21737
  }
21355
21738
  function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt) {
@@ -21411,6 +21794,53 @@ function bindingFor(request, callDigest, timestamp2, capability) {
21411
21794
  updated_at: timestamp2
21412
21795
  };
21413
21796
  }
21797
+ function encodeProjectResourceCursor(input) {
21798
+ return Buffer.from(JSON.stringify({
21799
+ version: PROJECT_RESOURCE_CURSOR_VERSION,
21800
+ ...input
21801
+ }), "utf8").toString("base64url");
21802
+ }
21803
+ function decodeProjectResourceCursor(cursor, expected) {
21804
+ if (!cursor)
21805
+ return null;
21806
+ let parsed;
21807
+ try {
21808
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
21809
+ } catch {
21810
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
21811
+ }
21812
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
21813
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
21814
+ }
21815
+ const value = parsed;
21816
+ 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) {
21817
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
21818
+ }
21819
+ return {
21820
+ kind_rank: Number(value["kind_rank"]),
21821
+ target_id: value["target_id"],
21822
+ collection_revision: value["collection_revision"]
21823
+ };
21824
+ }
21825
+ function projectResourceFromCandidate(sourceProjectId, candidate) {
21826
+ const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
21827
+ return {
21828
+ source_project_id: sourceProjectId,
21829
+ kind: candidate.kind,
21830
+ scope,
21831
+ target_id: candidate.target_id,
21832
+ parent_id: candidate.parent_id,
21833
+ revision: candidate.revision,
21834
+ digest: digestProjectRegistrationValue({
21835
+ source_project_id: sourceProjectId,
21836
+ kind: candidate.kind,
21837
+ scope,
21838
+ target_id: candidate.target_id,
21839
+ parent_id: candidate.parent_id,
21840
+ revision: candidate.revision
21841
+ })
21842
+ };
21843
+ }
21414
21844
 
21415
21845
  class PackageOwnedTodosProjectRegistrationAuthority {
21416
21846
  backend;
@@ -21432,6 +21862,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21432
21862
  immutable_receipts: true,
21433
21863
  exact_terminal_lookup: true,
21434
21864
  exact_readback: true,
21865
+ bind_existing_adoption: true,
21866
+ prior_registration_adoption_validation: true,
21867
+ project_resource_enumeration: true,
21868
+ project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
21435
21869
  conditional_inverse: true,
21436
21870
  ambiguous_outcome_reconciliation: true
21437
21871
  };
@@ -21492,7 +21926,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21492
21926
  if (!accepted2) {
21493
21927
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
21494
21928
  }
21495
- if (accepted2.normalized_call_digest !== callDigest) {
21929
+ if (!acceptedCallMatches(request, accepted2, callDigest)) {
21496
21930
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
21497
21931
  }
21498
21932
  return this.duplicateFor(transaction, request, callDigest, accepted2);
@@ -21506,7 +21940,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21506
21940
  });
21507
21941
  if (!accepted)
21508
21942
  return null;
21509
- if (accepted.normalized_call_digest === callDigest) {
21943
+ if (acceptedCallMatches(request, accepted, callDigest)) {
21510
21944
  return this.duplicateFor(transaction, request, callDigest, accepted);
21511
21945
  }
21512
21946
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
@@ -21517,6 +21951,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21517
21951
  const slug2 = taskListSlug(request.project_slug);
21518
21952
  const conflict2 = await transaction.findProjectConflict(path, slug2);
21519
21953
  if (conflict2) {
21954
+ if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
21955
+ return {
21956
+ record: boundExistingProjectRecord(conflict2),
21957
+ created_by_operation: false
21958
+ };
21959
+ }
21520
21960
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
21521
21961
  }
21522
21962
  await this.fault("before_object_write", request);
@@ -21528,7 +21968,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21528
21968
  task_prefix: deterministicTaskPrefix(request.project_slug)
21529
21969
  });
21530
21970
  await this.fault("after_object_write", request);
21531
- return projectRecord(project);
21971
+ return {
21972
+ record: projectRecord(project),
21973
+ created_by_operation: true
21974
+ };
21532
21975
  }
21533
21976
  const todosProjectId = String(request.desired["todos_project_id"]);
21534
21977
  const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
@@ -21542,6 +21985,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21542
21985
  const slug = taskListSlug(request.project_slug);
21543
21986
  const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
21544
21987
  if (conflict) {
21988
+ if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
21989
+ return {
21990
+ record: boundExistingTaskListRecord(conflict),
21991
+ created_by_operation: false
21992
+ };
21993
+ }
21545
21994
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
21546
21995
  }
21547
21996
  await this.fault("before_object_write", request);
@@ -21558,7 +22007,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21558
22007
  if (taskList.project_id !== todosProjectId) {
21559
22008
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
21560
22009
  }
21561
- return taskListRecord(taskList);
22010
+ return {
22011
+ record: taskListRecord(taskList),
22012
+ created_by_operation: true
22013
+ };
21562
22014
  }
21563
22015
  async create(request) {
21564
22016
  const startedAt = Date.now();
@@ -21580,9 +22032,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21580
22032
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
21581
22033
  if (!claimed) {
21582
22034
  const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
21583
- if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
22035
+ if (binding?.state === "accepted" && binding.accepted_receipt_id) {
21584
22036
  const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
21585
- if (accepted2) {
22037
+ if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
21586
22038
  return this.duplicateFor(transaction, request, callDigest, accepted2);
21587
22039
  }
21588
22040
  }
@@ -21593,15 +22045,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21593
22045
  await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
21594
22046
  return recordOrTerminal;
21595
22047
  }
21596
- const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
22048
+ const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
21597
22049
  await this.fault("before_receipt_write", request);
21598
22050
  const stored = await insertDeterministicReceipt(transaction, accepted);
21599
22051
  await this.fault("after_receipt_write", request);
21600
22052
  await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
21601
- target_id: recordOrTerminal.target_id,
22053
+ target_id: recordOrTerminal.record.target_id,
21602
22054
  accepted_receipt_id: stored.receipt_id,
21603
- result_revision: recordOrTerminal.revision,
21604
- result_digest: recordOrTerminal.digest,
22055
+ result_revision: recordOrTerminal.record.revision,
22056
+ result_digest: recordOrTerminal.record.digest,
21605
22057
  updated_at: this.now()
21606
22058
  });
21607
22059
  return stored;
@@ -21649,7 +22101,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21649
22101
  direction: request.direction
21650
22102
  });
21651
22103
  if (accepted) {
21652
- 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 });
22104
+ return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
21653
22105
  }
21654
22106
  const timestamp2 = this.now();
21655
22107
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
@@ -21729,6 +22181,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21729
22181
  }
21730
22182
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
21731
22183
  }
22184
+ async listProjectResources(request) {
22185
+ const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
22186
+ if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
22187
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
22188
+ }
22189
+ if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
22190
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
22191
+ }
22192
+ const includeAnchors = request.include_anchors === true;
22193
+ const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
22194
+ if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
22195
+ 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 });
22196
+ }
22197
+ const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
22198
+ if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
22199
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
22200
+ source_project_id: sourceProjectId,
22201
+ todos_project_id: projectBinding.target_id
22202
+ });
22203
+ }
22204
+ const cursor = decodeProjectResourceCursor(request.cursor, {
22205
+ source_project_id: sourceProjectId,
22206
+ include_anchors: includeAnchors
22207
+ });
22208
+ const collectionInput = {
22209
+ todos_project_id: projectBinding.target_id,
22210
+ task_list_id: taskListBinding.target_id,
22211
+ include_anchors: includeAnchors
22212
+ };
22213
+ const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
22214
+ if (cursor && cursor.collection_revision !== collectionRevision) {
22215
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
22216
+ source_project_id: sourceProjectId,
22217
+ expected_collection_revision: cursor.collection_revision,
22218
+ current_collection_revision: collectionRevision
22219
+ });
22220
+ }
22221
+ const candidates = await this.backend.listProjectResourceCandidates({
22222
+ ...collectionInput,
22223
+ after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
22224
+ limit: request.limit + 1
22225
+ });
22226
+ const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
22227
+ if (verifiedCollectionRevision !== collectionRevision) {
22228
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
22229
+ source_project_id: sourceProjectId,
22230
+ expected_collection_revision: collectionRevision,
22231
+ current_collection_revision: verifiedCollectionRevision
22232
+ });
22233
+ }
22234
+ const hasMore = candidates.length > request.limit;
22235
+ const pageCandidates = candidates.slice(0, request.limit);
22236
+ const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
22237
+ const last = pageCandidates.at(-1);
22238
+ return {
22239
+ authority: "todos",
22240
+ route: this.capabilityValue.route,
22241
+ package_version: this.capabilityValue.package_version,
22242
+ authority_id: this.capabilityValue.authority_id,
22243
+ tenant_id: this.capabilityValue.tenant_id,
22244
+ corpus_id: this.capabilityValue.corpus_id,
22245
+ source_project_id: sourceProjectId,
22246
+ todos_project_id: projectBinding.target_id,
22247
+ task_list_id: taskListBinding.target_id,
22248
+ include_anchors: includeAnchors,
22249
+ collection_revision: collectionRevision,
22250
+ limit: request.limit,
22251
+ count: resources.length,
22252
+ resources,
22253
+ has_more: hasMore,
22254
+ next_cursor: hasMore && last ? encodeProjectResourceCursor({
22255
+ source_project_id: sourceProjectId,
22256
+ include_anchors: includeAnchors,
22257
+ collection_revision: collectionRevision,
22258
+ kind_rank: last.kind_rank,
22259
+ target_id: last.target_id
22260
+ }) : null,
22261
+ complete: !hasMore,
22262
+ truncated: false
22263
+ };
22264
+ }
22265
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
22266
+ const startedAt = Date.now();
22267
+ if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
22268
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
22269
+ }
22270
+ requireString(sourceRequest.package_version, "package_version", {
22271
+ max: 128,
22272
+ pattern: PACKAGE_VERSION_PATTERN
22273
+ });
22274
+ assertForwardRequest(sourceRequest, {
22275
+ ...this.capabilityValue,
22276
+ package_version: sourceRequest.package_version
22277
+ });
22278
+ const validation = await this.backend.transaction(async (transaction) => {
22279
+ const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
22280
+ if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
22281
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
22282
+ }
22283
+ const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
22284
+ if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
22285
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
22286
+ }
22287
+ 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);
22288
+ 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)) {
22289
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
22290
+ }
22291
+ const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
22292
+ 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) {
22293
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
22294
+ }
22295
+ const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
22296
+ if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
22297
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
22298
+ }
22299
+ let stableMatch = false;
22300
+ if (sourceRequest.resource_kind === "task_list") {
22301
+ stableMatch = taskListRegistrationDigest({
22302
+ ...current,
22303
+ updated_at: accepted.result_revision
22304
+ }) === accepted.result_digest;
22305
+ } else {
22306
+ const project = current;
22307
+ if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
22308
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
22309
+ }
22310
+ for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
22311
+ if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
22312
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
22313
+ }
22314
+ if (projectRegistrationDigest({
22315
+ ...project,
22316
+ task_counter: priorTaskCounter,
22317
+ updated_at: accepted.result_revision
22318
+ }) === accepted.result_digest) {
22319
+ stableMatch = true;
22320
+ break;
22321
+ }
22322
+ }
22323
+ }
22324
+ if (!stableMatch) {
22325
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
22326
+ }
22327
+ return {
22328
+ valid: true,
22329
+ resource_kind: sourceRequest.resource_kind,
22330
+ target_id: accepted.target_id,
22331
+ source_receipt_id: storedSource.receipt_id,
22332
+ accepted_receipt_id: accepted.receipt_id,
22333
+ source_outcome: storedSource.outcome,
22334
+ created_at: current.created_at,
22335
+ current_revision: current.updated_at,
22336
+ accepted_result_digest: accepted.result_digest
22337
+ };
22338
+ });
22339
+ assertWithinBounds(validation, sourceRequest, startedAt);
22340
+ return validation;
22341
+ }
21732
22342
  async storedAcceptedReceipt(request, supplied) {
21733
22343
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
21734
22344
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -21897,7 +22507,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
21897
22507
  cursorTableName
21898
22508
  }), authorityOptions);
21899
22509
  }
21900
- var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
22510
+ 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;
21901
22511
  var init_authority = __esm(() => {
21902
22512
  init_package_version();
21903
22513
  init_postgres2();
@@ -21922,6 +22532,11 @@ var init_authority = __esm(() => {
21922
22532
  };
21923
22533
  });
21924
22534
 
22535
+ // src/project-registration/adoption-validation.ts
22536
+ var init_adoption_validation = __esm(() => {
22537
+ init_types3();
22538
+ });
22539
+
21925
22540
  // src/project-registration/http.ts
21926
22541
  function json(body, status = 200) {
21927
22542
  return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
@@ -21967,6 +22582,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
21967
22582
  if ((action === "" || action === "capability") && method === "GET") {
21968
22583
  return json({ capability: await authority.capability() });
21969
22584
  }
22585
+ if (action === "resources" && method === "GET") {
22586
+ const sourceProjectId = url.searchParams.get("source_project_id");
22587
+ const limit = Number(url.searchParams.get("limit") ?? "100");
22588
+ const includeAnchorsRaw = url.searchParams.get("include_anchors");
22589
+ const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
22590
+ return json({
22591
+ page: await authority.listProjectResources({
22592
+ source_project_id: sourceProjectId,
22593
+ limit,
22594
+ include_anchors: includeAnchors,
22595
+ cursor: url.searchParams.get("cursor") ?? undefined
22596
+ })
22597
+ });
22598
+ }
21970
22599
  if (method !== "POST")
21971
22600
  return json({ error: "method not allowed" }, 405);
21972
22601
  const body = await readJson(req);
@@ -21989,6 +22618,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
21989
22618
  record: await authority.readExact(body)
21990
22619
  });
21991
22620
  }
22621
+ if (action === "validate-prior-adoption") {
22622
+ const input = body;
22623
+ return json({
22624
+ validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
22625
+ });
22626
+ }
21992
22627
  if (action === "compensate") {
21993
22628
  return json({
21994
22629
  receipt: await authority.compensate(body)
@@ -22021,6 +22656,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
22021
22656
  var JSON_HEADERS;
22022
22657
  var init_http = __esm(() => {
22023
22658
  init_types3();
22659
+ init_adoption_validation();
22024
22660
  JSON_HEADERS = { "Content-Type": "application/json" };
22025
22661
  });
22026
22662
 
@@ -22028,6 +22664,7 @@ var init_http = __esm(() => {
22028
22664
  var init_project_registration = __esm(() => {
22029
22665
  init_authority();
22030
22666
  init_http();
22667
+ init_adoption_validation();
22031
22668
  init_postgres2();
22032
22669
  init_sqlite();
22033
22670
  init_types3();
@@ -26000,7 +26637,7 @@ var init_zod = __esm(() => {
26000
26637
  });
26001
26638
 
26002
26639
  // src/task-manifest/types.ts
26003
- var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
26640
+ 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;
26004
26641
  var init_types5 = __esm(() => {
26005
26642
  TodosTaskManifestError = class TodosTaskManifestError extends Error {
26006
26643
  code;
@@ -26087,7 +26724,7 @@ function parseTodosTaskManifestBindingLookup(input) {
26087
26724
  }
26088
26725
  return parsed.data;
26089
26726
  }
26090
- var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
26727
+ 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) => {
26091
26728
  if (Object.keys(value).length > limit) {
26092
26729
  context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
26093
26730
  }
@@ -26109,6 +26746,8 @@ var init_schema2 = __esm(() => {
26109
26746
  };
26110
26747
  key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
26111
26748
  identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
26749
+ digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
26750
+ idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
26112
26751
  uuid2 = exports_external.string().uuid();
26113
26752
  scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
26114
26753
  comment = exports_external.object({
@@ -26146,7 +26785,9 @@ var init_schema2 = __esm(() => {
26146
26785
  schema = exports_external.object({
26147
26786
  version: exports_external.literal(1),
26148
26787
  operation_id: identifier,
26149
- idempotency_key: identifier,
26788
+ step_id: identifier,
26789
+ idempotency_key: idempotencyKey,
26790
+ precondition_digest: digest,
26150
26791
  project_id: uuid2,
26151
26792
  task_list_id: uuid2.optional(),
26152
26793
  if_binding_version: exports_external.number().int().min(0).optional(),
@@ -26162,7 +26803,10 @@ var init_schema2 = __esm(() => {
26162
26803
  }).strict();
26163
26804
  compensationSchema = exports_external.object({
26164
26805
  receipt_id: uuid2,
26165
- idempotency_key: identifier,
26806
+ operation_id: identifier,
26807
+ step_id: identifier,
26808
+ idempotency_key: idempotencyKey,
26809
+ precondition_digest: digest,
26166
26810
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
26167
26811
  }).strict();
26168
26812
  bindingLookupSchema = exports_external.object({
@@ -26180,6 +26824,7 @@ function taskManifestPlanSlug(manifest, planId) {
26180
26824
  const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
26181
26825
  return `${base}-${planId}`;
26182
26826
  }
26827
+ var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
26183
26828
  var init_plan_slug = () => {};
26184
26829
 
26185
26830
  // src/task-manifest/backend.ts
@@ -26193,11 +26838,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
26193
26838
  const row = rows[0];
26194
26839
  const bindingVersion = Number(row.binding_version);
26195
26840
  const state = row.state;
26196
- 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") {
26841
+ 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") {
26197
26842
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
26198
26843
  }
26199
26844
  return {
26200
26845
  plan_id: planId,
26846
+ operation_id: row.binding_operation_id,
26847
+ step_id: row.binding_step_id,
26201
26848
  apply_receipt_id: row.apply_receipt_id,
26202
26849
  binding_version: bindingVersion,
26203
26850
  state
@@ -26250,9 +26897,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26250
26897
  schema_version integer NOT NULL CHECK(schema_version = 1),
26251
26898
  kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
26252
26899
  operation_id text NOT NULL,
26900
+ step_id text NOT NULL,
26253
26901
  idempotency_key text NOT NULL,
26254
26902
  request_digest text NOT NULL,
26903
+ precondition_digest text NOT NULL,
26255
26904
  result_digest text NOT NULL,
26905
+ slug_provenance text,
26906
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
26907
+ reason text,
26908
+ duplicate_of_receipt_id text,
26256
26909
  binding_version integer NOT NULL,
26257
26910
  apply_receipt_id text,
26258
26911
  manifest_json jsonb,
@@ -26264,12 +26917,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26264
26917
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
26265
26918
  `ALTER TABLE todos_task_manifest_receipts
26266
26919
  ALTER COLUMN tenant_id DROP DEFAULT`,
26920
+ `ALTER TABLE todos_task_manifest_receipts
26921
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
26922
+ `ALTER TABLE todos_task_manifest_receipts
26923
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
26924
+ `ALTER TABLE todos_task_manifest_receipts
26925
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
26926
+ `ALTER TABLE todos_task_manifest_receipts
26927
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
26928
+ `ALTER TABLE todos_task_manifest_receipts
26929
+ ADD COLUMN IF NOT EXISTS reason text`,
26930
+ `ALTER TABLE todos_task_manifest_receipts
26931
+ ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
26267
26932
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
26268
26933
  operation_id text PRIMARY KEY,
26269
26934
  tenant_id text NOT NULL,
26935
+ step_id text NOT NULL,
26270
26936
  idempotency_key text NOT NULL UNIQUE,
26271
26937
  request_digest text NOT NULL,
26938
+ precondition_digest text NOT NULL,
26272
26939
  result_digest text NOT NULL,
26940
+ slug_provenance text,
26941
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
26273
26942
  apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
26274
26943
  manifest_json jsonb NOT NULL,
26275
26944
  result_json jsonb NOT NULL,
@@ -26283,6 +26952,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26283
26952
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
26284
26953
  `ALTER TABLE todos_task_manifest_bindings
26285
26954
  ALTER COLUMN tenant_id DROP DEFAULT`,
26955
+ `ALTER TABLE todos_task_manifest_bindings
26956
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
26957
+ `ALTER TABLE todos_task_manifest_bindings
26958
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
26959
+ `ALTER TABLE todos_task_manifest_bindings
26960
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
26961
+ `ALTER TABLE todos_task_manifest_bindings
26962
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
26286
26963
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
26287
26964
  id text PRIMARY KEY,
26288
26965
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -26294,10 +26971,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26294
26971
  created_at timestamptz NOT NULL,
26295
26972
  delivered_at timestamptz
26296
26973
  )`,
26974
+ `CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
26975
+ receipt_id text PRIMARY KEY,
26976
+ tenant_id text NOT NULL,
26977
+ authority text NOT NULL CHECK(authority = 'todos'),
26978
+ route text NOT NULL,
26979
+ schema_version integer NOT NULL CHECK(schema_version = 1),
26980
+ kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
26981
+ operation_id text NOT NULL,
26982
+ step_id text NOT NULL,
26983
+ idempotency_key text NOT NULL,
26984
+ request_digest text NOT NULL,
26985
+ precondition_digest text NOT NULL,
26986
+ result_digest text NOT NULL,
26987
+ outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
26988
+ reason text NOT NULL,
26989
+ binding_version integer NOT NULL,
26990
+ apply_receipt_id text,
26991
+ manifest_json jsonb,
26992
+ result_json jsonb NOT NULL,
26993
+ created_at timestamptz NOT NULL
26994
+ )`,
26297
26995
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
26298
26996
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
26299
26997
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
26300
26998
  ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
26999
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
27000
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
27001
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
27002
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
27003
+ `CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
27004
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
26301
27005
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
26302
27006
  ON todos_task_manifest_bindings(
26303
27007
  tenant_id,
@@ -26310,6 +27014,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26310
27014
  `DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
26311
27015
  `CREATE TRIGGER todos_task_manifest_receipts_immutable
26312
27016
  BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
27017
+ FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
27018
+ `DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
27019
+ `CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
27020
+ BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
26313
27021
  FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
26314
27022
  ];
26315
27023
  }
@@ -26334,6 +27042,39 @@ function safeIdentifier2(value, field) {
26334
27042
  function parseJson(value) {
26335
27043
  return typeof value === "string" ? JSON.parse(value) : value;
26336
27044
  }
27045
+ function parseApplyResult(value, duplicate) {
27046
+ const parsed = parseJson(value);
27047
+ return {
27048
+ ...parsed,
27049
+ duplicate,
27050
+ receipt: {
27051
+ ...parsed.receipt,
27052
+ step_id: parsed.receipt.step_id ?? "legacy-apply",
27053
+ precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
27054
+ outcome: parsed.receipt.outcome ?? "accepted",
27055
+ reason: parsed.receipt.reason ?? null,
27056
+ duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
27057
+ }
27058
+ };
27059
+ }
27060
+ function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
27061
+ if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
27062
+ const expected = taskManifestPlanSlug(manifest, planId);
27063
+ if (slug !== expected) {
27064
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
27065
+ }
27066
+ return expected;
27067
+ }
27068
+ if (provenance !== null && provenance !== undefined) {
27069
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
27070
+ }
27071
+ if (slug === null || slug === undefined)
27072
+ return null;
27073
+ if (slug !== null && slug !== undefined) {
27074
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
27075
+ }
27076
+ return null;
27077
+ }
26337
27078
  function timestamp2(value) {
26338
27079
  return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
26339
27080
  }
@@ -26341,6 +27082,41 @@ function fault(faults, point) {
26341
27082
  if (faults.points.has(point))
26342
27083
  throw new Error(`Injected task-manifest fault at ${point}`);
26343
27084
  }
27085
+ function terminalApplyResult(input, reason) {
27086
+ const receipt = {
27087
+ receipt_id: input.terminal_receipt_id,
27088
+ authority: "todos",
27089
+ route: "todos.task-manifest.v1",
27090
+ schema_version: 1,
27091
+ kind: "apply",
27092
+ operation_id: input.manifest.operation_id,
27093
+ step_id: input.manifest.step_id,
27094
+ idempotency_key: input.manifest.idempotency_key,
27095
+ request_digest: input.request_digest,
27096
+ precondition_digest: input.manifest.precondition_digest,
27097
+ result_digest: canonicalDigest({
27098
+ outcome: "terminal_nonacceptance",
27099
+ reason,
27100
+ operation_id: input.manifest.operation_id,
27101
+ step_id: input.manifest.step_id,
27102
+ request_digest: input.request_digest
27103
+ }),
27104
+ outcome: "terminal_nonacceptance",
27105
+ reason,
27106
+ duplicate_of_receipt_id: null,
27107
+ binding_version: 0,
27108
+ apply_receipt_id: null,
27109
+ created_at: input.now
27110
+ };
27111
+ return {
27112
+ duplicate: false,
27113
+ receipt,
27114
+ graph: input.graph,
27115
+ readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
27116
+ outbox_ids: [],
27117
+ result_digest: receipt.result_digest
27118
+ };
27119
+ }
26344
27120
  function receiptFromRow3(row) {
26345
27121
  return {
26346
27122
  receipt_id: String(row["receipt_id"]),
@@ -26349,9 +27125,14 @@ function receiptFromRow3(row) {
26349
27125
  schema_version: 1,
26350
27126
  kind: row["kind"],
26351
27127
  operation_id: String(row["operation_id"]),
27128
+ step_id: String(row["step_id"] ?? "legacy-apply"),
26352
27129
  idempotency_key: String(row["idempotency_key"]),
26353
27130
  request_digest: String(row["request_digest"]),
27131
+ precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
26354
27132
  result_digest: String(row["result_digest"]),
27133
+ outcome: row["outcome"] ?? "accepted",
27134
+ reason: row["reason"] == null ? null : row["reason"],
27135
+ duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
26355
27136
  binding_version: Number(row["binding_version"]),
26356
27137
  apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
26357
27138
  created_at: timestamp2(row["created_at"])
@@ -26469,46 +27250,89 @@ class PostgresTodosTaskManifestBackend {
26469
27250
  now3
26470
27251
  ]);
26471
27252
  }
27253
+ async persistTerminal(tx, input, reason) {
27254
+ const result = terminalApplyResult(input, reason);
27255
+ const resultJson = canonicalJson(result);
27256
+ await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
27257
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
27258
+ idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
27259
+ binding_version, apply_receipt_id, manifest_json, result_json, created_at
27260
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
27261
+ 'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
27262
+ ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
27263
+ result.receipt.receipt_id,
27264
+ this.tenantId,
27265
+ input.manifest.operation_id,
27266
+ input.manifest.step_id,
27267
+ input.manifest.idempotency_key,
27268
+ input.request_digest,
27269
+ input.manifest.precondition_digest,
27270
+ result.receipt.result_digest,
27271
+ reason,
27272
+ canonicalJson(input.manifest),
27273
+ resultJson,
27274
+ input.now
27275
+ ]);
27276
+ const stored = await tx.query(`SELECT receipt_id, result_json
27277
+ FROM todos_task_manifest_terminal_receipts
27278
+ WHERE tenant_id = $1 AND kind = 'apply'
27279
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
27280
+ ORDER BY created_at ASC, receipt_id ASC
27281
+ LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
27282
+ return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
27283
+ }
26472
27284
  async apply(input, faults) {
26473
27285
  await this.ensureSchema();
26474
27286
  return this.client.transaction(async (tx) => {
26475
27287
  const { manifest } = input;
26476
27288
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
26477
27289
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
27290
+ const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
27291
+ WHERE tenant_id = $1
27292
+ AND kind = 'apply'
27293
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
27294
+ ORDER BY created_at ASC, receipt_id ASC
27295
+ LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
27296
+ if (terminal.rows[0]) {
27297
+ return parseApplyResult(terminal.rows[0]["result_json"], true);
27298
+ }
26478
27299
  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]);
26479
27300
  if (existing.rows[0]) {
26480
27301
  const binding = existing.rows[0];
26481
- if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
26482
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
27302
+ 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) {
27303
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
26483
27304
  }
26484
27305
  if (binding["state"] !== "applied") {
26485
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
27306
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
26486
27307
  }
26487
- return { ...parseJson(binding["result_json"]), duplicate: true };
27308
+ return parseApplyResult(binding["result_json"], true);
26488
27309
  }
26489
27310
  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]);
26490
27311
  if (reused.rows[0])
26491
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
27312
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
27313
+ if (manifest.idempotency_key !== input.expected_idempotency_key) {
27314
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
27315
+ }
26492
27316
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
26493
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "New manifest binding version must be 0");
27317
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
26494
27318
  }
26495
27319
  const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
26496
27320
  WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
26497
27321
  if (!project.rows[0])
26498
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
27322
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
26499
27323
  if (manifest.task_list_id) {
26500
27324
  const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
26501
27325
  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]);
26502
27326
  const payload = taskList.rows[0] ? parseJson(taskList.rows[0]["payload"]) : null;
26503
27327
  if (!payload || payload["project_id"] !== manifest.project_id) {
26504
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Task list does not belong to the project");
27328
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
26505
27329
  }
26506
27330
  }
26507
27331
  const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
26508
27332
  const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
26509
27333
  WHERE service = $1 AND object_id IN (${placeholders(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
26510
27334
  if (conflict.rows[0])
26511
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "A deterministic graph id already exists");
27335
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
26512
27336
  await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
26513
27337
  fault(faults, "after_plan_write");
26514
27338
  for (const task2 of manifest.tasks) {
@@ -26578,9 +27402,14 @@ class PostgresTodosTaskManifestBackend {
26578
27402
  schema_version: 1,
26579
27403
  kind: "apply",
26580
27404
  operation_id: manifest.operation_id,
27405
+ step_id: manifest.step_id,
26581
27406
  idempotency_key: manifest.idempotency_key,
26582
27407
  request_digest: input.request_digest,
27408
+ precondition_digest: manifest.precondition_digest,
26583
27409
  result_digest: input.result_digest,
27410
+ outcome: "accepted",
27411
+ reason: null,
27412
+ duplicate_of_receipt_id: null,
26584
27413
  binding_version: 1,
26585
27414
  apply_receipt_id: null,
26586
27415
  created_at: input.now
@@ -26597,14 +27426,18 @@ class PostgresTodosTaskManifestBackend {
26597
27426
  const resultJson = canonicalJson(result);
26598
27427
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
26599
27428
  receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
26600
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
26601
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
27429
+ step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
27430
+ reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
27431
+ ) 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)`, [
26602
27432
  input.receipt_id,
26603
27433
  this.tenantId,
26604
27434
  manifest.operation_id,
26605
27435
  manifest.idempotency_key,
27436
+ manifest.step_id,
26606
27437
  input.request_digest,
27438
+ manifest.precondition_digest,
26607
27439
  input.result_digest,
27440
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
26608
27441
  manifestJson,
26609
27442
  resultJson,
26610
27443
  input.now
@@ -26623,14 +27456,18 @@ class PostgresTodosTaskManifestBackend {
26623
27456
  }
26624
27457
  fault(faults, "after_outbox_write");
26625
27458
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
26626
- operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
26627
- manifest_json, result_json, state, version, created_at, updated_at
26628
- ) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
27459
+ operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
27460
+ result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
27461
+ state, version, created_at, updated_at
27462
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
26629
27463
  manifest.operation_id,
26630
27464
  this.tenantId,
27465
+ manifest.step_id,
26631
27466
  manifest.idempotency_key,
26632
27467
  input.request_digest,
27468
+ manifest.precondition_digest,
26633
27469
  input.result_digest,
27470
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
26634
27471
  input.receipt_id,
26635
27472
  manifestJson,
26636
27473
  resultJson,
@@ -26643,9 +27480,12 @@ class PostgresTodosTaskManifestBackend {
26643
27480
  async readExact(receiptId2) {
26644
27481
  await this.ensureSchema();
26645
27482
  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]);
26646
- if (!result.rows[0])
27483
+ if (result.rows[0])
27484
+ return parseApplyResult(result.rows[0]["result_json"], false);
27485
+ 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]);
27486
+ if (!terminal.rows[0])
26647
27487
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
26648
- return { ...parseJson(result.rows[0]["result_json"]), duplicate: false };
27488
+ return parseApplyResult(terminal.rows[0]["result_json"], false);
26649
27489
  }
26650
27490
  async lookupBindingByPlanId(planId) {
26651
27491
  await this.ensureSchema();
@@ -26656,6 +27496,7 @@ class PostgresTodosTaskManifestBackend {
26656
27496
  b.version AS binding_version,
26657
27497
  b.tenant_id AS binding_tenant_id,
26658
27498
  b.operation_id AS binding_operation_id,
27499
+ b.step_id AS binding_step_id,
26659
27500
  b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
26660
27501
  r.tenant_id AS receipt_tenant_id,
26661
27502
  r.authority AS receipt_authority,
@@ -26663,6 +27504,7 @@ class PostgresTodosTaskManifestBackend {
26663
27504
  r.schema_version AS receipt_schema_version,
26664
27505
  r.kind AS receipt_kind,
26665
27506
  r.operation_id AS receipt_operation_id,
27507
+ r.step_id AS receipt_step_id,
26666
27508
  r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
26667
27509
  FROM todos_task_manifest_bindings b
26668
27510
  LEFT JOIN todos_task_manifest_receipts r
@@ -26749,6 +27591,10 @@ class PostgresTodosTaskManifestBackend {
26749
27591
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
26750
27592
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
26751
27593
  }
27594
+ const appliedReceipt = receiptFromRow3(applyRow);
27595
+ 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"]) {
27596
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
27597
+ }
26752
27598
  if (binding["state"] !== "applied")
26753
27599
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
26754
27600
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -26763,12 +27609,18 @@ class PostgresTodosTaskManifestBackend {
26763
27609
  LIMIT 1`, [this.tenantId, input.receipt_id]);
26764
27610
  if (delivered.rows[0])
26765
27611
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
26766
- const applyResult = parseJson(applyRow["result_json"]);
27612
+ const applyResult = parseApplyResult(applyRow["result_json"], false);
26767
27613
  const manifest = parseJson(applyRow["manifest_json"]);
27614
+ const manifestRecord = manifest;
27615
+ const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
26768
27616
  const expectedEffects = [
26769
27617
  {
26770
27618
  topic: "todos.task-manifest.applied",
26771
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
27619
+ payload: {
27620
+ operation_id: manifest.operation_id,
27621
+ ...applyStepId ? { step_id: applyStepId } : {},
27622
+ project_id: manifest.project_id
27623
+ }
26772
27624
  },
26773
27625
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
26774
27626
  ];
@@ -26816,9 +27668,15 @@ class PostgresTodosTaskManifestBackend {
26816
27668
  }
26817
27669
  const appliedAt = receiptFromRow3(applyRow).created_at;
26818
27670
  const expectedPayloads = new Map;
27671
+ const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
27672
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
27673
+ LIMIT 1`, [this.service, applyResult.graph.plan_id]);
27674
+ const actualPlan = planRow.rows[0] ? parseJson(planRow.rows[0]["payload"]) : null;
27675
+ const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
27676
+ planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
26819
27677
  expectedPayloads.set(applyResult.graph.plan_id, {
26820
27678
  type: "plans",
26821
- payload: canonicalJson(planPayload({ manifest, graph: applyResult.graph, now: appliedAt }))
27679
+ payload: canonicalJson(planExpected)
26822
27680
  });
26823
27681
  for (const task2 of manifest.tasks)
26824
27682
  expectedPayloads.set(applyResult.graph.task_ids[task2.key], {
@@ -26918,14 +27776,17 @@ class PostgresTodosTaskManifestBackend {
26918
27776
  const readback = await this.readback(tx, applyResult.graph);
26919
27777
  const result = { duplicate: false, receipt, absent: true, readback };
26920
27778
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
26921
- receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
26922
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
26923
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
27779
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
27780
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
27781
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
27782
+ ) 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)`, [
26924
27783
  compensationReceiptId,
26925
27784
  this.tenantId,
26926
27785
  receipt.operation_id,
27786
+ receipt.step_id,
26927
27787
  input.idempotency_key,
26928
27788
  requestDigest,
27789
+ input.precondition_digest,
26929
27790
  receipt.result_digest,
26930
27791
  receipt.binding_version,
26931
27792
  input.receipt_id,
@@ -26982,36 +27843,83 @@ function resolveTenantId(value) {
26982
27843
  }
26983
27844
  return tenantId;
26984
27845
  }
27846
+ function taskManifestRequestDigest(manifest) {
27847
+ const { idempotency_key: _idempotencyKey, ...request } = manifest;
27848
+ return canonicalDigest(request);
27849
+ }
27850
+ function taskManifestCompensationRequestDigest(request) {
27851
+ return canonicalDigest(request);
27852
+ }
27853
+ function deriveTodosTaskManifestApplyPreconditionDigest(input) {
27854
+ return canonicalDigest({
27855
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27856
+ direction: "apply",
27857
+ operation_id: input.operation_id,
27858
+ step_id: input.step_id,
27859
+ project_id: input.project_id,
27860
+ task_list_id: input.task_list_id ?? null,
27861
+ expected_binding_version: input.if_binding_version ?? 0
27862
+ });
27863
+ }
27864
+ function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
27865
+ return canonicalDigest({
27866
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27867
+ direction: "compensate",
27868
+ operation_id: input.operation_id,
27869
+ step_id: input.step_id,
27870
+ apply_receipt_id: input.receipt_id,
27871
+ expected_binding_version: input.if_binding_version
27872
+ });
27873
+ }
27874
+ function deriveTodosTaskManifestIdempotencyKey(input) {
27875
+ return `tmk_${canonicalDigest({
27876
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27877
+ ...input
27878
+ }).slice(0, 48)}`;
27879
+ }
26985
27880
  function normalize(input, now3) {
26986
27881
  const parsed = parseTodosTaskManifest(input);
26987
27882
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
26988
27883
  if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
26989
27884
  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 });
26990
27885
  }
27886
+ const { idempotency_key: _idempotencyKey, ...request } = parsed;
27887
+ const request_digest = taskManifestRequestDigest(request);
26991
27888
  const manifest = sanitizeManifest(parsed);
27889
+ const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
27890
+ if (manifest.precondition_digest !== expectedPreconditionDigest) {
27891
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
27892
+ }
27893
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
27894
+ operation_id: manifest.operation_id,
27895
+ step_id: manifest.step_id,
27896
+ direction: "apply",
27897
+ target_selector: manifest.project_id,
27898
+ request_digest,
27899
+ precondition_digest: manifest.precondition_digest
27900
+ });
26992
27901
  const task_ids = Object.fromEntries(manifest.tasks.map((task2) => [
26993
27902
  task2.key,
26994
- deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task2.key)
27903
+ deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task2.key)
26995
27904
  ]));
26996
27905
  const graph = {
26997
- plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
27906
+ plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
26998
27907
  task_ids,
26999
- comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task2.key, String(index)))),
27000
- verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task2.key, String(index)))),
27908
+ comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "comment", task2.key, String(index)))),
27909
+ verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "verification", task2.key, String(index)))),
27001
27910
  dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
27002
27911
  };
27003
- const request_digest = canonicalDigest(parsed);
27004
27912
  const effectInputs = [
27005
27913
  {
27006
27914
  topic: "todos.task-manifest.applied",
27007
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
27915
+ payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
27008
27916
  },
27009
27917
  ...manifest.effects ?? []
27010
27918
  ];
27011
27919
  const outbox = effectInputs.map((effect2, index) => {
27012
27920
  const payload = { ...effect2.payload };
27013
27921
  return {
27014
- id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
27922
+ id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
27015
27923
  topic: effect2.topic,
27016
27924
  payload,
27017
27925
  digest: canonicalDigest({ topic: effect2.topic, payload })
@@ -27021,11 +27929,14 @@ function normalize(input, now3) {
27021
27929
  return {
27022
27930
  manifest,
27023
27931
  request_digest,
27932
+ expected_idempotency_key: expectedIdempotencyKey,
27024
27933
  result_digest,
27025
- receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
27934
+ receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
27935
+ terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
27026
27936
  graph,
27027
27937
  outbox,
27028
- now: now3
27938
+ now: now3,
27939
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
27029
27940
  };
27030
27941
  }
27031
27942
  function sanitizeManifest(manifest) {
@@ -27083,6 +27994,10 @@ class PackageOwnedTodosTaskManifestAuthority {
27083
27994
  tenant_id: this.tenantId,
27084
27995
  backend: this.backend.kind,
27085
27996
  deterministic_ids: true,
27997
+ operation_step_identity: true,
27998
+ deterministic_idempotency_keys: true,
27999
+ terminal_nonacceptance_receipts: true,
28000
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
27086
28001
  immutable_receipts: true,
27087
28002
  transactional_outbox: true,
27088
28003
  idempotent_outbox_delivery: true,
@@ -27112,7 +28027,11 @@ class PackageOwnedTodosTaskManifestAuthority {
27112
28027
  async apply(input) {
27113
28028
  const normalized = normalize(input, this.now());
27114
28029
  const faults = await this.prepareFaults();
27115
- return this.bounded(await this.backend.apply(normalized, faults));
28030
+ const result = this.bounded(await this.backend.apply(normalized, faults));
28031
+ if (result.receipt.outcome === "terminal_nonacceptance") {
28032
+ throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
28033
+ }
28034
+ return result;
27116
28035
  }
27117
28036
  readExact(receiptId2) {
27118
28037
  if (!receiptId2 || receiptId2.length > 200) {
@@ -27145,18 +28064,48 @@ class PackageOwnedTodosTaskManifestAuthority {
27145
28064
  async compensate(input) {
27146
28065
  const request = parseTodosTaskManifestCompensation(input);
27147
28066
  const applied = await this.backend.readExact(request.receipt_id);
27148
- const requestDigest = canonicalDigest(request);
27149
- const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", applied.receipt.operation_id, request.idempotency_key, requestDigest);
28067
+ if (applied.receipt.outcome !== "accepted") {
28068
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
28069
+ }
28070
+ if (request.operation_id !== applied.receipt.operation_id) {
28071
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
28072
+ }
28073
+ if (request.step_id === applied.receipt.step_id) {
28074
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
28075
+ }
28076
+ const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
28077
+ if (request.precondition_digest !== expectedPreconditionDigest) {
28078
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
28079
+ }
28080
+ const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
28081
+ const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
28082
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
28083
+ operation_id: request.operation_id,
28084
+ step_id: request.step_id,
28085
+ direction: "compensate",
28086
+ target_selector: request.receipt_id,
28087
+ request_digest: requestDigest,
28088
+ precondition_digest: request.precondition_digest
28089
+ });
28090
+ if (request.idempotency_key !== expectedIdempotencyKey) {
28091
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
28092
+ }
28093
+ const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
27150
28094
  const receipt = {
27151
28095
  receipt_id: compensationReceiptId,
27152
28096
  authority: "todos",
27153
28097
  route: TODOS_TASK_MANIFEST_ROUTE,
27154
28098
  schema_version: 1,
27155
28099
  kind: "compensate",
27156
- operation_id: applied.receipt.operation_id,
28100
+ operation_id: request.operation_id,
28101
+ step_id: request.step_id,
27157
28102
  idempotency_key: request.idempotency_key,
27158
28103
  request_digest: requestDigest,
28104
+ precondition_digest: request.precondition_digest,
27159
28105
  result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
28106
+ outcome: "accepted",
28107
+ reason: null,
28108
+ duplicate_of_receipt_id: null,
27160
28109
  binding_version: request.if_binding_version + 1,
27161
28110
  apply_receipt_id: applied.receipt.receipt_id,
27162
28111
  created_at: this.now()
@@ -29661,9 +30610,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29661
30610
  TaskManifestBounds: taskManifestBoundsSchema,
29662
30611
  TaskManifestCapability: taskManifestCapabilitySchema,
29663
30612
  TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
30613
+ TaskManifest: taskManifestSchema,
30614
+ TaskManifestReceipt: taskManifestReceiptSchema,
30615
+ TaskManifestApplyResult: taskManifestApplyResultSchema,
30616
+ TaskManifestApplyResponse: taskManifestApplyResponseSchema,
30617
+ TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
30618
+ TaskManifestCompensationResult: taskManifestCompensationResultSchema,
30619
+ TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
30620
+ TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
29664
30621
  TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
29665
30622
  TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
29666
30623
  TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
30624
+ ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
30625
+ ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
30626
+ ProjectRegistrationRequest: projectRegistrationRequestSchema,
30627
+ ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
30628
+ PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
30629
+ ProjectResource: projectResourceSchema,
30630
+ ProjectResourcePage: projectResourcePageSchema,
29667
30631
  TaskList: taskListSchema,
29668
30632
  ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
29669
30633
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
@@ -29706,6 +30670,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29706
30670
  priority: { type: "string", enum: [...TASK_PRIORITIES] },
29707
30671
  assigned_to: { type: "string" },
29708
30672
  project_id: { type: "string", nullable: true },
30673
+ parent_id: { type: "string", nullable: true },
29709
30674
  plan_id: { type: "string", nullable: true },
29710
30675
  task_list_id: { type: "string", nullable: true },
29711
30676
  version: { type: "number" }
@@ -30660,6 +31625,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30660
31625
  },
30661
31626
  security: [{ apiKey: [] }],
30662
31627
  paths: {
31628
+ "/v1/project-registration/capability": {
31629
+ get: {
31630
+ operationId: "getProjectRegistrationCapability",
31631
+ summary: "Read the live package-owned Projects to Todos registration capability",
31632
+ responses: {
31633
+ "200": {
31634
+ content: {
31635
+ "application/json": {
31636
+ schema: {
31637
+ type: "object",
31638
+ additionalProperties: false,
31639
+ required: ["capability"],
31640
+ properties: {
31641
+ capability: {
31642
+ $ref: "#/components/schemas/ProjectRegistrationCapability"
31643
+ }
31644
+ }
31645
+ }
31646
+ }
31647
+ }
31648
+ }
31649
+ }
31650
+ }
31651
+ },
31652
+ "/v1/project-registration/resources": {
31653
+ get: {
31654
+ operationId: "listProjectRegistrationResources",
31655
+ summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
31656
+ parameters: [
31657
+ {
31658
+ name: "source_project_id",
31659
+ in: "query",
31660
+ required: true,
31661
+ schema: { type: "string" }
31662
+ },
31663
+ {
31664
+ name: "include_anchors",
31665
+ in: "query",
31666
+ schema: { type: "boolean", default: false }
31667
+ },
31668
+ {
31669
+ name: "limit",
31670
+ in: "query",
31671
+ schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
31672
+ },
31673
+ {
31674
+ name: "cursor",
31675
+ in: "query",
31676
+ schema: { type: "string" }
31677
+ }
31678
+ ],
31679
+ responses: {
31680
+ "200": {
31681
+ content: {
31682
+ "application/json": {
31683
+ schema: {
31684
+ type: "object",
31685
+ additionalProperties: false,
31686
+ required: ["page"],
31687
+ properties: {
31688
+ page: { $ref: "#/components/schemas/ProjectResourcePage" }
31689
+ }
31690
+ }
31691
+ }
31692
+ }
31693
+ },
31694
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31695
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31696
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31697
+ }
31698
+ }
31699
+ },
31700
+ "/v1/project-registration/create": {
31701
+ post: {
31702
+ operationId: "createProjectRegistrationResource",
31703
+ summary: "Create or deterministically bind one Projects to Todos resource",
31704
+ requestBody: {
31705
+ required: true,
31706
+ content: {
31707
+ "application/json": {
31708
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31709
+ }
31710
+ }
31711
+ },
31712
+ responses: {
31713
+ "201": {
31714
+ content: {
31715
+ "application/json": {
31716
+ schema: {
31717
+ type: "object",
31718
+ additionalProperties: false,
31719
+ required: ["receipt"],
31720
+ properties: {
31721
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
31722
+ }
31723
+ }
31724
+ }
31725
+ }
31726
+ },
31727
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31728
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31729
+ }
31730
+ }
31731
+ },
31732
+ "/v1/project-registration/read-exact": {
31733
+ post: {
31734
+ operationId: "readExactProjectRegistrationResource",
31735
+ summary: "Read one registered project or task list by exact full UUID",
31736
+ requestBody: {
31737
+ required: true,
31738
+ content: {
31739
+ "application/json": {
31740
+ schema: {
31741
+ type: "object",
31742
+ additionalProperties: false,
31743
+ required: [
31744
+ "resource_kind",
31745
+ "target_id",
31746
+ "response_byte_limit",
31747
+ "time_budget_ms"
31748
+ ],
31749
+ properties: {
31750
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
31751
+ target_id: { type: "string", format: "uuid" },
31752
+ ...projectRegistrationBoundsProperties
31753
+ }
31754
+ }
31755
+ }
31756
+ }
31757
+ },
31758
+ responses: {
31759
+ "200": {
31760
+ content: {
31761
+ "application/json": {
31762
+ schema: {
31763
+ type: "object",
31764
+ additionalProperties: false,
31765
+ required: ["record"],
31766
+ properties: {
31767
+ record: {
31768
+ type: "object",
31769
+ additionalProperties: false,
31770
+ required: ["target_id", "revision", "digest"],
31771
+ properties: {
31772
+ target_id: { type: "string", format: "uuid" },
31773
+ revision: { type: "string" },
31774
+ digest: { type: "string" }
31775
+ }
31776
+ }
31777
+ }
31778
+ }
31779
+ }
31780
+ }
31781
+ },
31782
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31783
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31784
+ }
31785
+ }
31786
+ },
31787
+ "/v1/project-registration/receipts/lookup": {
31788
+ post: {
31789
+ operationId: "lookupProjectRegistrationReceipt",
31790
+ summary: "Recover one exact immutable terminal registration receipt",
31791
+ requestBody: {
31792
+ required: true,
31793
+ content: {
31794
+ "application/json": {
31795
+ schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
31796
+ }
31797
+ }
31798
+ },
31799
+ responses: {
31800
+ "200": {
31801
+ content: {
31802
+ "application/json": {
31803
+ schema: {
31804
+ type: "object",
31805
+ additionalProperties: false,
31806
+ required: ["receipt", "response_control"],
31807
+ properties: {
31808
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
31809
+ response_control: {
31810
+ type: "object",
31811
+ additionalProperties: false,
31812
+ required: [
31813
+ "response_byte_limit",
31814
+ "time_budget_ms",
31815
+ "response_bytes",
31816
+ "elapsed_ms",
31817
+ "complete",
31818
+ "truncated"
31819
+ ],
31820
+ properties: {
31821
+ ...projectRegistrationBoundsProperties,
31822
+ response_bytes: { type: "integer", minimum: 0 },
31823
+ elapsed_ms: { type: "integer", minimum: 0 },
31824
+ complete: { type: "boolean", enum: [true] },
31825
+ truncated: { type: "boolean", enum: [false] }
31826
+ }
31827
+ }
31828
+ }
31829
+ }
31830
+ }
31831
+ }
31832
+ },
31833
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31834
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31835
+ }
31836
+ }
31837
+ },
31838
+ "/v1/project-registration/validate-prior-adoption": {
31839
+ post: {
31840
+ operationId: "validatePriorRegistrationAdoption",
31841
+ summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
31842
+ requestBody: {
31843
+ required: true,
31844
+ content: {
31845
+ "application/json": {
31846
+ schema: {
31847
+ type: "object",
31848
+ additionalProperties: false,
31849
+ required: ["source_request", "source_receipt", "current_record"],
31850
+ properties: {
31851
+ source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
31852
+ source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
31853
+ current_record: {
31854
+ oneOf: [
31855
+ { $ref: "#/components/schemas/Project" },
31856
+ { $ref: "#/components/schemas/TaskList" }
31857
+ ]
31858
+ }
31859
+ }
31860
+ }
31861
+ }
31862
+ }
31863
+ },
31864
+ responses: {
31865
+ "200": {
31866
+ content: {
31867
+ "application/json": {
31868
+ schema: {
31869
+ type: "object",
31870
+ additionalProperties: false,
31871
+ required: ["validation"],
31872
+ properties: {
31873
+ validation: {
31874
+ $ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
31875
+ }
31876
+ }
31877
+ }
31878
+ }
31879
+ }
31880
+ },
31881
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31882
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31883
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31884
+ }
31885
+ }
31886
+ },
31887
+ "/v1/project-registration/compensate": {
31888
+ post: {
31889
+ operationId: "compensateProjectRegistrationResource",
31890
+ summary: "Conditionally remove an unchanged receipt-owned registration resource",
31891
+ requestBody: {
31892
+ required: true,
31893
+ content: {
31894
+ "application/json": {
31895
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31896
+ }
31897
+ }
31898
+ },
31899
+ responses: {
31900
+ "201": {
31901
+ content: {
31902
+ "application/json": {
31903
+ schema: {
31904
+ type: "object",
31905
+ additionalProperties: false,
31906
+ required: ["receipt"],
31907
+ properties: {
31908
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
31909
+ }
31910
+ }
31911
+ }
31912
+ }
31913
+ },
31914
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31915
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31916
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31917
+ }
31918
+ }
31919
+ },
31920
+ "/v1/project-registration/verify-inverse": {
31921
+ post: {
31922
+ operationId: "verifyInverseProjectRegistrationResource",
31923
+ summary: "Verify exact absence after conditional registration compensation",
31924
+ requestBody: {
31925
+ required: true,
31926
+ content: {
31927
+ "application/json": {
31928
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31929
+ }
31930
+ }
31931
+ },
31932
+ responses: {
31933
+ "200": {
31934
+ content: {
31935
+ "application/json": {
31936
+ schema: {
31937
+ type: "object",
31938
+ additionalProperties: false,
31939
+ required: ["verification"],
31940
+ properties: {
31941
+ verification: {
31942
+ type: "object",
31943
+ additionalProperties: false,
31944
+ required: ["target_id", "accepted_receipt_id", "absent", "digest"],
31945
+ properties: {
31946
+ target_id: { type: "string", format: "uuid" },
31947
+ accepted_receipt_id: { type: "string" },
31948
+ absent: { type: "boolean", enum: [true] },
31949
+ digest: { type: "string" }
31950
+ }
31951
+ }
31952
+ }
31953
+ }
31954
+ }
31955
+ }
31956
+ },
31957
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31958
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31959
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31960
+ }
31961
+ }
31962
+ },
30663
31963
  "/v1/task-manifest/capability": {
30664
31964
  get: {
30665
31965
  operationId: "getTaskManifestCapability",
@@ -30677,6 +31977,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30677
31977
  }
30678
31978
  }
30679
31979
  },
31980
+ "/v1/task-manifest/apply": {
31981
+ post: {
31982
+ operationId: "applyTaskManifest",
31983
+ summary: "Apply one exact task-manifest graph through the Todos authority",
31984
+ requestBody: {
31985
+ required: true,
31986
+ content: {
31987
+ "application/json": {
31988
+ schema: { $ref: "#/components/schemas/TaskManifest" }
31989
+ }
31990
+ }
31991
+ },
31992
+ responses: {
31993
+ "201": {
31994
+ content: {
31995
+ "application/json": {
31996
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
31997
+ }
31998
+ }
31999
+ },
32000
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32001
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32002
+ "503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32003
+ }
32004
+ }
32005
+ },
32006
+ "/v1/task-manifest/read-exact": {
32007
+ post: {
32008
+ operationId: "readExactTaskManifest",
32009
+ summary: "Read one exact immutable task-manifest apply receipt",
32010
+ requestBody: {
32011
+ required: true,
32012
+ content: {
32013
+ "application/json": {
32014
+ schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
32015
+ }
32016
+ }
32017
+ },
32018
+ responses: {
32019
+ "200": {
32020
+ content: {
32021
+ "application/json": {
32022
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
32023
+ }
32024
+ }
32025
+ },
32026
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32027
+ }
32028
+ }
32029
+ },
32030
+ "/v1/task-manifest/compensate": {
32031
+ post: {
32032
+ operationId: "compensateTaskManifest",
32033
+ summary: "Compensate one exact untouched task-manifest graph with CAS protection",
32034
+ requestBody: {
32035
+ required: true,
32036
+ content: {
32037
+ "application/json": {
32038
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
32039
+ }
32040
+ }
32041
+ },
32042
+ responses: {
32043
+ "201": {
32044
+ content: {
32045
+ "application/json": {
32046
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
32047
+ }
32048
+ }
32049
+ },
32050
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32051
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32052
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32053
+ }
32054
+ }
32055
+ },
30680
32056
  "/v1/task-manifest/bindings/lookup": {
30681
32057
  post: {
30682
32058
  operationId: "lookupTaskManifestBinding",
@@ -31537,7 +32913,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
31537
32913
  }
31538
32914
  });
31539
32915
  }
31540
- 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;
32916
+ 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;
31541
32917
  var init_openapi = __esm(() => {
31542
32918
  init_package_version();
31543
32919
  init_types();
@@ -31599,6 +32975,10 @@ var init_openapi = __esm(() => {
31599
32975
  "tenant_id",
31600
32976
  "backend",
31601
32977
  "deterministic_ids",
32978
+ "operation_step_identity",
32979
+ "deterministic_idempotency_keys",
32980
+ "terminal_nonacceptance_receipts",
32981
+ "plan_slug_provenance",
31602
32982
  "immutable_receipts",
31603
32983
  "transactional_outbox",
31604
32984
  "idempotent_outbox_delivery",
@@ -31614,6 +32994,10 @@ var init_openapi = __esm(() => {
31614
32994
  tenant_id: { type: "string", minLength: 1, maxLength: 200 },
31615
32995
  backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
31616
32996
  deterministic_ids: { type: "boolean", enum: [true] },
32997
+ operation_step_identity: { type: "boolean", enum: [true] },
32998
+ deterministic_idempotency_keys: { type: "boolean", enum: [true] },
32999
+ terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
33000
+ plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
31617
33001
  immutable_receipts: { type: "boolean", enum: [true] },
31618
33002
  transactional_outbox: { type: "boolean", enum: [true] },
31619
33003
  idempotent_outbox_delivery: { type: "boolean", enum: [true] },
@@ -31667,6 +33051,8 @@ var init_openapi = __esm(() => {
31667
33051
  "schema_version",
31668
33052
  "tenant_id",
31669
33053
  "plan_id",
33054
+ "operation_id",
33055
+ "step_id",
31670
33056
  "apply_receipt_id",
31671
33057
  "binding_version",
31672
33058
  "state"
@@ -31677,11 +33063,169 @@ var init_openapi = __esm(() => {
31677
33063
  schema_version: { type: "integer", enum: [1] },
31678
33064
  tenant_id: { type: "string" },
31679
33065
  plan_id: { type: "string", format: "uuid" },
33066
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
33067
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
31680
33068
  apply_receipt_id: { type: "string", format: "uuid" },
31681
33069
  binding_version: { type: "integer", minimum: 1 },
31682
33070
  state: { type: "string", enum: ["applied", "compensated"] }
31683
33071
  }
31684
33072
  };
33073
+ taskManifestSchema = {
33074
+ type: "object",
33075
+ additionalProperties: false,
33076
+ required: [
33077
+ "version",
33078
+ "operation_id",
33079
+ "step_id",
33080
+ "idempotency_key",
33081
+ "precondition_digest",
33082
+ "project_id",
33083
+ "plan",
33084
+ "tasks"
33085
+ ],
33086
+ properties: {
33087
+ version: { type: "integer", enum: [1] },
33088
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
33089
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
33090
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
33091
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33092
+ project_id: { type: "string", format: "uuid" },
33093
+ task_list_id: { type: "string", format: "uuid" },
33094
+ if_binding_version: { type: "integer", minimum: 0 },
33095
+ plan: {
33096
+ type: "object",
33097
+ additionalProperties: false,
33098
+ required: ["key", "name"],
33099
+ properties: {
33100
+ key: { type: "string", minLength: 1, maxLength: 200 },
33101
+ name: { type: "string", minLength: 1, maxLength: 200 },
33102
+ description: { type: "string" },
33103
+ status: { type: "string", enum: ["active", "completed", "archived"] }
33104
+ }
33105
+ },
33106
+ tasks: {
33107
+ type: "array",
33108
+ minItems: 1,
33109
+ items: {
33110
+ type: "object",
33111
+ additionalProperties: false,
33112
+ required: ["key", "title"],
33113
+ properties: {
33114
+ key: { type: "string", minLength: 1, maxLength: 200 },
33115
+ title: { type: "string", minLength: 1, maxLength: 200 },
33116
+ description: { type: "string" },
33117
+ status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
33118
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
33119
+ assigned_to: { type: "string" },
33120
+ created_by: { type: "string" },
33121
+ tags: { type: "array", items: { type: "string" } },
33122
+ metadata: { type: "object", additionalProperties: true },
33123
+ comments: { type: "array", items: { type: "object", additionalProperties: true } },
33124
+ verifications: { type: "array", items: { type: "object", additionalProperties: true } }
33125
+ }
33126
+ }
33127
+ },
33128
+ dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
33129
+ effects: { type: "array", items: { type: "object", additionalProperties: true } }
33130
+ }
33131
+ };
33132
+ taskManifestReceiptSchema = {
33133
+ type: "object",
33134
+ additionalProperties: false,
33135
+ required: [
33136
+ "receipt_id",
33137
+ "authority",
33138
+ "route",
33139
+ "schema_version",
33140
+ "kind",
33141
+ "operation_id",
33142
+ "step_id",
33143
+ "idempotency_key",
33144
+ "request_digest",
33145
+ "precondition_digest",
33146
+ "result_digest",
33147
+ "outcome",
33148
+ "reason",
33149
+ "duplicate_of_receipt_id",
33150
+ "binding_version",
33151
+ "apply_receipt_id",
33152
+ "created_at"
33153
+ ],
33154
+ properties: {
33155
+ receipt_id: { type: "string", format: "uuid" },
33156
+ authority: { type: "string", enum: ["todos"] },
33157
+ route: { type: "string", enum: ["todos.task-manifest.v1"] },
33158
+ schema_version: { type: "integer", enum: [1] },
33159
+ kind: { type: "string", enum: ["apply", "compensate"] },
33160
+ operation_id: { type: "string" },
33161
+ step_id: { type: "string" },
33162
+ idempotency_key: { type: "string" },
33163
+ request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33164
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33165
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33166
+ outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
33167
+ reason: { type: "string", nullable: true },
33168
+ duplicate_of_receipt_id: { type: "string", nullable: true },
33169
+ binding_version: { type: "integer", minimum: 0 },
33170
+ apply_receipt_id: { type: "string", nullable: true },
33171
+ created_at: { type: "string", format: "date-time" }
33172
+ }
33173
+ };
33174
+ taskManifestApplyResultSchema = {
33175
+ type: "object",
33176
+ additionalProperties: false,
33177
+ required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
33178
+ properties: {
33179
+ duplicate: { type: "boolean" },
33180
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
33181
+ graph: { type: "object", additionalProperties: true },
33182
+ readback: { type: "object", additionalProperties: true },
33183
+ outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
33184
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
33185
+ }
33186
+ };
33187
+ taskManifestApplyResponseSchema = {
33188
+ type: "object",
33189
+ additionalProperties: false,
33190
+ required: ["result"],
33191
+ properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
33192
+ };
33193
+ taskManifestCompensateRequestSchema = {
33194
+ type: "object",
33195
+ additionalProperties: false,
33196
+ required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
33197
+ properties: {
33198
+ receipt_id: { type: "string", format: "uuid" },
33199
+ operation_id: { type: "string" },
33200
+ step_id: { type: "string" },
33201
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
33202
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33203
+ if_binding_version: { type: "integer", minimum: 1 }
33204
+ }
33205
+ };
33206
+ taskManifestCompensationResultSchema = {
33207
+ type: "object",
33208
+ additionalProperties: false,
33209
+ required: ["duplicate", "receipt", "absent", "readback"],
33210
+ properties: {
33211
+ duplicate: { type: "boolean" },
33212
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
33213
+ absent: { type: "boolean", enum: [true] },
33214
+ readback: { type: "object", additionalProperties: true }
33215
+ }
33216
+ };
33217
+ taskManifestCompensateResponseSchema = {
33218
+ type: "object",
33219
+ additionalProperties: false,
33220
+ required: ["result"],
33221
+ properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
33222
+ };
33223
+ taskManifestReadExactRequestSchema = {
33224
+ type: "object",
33225
+ additionalProperties: false,
33226
+ required: ["receipt_id"],
33227
+ properties: { receipt_id: { type: "string", format: "uuid" } }
33228
+ };
31685
33229
  taskManifestBindingLookupResponseSchema = {
31686
33230
  type: "object",
31687
33231
  additionalProperties: false,
@@ -32031,6 +33575,292 @@ var init_openapi = __esm(() => {
32031
33575
  metadata: { type: "object", additionalProperties: true }
32032
33576
  }
32033
33577
  };
33578
+ projectRegistrationBoundsProperties = {
33579
+ response_byte_limit: { type: "integer", minimum: 1 },
33580
+ time_budget_ms: { type: "integer", minimum: 1 }
33581
+ };
33582
+ projectRegistrationReceiptSchema = {
33583
+ type: "object",
33584
+ additionalProperties: false,
33585
+ required: [
33586
+ "receipt_id",
33587
+ "authority",
33588
+ "route",
33589
+ "package_version",
33590
+ "authority_id",
33591
+ "tenant_id",
33592
+ "corpus_id",
33593
+ "operation_id",
33594
+ "step_id",
33595
+ "resource_kind",
33596
+ "direction",
33597
+ "idempotency_key",
33598
+ "request_digest",
33599
+ "precondition_digest",
33600
+ "outcome",
33601
+ "reason",
33602
+ "target_id",
33603
+ "result_revision",
33604
+ "result_digest",
33605
+ "duplicate_of_receipt_id",
33606
+ "accepted_receipt_id",
33607
+ "created_by_operation",
33608
+ "created_at"
33609
+ ],
33610
+ properties: {
33611
+ receipt_id: { type: "string" },
33612
+ authority: { type: "string", enum: ["todos"] },
33613
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33614
+ package_version: { type: "string" },
33615
+ authority_id: { type: "string" },
33616
+ tenant_id: { type: "string" },
33617
+ corpus_id: { type: "string" },
33618
+ operation_id: { type: "string" },
33619
+ step_id: { type: "string" },
33620
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33621
+ direction: { type: "string", enum: ["forward", "inverse"] },
33622
+ idempotency_key: { type: "string" },
33623
+ request_digest: { type: "string" },
33624
+ precondition_digest: { type: "string" },
33625
+ outcome: {
33626
+ type: "string",
33627
+ enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
33628
+ },
33629
+ reason: { type: "string", nullable: true },
33630
+ target_id: { type: "string", format: "uuid", nullable: true },
33631
+ result_revision: { type: "string", nullable: true },
33632
+ result_digest: { type: "string", nullable: true },
33633
+ duplicate_of_receipt_id: { type: "string", nullable: true },
33634
+ accepted_receipt_id: { type: "string", nullable: true },
33635
+ created_by_operation: { type: "boolean" },
33636
+ created_at: { type: "string", format: "date-time" }
33637
+ }
33638
+ };
33639
+ projectRegistrationCapabilitySchema = {
33640
+ type: "object",
33641
+ additionalProperties: false,
33642
+ required: [
33643
+ "authority",
33644
+ "route",
33645
+ "package_version",
33646
+ "authority_id",
33647
+ "tenant_id",
33648
+ "corpus_id",
33649
+ "supported_resources",
33650
+ "conditional_create",
33651
+ "immutable_receipts",
33652
+ "exact_terminal_lookup",
33653
+ "exact_readback",
33654
+ "bind_existing_adoption",
33655
+ "prior_registration_adoption_validation",
33656
+ "project_resource_enumeration",
33657
+ "project_resource_page_limit",
33658
+ "conditional_inverse",
33659
+ "ambiguous_outcome_reconciliation"
33660
+ ],
33661
+ properties: {
33662
+ authority: { type: "string", enum: ["todos"] },
33663
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33664
+ package_version: { type: "string" },
33665
+ authority_id: { type: "string" },
33666
+ tenant_id: { type: "string" },
33667
+ corpus_id: { type: "string" },
33668
+ supported_resources: {
33669
+ type: "array",
33670
+ items: { type: "string", enum: ["project", "task_list"] }
33671
+ },
33672
+ conditional_create: { type: "boolean", enum: [true] },
33673
+ immutable_receipts: { type: "boolean", enum: [true] },
33674
+ exact_terminal_lookup: { type: "boolean", enum: [true] },
33675
+ exact_readback: { type: "boolean", enum: [true] },
33676
+ bind_existing_adoption: { type: "boolean", enum: [true] },
33677
+ prior_registration_adoption_validation: { type: "boolean", enum: [true] },
33678
+ project_resource_enumeration: { type: "boolean", enum: [true] },
33679
+ project_resource_page_limit: { type: "integer", minimum: 1 },
33680
+ conditional_inverse: { type: "boolean", enum: [true] },
33681
+ ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
33682
+ }
33683
+ };
33684
+ projectRegistrationRequestSchema = {
33685
+ type: "object",
33686
+ additionalProperties: false,
33687
+ required: [
33688
+ "operation_id",
33689
+ "step_id",
33690
+ "resource_kind",
33691
+ "direction",
33692
+ "authority_route",
33693
+ "package_version",
33694
+ "authority_id",
33695
+ "tenant_id",
33696
+ "corpus_id",
33697
+ "target_selector",
33698
+ "idempotency_key",
33699
+ "request_digest",
33700
+ "precondition_digest",
33701
+ "project_id",
33702
+ "project_slug",
33703
+ "project_name",
33704
+ "desired",
33705
+ "response_byte_limit",
33706
+ "time_budget_ms"
33707
+ ],
33708
+ properties: {
33709
+ operation_id: { type: "string" },
33710
+ step_id: { type: "string" },
33711
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33712
+ direction: { type: "string", enum: ["forward", "inverse"] },
33713
+ authority_route: { type: "string" },
33714
+ package_version: { type: "string" },
33715
+ authority_id: { type: "string" },
33716
+ tenant_id: { type: "string" },
33717
+ corpus_id: { type: "string" },
33718
+ target_selector: { type: "string" },
33719
+ idempotency_key: { type: "string" },
33720
+ request_digest: { type: "string" },
33721
+ precondition_digest: { type: "string" },
33722
+ project_id: { type: "string" },
33723
+ project_slug: { type: "string" },
33724
+ project_name: { type: "string" },
33725
+ desired: { type: "object", additionalProperties: true },
33726
+ bind_existing: { type: "boolean" },
33727
+ accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
33728
+ ...projectRegistrationBoundsProperties
33729
+ }
33730
+ };
33731
+ projectRegistrationLookupRequestSchema = {
33732
+ type: "object",
33733
+ additionalProperties: false,
33734
+ required: [
33735
+ "operation_id",
33736
+ "step_id",
33737
+ "resource_kind",
33738
+ "direction",
33739
+ "authority",
33740
+ "authority_route",
33741
+ "package_version",
33742
+ "authority_id",
33743
+ "tenant_id",
33744
+ "corpus_id",
33745
+ "target_selector",
33746
+ "idempotency_key",
33747
+ "max_items",
33748
+ "response_byte_limit",
33749
+ "time_budget_ms"
33750
+ ],
33751
+ properties: {
33752
+ operation_id: { type: "string" },
33753
+ step_id: { type: "string" },
33754
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33755
+ direction: { type: "string", enum: ["forward", "inverse"] },
33756
+ authority: { type: "string", enum: ["todos"] },
33757
+ authority_route: { type: "string" },
33758
+ package_version: { type: "string" },
33759
+ authority_id: { type: "string" },
33760
+ tenant_id: { type: "string" },
33761
+ corpus_id: { type: "string" },
33762
+ target_selector: { type: "string" },
33763
+ idempotency_key: { type: "string" },
33764
+ target_id: { type: "string", format: "uuid" },
33765
+ max_items: { type: "integer", enum: [1] },
33766
+ ...projectRegistrationBoundsProperties
33767
+ }
33768
+ };
33769
+ priorRegistrationAdoptionValidationSchema = {
33770
+ type: "object",
33771
+ additionalProperties: false,
33772
+ required: [
33773
+ "valid",
33774
+ "resource_kind",
33775
+ "target_id",
33776
+ "source_receipt_id",
33777
+ "accepted_receipt_id",
33778
+ "source_outcome",
33779
+ "created_at",
33780
+ "current_revision",
33781
+ "accepted_result_digest"
33782
+ ],
33783
+ properties: {
33784
+ valid: { type: "boolean", enum: [true] },
33785
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33786
+ target_id: { type: "string", format: "uuid" },
33787
+ source_receipt_id: { type: "string" },
33788
+ accepted_receipt_id: { type: "string" },
33789
+ source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
33790
+ created_at: { type: "string", format: "date-time" },
33791
+ current_revision: { type: "string", format: "date-time" },
33792
+ accepted_result_digest: { type: "string" }
33793
+ }
33794
+ };
33795
+ projectResourceSchema = {
33796
+ type: "object",
33797
+ additionalProperties: false,
33798
+ required: [
33799
+ "source_project_id",
33800
+ "kind",
33801
+ "scope",
33802
+ "target_id",
33803
+ "parent_id",
33804
+ "revision",
33805
+ "digest"
33806
+ ],
33807
+ properties: {
33808
+ source_project_id: { type: "string" },
33809
+ kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
33810
+ scope: { type: "string", enum: ["collection", "resource"] },
33811
+ target_id: { type: "string", format: "uuid" },
33812
+ parent_id: { type: "string", format: "uuid", nullable: true },
33813
+ revision: { type: "string" },
33814
+ digest: { type: "string" }
33815
+ }
33816
+ };
33817
+ projectResourcePageSchema = {
33818
+ type: "object",
33819
+ additionalProperties: false,
33820
+ required: [
33821
+ "authority",
33822
+ "route",
33823
+ "package_version",
33824
+ "authority_id",
33825
+ "tenant_id",
33826
+ "corpus_id",
33827
+ "source_project_id",
33828
+ "todos_project_id",
33829
+ "task_list_id",
33830
+ "include_anchors",
33831
+ "collection_revision",
33832
+ "limit",
33833
+ "count",
33834
+ "resources",
33835
+ "has_more",
33836
+ "next_cursor",
33837
+ "complete",
33838
+ "truncated"
33839
+ ],
33840
+ properties: {
33841
+ authority: { type: "string", enum: ["todos"] },
33842
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33843
+ package_version: { type: "string" },
33844
+ authority_id: { type: "string" },
33845
+ tenant_id: { type: "string" },
33846
+ corpus_id: { type: "string" },
33847
+ source_project_id: { type: "string" },
33848
+ todos_project_id: { type: "string", format: "uuid" },
33849
+ task_list_id: { type: "string", format: "uuid" },
33850
+ include_anchors: { type: "boolean" },
33851
+ collection_revision: { type: "string" },
33852
+ limit: { type: "integer", minimum: 1, maximum: 500 },
33853
+ count: { type: "integer", minimum: 0 },
33854
+ resources: {
33855
+ type: "array",
33856
+ items: { $ref: "#/components/schemas/ProjectResource" }
33857
+ },
33858
+ has_more: { type: "boolean" },
33859
+ next_cursor: { type: "string", nullable: true },
33860
+ complete: { type: "boolean" },
33861
+ truncated: { type: "boolean", enum: [false] }
33862
+ }
33863
+ };
32034
33864
  });
32035
33865
 
32036
33866
  // src/server/pr-groups.ts
@@ -32177,11 +34007,11 @@ function canonicalJson2(value) {
32177
34007
  return `[${value.map(canonicalJson2).join(",")}]`;
32178
34008
  return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
32179
34009
  }
32180
- function digest(value) {
34010
+ function digest2(value) {
32181
34011
  return createHash9("sha256").update(canonicalJson2(value)).digest("hex");
32182
34012
  }
32183
34013
  function deriveIdempotencyKey(projectId, slug) {
32184
- return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
34014
+ return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
32185
34015
  }
32186
34016
  function normalizeIdempotencyKey(value, projectId, slug) {
32187
34017
  const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
@@ -32190,13 +34020,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
32190
34020
  }
32191
34021
  return key2;
32192
34022
  }
32193
- function receiptId2(projectId, slug, idempotencyKey) {
32194
- return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
34023
+ function receiptId2(projectId, slug, idempotencyKey2) {
34024
+ return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
32195
34025
  }
32196
34026
  function semanticListDigest(list) {
32197
34027
  const metadata = { ...list.metadata ?? {} };
32198
34028
  delete metadata[RECEIPT_METADATA_KEY];
32199
- return digest({
34029
+ return digest2({
32200
34030
  project_id: list.project_id,
32201
34031
  slug: list.slug,
32202
34032
  name: list.name,
@@ -32213,10 +34043,10 @@ function storedMarker(list) {
32213
34043
  return null;
32214
34044
  return marker;
32215
34045
  }
32216
- function receiptFor(store, project, list, idempotencyKey) {
34046
+ function receiptFor(store, project, list, idempotencyKey2) {
32217
34047
  const marker = storedMarker(list);
32218
34048
  const owned = marker?.project_id === project.id && marker.slug === list.slug;
32219
- if (owned && marker.idempotency_key !== idempotencyKey) {
34049
+ if (owned && marker.idempotency_key !== idempotencyKey2) {
32220
34050
  throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
32221
34051
  project_id: project.id,
32222
34052
  task_list_id: list.id,
@@ -32225,8 +34055,8 @@ function receiptFor(store, project, list, idempotencyKey) {
32225
34055
  }
32226
34056
  return {
32227
34057
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
32228
- receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
32229
- idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
34058
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
34059
+ idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
32230
34060
  project_id: project.id,
32231
34061
  task_list_id: list.id,
32232
34062
  slug: list.slug,
@@ -32278,20 +34108,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32278
34108
  });
32279
34109
  }
32280
34110
  const slug = project.task_list_id;
32281
- const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
34111
+ const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
32282
34112
  if (state.scoped) {
32283
34113
  return {
32284
34114
  mode: "apply",
32285
34115
  action: "already_present",
32286
34116
  project,
32287
34117
  task_list: state.scoped,
32288
- receipt: receiptFor(store, project, state.scoped, idempotencyKey)
34118
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
32289
34119
  };
32290
34120
  }
32291
34121
  const marker = {
32292
34122
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
32293
- receipt_id: receiptId2(project.id, slug, idempotencyKey),
32294
- idempotency_key: idempotencyKey,
34123
+ receipt_id: receiptId2(project.id, slug, idempotencyKey2),
34124
+ idempotency_key: idempotencyKey2,
32295
34125
  project_id: project.id,
32296
34126
  slug,
32297
34127
  result_digest: semanticListDigest({
@@ -32329,7 +34159,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32329
34159
  action: "already_present",
32330
34160
  project: raced.project,
32331
34161
  task_list: raced.scoped,
32332
- receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
34162
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
32333
34163
  };
32334
34164
  }
32335
34165
  const projectReadback = await store.projects.get(project.id);
@@ -32359,7 +34189,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32359
34189
  action: "created",
32360
34190
  project: projectReadback,
32361
34191
  task_list: readback,
32362
- receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
34192
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
32363
34193
  };
32364
34194
  }
32365
34195
  async function rollbackProjectTaskListEnsure(store, projectId, options) {
@@ -32404,7 +34234,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
32404
34234
  project_id: project.id,
32405
34235
  task_list_id: list.id,
32406
34236
  accepted_receipt_id: options.receipt_id,
32407
- rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
34237
+ rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
32408
34238
  removed_at: new Date().toISOString()
32409
34239
  };
32410
34240
  }
@@ -32603,6 +34433,9 @@ function validateTaskPatchVocabulary(value) {
32603
34433
  if (!parsed.ok)
32604
34434
  return { ok: false, message: parsed.message };
32605
34435
  }
34436
+ if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
34437
+ return { ok: false, message: "parent_id must be a non-empty task id or null" };
34438
+ }
32606
34439
  return { ok: true, patch: body2 };
32607
34440
  }
32608
34441
  function validateProjectPatch(value) {
@@ -33087,8 +34920,8 @@ async function handleV1Request(req, url, dependencies = {}) {
33087
34920
  }
33088
34921
  const created = await store.tasks.create(body2, storageContext);
33089
34922
  const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
33090
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
33091
- 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" });
34923
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null)) {
34924
+ return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, and plan_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
33092
34925
  }
33093
34926
  return json5({ task: persisted }, 201);
33094
34927
  }
@@ -33962,6 +35795,15 @@ async function handleV1Request(req, url, dependencies = {}) {
33962
35795
  if (e instanceof TaskNotFoundError) {
33963
35796
  return error(404, e.message, { code: TaskNotFoundError.code });
33964
35797
  }
35798
+ if (e instanceof VersionConflictError) {
35799
+ return error(409, e.message, {
35800
+ code: VersionConflictError.code,
35801
+ conflict: true,
35802
+ task_id: e.taskId,
35803
+ expected_version: e.expectedVersion,
35804
+ current_version: e.actualVersion
35805
+ });
35806
+ }
33965
35807
  if (e instanceof StaleLockHandoffError) {
33966
35808
  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;
33967
35809
  return error(status2, e.message, {
@@ -52577,16 +54419,16 @@ function createHasnaStorageClient(name, transport) {
52577
54419
  }
52578
54420
  },
52579
54421
  async create(resource, body2, options = {}) {
52580
- const { idempotencyKey, ...rest } = options;
54422
+ const { idempotencyKey: idempotencyKey2, ...rest } = options;
52581
54423
  return transport.post(resourcePath(resource), body2, {
52582
54424
  ...rest,
52583
- idempotencyKey: idempotencyKey ?? newIdempotencyKey()
54425
+ idempotencyKey: idempotencyKey2 ?? newIdempotencyKey()
52584
54426
  });
52585
54427
  },
52586
54428
  async update(resource, id, patch, options = {}) {
52587
- const { method = "PATCH", idempotencyKey, ...rest } = options;
54429
+ const { method = "PATCH", idempotencyKey: idempotencyKey2, ...rest } = options;
52588
54430
  const call = method === "PUT" ? transport.put : transport.patch;
52589
- return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
54431
+ return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey2 ? { idempotencyKey: idempotencyKey2 } : {} });
52590
54432
  },
52591
54433
  async delete(resource, id, options = {}) {
52592
54434
  try {
@@ -64770,6 +66612,11 @@ var init_http_client = __esm(() => {
64770
66612
  ]);
64771
66613
  });
64772
66614
 
66615
+ // src/project-registration/page-validation.ts
66616
+ var init_page_validation = __esm(() => {
66617
+ init_types3();
66618
+ });
66619
+
64773
66620
  // src/cli/cloud-router.ts
64774
66621
  import { randomUUID as randomUUID4 } from "crypto";
64775
66622
  import { resolve as resolvePath } from "path";
@@ -65168,12 +67015,39 @@ async function cloudListTasks(client, filter = {}) {
65168
67015
  union3.sort(compareCloudTaskOrder);
65169
67016
  return union3.slice(start, windowEnd);
65170
67017
  }
67018
+ async function cloudResolveTaskRef(client, ref) {
67019
+ const input = ref.trim().toLowerCase();
67020
+ if (!input)
67021
+ throw new Error("Task reference must not be empty");
67022
+ if (UUID_RE.test(input))
67023
+ return input;
67024
+ let task2;
67025
+ try {
67026
+ task2 = await cloudGetTask(client, input);
67027
+ } catch (error3) {
67028
+ const status2 = error3 && typeof error3 === "object" ? error3.status : undefined;
67029
+ if (status2 === 409) {
67030
+ const body2 = error3 && typeof error3 === "object" ? error3.body : undefined;
67031
+ const authorityMessage = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.error : undefined;
67032
+ throw new Error(typeof authorityMessage === "string" && authorityMessage.trim() ? authorityMessage : `Task reference is ambiguous: "${ref}"`);
67033
+ }
67034
+ throw error3;
67035
+ }
67036
+ if (task2 && typeof task2.id === "string" && (task2.short_id?.toLowerCase() === input || task2.id.toLowerCase().startsWith(input))) {
67037
+ return task2.id;
67038
+ }
67039
+ if (task2) {
67040
+ 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.");
67041
+ }
67042
+ 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.");
67043
+ }
65171
67044
  async function cloudGetTask(client, id) {
65172
67045
  const raw = await client.get("tasks", id);
65173
67046
  return raw == null ? null : unwrapTask(raw);
65174
67047
  }
65175
67048
  async function cloudCreateTask(client, input, verification2 = {}) {
65176
67049
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
67050
+ const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
65177
67051
  const expectedCreatedBy = typeof verification2.expectedCreatedBy === "string" && verification2.expectedCreatedBy.trim() ? verification2.expectedCreatedBy : null;
65178
67052
  if (expectedCreatedBy !== null) {
65179
67053
  await requireTaskCreatorCapability(client);
@@ -65186,9 +67060,9 @@ async function cloudCreateTask(client, input, verification2 = {}) {
65186
67060
  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");
65187
67061
  }
65188
67062
  const persisted = await cloudGetTask(client, created.id);
65189
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
67063
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
65190
67064
  const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
65191
- 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`);
67065
+ 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`);
65192
67066
  }
65193
67067
  return persisted;
65194
67068
  }
@@ -65457,6 +67331,8 @@ var init_cloud_router = __esm(() => {
65457
67331
  init_redaction();
65458
67332
  init_plan_project_link_contract();
65459
67333
  init_http_client();
67334
+ init_adoption_validation();
67335
+ init_page_validation();
65460
67336
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
65461
67337
  TRANSPORT_TOKENS = {
65462
67338
  sqlite: "sqlite",
@@ -65493,6 +67369,7 @@ function registerTaskCrudTools(server, ctx) {
65493
67369
  compact["version"] = task2.version;
65494
67370
  compact["created_at"] = task2.created_at;
65495
67371
  compact["task_list_id"] = task2.task_list_id;
67372
+ compact["parent_id"] = task2.parent_id;
65496
67373
  return compactJson(compact);
65497
67374
  }
65498
67375
  function versionFor(taskId, version2) {
@@ -65762,6 +67639,7 @@ ${task2.description}` : null
65762
67639
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
65763
67640
  assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
65764
67641
  project_id: exports_external.string().nullable().optional(),
67642
+ parent_id: exports_external.string().nullable().optional().describe("Existing parent task ID/reference, null to detach"),
65765
67643
  task_list_id: exports_external.string().nullable().optional(),
65766
67644
  depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
65767
67645
  tags: exports_external.array(exports_external.string()).optional(),
@@ -65793,6 +67671,9 @@ ${task2.description}` : null
65793
67671
  if (typeof patch.project_id === "string" && patch.project_id) {
65794
67672
  patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
65795
67673
  }
67674
+ if (typeof patch.parent_id === "string" && patch.parent_id) {
67675
+ patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
67676
+ }
65796
67677
  if (typeof patch.task_list_id === "string" && patch.task_list_id) {
65797
67678
  let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
65798
67679
  if (!scope) {
@@ -65801,9 +67682,22 @@ ${task2.description}` : null
65801
67682
  }
65802
67683
  patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
65803
67684
  }
67685
+ if (patch.parent_id !== undefined && version3 === undefined) {
67686
+ const current = await cloudGetTask(cloud, task_id2);
67687
+ if (!current)
67688
+ throw new TaskNotFoundError(task_id2);
67689
+ patch.version = current.version;
67690
+ }
65804
67691
  if (version3 !== undefined)
65805
67692
  patch.version = version3;
65806
- const updated = await cloudUpdateTask(cloud, task_id2, patch);
67693
+ let updated = await cloudUpdateTask(cloud, task_id2, patch);
67694
+ if (patch.parent_id !== undefined) {
67695
+ const persisted = await cloudGetTask(cloud, task_id2);
67696
+ if (!persisted || (persisted.parent_id ?? null) !== patch.parent_id) {
67697
+ throw new Error(`TASK_REPARENT_PERSISTENCE_UNVERIFIED: parent_id expected ${patch.parent_id ?? "null"}, ` + `received ${persisted?.parent_id ?? "missing task"}`);
67698
+ }
67699
+ updated = persisted;
67700
+ }
65807
67701
  return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
65808
67702
  }
65809
67703
  const resolvedId = resolveId(params.task_id);
@@ -65815,6 +67709,8 @@ ${task2.description}` : null
65815
67709
  resolved.assigned_to = resolveAssignee(resolved.assigned_to);
65816
67710
  if (resolved.project_id && typeof resolved.project_id === "string")
65817
67711
  resolved.project_id = resolveId(resolved.project_id, "projects");
67712
+ if (resolved.parent_id && typeof resolved.parent_id === "string")
67713
+ resolved.parent_id = resolveId(resolved.parent_id);
65818
67714
  if (resolved.task_list_id && typeof resolved.task_list_id === "string")
65819
67715
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
65820
67716
  if (resolved.depends_on && Array.isArray(resolved.depends_on))
@@ -99497,7 +101393,7 @@ function registerTaskMetaTools(server, ctx) {
99497
101393
  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",
99498
101394
  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",
99499
101395
  get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
99500
- 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",
101396
+ 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",
99501
101397
  delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
99502
101398
  start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
99503
101399
  complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
@@ -111312,8 +113208,8 @@ function defaultSnapshotDir() {
111312
113208
  return join15(dirname8(resolve16(dbPath)), "environment-snapshots");
111313
113209
  }
111314
113210
  function snapshotWithId(snapshot) {
111315
- const digest2 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
111316
- return { id: `env_${digest2}`, ...snapshot };
113211
+ const digest3 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
113212
+ return { id: `env_${digest3}`, ...snapshot };
111317
113213
  }
111318
113214
  function captureEnvironmentSnapshot(input = {}) {
111319
113215
  const root = resolve16(input.root || process.cwd());