@hasna/todos 0.15.29 → 0.15.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cli/cloud-router.d.ts +13 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-registration-commands.d.ts +2 -0
  4. package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +2861 -342
  8. package/dist/contracts.js +52 -2
  9. package/dist/db/task-crud.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1466 -147
  13. package/dist/lib/task-parent-integrity.d.ts +14 -0
  14. package/dist/lib/task-parent-integrity.d.ts.map +1 -0
  15. package/dist/mcp/index.js +2050 -138
  16. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/project-registration/adoption-validation.d.ts +3 -0
  19. package/dist/project-registration/adoption-validation.d.ts.map +1 -0
  20. package/dist/project-registration/authority.d.ts +3 -1
  21. package/dist/project-registration/authority.d.ts.map +1 -1
  22. package/dist/project-registration/backend.d.ts +24 -1
  23. package/dist/project-registration/backend.d.ts.map +1 -1
  24. package/dist/project-registration/http.d.ts +3 -1
  25. package/dist/project-registration/http.d.ts.map +1 -1
  26. package/dist/project-registration/index.d.ts +2 -1
  27. package/dist/project-registration/index.d.ts.map +1 -1
  28. package/dist/project-registration/page-validation.d.ts +5 -0
  29. package/dist/project-registration/page-validation.d.ts.map +1 -0
  30. package/dist/project-registration/postgres.d.ts +13 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration/sqlite.d.ts +13 -1
  33. package/dist/project-registration/sqlite.d.ts.map +1 -1
  34. package/dist/project-registration/types.d.ts +68 -1
  35. package/dist/project-registration/types.d.ts.map +1 -1
  36. package/dist/project-registration.js +849 -60
  37. package/dist/registry.js +52 -2
  38. package/dist/release-provenance.json +5 -5
  39. package/dist/sdk/index.d.ts +1 -1
  40. package/dist/sdk/index.d.ts.map +1 -1
  41. package/dist/sdk/index.js +151 -0
  42. package/dist/sdk/v1.generated.d.ts +282 -0
  43. package/dist/sdk/v1.generated.d.ts.map +1 -1
  44. package/dist/server/index.js +2041 -129
  45. package/dist/server/openapi.d.ts +2856 -1357
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/v1.d.ts.map +1 -1
  48. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  49. package/dist/storage/postgres-sync.d.ts +8 -0
  50. package/dist/storage/postgres-sync.d.ts.map +1 -1
  51. package/dist/storage.js +233 -20
  52. package/dist/task-manifest/authority.d.ts +13 -1
  53. package/dist/task-manifest/authority.d.ts.map +1 -1
  54. package/dist/task-manifest/backend.d.ts +5 -0
  55. package/dist/task-manifest/backend.d.ts.map +1 -1
  56. package/dist/task-manifest/index.d.ts +2 -2
  57. package/dist/task-manifest/index.d.ts.map +1 -1
  58. package/dist/task-manifest/plan-slug.d.ts +20 -0
  59. package/dist/task-manifest/plan-slug.d.ts.map +1 -1
  60. package/dist/task-manifest/postgres.d.ts +1 -0
  61. package/dist/task-manifest/postgres.d.ts.map +1 -1
  62. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  63. package/dist/task-manifest/schema.d.ts.map +1 -1
  64. package/dist/task-manifest/sqlite.d.ts +1 -0
  65. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  66. package/dist/task-manifest/types.d.ts +21 -1
  67. package/dist/task-manifest/types.d.ts.map +1 -1
  68. package/dist/task-manifest.js +593 -62
  69. package/dist/types/index.d.ts +4 -2
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/package.json +1 -1
@@ -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.33",
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",
@@ -1806,7 +1806,7 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
1806
1806
  AS $$ SELECT unaccent('unaccent', $1) $$`,
1807
1807
  `CREATE OR REPLACE FUNCTION todos_try_timestamptz(text)
1808
1808
  RETURNS timestamptz
1809
- LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
1809
+ LANGUAGE plpgsql IMMUTABLE PARALLEL UNSAFE
1810
1810
  SET DateStyle TO 'ISO, YMD'
1811
1811
  AS $$
1812
1812
  BEGIN
@@ -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
21468
+ })
21469
+ };
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
21158
21478
  })
21159
21479
  };
21160
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
  }
@@ -21195,9 +21547,37 @@ function normalizedCallDigest(request) {
21195
21547
  project_slug: request.project_slug,
21196
21548
  project_name: request.project_name,
21197
21549
  desired: request.desired,
21550
+ bind_existing: request.bind_existing === true,
21198
21551
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
21199
21552
  });
21200
21553
  }
21554
+ function legacyNormalizedCallDigestBeforeBindExisting(request) {
21555
+ return digestProjectRegistrationValue({
21556
+ authority_route: request.authority_route,
21557
+ package_version: request.package_version,
21558
+ authority_id: request.authority_id,
21559
+ tenant_id: request.tenant_id,
21560
+ corpus_id: request.corpus_id,
21561
+ operation_id: request.operation_id,
21562
+ step_id: request.step_id,
21563
+ resource_kind: request.resource_kind,
21564
+ direction: request.direction,
21565
+ target_selector: request.target_selector,
21566
+ idempotency_key: request.idempotency_key,
21567
+ request_digest: request.request_digest,
21568
+ precondition_digest: request.precondition_digest,
21569
+ project_id: request.project_id,
21570
+ project_slug: request.project_slug,
21571
+ project_name: request.project_name,
21572
+ desired: request.desired,
21573
+ accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
21574
+ });
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));
@@ -21682,9 +22134,14 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21682
22134
  if (request.max_items !== 1) {
21683
22135
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
21684
22136
  }
21685
- if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
22137
+ if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id) {
21686
22138
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
21687
22139
  }
22140
+ requireString(request.corpus_id, "corpus_id", {
22141
+ min: 3,
22142
+ max: 128,
22143
+ pattern: AUTHORITY_ROUTE_PATTERN
22144
+ });
21688
22145
  requireString(request.authority_route, "authority_route", {
21689
22146
  min: 3,
21690
22147
  max: 128,
@@ -21715,6 +22172,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21715
22172
  }
21716
22173
  const receipt = await this.backend.getReceiptForLookup({
21717
22174
  ...authorityScope(this.capabilityValue),
22175
+ corpus_id: request.corpus_id,
21718
22176
  route: request.authority_route,
21719
22177
  package_version: request.package_version,
21720
22178
  operation_id: request.operation_id,
@@ -21729,6 +22187,174 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21729
22187
  }
21730
22188
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
21731
22189
  }
22190
+ async listProjectResources(request) {
22191
+ const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
22192
+ if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
22193
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
22194
+ }
22195
+ if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
22196
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
22197
+ }
22198
+ const includeAnchors = request.include_anchors === true;
22199
+ const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
22200
+ if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
22201
+ 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 });
22202
+ }
22203
+ const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
22204
+ if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
22205
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
22206
+ source_project_id: sourceProjectId,
22207
+ todos_project_id: projectBinding.target_id
22208
+ });
22209
+ }
22210
+ const cursor = decodeProjectResourceCursor(request.cursor, {
22211
+ source_project_id: sourceProjectId,
22212
+ include_anchors: includeAnchors
22213
+ });
22214
+ const collectionInput = {
22215
+ todos_project_id: projectBinding.target_id,
22216
+ task_list_id: taskListBinding.target_id,
22217
+ include_anchors: includeAnchors
22218
+ };
22219
+ const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
22220
+ if (cursor && cursor.collection_revision !== collectionRevision) {
22221
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
22222
+ source_project_id: sourceProjectId,
22223
+ expected_collection_revision: cursor.collection_revision,
22224
+ current_collection_revision: collectionRevision
22225
+ });
22226
+ }
22227
+ const candidates = await this.backend.listProjectResourceCandidates({
22228
+ ...collectionInput,
22229
+ after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
22230
+ limit: request.limit + 1
22231
+ });
22232
+ const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
22233
+ if (verifiedCollectionRevision !== collectionRevision) {
22234
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
22235
+ source_project_id: sourceProjectId,
22236
+ expected_collection_revision: collectionRevision,
22237
+ current_collection_revision: verifiedCollectionRevision
22238
+ });
22239
+ }
22240
+ const hasMore = candidates.length > request.limit;
22241
+ const pageCandidates = candidates.slice(0, request.limit);
22242
+ const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
22243
+ const last = pageCandidates.at(-1);
22244
+ return {
22245
+ authority: "todos",
22246
+ route: this.capabilityValue.route,
22247
+ package_version: this.capabilityValue.package_version,
22248
+ authority_id: this.capabilityValue.authority_id,
22249
+ tenant_id: this.capabilityValue.tenant_id,
22250
+ corpus_id: this.capabilityValue.corpus_id,
22251
+ source_project_id: sourceProjectId,
22252
+ todos_project_id: projectBinding.target_id,
22253
+ task_list_id: taskListBinding.target_id,
22254
+ include_anchors: includeAnchors,
22255
+ collection_revision: collectionRevision,
22256
+ limit: request.limit,
22257
+ count: resources.length,
22258
+ resources,
22259
+ has_more: hasMore,
22260
+ next_cursor: hasMore && last ? encodeProjectResourceCursor({
22261
+ source_project_id: sourceProjectId,
22262
+ include_anchors: includeAnchors,
22263
+ collection_revision: collectionRevision,
22264
+ kind_rank: last.kind_rank,
22265
+ target_id: last.target_id
22266
+ }) : null,
22267
+ complete: !hasMore,
22268
+ truncated: false
22269
+ };
22270
+ }
22271
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
22272
+ const startedAt = Date.now();
22273
+ if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
22274
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
22275
+ }
22276
+ requireString(sourceRequest.package_version, "package_version", {
22277
+ max: 128,
22278
+ pattern: PACKAGE_VERSION_PATTERN
22279
+ });
22280
+ requireString(sourceRequest.corpus_id, "corpus_id", {
22281
+ min: 3,
22282
+ max: 128,
22283
+ pattern: AUTHORITY_ROUTE_PATTERN
22284
+ });
22285
+ assertForwardRequest(sourceRequest, {
22286
+ ...this.capabilityValue,
22287
+ package_version: sourceRequest.package_version,
22288
+ corpus_id: sourceRequest.corpus_id
22289
+ });
22290
+ const validation = await this.backend.transaction(async (transaction) => {
22291
+ const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
22292
+ if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
22293
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
22294
+ }
22295
+ const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
22296
+ if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
22297
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
22298
+ }
22299
+ 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);
22300
+ 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)) {
22301
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
22302
+ }
22303
+ const binding = await transaction.getBinding({
22304
+ authority_id: sourceRequest.authority_id,
22305
+ tenant_id: sourceRequest.tenant_id,
22306
+ corpus_id: sourceRequest.corpus_id
22307
+ }, sourceRequest.resource_kind, sourceRequest.target_selector);
22308
+ 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) {
22309
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
22310
+ }
22311
+ const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
22312
+ if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
22313
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
22314
+ }
22315
+ let stableMatch = false;
22316
+ if (sourceRequest.resource_kind === "task_list") {
22317
+ stableMatch = taskListRegistrationDigest({
22318
+ ...current,
22319
+ updated_at: accepted.result_revision
22320
+ }) === accepted.result_digest;
22321
+ } else {
22322
+ const project = current;
22323
+ if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
22324
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
22325
+ }
22326
+ for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
22327
+ if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
22328
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
22329
+ }
22330
+ if (projectRegistrationDigest({
22331
+ ...project,
22332
+ task_counter: priorTaskCounter,
22333
+ updated_at: accepted.result_revision
22334
+ }) === accepted.result_digest) {
22335
+ stableMatch = true;
22336
+ break;
22337
+ }
22338
+ }
22339
+ }
22340
+ if (!stableMatch) {
22341
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
22342
+ }
22343
+ return {
22344
+ valid: true,
22345
+ resource_kind: sourceRequest.resource_kind,
22346
+ target_id: accepted.target_id,
22347
+ source_receipt_id: storedSource.receipt_id,
22348
+ accepted_receipt_id: accepted.receipt_id,
22349
+ source_outcome: storedSource.outcome,
22350
+ created_at: current.created_at,
22351
+ current_revision: current.updated_at,
22352
+ accepted_result_digest: accepted.result_digest
22353
+ };
22354
+ });
22355
+ assertWithinBounds(validation, sourceRequest, startedAt);
22356
+ return validation;
22357
+ }
21732
22358
  async storedAcceptedReceipt(request, supplied) {
21733
22359
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
21734
22360
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -21897,7 +22523,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
21897
22523
  cursorTableName
21898
22524
  }), authorityOptions);
21899
22525
  }
21900
- var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
22526
+ 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
22527
  var init_authority = __esm(() => {
21902
22528
  init_package_version();
21903
22529
  init_postgres2();
@@ -21922,6 +22548,11 @@ var init_authority = __esm(() => {
21922
22548
  };
21923
22549
  });
21924
22550
 
22551
+ // src/project-registration/adoption-validation.ts
22552
+ var init_adoption_validation = __esm(() => {
22553
+ init_types3();
22554
+ });
22555
+
21925
22556
  // src/project-registration/http.ts
21926
22557
  function json(body, status = 200) {
21927
22558
  return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
@@ -21967,6 +22598,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
21967
22598
  if ((action === "" || action === "capability") && method === "GET") {
21968
22599
  return json({ capability: await authority.capability() });
21969
22600
  }
22601
+ if (action === "resources" && method === "GET") {
22602
+ const sourceProjectId = url.searchParams.get("source_project_id");
22603
+ const limit = Number(url.searchParams.get("limit") ?? "100");
22604
+ const includeAnchorsRaw = url.searchParams.get("include_anchors");
22605
+ const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
22606
+ return json({
22607
+ page: await authority.listProjectResources({
22608
+ source_project_id: sourceProjectId,
22609
+ limit,
22610
+ include_anchors: includeAnchors,
22611
+ cursor: url.searchParams.get("cursor") ?? undefined
22612
+ })
22613
+ });
22614
+ }
21970
22615
  if (method !== "POST")
21971
22616
  return json({ error: "method not allowed" }, 405);
21972
22617
  const body = await readJson(req);
@@ -21989,6 +22634,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
21989
22634
  record: await authority.readExact(body)
21990
22635
  });
21991
22636
  }
22637
+ if (action === "validate-prior-adoption") {
22638
+ const input = body;
22639
+ return json({
22640
+ validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
22641
+ });
22642
+ }
21992
22643
  if (action === "compensate") {
21993
22644
  return json({
21994
22645
  receipt: await authority.compensate(body)
@@ -22021,6 +22672,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
22021
22672
  var JSON_HEADERS;
22022
22673
  var init_http = __esm(() => {
22023
22674
  init_types3();
22675
+ init_adoption_validation();
22024
22676
  JSON_HEADERS = { "Content-Type": "application/json" };
22025
22677
  });
22026
22678
 
@@ -22028,6 +22680,7 @@ var init_http = __esm(() => {
22028
22680
  var init_project_registration = __esm(() => {
22029
22681
  init_authority();
22030
22682
  init_http();
22683
+ init_adoption_validation();
22031
22684
  init_postgres2();
22032
22685
  init_sqlite();
22033
22686
  init_types3();
@@ -26000,7 +26653,7 @@ var init_zod = __esm(() => {
26000
26653
  });
26001
26654
 
26002
26655
  // src/task-manifest/types.ts
26003
- var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
26656
+ 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
26657
  var init_types5 = __esm(() => {
26005
26658
  TodosTaskManifestError = class TodosTaskManifestError extends Error {
26006
26659
  code;
@@ -26087,7 +26740,7 @@ function parseTodosTaskManifestBindingLookup(input) {
26087
26740
  }
26088
26741
  return parsed.data;
26089
26742
  }
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) => {
26743
+ 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
26744
  if (Object.keys(value).length > limit) {
26092
26745
  context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
26093
26746
  }
@@ -26109,6 +26762,8 @@ var init_schema2 = __esm(() => {
26109
26762
  };
26110
26763
  key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
26111
26764
  identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
26765
+ digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
26766
+ idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
26112
26767
  uuid2 = exports_external.string().uuid();
26113
26768
  scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
26114
26769
  comment = exports_external.object({
@@ -26146,7 +26801,9 @@ var init_schema2 = __esm(() => {
26146
26801
  schema = exports_external.object({
26147
26802
  version: exports_external.literal(1),
26148
26803
  operation_id: identifier,
26149
- idempotency_key: identifier,
26804
+ step_id: identifier,
26805
+ idempotency_key: idempotencyKey,
26806
+ precondition_digest: digest,
26150
26807
  project_id: uuid2,
26151
26808
  task_list_id: uuid2.optional(),
26152
26809
  if_binding_version: exports_external.number().int().min(0).optional(),
@@ -26162,7 +26819,10 @@ var init_schema2 = __esm(() => {
26162
26819
  }).strict();
26163
26820
  compensationSchema = exports_external.object({
26164
26821
  receipt_id: uuid2,
26165
- idempotency_key: identifier,
26822
+ operation_id: identifier,
26823
+ step_id: identifier,
26824
+ idempotency_key: idempotencyKey,
26825
+ precondition_digest: digest,
26166
26826
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
26167
26827
  }).strict();
26168
26828
  bindingLookupSchema = exports_external.object({
@@ -26180,6 +26840,7 @@ function taskManifestPlanSlug(manifest, planId) {
26180
26840
  const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
26181
26841
  return `${base}-${planId}`;
26182
26842
  }
26843
+ var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
26183
26844
  var init_plan_slug = () => {};
26184
26845
 
26185
26846
  // src/task-manifest/backend.ts
@@ -26193,11 +26854,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
26193
26854
  const row = rows[0];
26194
26855
  const bindingVersion = Number(row.binding_version);
26195
26856
  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") {
26857
+ 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
26858
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
26198
26859
  }
26199
26860
  return {
26200
26861
  plan_id: planId,
26862
+ operation_id: row.binding_operation_id,
26863
+ step_id: row.binding_step_id,
26201
26864
  apply_receipt_id: row.apply_receipt_id,
26202
26865
  binding_version: bindingVersion,
26203
26866
  state
@@ -26250,9 +26913,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26250
26913
  schema_version integer NOT NULL CHECK(schema_version = 1),
26251
26914
  kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
26252
26915
  operation_id text NOT NULL,
26916
+ step_id text NOT NULL,
26253
26917
  idempotency_key text NOT NULL,
26254
26918
  request_digest text NOT NULL,
26919
+ precondition_digest text NOT NULL,
26255
26920
  result_digest text NOT NULL,
26921
+ slug_provenance text,
26922
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
26923
+ reason text,
26924
+ duplicate_of_receipt_id text,
26256
26925
  binding_version integer NOT NULL,
26257
26926
  apply_receipt_id text,
26258
26927
  manifest_json jsonb,
@@ -26264,12 +26933,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26264
26933
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
26265
26934
  `ALTER TABLE todos_task_manifest_receipts
26266
26935
  ALTER COLUMN tenant_id DROP DEFAULT`,
26936
+ `ALTER TABLE todos_task_manifest_receipts
26937
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
26938
+ `ALTER TABLE todos_task_manifest_receipts
26939
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
26940
+ `ALTER TABLE todos_task_manifest_receipts
26941
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
26942
+ `ALTER TABLE todos_task_manifest_receipts
26943
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
26944
+ `ALTER TABLE todos_task_manifest_receipts
26945
+ ADD COLUMN IF NOT EXISTS reason text`,
26946
+ `ALTER TABLE todos_task_manifest_receipts
26947
+ ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
26267
26948
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
26268
26949
  operation_id text PRIMARY KEY,
26269
26950
  tenant_id text NOT NULL,
26951
+ step_id text NOT NULL,
26270
26952
  idempotency_key text NOT NULL UNIQUE,
26271
26953
  request_digest text NOT NULL,
26954
+ precondition_digest text NOT NULL,
26272
26955
  result_digest text NOT NULL,
26956
+ slug_provenance text,
26957
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
26273
26958
  apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
26274
26959
  manifest_json jsonb NOT NULL,
26275
26960
  result_json jsonb NOT NULL,
@@ -26283,6 +26968,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26283
26968
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
26284
26969
  `ALTER TABLE todos_task_manifest_bindings
26285
26970
  ALTER COLUMN tenant_id DROP DEFAULT`,
26971
+ `ALTER TABLE todos_task_manifest_bindings
26972
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
26973
+ `ALTER TABLE todos_task_manifest_bindings
26974
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
26975
+ `ALTER TABLE todos_task_manifest_bindings
26976
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
26977
+ `ALTER TABLE todos_task_manifest_bindings
26978
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
26286
26979
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
26287
26980
  id text PRIMARY KEY,
26288
26981
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -26294,10 +26987,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26294
26987
  created_at timestamptz NOT NULL,
26295
26988
  delivered_at timestamptz
26296
26989
  )`,
26990
+ `CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
26991
+ receipt_id text PRIMARY KEY,
26992
+ tenant_id text NOT NULL,
26993
+ authority text NOT NULL CHECK(authority = 'todos'),
26994
+ route text NOT NULL,
26995
+ schema_version integer NOT NULL CHECK(schema_version = 1),
26996
+ kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
26997
+ operation_id text NOT NULL,
26998
+ step_id text NOT NULL,
26999
+ idempotency_key text NOT NULL,
27000
+ request_digest text NOT NULL,
27001
+ precondition_digest text NOT NULL,
27002
+ result_digest text NOT NULL,
27003
+ outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
27004
+ reason text NOT NULL,
27005
+ binding_version integer NOT NULL,
27006
+ apply_receipt_id text,
27007
+ manifest_json jsonb,
27008
+ result_json jsonb NOT NULL,
27009
+ created_at timestamptz NOT NULL
27010
+ )`,
26297
27011
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
26298
27012
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
26299
27013
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
26300
27014
  ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
27015
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
27016
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
27017
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
27018
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
27019
+ `CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
27020
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
26301
27021
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
26302
27022
  ON todos_task_manifest_bindings(
26303
27023
  tenant_id,
@@ -26310,6 +27030,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26310
27030
  `DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
26311
27031
  `CREATE TRIGGER todos_task_manifest_receipts_immutable
26312
27032
  BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
27033
+ FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
27034
+ `DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
27035
+ `CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
27036
+ BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
26313
27037
  FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
26314
27038
  ];
26315
27039
  }
@@ -26334,6 +27058,39 @@ function safeIdentifier2(value, field) {
26334
27058
  function parseJson(value) {
26335
27059
  return typeof value === "string" ? JSON.parse(value) : value;
26336
27060
  }
27061
+ function parseApplyResult(value, duplicate) {
27062
+ const parsed = parseJson(value);
27063
+ return {
27064
+ ...parsed,
27065
+ duplicate,
27066
+ receipt: {
27067
+ ...parsed.receipt,
27068
+ step_id: parsed.receipt.step_id ?? "legacy-apply",
27069
+ precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
27070
+ outcome: parsed.receipt.outcome ?? "accepted",
27071
+ reason: parsed.receipt.reason ?? null,
27072
+ duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
27073
+ }
27074
+ };
27075
+ }
27076
+ function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
27077
+ if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
27078
+ const expected = taskManifestPlanSlug(manifest, planId);
27079
+ if (slug !== expected) {
27080
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
27081
+ }
27082
+ return expected;
27083
+ }
27084
+ if (provenance !== null && provenance !== undefined) {
27085
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
27086
+ }
27087
+ if (slug === null || slug === undefined)
27088
+ return null;
27089
+ if (slug !== null && slug !== undefined) {
27090
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
27091
+ }
27092
+ return null;
27093
+ }
26337
27094
  function timestamp2(value) {
26338
27095
  return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
26339
27096
  }
@@ -26341,6 +27098,41 @@ function fault(faults, point) {
26341
27098
  if (faults.points.has(point))
26342
27099
  throw new Error(`Injected task-manifest fault at ${point}`);
26343
27100
  }
27101
+ function terminalApplyResult(input, reason) {
27102
+ const receipt = {
27103
+ receipt_id: input.terminal_receipt_id,
27104
+ authority: "todos",
27105
+ route: "todos.task-manifest.v1",
27106
+ schema_version: 1,
27107
+ kind: "apply",
27108
+ operation_id: input.manifest.operation_id,
27109
+ step_id: input.manifest.step_id,
27110
+ idempotency_key: input.manifest.idempotency_key,
27111
+ request_digest: input.request_digest,
27112
+ precondition_digest: input.manifest.precondition_digest,
27113
+ result_digest: canonicalDigest({
27114
+ outcome: "terminal_nonacceptance",
27115
+ reason,
27116
+ operation_id: input.manifest.operation_id,
27117
+ step_id: input.manifest.step_id,
27118
+ request_digest: input.request_digest
27119
+ }),
27120
+ outcome: "terminal_nonacceptance",
27121
+ reason,
27122
+ duplicate_of_receipt_id: null,
27123
+ binding_version: 0,
27124
+ apply_receipt_id: null,
27125
+ created_at: input.now
27126
+ };
27127
+ return {
27128
+ duplicate: false,
27129
+ receipt,
27130
+ graph: input.graph,
27131
+ readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
27132
+ outbox_ids: [],
27133
+ result_digest: receipt.result_digest
27134
+ };
27135
+ }
26344
27136
  function receiptFromRow3(row) {
26345
27137
  return {
26346
27138
  receipt_id: String(row["receipt_id"]),
@@ -26349,9 +27141,14 @@ function receiptFromRow3(row) {
26349
27141
  schema_version: 1,
26350
27142
  kind: row["kind"],
26351
27143
  operation_id: String(row["operation_id"]),
27144
+ step_id: String(row["step_id"] ?? "legacy-apply"),
26352
27145
  idempotency_key: String(row["idempotency_key"]),
26353
27146
  request_digest: String(row["request_digest"]),
27147
+ precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
26354
27148
  result_digest: String(row["result_digest"]),
27149
+ outcome: row["outcome"] ?? "accepted",
27150
+ reason: row["reason"] == null ? null : row["reason"],
27151
+ duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
26355
27152
  binding_version: Number(row["binding_version"]),
26356
27153
  apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
26357
27154
  created_at: timestamp2(row["created_at"])
@@ -26469,46 +27266,89 @@ class PostgresTodosTaskManifestBackend {
26469
27266
  now3
26470
27267
  ]);
26471
27268
  }
27269
+ async persistTerminal(tx, input, reason) {
27270
+ const result = terminalApplyResult(input, reason);
27271
+ const resultJson = canonicalJson(result);
27272
+ await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
27273
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
27274
+ idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
27275
+ binding_version, apply_receipt_id, manifest_json, result_json, created_at
27276
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
27277
+ 'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
27278
+ ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
27279
+ result.receipt.receipt_id,
27280
+ this.tenantId,
27281
+ input.manifest.operation_id,
27282
+ input.manifest.step_id,
27283
+ input.manifest.idempotency_key,
27284
+ input.request_digest,
27285
+ input.manifest.precondition_digest,
27286
+ result.receipt.result_digest,
27287
+ reason,
27288
+ canonicalJson(input.manifest),
27289
+ resultJson,
27290
+ input.now
27291
+ ]);
27292
+ const stored = await tx.query(`SELECT receipt_id, result_json
27293
+ FROM todos_task_manifest_terminal_receipts
27294
+ WHERE tenant_id = $1 AND kind = 'apply'
27295
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
27296
+ ORDER BY created_at ASC, receipt_id ASC
27297
+ LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
27298
+ return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
27299
+ }
26472
27300
  async apply(input, faults) {
26473
27301
  await this.ensureSchema();
26474
27302
  return this.client.transaction(async (tx) => {
26475
27303
  const { manifest } = input;
26476
27304
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
26477
27305
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
27306
+ const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
27307
+ WHERE tenant_id = $1
27308
+ AND kind = 'apply'
27309
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
27310
+ ORDER BY created_at ASC, receipt_id ASC
27311
+ LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
27312
+ if (terminal.rows[0]) {
27313
+ return parseApplyResult(terminal.rows[0]["result_json"], true);
27314
+ }
26478
27315
  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
27316
  if (existing.rows[0]) {
26480
27317
  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");
27318
+ 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) {
27319
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
26483
27320
  }
26484
27321
  if (binding["state"] !== "applied") {
26485
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
27322
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
26486
27323
  }
26487
- return { ...parseJson(binding["result_json"]), duplicate: true };
27324
+ return parseApplyResult(binding["result_json"], true);
26488
27325
  }
26489
27326
  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
27327
  if (reused.rows[0])
26491
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
27328
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
27329
+ if (manifest.idempotency_key !== input.expected_idempotency_key) {
27330
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
27331
+ }
26492
27332
  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");
27333
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
26494
27334
  }
26495
27335
  const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
26496
27336
  WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
26497
27337
  if (!project.rows[0])
26498
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
27338
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
26499
27339
  if (manifest.task_list_id) {
26500
27340
  const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
26501
27341
  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
27342
  const payload = taskList.rows[0] ? parseJson(taskList.rows[0]["payload"]) : null;
26503
27343
  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");
27344
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
26505
27345
  }
26506
27346
  }
26507
27347
  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
27348
  const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
26509
27349
  WHERE service = $1 AND object_id IN (${placeholders(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
26510
27350
  if (conflict.rows[0])
26511
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "A deterministic graph id already exists");
27351
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
26512
27352
  await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
26513
27353
  fault(faults, "after_plan_write");
26514
27354
  for (const task2 of manifest.tasks) {
@@ -26578,9 +27418,14 @@ class PostgresTodosTaskManifestBackend {
26578
27418
  schema_version: 1,
26579
27419
  kind: "apply",
26580
27420
  operation_id: manifest.operation_id,
27421
+ step_id: manifest.step_id,
26581
27422
  idempotency_key: manifest.idempotency_key,
26582
27423
  request_digest: input.request_digest,
27424
+ precondition_digest: manifest.precondition_digest,
26583
27425
  result_digest: input.result_digest,
27426
+ outcome: "accepted",
27427
+ reason: null,
27428
+ duplicate_of_receipt_id: null,
26584
27429
  binding_version: 1,
26585
27430
  apply_receipt_id: null,
26586
27431
  created_at: input.now
@@ -26597,14 +27442,18 @@ class PostgresTodosTaskManifestBackend {
26597
27442
  const resultJson = canonicalJson(result);
26598
27443
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
26599
27444
  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)`, [
27445
+ step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
27446
+ reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
27447
+ ) 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
27448
  input.receipt_id,
26603
27449
  this.tenantId,
26604
27450
  manifest.operation_id,
26605
27451
  manifest.idempotency_key,
27452
+ manifest.step_id,
26606
27453
  input.request_digest,
27454
+ manifest.precondition_digest,
26607
27455
  input.result_digest,
27456
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
26608
27457
  manifestJson,
26609
27458
  resultJson,
26610
27459
  input.now
@@ -26623,14 +27472,18 @@ class PostgresTodosTaskManifestBackend {
26623
27472
  }
26624
27473
  fault(faults, "after_outbox_write");
26625
27474
  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)`, [
27475
+ operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
27476
+ result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
27477
+ state, version, created_at, updated_at
27478
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
26629
27479
  manifest.operation_id,
26630
27480
  this.tenantId,
27481
+ manifest.step_id,
26631
27482
  manifest.idempotency_key,
26632
27483
  input.request_digest,
27484
+ manifest.precondition_digest,
26633
27485
  input.result_digest,
27486
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
26634
27487
  input.receipt_id,
26635
27488
  manifestJson,
26636
27489
  resultJson,
@@ -26643,9 +27496,12 @@ class PostgresTodosTaskManifestBackend {
26643
27496
  async readExact(receiptId2) {
26644
27497
  await this.ensureSchema();
26645
27498
  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])
27499
+ if (result.rows[0])
27500
+ return parseApplyResult(result.rows[0]["result_json"], false);
27501
+ 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]);
27502
+ if (!terminal.rows[0])
26647
27503
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
26648
- return { ...parseJson(result.rows[0]["result_json"]), duplicate: false };
27504
+ return parseApplyResult(terminal.rows[0]["result_json"], false);
26649
27505
  }
26650
27506
  async lookupBindingByPlanId(planId) {
26651
27507
  await this.ensureSchema();
@@ -26656,6 +27512,7 @@ class PostgresTodosTaskManifestBackend {
26656
27512
  b.version AS binding_version,
26657
27513
  b.tenant_id AS binding_tenant_id,
26658
27514
  b.operation_id AS binding_operation_id,
27515
+ b.step_id AS binding_step_id,
26659
27516
  b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
26660
27517
  r.tenant_id AS receipt_tenant_id,
26661
27518
  r.authority AS receipt_authority,
@@ -26663,6 +27520,7 @@ class PostgresTodosTaskManifestBackend {
26663
27520
  r.schema_version AS receipt_schema_version,
26664
27521
  r.kind AS receipt_kind,
26665
27522
  r.operation_id AS receipt_operation_id,
27523
+ r.step_id AS receipt_step_id,
26666
27524
  r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
26667
27525
  FROM todos_task_manifest_bindings b
26668
27526
  LEFT JOIN todos_task_manifest_receipts r
@@ -26749,6 +27607,10 @@ class PostgresTodosTaskManifestBackend {
26749
27607
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
26750
27608
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
26751
27609
  }
27610
+ const appliedReceipt = receiptFromRow3(applyRow);
27611
+ 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"]) {
27612
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
27613
+ }
26752
27614
  if (binding["state"] !== "applied")
26753
27615
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
26754
27616
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -26763,12 +27625,18 @@ class PostgresTodosTaskManifestBackend {
26763
27625
  LIMIT 1`, [this.tenantId, input.receipt_id]);
26764
27626
  if (delivered.rows[0])
26765
27627
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
26766
- const applyResult = parseJson(applyRow["result_json"]);
27628
+ const applyResult = parseApplyResult(applyRow["result_json"], false);
26767
27629
  const manifest = parseJson(applyRow["manifest_json"]);
27630
+ const manifestRecord = manifest;
27631
+ const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
26768
27632
  const expectedEffects = [
26769
27633
  {
26770
27634
  topic: "todos.task-manifest.applied",
26771
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
27635
+ payload: {
27636
+ operation_id: manifest.operation_id,
27637
+ ...applyStepId ? { step_id: applyStepId } : {},
27638
+ project_id: manifest.project_id
27639
+ }
26772
27640
  },
26773
27641
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
26774
27642
  ];
@@ -26816,9 +27684,15 @@ class PostgresTodosTaskManifestBackend {
26816
27684
  }
26817
27685
  const appliedAt = receiptFromRow3(applyRow).created_at;
26818
27686
  const expectedPayloads = new Map;
27687
+ const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
27688
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
27689
+ LIMIT 1`, [this.service, applyResult.graph.plan_id]);
27690
+ const actualPlan = planRow.rows[0] ? parseJson(planRow.rows[0]["payload"]) : null;
27691
+ const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
27692
+ planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
26819
27693
  expectedPayloads.set(applyResult.graph.plan_id, {
26820
27694
  type: "plans",
26821
- payload: canonicalJson(planPayload({ manifest, graph: applyResult.graph, now: appliedAt }))
27695
+ payload: canonicalJson(planExpected)
26822
27696
  });
26823
27697
  for (const task2 of manifest.tasks)
26824
27698
  expectedPayloads.set(applyResult.graph.task_ids[task2.key], {
@@ -26918,14 +27792,17 @@ class PostgresTodosTaskManifestBackend {
26918
27792
  const readback = await this.readback(tx, applyResult.graph);
26919
27793
  const result = { duplicate: false, receipt, absent: true, readback };
26920
27794
  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)`, [
27795
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
27796
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
27797
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
27798
+ ) 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
27799
  compensationReceiptId,
26925
27800
  this.tenantId,
26926
27801
  receipt.operation_id,
27802
+ receipt.step_id,
26927
27803
  input.idempotency_key,
26928
27804
  requestDigest,
27805
+ input.precondition_digest,
26929
27806
  receipt.result_digest,
26930
27807
  receipt.binding_version,
26931
27808
  input.receipt_id,
@@ -26982,36 +27859,83 @@ function resolveTenantId(value) {
26982
27859
  }
26983
27860
  return tenantId;
26984
27861
  }
27862
+ function taskManifestRequestDigest(manifest) {
27863
+ const { idempotency_key: _idempotencyKey, ...request } = manifest;
27864
+ return canonicalDigest(request);
27865
+ }
27866
+ function taskManifestCompensationRequestDigest(request) {
27867
+ return canonicalDigest(request);
27868
+ }
27869
+ function deriveTodosTaskManifestApplyPreconditionDigest(input) {
27870
+ return canonicalDigest({
27871
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27872
+ direction: "apply",
27873
+ operation_id: input.operation_id,
27874
+ step_id: input.step_id,
27875
+ project_id: input.project_id,
27876
+ task_list_id: input.task_list_id ?? null,
27877
+ expected_binding_version: input.if_binding_version ?? 0
27878
+ });
27879
+ }
27880
+ function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
27881
+ return canonicalDigest({
27882
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27883
+ direction: "compensate",
27884
+ operation_id: input.operation_id,
27885
+ step_id: input.step_id,
27886
+ apply_receipt_id: input.receipt_id,
27887
+ expected_binding_version: input.if_binding_version
27888
+ });
27889
+ }
27890
+ function deriveTodosTaskManifestIdempotencyKey(input) {
27891
+ return `tmk_${canonicalDigest({
27892
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27893
+ ...input
27894
+ }).slice(0, 48)}`;
27895
+ }
26985
27896
  function normalize(input, now3) {
26986
27897
  const parsed = parseTodosTaskManifest(input);
26987
27898
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
26988
27899
  if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
26989
27900
  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
27901
  }
27902
+ const { idempotency_key: _idempotencyKey, ...request } = parsed;
27903
+ const request_digest = taskManifestRequestDigest(request);
26991
27904
  const manifest = sanitizeManifest(parsed);
27905
+ const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
27906
+ if (manifest.precondition_digest !== expectedPreconditionDigest) {
27907
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
27908
+ }
27909
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
27910
+ operation_id: manifest.operation_id,
27911
+ step_id: manifest.step_id,
27912
+ direction: "apply",
27913
+ target_selector: manifest.project_id,
27914
+ request_digest,
27915
+ precondition_digest: manifest.precondition_digest
27916
+ });
26992
27917
  const task_ids = Object.fromEntries(manifest.tasks.map((task2) => [
26993
27918
  task2.key,
26994
- deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task2.key)
27919
+ deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task2.key)
26995
27920
  ]));
26996
27921
  const graph = {
26997
- plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
27922
+ plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
26998
27923
  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)))),
27924
+ 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)))),
27925
+ 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
27926
  dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
27002
27927
  };
27003
- const request_digest = canonicalDigest(parsed);
27004
27928
  const effectInputs = [
27005
27929
  {
27006
27930
  topic: "todos.task-manifest.applied",
27007
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
27931
+ payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
27008
27932
  },
27009
27933
  ...manifest.effects ?? []
27010
27934
  ];
27011
27935
  const outbox = effectInputs.map((effect2, index) => {
27012
27936
  const payload = { ...effect2.payload };
27013
27937
  return {
27014
- id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
27938
+ id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
27015
27939
  topic: effect2.topic,
27016
27940
  payload,
27017
27941
  digest: canonicalDigest({ topic: effect2.topic, payload })
@@ -27021,11 +27945,14 @@ function normalize(input, now3) {
27021
27945
  return {
27022
27946
  manifest,
27023
27947
  request_digest,
27948
+ expected_idempotency_key: expectedIdempotencyKey,
27024
27949
  result_digest,
27025
- receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
27950
+ receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
27951
+ terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
27026
27952
  graph,
27027
27953
  outbox,
27028
- now: now3
27954
+ now: now3,
27955
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
27029
27956
  };
27030
27957
  }
27031
27958
  function sanitizeManifest(manifest) {
@@ -27083,6 +28010,10 @@ class PackageOwnedTodosTaskManifestAuthority {
27083
28010
  tenant_id: this.tenantId,
27084
28011
  backend: this.backend.kind,
27085
28012
  deterministic_ids: true,
28013
+ operation_step_identity: true,
28014
+ deterministic_idempotency_keys: true,
28015
+ terminal_nonacceptance_receipts: true,
28016
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
27086
28017
  immutable_receipts: true,
27087
28018
  transactional_outbox: true,
27088
28019
  idempotent_outbox_delivery: true,
@@ -27112,7 +28043,11 @@ class PackageOwnedTodosTaskManifestAuthority {
27112
28043
  async apply(input) {
27113
28044
  const normalized = normalize(input, this.now());
27114
28045
  const faults = await this.prepareFaults();
27115
- return this.bounded(await this.backend.apply(normalized, faults));
28046
+ const result = this.bounded(await this.backend.apply(normalized, faults));
28047
+ if (result.receipt.outcome === "terminal_nonacceptance") {
28048
+ throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
28049
+ }
28050
+ return result;
27116
28051
  }
27117
28052
  readExact(receiptId2) {
27118
28053
  if (!receiptId2 || receiptId2.length > 200) {
@@ -27145,18 +28080,48 @@ class PackageOwnedTodosTaskManifestAuthority {
27145
28080
  async compensate(input) {
27146
28081
  const request = parseTodosTaskManifestCompensation(input);
27147
28082
  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);
28083
+ if (applied.receipt.outcome !== "accepted") {
28084
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
28085
+ }
28086
+ if (request.operation_id !== applied.receipt.operation_id) {
28087
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
28088
+ }
28089
+ if (request.step_id === applied.receipt.step_id) {
28090
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
28091
+ }
28092
+ const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
28093
+ if (request.precondition_digest !== expectedPreconditionDigest) {
28094
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
28095
+ }
28096
+ const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
28097
+ const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
28098
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
28099
+ operation_id: request.operation_id,
28100
+ step_id: request.step_id,
28101
+ direction: "compensate",
28102
+ target_selector: request.receipt_id,
28103
+ request_digest: requestDigest,
28104
+ precondition_digest: request.precondition_digest
28105
+ });
28106
+ if (request.idempotency_key !== expectedIdempotencyKey) {
28107
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
28108
+ }
28109
+ const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
27150
28110
  const receipt = {
27151
28111
  receipt_id: compensationReceiptId,
27152
28112
  authority: "todos",
27153
28113
  route: TODOS_TASK_MANIFEST_ROUTE,
27154
28114
  schema_version: 1,
27155
28115
  kind: "compensate",
27156
- operation_id: applied.receipt.operation_id,
28116
+ operation_id: request.operation_id,
28117
+ step_id: request.step_id,
27157
28118
  idempotency_key: request.idempotency_key,
27158
28119
  request_digest: requestDigest,
28120
+ precondition_digest: request.precondition_digest,
27159
28121
  result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
28122
+ outcome: "accepted",
28123
+ reason: null,
28124
+ duplicate_of_receipt_id: null,
27160
28125
  binding_version: request.if_binding_version + 1,
27161
28126
  apply_receipt_id: applied.receipt.receipt_id,
27162
28127
  created_at: this.now()
@@ -29661,9 +30626,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29661
30626
  TaskManifestBounds: taskManifestBoundsSchema,
29662
30627
  TaskManifestCapability: taskManifestCapabilitySchema,
29663
30628
  TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
30629
+ TaskManifest: taskManifestSchema,
30630
+ TaskManifestReceipt: taskManifestReceiptSchema,
30631
+ TaskManifestApplyResult: taskManifestApplyResultSchema,
30632
+ TaskManifestApplyResponse: taskManifestApplyResponseSchema,
30633
+ TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
30634
+ TaskManifestCompensationResult: taskManifestCompensationResultSchema,
30635
+ TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
30636
+ TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
29664
30637
  TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
29665
30638
  TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
29666
30639
  TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
30640
+ ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
30641
+ ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
30642
+ ProjectRegistrationRequest: projectRegistrationRequestSchema,
30643
+ ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
30644
+ PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
30645
+ ProjectResource: projectResourceSchema,
30646
+ ProjectResourcePage: projectResourcePageSchema,
29667
30647
  TaskList: taskListSchema,
29668
30648
  ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
29669
30649
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
@@ -29706,6 +30686,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29706
30686
  priority: { type: "string", enum: [...TASK_PRIORITIES] },
29707
30687
  assigned_to: { type: "string" },
29708
30688
  project_id: { type: "string", nullable: true },
30689
+ parent_id: { type: "string", nullable: true },
29709
30690
  plan_id: { type: "string", nullable: true },
29710
30691
  task_list_id: { type: "string", nullable: true },
29711
30692
  version: { type: "number" }
@@ -30660,6 +31641,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30660
31641
  },
30661
31642
  security: [{ apiKey: [] }],
30662
31643
  paths: {
31644
+ "/v1/project-registration/capability": {
31645
+ get: {
31646
+ operationId: "getProjectRegistrationCapability",
31647
+ summary: "Read the live package-owned Projects to Todos registration capability",
31648
+ responses: {
31649
+ "200": {
31650
+ content: {
31651
+ "application/json": {
31652
+ schema: {
31653
+ type: "object",
31654
+ additionalProperties: false,
31655
+ required: ["capability"],
31656
+ properties: {
31657
+ capability: {
31658
+ $ref: "#/components/schemas/ProjectRegistrationCapability"
31659
+ }
31660
+ }
31661
+ }
31662
+ }
31663
+ }
31664
+ }
31665
+ }
31666
+ }
31667
+ },
31668
+ "/v1/project-registration/resources": {
31669
+ get: {
31670
+ operationId: "listProjectRegistrationResources",
31671
+ summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
31672
+ parameters: [
31673
+ {
31674
+ name: "source_project_id",
31675
+ in: "query",
31676
+ required: true,
31677
+ schema: { type: "string" }
31678
+ },
31679
+ {
31680
+ name: "include_anchors",
31681
+ in: "query",
31682
+ schema: { type: "boolean", default: false }
31683
+ },
31684
+ {
31685
+ name: "limit",
31686
+ in: "query",
31687
+ schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
31688
+ },
31689
+ {
31690
+ name: "cursor",
31691
+ in: "query",
31692
+ schema: { type: "string" }
31693
+ }
31694
+ ],
31695
+ responses: {
31696
+ "200": {
31697
+ content: {
31698
+ "application/json": {
31699
+ schema: {
31700
+ type: "object",
31701
+ additionalProperties: false,
31702
+ required: ["page"],
31703
+ properties: {
31704
+ page: { $ref: "#/components/schemas/ProjectResourcePage" }
31705
+ }
31706
+ }
31707
+ }
31708
+ }
31709
+ },
31710
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31711
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31712
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31713
+ }
31714
+ }
31715
+ },
31716
+ "/v1/project-registration/create": {
31717
+ post: {
31718
+ operationId: "createProjectRegistrationResource",
31719
+ summary: "Create or deterministically bind one Projects to Todos resource",
31720
+ requestBody: {
31721
+ required: true,
31722
+ content: {
31723
+ "application/json": {
31724
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31725
+ }
31726
+ }
31727
+ },
31728
+ responses: {
31729
+ "201": {
31730
+ content: {
31731
+ "application/json": {
31732
+ schema: {
31733
+ type: "object",
31734
+ additionalProperties: false,
31735
+ required: ["receipt"],
31736
+ properties: {
31737
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
31738
+ }
31739
+ }
31740
+ }
31741
+ }
31742
+ },
31743
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31744
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31745
+ }
31746
+ }
31747
+ },
31748
+ "/v1/project-registration/read-exact": {
31749
+ post: {
31750
+ operationId: "readExactProjectRegistrationResource",
31751
+ summary: "Read one registered project or task list by exact full UUID",
31752
+ requestBody: {
31753
+ required: true,
31754
+ content: {
31755
+ "application/json": {
31756
+ schema: {
31757
+ type: "object",
31758
+ additionalProperties: false,
31759
+ required: [
31760
+ "resource_kind",
31761
+ "target_id",
31762
+ "response_byte_limit",
31763
+ "time_budget_ms"
31764
+ ],
31765
+ properties: {
31766
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
31767
+ target_id: { type: "string", format: "uuid" },
31768
+ ...projectRegistrationBoundsProperties
31769
+ }
31770
+ }
31771
+ }
31772
+ }
31773
+ },
31774
+ responses: {
31775
+ "200": {
31776
+ content: {
31777
+ "application/json": {
31778
+ schema: {
31779
+ type: "object",
31780
+ additionalProperties: false,
31781
+ required: ["record"],
31782
+ properties: {
31783
+ record: {
31784
+ type: "object",
31785
+ additionalProperties: false,
31786
+ required: ["target_id", "revision", "digest"],
31787
+ properties: {
31788
+ target_id: { type: "string", format: "uuid" },
31789
+ revision: { type: "string" },
31790
+ digest: { type: "string" }
31791
+ }
31792
+ }
31793
+ }
31794
+ }
31795
+ }
31796
+ }
31797
+ },
31798
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31799
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31800
+ }
31801
+ }
31802
+ },
31803
+ "/v1/project-registration/receipts/lookup": {
31804
+ post: {
31805
+ operationId: "lookupProjectRegistrationReceipt",
31806
+ summary: "Recover one exact immutable terminal registration receipt",
31807
+ requestBody: {
31808
+ required: true,
31809
+ content: {
31810
+ "application/json": {
31811
+ schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
31812
+ }
31813
+ }
31814
+ },
31815
+ responses: {
31816
+ "200": {
31817
+ content: {
31818
+ "application/json": {
31819
+ schema: {
31820
+ type: "object",
31821
+ additionalProperties: false,
31822
+ required: ["receipt", "response_control"],
31823
+ properties: {
31824
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
31825
+ response_control: {
31826
+ type: "object",
31827
+ additionalProperties: false,
31828
+ required: [
31829
+ "response_byte_limit",
31830
+ "time_budget_ms",
31831
+ "response_bytes",
31832
+ "elapsed_ms",
31833
+ "complete",
31834
+ "truncated"
31835
+ ],
31836
+ properties: {
31837
+ ...projectRegistrationBoundsProperties,
31838
+ response_bytes: { type: "integer", minimum: 0 },
31839
+ elapsed_ms: { type: "integer", minimum: 0 },
31840
+ complete: { type: "boolean", enum: [true] },
31841
+ truncated: { type: "boolean", enum: [false] }
31842
+ }
31843
+ }
31844
+ }
31845
+ }
31846
+ }
31847
+ }
31848
+ },
31849
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31850
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31851
+ }
31852
+ }
31853
+ },
31854
+ "/v1/project-registration/validate-prior-adoption": {
31855
+ post: {
31856
+ operationId: "validatePriorRegistrationAdoption",
31857
+ summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
31858
+ requestBody: {
31859
+ required: true,
31860
+ content: {
31861
+ "application/json": {
31862
+ schema: {
31863
+ type: "object",
31864
+ additionalProperties: false,
31865
+ required: ["source_request", "source_receipt", "current_record"],
31866
+ properties: {
31867
+ source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
31868
+ source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
31869
+ current_record: {
31870
+ oneOf: [
31871
+ { $ref: "#/components/schemas/Project" },
31872
+ { $ref: "#/components/schemas/TaskList" }
31873
+ ]
31874
+ }
31875
+ }
31876
+ }
31877
+ }
31878
+ }
31879
+ },
31880
+ responses: {
31881
+ "200": {
31882
+ content: {
31883
+ "application/json": {
31884
+ schema: {
31885
+ type: "object",
31886
+ additionalProperties: false,
31887
+ required: ["validation"],
31888
+ properties: {
31889
+ validation: {
31890
+ $ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
31891
+ }
31892
+ }
31893
+ }
31894
+ }
31895
+ }
31896
+ },
31897
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31898
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31899
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31900
+ }
31901
+ }
31902
+ },
31903
+ "/v1/project-registration/compensate": {
31904
+ post: {
31905
+ operationId: "compensateProjectRegistrationResource",
31906
+ summary: "Conditionally remove an unchanged receipt-owned registration resource",
31907
+ requestBody: {
31908
+ required: true,
31909
+ content: {
31910
+ "application/json": {
31911
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31912
+ }
31913
+ }
31914
+ },
31915
+ responses: {
31916
+ "201": {
31917
+ content: {
31918
+ "application/json": {
31919
+ schema: {
31920
+ type: "object",
31921
+ additionalProperties: false,
31922
+ required: ["receipt"],
31923
+ properties: {
31924
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
31925
+ }
31926
+ }
31927
+ }
31928
+ }
31929
+ },
31930
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31931
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31932
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31933
+ }
31934
+ }
31935
+ },
31936
+ "/v1/project-registration/verify-inverse": {
31937
+ post: {
31938
+ operationId: "verifyInverseProjectRegistrationResource",
31939
+ summary: "Verify exact absence after conditional registration compensation",
31940
+ requestBody: {
31941
+ required: true,
31942
+ content: {
31943
+ "application/json": {
31944
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31945
+ }
31946
+ }
31947
+ },
31948
+ responses: {
31949
+ "200": {
31950
+ content: {
31951
+ "application/json": {
31952
+ schema: {
31953
+ type: "object",
31954
+ additionalProperties: false,
31955
+ required: ["verification"],
31956
+ properties: {
31957
+ verification: {
31958
+ type: "object",
31959
+ additionalProperties: false,
31960
+ required: ["target_id", "accepted_receipt_id", "absent", "digest"],
31961
+ properties: {
31962
+ target_id: { type: "string", format: "uuid" },
31963
+ accepted_receipt_id: { type: "string" },
31964
+ absent: { type: "boolean", enum: [true] },
31965
+ digest: { type: "string" }
31966
+ }
31967
+ }
31968
+ }
31969
+ }
31970
+ }
31971
+ }
31972
+ },
31973
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31974
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31975
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31976
+ }
31977
+ }
31978
+ },
30663
31979
  "/v1/task-manifest/capability": {
30664
31980
  get: {
30665
31981
  operationId: "getTaskManifestCapability",
@@ -30677,6 +31993,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30677
31993
  }
30678
31994
  }
30679
31995
  },
31996
+ "/v1/task-manifest/apply": {
31997
+ post: {
31998
+ operationId: "applyTaskManifest",
31999
+ summary: "Apply one exact task-manifest graph through the Todos authority",
32000
+ requestBody: {
32001
+ required: true,
32002
+ content: {
32003
+ "application/json": {
32004
+ schema: { $ref: "#/components/schemas/TaskManifest" }
32005
+ }
32006
+ }
32007
+ },
32008
+ responses: {
32009
+ "201": {
32010
+ content: {
32011
+ "application/json": {
32012
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
32013
+ }
32014
+ }
32015
+ },
32016
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32017
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32018
+ "503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32019
+ }
32020
+ }
32021
+ },
32022
+ "/v1/task-manifest/read-exact": {
32023
+ post: {
32024
+ operationId: "readExactTaskManifest",
32025
+ summary: "Read one exact immutable task-manifest apply receipt",
32026
+ requestBody: {
32027
+ required: true,
32028
+ content: {
32029
+ "application/json": {
32030
+ schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
32031
+ }
32032
+ }
32033
+ },
32034
+ responses: {
32035
+ "200": {
32036
+ content: {
32037
+ "application/json": {
32038
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
32039
+ }
32040
+ }
32041
+ },
32042
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32043
+ }
32044
+ }
32045
+ },
32046
+ "/v1/task-manifest/compensate": {
32047
+ post: {
32048
+ operationId: "compensateTaskManifest",
32049
+ summary: "Compensate one exact untouched task-manifest graph with CAS protection",
32050
+ requestBody: {
32051
+ required: true,
32052
+ content: {
32053
+ "application/json": {
32054
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
32055
+ }
32056
+ }
32057
+ },
32058
+ responses: {
32059
+ "201": {
32060
+ content: {
32061
+ "application/json": {
32062
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
32063
+ }
32064
+ }
32065
+ },
32066
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32067
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32068
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32069
+ }
32070
+ }
32071
+ },
30680
32072
  "/v1/task-manifest/bindings/lookup": {
30681
32073
  post: {
30682
32074
  operationId: "lookupTaskManifestBinding",
@@ -31537,7 +32929,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
31537
32929
  }
31538
32930
  });
31539
32931
  }
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;
32932
+ 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
32933
  var init_openapi = __esm(() => {
31542
32934
  init_package_version();
31543
32935
  init_types();
@@ -31599,6 +32991,10 @@ var init_openapi = __esm(() => {
31599
32991
  "tenant_id",
31600
32992
  "backend",
31601
32993
  "deterministic_ids",
32994
+ "operation_step_identity",
32995
+ "deterministic_idempotency_keys",
32996
+ "terminal_nonacceptance_receipts",
32997
+ "plan_slug_provenance",
31602
32998
  "immutable_receipts",
31603
32999
  "transactional_outbox",
31604
33000
  "idempotent_outbox_delivery",
@@ -31614,6 +33010,10 @@ var init_openapi = __esm(() => {
31614
33010
  tenant_id: { type: "string", minLength: 1, maxLength: 200 },
31615
33011
  backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
31616
33012
  deterministic_ids: { type: "boolean", enum: [true] },
33013
+ operation_step_identity: { type: "boolean", enum: [true] },
33014
+ deterministic_idempotency_keys: { type: "boolean", enum: [true] },
33015
+ terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
33016
+ plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
31617
33017
  immutable_receipts: { type: "boolean", enum: [true] },
31618
33018
  transactional_outbox: { type: "boolean", enum: [true] },
31619
33019
  idempotent_outbox_delivery: { type: "boolean", enum: [true] },
@@ -31667,6 +33067,8 @@ var init_openapi = __esm(() => {
31667
33067
  "schema_version",
31668
33068
  "tenant_id",
31669
33069
  "plan_id",
33070
+ "operation_id",
33071
+ "step_id",
31670
33072
  "apply_receipt_id",
31671
33073
  "binding_version",
31672
33074
  "state"
@@ -31677,11 +33079,169 @@ var init_openapi = __esm(() => {
31677
33079
  schema_version: { type: "integer", enum: [1] },
31678
33080
  tenant_id: { type: "string" },
31679
33081
  plan_id: { type: "string", format: "uuid" },
33082
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
33083
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
31680
33084
  apply_receipt_id: { type: "string", format: "uuid" },
31681
33085
  binding_version: { type: "integer", minimum: 1 },
31682
33086
  state: { type: "string", enum: ["applied", "compensated"] }
31683
33087
  }
31684
33088
  };
33089
+ taskManifestSchema = {
33090
+ type: "object",
33091
+ additionalProperties: false,
33092
+ required: [
33093
+ "version",
33094
+ "operation_id",
33095
+ "step_id",
33096
+ "idempotency_key",
33097
+ "precondition_digest",
33098
+ "project_id",
33099
+ "plan",
33100
+ "tasks"
33101
+ ],
33102
+ properties: {
33103
+ version: { type: "integer", enum: [1] },
33104
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
33105
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
33106
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
33107
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33108
+ project_id: { type: "string", format: "uuid" },
33109
+ task_list_id: { type: "string", format: "uuid" },
33110
+ if_binding_version: { type: "integer", minimum: 0 },
33111
+ plan: {
33112
+ type: "object",
33113
+ additionalProperties: false,
33114
+ required: ["key", "name"],
33115
+ properties: {
33116
+ key: { type: "string", minLength: 1, maxLength: 200 },
33117
+ name: { type: "string", minLength: 1, maxLength: 200 },
33118
+ description: { type: "string" },
33119
+ status: { type: "string", enum: ["active", "completed", "archived"] }
33120
+ }
33121
+ },
33122
+ tasks: {
33123
+ type: "array",
33124
+ minItems: 1,
33125
+ items: {
33126
+ type: "object",
33127
+ additionalProperties: false,
33128
+ required: ["key", "title"],
33129
+ properties: {
33130
+ key: { type: "string", minLength: 1, maxLength: 200 },
33131
+ title: { type: "string", minLength: 1, maxLength: 200 },
33132
+ description: { type: "string" },
33133
+ status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
33134
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
33135
+ assigned_to: { type: "string" },
33136
+ created_by: { type: "string" },
33137
+ tags: { type: "array", items: { type: "string" } },
33138
+ metadata: { type: "object", additionalProperties: true },
33139
+ comments: { type: "array", items: { type: "object", additionalProperties: true } },
33140
+ verifications: { type: "array", items: { type: "object", additionalProperties: true } }
33141
+ }
33142
+ }
33143
+ },
33144
+ dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
33145
+ effects: { type: "array", items: { type: "object", additionalProperties: true } }
33146
+ }
33147
+ };
33148
+ taskManifestReceiptSchema = {
33149
+ type: "object",
33150
+ additionalProperties: false,
33151
+ required: [
33152
+ "receipt_id",
33153
+ "authority",
33154
+ "route",
33155
+ "schema_version",
33156
+ "kind",
33157
+ "operation_id",
33158
+ "step_id",
33159
+ "idempotency_key",
33160
+ "request_digest",
33161
+ "precondition_digest",
33162
+ "result_digest",
33163
+ "outcome",
33164
+ "reason",
33165
+ "duplicate_of_receipt_id",
33166
+ "binding_version",
33167
+ "apply_receipt_id",
33168
+ "created_at"
33169
+ ],
33170
+ properties: {
33171
+ receipt_id: { type: "string", format: "uuid" },
33172
+ authority: { type: "string", enum: ["todos"] },
33173
+ route: { type: "string", enum: ["todos.task-manifest.v1"] },
33174
+ schema_version: { type: "integer", enum: [1] },
33175
+ kind: { type: "string", enum: ["apply", "compensate"] },
33176
+ operation_id: { type: "string" },
33177
+ step_id: { type: "string" },
33178
+ idempotency_key: { type: "string" },
33179
+ request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33180
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33181
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33182
+ outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
33183
+ reason: { type: "string", nullable: true },
33184
+ duplicate_of_receipt_id: { type: "string", nullable: true },
33185
+ binding_version: { type: "integer", minimum: 0 },
33186
+ apply_receipt_id: { type: "string", nullable: true },
33187
+ created_at: { type: "string", format: "date-time" }
33188
+ }
33189
+ };
33190
+ taskManifestApplyResultSchema = {
33191
+ type: "object",
33192
+ additionalProperties: false,
33193
+ required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
33194
+ properties: {
33195
+ duplicate: { type: "boolean" },
33196
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
33197
+ graph: { type: "object", additionalProperties: true },
33198
+ readback: { type: "object", additionalProperties: true },
33199
+ outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
33200
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
33201
+ }
33202
+ };
33203
+ taskManifestApplyResponseSchema = {
33204
+ type: "object",
33205
+ additionalProperties: false,
33206
+ required: ["result"],
33207
+ properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
33208
+ };
33209
+ taskManifestCompensateRequestSchema = {
33210
+ type: "object",
33211
+ additionalProperties: false,
33212
+ required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
33213
+ properties: {
33214
+ receipt_id: { type: "string", format: "uuid" },
33215
+ operation_id: { type: "string" },
33216
+ step_id: { type: "string" },
33217
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
33218
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33219
+ if_binding_version: { type: "integer", minimum: 1 }
33220
+ }
33221
+ };
33222
+ taskManifestCompensationResultSchema = {
33223
+ type: "object",
33224
+ additionalProperties: false,
33225
+ required: ["duplicate", "receipt", "absent", "readback"],
33226
+ properties: {
33227
+ duplicate: { type: "boolean" },
33228
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
33229
+ absent: { type: "boolean", enum: [true] },
33230
+ readback: { type: "object", additionalProperties: true }
33231
+ }
33232
+ };
33233
+ taskManifestCompensateResponseSchema = {
33234
+ type: "object",
33235
+ additionalProperties: false,
33236
+ required: ["result"],
33237
+ properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
33238
+ };
33239
+ taskManifestReadExactRequestSchema = {
33240
+ type: "object",
33241
+ additionalProperties: false,
33242
+ required: ["receipt_id"],
33243
+ properties: { receipt_id: { type: "string", format: "uuid" } }
33244
+ };
31685
33245
  taskManifestBindingLookupResponseSchema = {
31686
33246
  type: "object",
31687
33247
  additionalProperties: false,
@@ -32031,6 +33591,292 @@ var init_openapi = __esm(() => {
32031
33591
  metadata: { type: "object", additionalProperties: true }
32032
33592
  }
32033
33593
  };
33594
+ projectRegistrationBoundsProperties = {
33595
+ response_byte_limit: { type: "integer", minimum: 1 },
33596
+ time_budget_ms: { type: "integer", minimum: 1 }
33597
+ };
33598
+ projectRegistrationReceiptSchema = {
33599
+ type: "object",
33600
+ additionalProperties: false,
33601
+ required: [
33602
+ "receipt_id",
33603
+ "authority",
33604
+ "route",
33605
+ "package_version",
33606
+ "authority_id",
33607
+ "tenant_id",
33608
+ "corpus_id",
33609
+ "operation_id",
33610
+ "step_id",
33611
+ "resource_kind",
33612
+ "direction",
33613
+ "idempotency_key",
33614
+ "request_digest",
33615
+ "precondition_digest",
33616
+ "outcome",
33617
+ "reason",
33618
+ "target_id",
33619
+ "result_revision",
33620
+ "result_digest",
33621
+ "duplicate_of_receipt_id",
33622
+ "accepted_receipt_id",
33623
+ "created_by_operation",
33624
+ "created_at"
33625
+ ],
33626
+ properties: {
33627
+ receipt_id: { type: "string" },
33628
+ authority: { type: "string", enum: ["todos"] },
33629
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33630
+ package_version: { type: "string" },
33631
+ authority_id: { type: "string" },
33632
+ tenant_id: { type: "string" },
33633
+ corpus_id: { type: "string" },
33634
+ operation_id: { type: "string" },
33635
+ step_id: { type: "string" },
33636
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33637
+ direction: { type: "string", enum: ["forward", "inverse"] },
33638
+ idempotency_key: { type: "string" },
33639
+ request_digest: { type: "string" },
33640
+ precondition_digest: { type: "string" },
33641
+ outcome: {
33642
+ type: "string",
33643
+ enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
33644
+ },
33645
+ reason: { type: "string", nullable: true },
33646
+ target_id: { type: "string", format: "uuid", nullable: true },
33647
+ result_revision: { type: "string", nullable: true },
33648
+ result_digest: { type: "string", nullable: true },
33649
+ duplicate_of_receipt_id: { type: "string", nullable: true },
33650
+ accepted_receipt_id: { type: "string", nullable: true },
33651
+ created_by_operation: { type: "boolean" },
33652
+ created_at: { type: "string", format: "date-time" }
33653
+ }
33654
+ };
33655
+ projectRegistrationCapabilitySchema = {
33656
+ type: "object",
33657
+ additionalProperties: false,
33658
+ required: [
33659
+ "authority",
33660
+ "route",
33661
+ "package_version",
33662
+ "authority_id",
33663
+ "tenant_id",
33664
+ "corpus_id",
33665
+ "supported_resources",
33666
+ "conditional_create",
33667
+ "immutable_receipts",
33668
+ "exact_terminal_lookup",
33669
+ "exact_readback",
33670
+ "bind_existing_adoption",
33671
+ "prior_registration_adoption_validation",
33672
+ "project_resource_enumeration",
33673
+ "project_resource_page_limit",
33674
+ "conditional_inverse",
33675
+ "ambiguous_outcome_reconciliation"
33676
+ ],
33677
+ properties: {
33678
+ authority: { type: "string", enum: ["todos"] },
33679
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33680
+ package_version: { type: "string" },
33681
+ authority_id: { type: "string" },
33682
+ tenant_id: { type: "string" },
33683
+ corpus_id: { type: "string" },
33684
+ supported_resources: {
33685
+ type: "array",
33686
+ items: { type: "string", enum: ["project", "task_list"] }
33687
+ },
33688
+ conditional_create: { type: "boolean", enum: [true] },
33689
+ immutable_receipts: { type: "boolean", enum: [true] },
33690
+ exact_terminal_lookup: { type: "boolean", enum: [true] },
33691
+ exact_readback: { type: "boolean", enum: [true] },
33692
+ bind_existing_adoption: { type: "boolean", enum: [true] },
33693
+ prior_registration_adoption_validation: { type: "boolean", enum: [true] },
33694
+ project_resource_enumeration: { type: "boolean", enum: [true] },
33695
+ project_resource_page_limit: { type: "integer", minimum: 1 },
33696
+ conditional_inverse: { type: "boolean", enum: [true] },
33697
+ ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
33698
+ }
33699
+ };
33700
+ projectRegistrationRequestSchema = {
33701
+ type: "object",
33702
+ additionalProperties: false,
33703
+ required: [
33704
+ "operation_id",
33705
+ "step_id",
33706
+ "resource_kind",
33707
+ "direction",
33708
+ "authority_route",
33709
+ "package_version",
33710
+ "authority_id",
33711
+ "tenant_id",
33712
+ "corpus_id",
33713
+ "target_selector",
33714
+ "idempotency_key",
33715
+ "request_digest",
33716
+ "precondition_digest",
33717
+ "project_id",
33718
+ "project_slug",
33719
+ "project_name",
33720
+ "desired",
33721
+ "response_byte_limit",
33722
+ "time_budget_ms"
33723
+ ],
33724
+ properties: {
33725
+ operation_id: { type: "string" },
33726
+ step_id: { type: "string" },
33727
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33728
+ direction: { type: "string", enum: ["forward", "inverse"] },
33729
+ authority_route: { type: "string" },
33730
+ package_version: { type: "string" },
33731
+ authority_id: { type: "string" },
33732
+ tenant_id: { type: "string" },
33733
+ corpus_id: { type: "string" },
33734
+ target_selector: { type: "string" },
33735
+ idempotency_key: { type: "string" },
33736
+ request_digest: { type: "string" },
33737
+ precondition_digest: { type: "string" },
33738
+ project_id: { type: "string" },
33739
+ project_slug: { type: "string" },
33740
+ project_name: { type: "string" },
33741
+ desired: { type: "object", additionalProperties: true },
33742
+ bind_existing: { type: "boolean" },
33743
+ accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
33744
+ ...projectRegistrationBoundsProperties
33745
+ }
33746
+ };
33747
+ projectRegistrationLookupRequestSchema = {
33748
+ type: "object",
33749
+ additionalProperties: false,
33750
+ required: [
33751
+ "operation_id",
33752
+ "step_id",
33753
+ "resource_kind",
33754
+ "direction",
33755
+ "authority",
33756
+ "authority_route",
33757
+ "package_version",
33758
+ "authority_id",
33759
+ "tenant_id",
33760
+ "corpus_id",
33761
+ "target_selector",
33762
+ "idempotency_key",
33763
+ "max_items",
33764
+ "response_byte_limit",
33765
+ "time_budget_ms"
33766
+ ],
33767
+ properties: {
33768
+ operation_id: { type: "string" },
33769
+ step_id: { type: "string" },
33770
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33771
+ direction: { type: "string", enum: ["forward", "inverse"] },
33772
+ authority: { type: "string", enum: ["todos"] },
33773
+ authority_route: { type: "string" },
33774
+ package_version: { type: "string" },
33775
+ authority_id: { type: "string" },
33776
+ tenant_id: { type: "string" },
33777
+ corpus_id: { type: "string" },
33778
+ target_selector: { type: "string" },
33779
+ idempotency_key: { type: "string" },
33780
+ target_id: { type: "string", format: "uuid" },
33781
+ max_items: { type: "integer", enum: [1] },
33782
+ ...projectRegistrationBoundsProperties
33783
+ }
33784
+ };
33785
+ priorRegistrationAdoptionValidationSchema = {
33786
+ type: "object",
33787
+ additionalProperties: false,
33788
+ required: [
33789
+ "valid",
33790
+ "resource_kind",
33791
+ "target_id",
33792
+ "source_receipt_id",
33793
+ "accepted_receipt_id",
33794
+ "source_outcome",
33795
+ "created_at",
33796
+ "current_revision",
33797
+ "accepted_result_digest"
33798
+ ],
33799
+ properties: {
33800
+ valid: { type: "boolean", enum: [true] },
33801
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33802
+ target_id: { type: "string", format: "uuid" },
33803
+ source_receipt_id: { type: "string" },
33804
+ accepted_receipt_id: { type: "string" },
33805
+ source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
33806
+ created_at: { type: "string", format: "date-time" },
33807
+ current_revision: { type: "string", format: "date-time" },
33808
+ accepted_result_digest: { type: "string" }
33809
+ }
33810
+ };
33811
+ projectResourceSchema = {
33812
+ type: "object",
33813
+ additionalProperties: false,
33814
+ required: [
33815
+ "source_project_id",
33816
+ "kind",
33817
+ "scope",
33818
+ "target_id",
33819
+ "parent_id",
33820
+ "revision",
33821
+ "digest"
33822
+ ],
33823
+ properties: {
33824
+ source_project_id: { type: "string" },
33825
+ kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
33826
+ scope: { type: "string", enum: ["collection", "resource"] },
33827
+ target_id: { type: "string", format: "uuid" },
33828
+ parent_id: { type: "string", format: "uuid", nullable: true },
33829
+ revision: { type: "string" },
33830
+ digest: { type: "string" }
33831
+ }
33832
+ };
33833
+ projectResourcePageSchema = {
33834
+ type: "object",
33835
+ additionalProperties: false,
33836
+ required: [
33837
+ "authority",
33838
+ "route",
33839
+ "package_version",
33840
+ "authority_id",
33841
+ "tenant_id",
33842
+ "corpus_id",
33843
+ "source_project_id",
33844
+ "todos_project_id",
33845
+ "task_list_id",
33846
+ "include_anchors",
33847
+ "collection_revision",
33848
+ "limit",
33849
+ "count",
33850
+ "resources",
33851
+ "has_more",
33852
+ "next_cursor",
33853
+ "complete",
33854
+ "truncated"
33855
+ ],
33856
+ properties: {
33857
+ authority: { type: "string", enum: ["todos"] },
33858
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33859
+ package_version: { type: "string" },
33860
+ authority_id: { type: "string" },
33861
+ tenant_id: { type: "string" },
33862
+ corpus_id: { type: "string" },
33863
+ source_project_id: { type: "string" },
33864
+ todos_project_id: { type: "string", format: "uuid" },
33865
+ task_list_id: { type: "string", format: "uuid" },
33866
+ include_anchors: { type: "boolean" },
33867
+ collection_revision: { type: "string" },
33868
+ limit: { type: "integer", minimum: 1, maximum: 500 },
33869
+ count: { type: "integer", minimum: 0 },
33870
+ resources: {
33871
+ type: "array",
33872
+ items: { $ref: "#/components/schemas/ProjectResource" }
33873
+ },
33874
+ has_more: { type: "boolean" },
33875
+ next_cursor: { type: "string", nullable: true },
33876
+ complete: { type: "boolean" },
33877
+ truncated: { type: "boolean", enum: [false] }
33878
+ }
33879
+ };
32034
33880
  });
32035
33881
 
32036
33882
  // src/server/pr-groups.ts
@@ -32177,11 +34023,11 @@ function canonicalJson2(value) {
32177
34023
  return `[${value.map(canonicalJson2).join(",")}]`;
32178
34024
  return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
32179
34025
  }
32180
- function digest(value) {
34026
+ function digest2(value) {
32181
34027
  return createHash9("sha256").update(canonicalJson2(value)).digest("hex");
32182
34028
  }
32183
34029
  function deriveIdempotencyKey(projectId, slug) {
32184
- return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
34030
+ return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
32185
34031
  }
32186
34032
  function normalizeIdempotencyKey(value, projectId, slug) {
32187
34033
  const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
@@ -32190,13 +34036,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
32190
34036
  }
32191
34037
  return key2;
32192
34038
  }
32193
- function receiptId2(projectId, slug, idempotencyKey) {
32194
- return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
34039
+ function receiptId2(projectId, slug, idempotencyKey2) {
34040
+ return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
32195
34041
  }
32196
34042
  function semanticListDigest(list) {
32197
34043
  const metadata = { ...list.metadata ?? {} };
32198
34044
  delete metadata[RECEIPT_METADATA_KEY];
32199
- return digest({
34045
+ return digest2({
32200
34046
  project_id: list.project_id,
32201
34047
  slug: list.slug,
32202
34048
  name: list.name,
@@ -32213,10 +34059,10 @@ function storedMarker(list) {
32213
34059
  return null;
32214
34060
  return marker;
32215
34061
  }
32216
- function receiptFor(store, project, list, idempotencyKey) {
34062
+ function receiptFor(store, project, list, idempotencyKey2) {
32217
34063
  const marker = storedMarker(list);
32218
34064
  const owned = marker?.project_id === project.id && marker.slug === list.slug;
32219
- if (owned && marker.idempotency_key !== idempotencyKey) {
34065
+ if (owned && marker.idempotency_key !== idempotencyKey2) {
32220
34066
  throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
32221
34067
  project_id: project.id,
32222
34068
  task_list_id: list.id,
@@ -32225,8 +34071,8 @@ function receiptFor(store, project, list, idempotencyKey) {
32225
34071
  }
32226
34072
  return {
32227
34073
  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,
34074
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
34075
+ idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
32230
34076
  project_id: project.id,
32231
34077
  task_list_id: list.id,
32232
34078
  slug: list.slug,
@@ -32278,20 +34124,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32278
34124
  });
32279
34125
  }
32280
34126
  const slug = project.task_list_id;
32281
- const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
34127
+ const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
32282
34128
  if (state.scoped) {
32283
34129
  return {
32284
34130
  mode: "apply",
32285
34131
  action: "already_present",
32286
34132
  project,
32287
34133
  task_list: state.scoped,
32288
- receipt: receiptFor(store, project, state.scoped, idempotencyKey)
34134
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
32289
34135
  };
32290
34136
  }
32291
34137
  const marker = {
32292
34138
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
32293
- receipt_id: receiptId2(project.id, slug, idempotencyKey),
32294
- idempotency_key: idempotencyKey,
34139
+ receipt_id: receiptId2(project.id, slug, idempotencyKey2),
34140
+ idempotency_key: idempotencyKey2,
32295
34141
  project_id: project.id,
32296
34142
  slug,
32297
34143
  result_digest: semanticListDigest({
@@ -32329,7 +34175,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32329
34175
  action: "already_present",
32330
34176
  project: raced.project,
32331
34177
  task_list: raced.scoped,
32332
- receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
34178
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
32333
34179
  };
32334
34180
  }
32335
34181
  const projectReadback = await store.projects.get(project.id);
@@ -32359,7 +34205,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32359
34205
  action: "created",
32360
34206
  project: projectReadback,
32361
34207
  task_list: readback,
32362
- receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
34208
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
32363
34209
  };
32364
34210
  }
32365
34211
  async function rollbackProjectTaskListEnsure(store, projectId, options) {
@@ -32404,7 +34250,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
32404
34250
  project_id: project.id,
32405
34251
  task_list_id: list.id,
32406
34252
  accepted_receipt_id: options.receipt_id,
32407
- rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
34253
+ rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
32408
34254
  removed_at: new Date().toISOString()
32409
34255
  };
32410
34256
  }
@@ -32603,6 +34449,9 @@ function validateTaskPatchVocabulary(value) {
32603
34449
  if (!parsed.ok)
32604
34450
  return { ok: false, message: parsed.message };
32605
34451
  }
34452
+ if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
34453
+ return { ok: false, message: "parent_id must be a non-empty task id or null" };
34454
+ }
32606
34455
  return { ok: true, patch: body2 };
32607
34456
  }
32608
34457
  function validateProjectPatch(value) {
@@ -33087,8 +34936,8 @@ async function handleV1Request(req, url, dependencies = {}) {
33087
34936
  }
33088
34937
  const created = await store.tasks.create(body2, storageContext);
33089
34938
  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" });
34939
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null) || body2.created_by !== undefined && persisted.created_by !== body2.created_by) {
34940
+ return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, plan_id, and explicit created_by", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
33092
34941
  }
33093
34942
  return json5({ task: persisted }, 201);
33094
34943
  }
@@ -33962,6 +35811,15 @@ async function handleV1Request(req, url, dependencies = {}) {
33962
35811
  if (e instanceof TaskNotFoundError) {
33963
35812
  return error(404, e.message, { code: TaskNotFoundError.code });
33964
35813
  }
35814
+ if (e instanceof VersionConflictError) {
35815
+ return error(409, e.message, {
35816
+ code: VersionConflictError.code,
35817
+ conflict: true,
35818
+ task_id: e.taskId,
35819
+ expected_version: e.expectedVersion,
35820
+ current_version: e.actualVersion
35821
+ });
35822
+ }
33965
35823
  if (e instanceof StaleLockHandoffError) {
33966
35824
  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
35825
  return error(status2, e.message, {
@@ -52577,16 +54435,16 @@ function createHasnaStorageClient(name, transport) {
52577
54435
  }
52578
54436
  },
52579
54437
  async create(resource, body2, options = {}) {
52580
- const { idempotencyKey, ...rest } = options;
54438
+ const { idempotencyKey: idempotencyKey2, ...rest } = options;
52581
54439
  return transport.post(resourcePath(resource), body2, {
52582
54440
  ...rest,
52583
- idempotencyKey: idempotencyKey ?? newIdempotencyKey()
54441
+ idempotencyKey: idempotencyKey2 ?? newIdempotencyKey()
52584
54442
  });
52585
54443
  },
52586
54444
  async update(resource, id, patch, options = {}) {
52587
- const { method = "PATCH", idempotencyKey, ...rest } = options;
54445
+ const { method = "PATCH", idempotencyKey: idempotencyKey2, ...rest } = options;
52588
54446
  const call = method === "PUT" ? transport.put : transport.patch;
52589
- return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
54447
+ return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey2 ? { idempotencyKey: idempotencyKey2 } : {} });
52590
54448
  },
52591
54449
  async delete(resource, id, options = {}) {
52592
54450
  try {
@@ -64770,6 +66628,11 @@ var init_http_client = __esm(() => {
64770
66628
  ]);
64771
66629
  });
64772
66630
 
66631
+ // src/project-registration/page-validation.ts
66632
+ var init_page_validation = __esm(() => {
66633
+ init_types3();
66634
+ });
66635
+
64773
66636
  // src/cli/cloud-router.ts
64774
66637
  import { randomUUID as randomUUID4 } from "crypto";
64775
66638
  import { resolve as resolvePath } from "path";
@@ -64989,7 +66852,7 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
64989
66852
  return await request();
64990
66853
  } catch (error3) {
64991
66854
  const status2 = error3 && typeof error3 === "object" ? error3.status : undefined;
64992
- if (status2 === 404) {
66855
+ if (status2 === 404 || status2 === 405) {
64993
66856
  const body2 = error3 && typeof error3 === "object" ? error3.body : undefined;
64994
66857
  const code = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.code : undefined;
64995
66858
  if (typeof code === "string" && recognized404Codes.includes(code))
@@ -65168,12 +67031,39 @@ async function cloudListTasks(client, filter = {}) {
65168
67031
  union3.sort(compareCloudTaskOrder);
65169
67032
  return union3.slice(start, windowEnd);
65170
67033
  }
67034
+ async function cloudResolveTaskRef(client, ref) {
67035
+ const input = ref.trim().toLowerCase();
67036
+ if (!input)
67037
+ throw new Error("Task reference must not be empty");
67038
+ if (UUID_RE.test(input))
67039
+ return input;
67040
+ let task2;
67041
+ try {
67042
+ task2 = await cloudGetTask(client, input);
67043
+ } catch (error3) {
67044
+ const status2 = error3 && typeof error3 === "object" ? error3.status : undefined;
67045
+ if (status2 === 409) {
67046
+ const body2 = error3 && typeof error3 === "object" ? error3.body : undefined;
67047
+ const authorityMessage = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.error : undefined;
67048
+ throw new Error(typeof authorityMessage === "string" && authorityMessage.trim() ? authorityMessage : `Task reference is ambiguous: "${ref}"`);
67049
+ }
67050
+ throw error3;
67051
+ }
67052
+ if (task2 && typeof task2.id === "string" && (task2.short_id?.toLowerCase() === input || task2.id.toLowerCase().startsWith(input))) {
67053
+ return task2.id;
67054
+ }
67055
+ if (task2) {
67056
+ 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.");
67057
+ }
67058
+ 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.");
67059
+ }
65171
67060
  async function cloudGetTask(client, id) {
65172
67061
  const raw = await client.get("tasks", id);
65173
67062
  return raw == null ? null : unwrapTask(raw);
65174
67063
  }
65175
67064
  async function cloudCreateTask(client, input, verification2 = {}) {
65176
67065
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
67066
+ const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
65177
67067
  const expectedCreatedBy = typeof verification2.expectedCreatedBy === "string" && verification2.expectedCreatedBy.trim() ? verification2.expectedCreatedBy : null;
65178
67068
  if (expectedCreatedBy !== null) {
65179
67069
  await requireTaskCreatorCapability(client);
@@ -65186,9 +67076,9 @@ async function cloudCreateTask(client, input, verification2 = {}) {
65186
67076
  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
67077
  }
65188
67078
  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) {
67079
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
65190
67080
  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`);
67081
+ 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
67082
  }
65193
67083
  return persisted;
65194
67084
  }
@@ -65457,6 +67347,8 @@ var init_cloud_router = __esm(() => {
65457
67347
  init_redaction();
65458
67348
  init_plan_project_link_contract();
65459
67349
  init_http_client();
67350
+ init_adoption_validation();
67351
+ init_page_validation();
65460
67352
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
65461
67353
  TRANSPORT_TOKENS = {
65462
67354
  sqlite: "sqlite",
@@ -65493,6 +67385,7 @@ function registerTaskCrudTools(server, ctx) {
65493
67385
  compact["version"] = task2.version;
65494
67386
  compact["created_at"] = task2.created_at;
65495
67387
  compact["task_list_id"] = task2.task_list_id;
67388
+ compact["parent_id"] = task2.parent_id;
65496
67389
  return compactJson(compact);
65497
67390
  }
65498
67391
  function versionFor(taskId, version2) {
@@ -65762,6 +67655,7 @@ ${task2.description}` : null
65762
67655
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
65763
67656
  assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
65764
67657
  project_id: exports_external.string().nullable().optional(),
67658
+ parent_id: exports_external.string().nullable().optional().describe("Existing parent task ID/reference, null to detach"),
65765
67659
  task_list_id: exports_external.string().nullable().optional(),
65766
67660
  depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
65767
67661
  tags: exports_external.array(exports_external.string()).optional(),
@@ -65793,6 +67687,9 @@ ${task2.description}` : null
65793
67687
  if (typeof patch.project_id === "string" && patch.project_id) {
65794
67688
  patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
65795
67689
  }
67690
+ if (typeof patch.parent_id === "string" && patch.parent_id) {
67691
+ patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
67692
+ }
65796
67693
  if (typeof patch.task_list_id === "string" && patch.task_list_id) {
65797
67694
  let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
65798
67695
  if (!scope) {
@@ -65801,9 +67698,22 @@ ${task2.description}` : null
65801
67698
  }
65802
67699
  patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
65803
67700
  }
67701
+ if (patch.parent_id !== undefined && version3 === undefined) {
67702
+ const current = await cloudGetTask(cloud, task_id2);
67703
+ if (!current)
67704
+ throw new TaskNotFoundError(task_id2);
67705
+ patch.version = current.version;
67706
+ }
65804
67707
  if (version3 !== undefined)
65805
67708
  patch.version = version3;
65806
- const updated = await cloudUpdateTask(cloud, task_id2, patch);
67709
+ let updated = await cloudUpdateTask(cloud, task_id2, patch);
67710
+ if (patch.parent_id !== undefined) {
67711
+ const persisted = await cloudGetTask(cloud, task_id2);
67712
+ if (!persisted || (persisted.parent_id ?? null) !== patch.parent_id) {
67713
+ throw new Error(`TASK_REPARENT_PERSISTENCE_UNVERIFIED: parent_id expected ${patch.parent_id ?? "null"}, ` + `received ${persisted?.parent_id ?? "missing task"}`);
67714
+ }
67715
+ updated = persisted;
67716
+ }
65807
67717
  return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
65808
67718
  }
65809
67719
  const resolvedId = resolveId(params.task_id);
@@ -65815,6 +67725,8 @@ ${task2.description}` : null
65815
67725
  resolved.assigned_to = resolveAssignee(resolved.assigned_to);
65816
67726
  if (resolved.project_id && typeof resolved.project_id === "string")
65817
67727
  resolved.project_id = resolveId(resolved.project_id, "projects");
67728
+ if (resolved.parent_id && typeof resolved.parent_id === "string")
67729
+ resolved.parent_id = resolveId(resolved.parent_id);
65818
67730
  if (resolved.task_list_id && typeof resolved.task_list_id === "string")
65819
67731
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
65820
67732
  if (resolved.depends_on && Array.isArray(resolved.depends_on))
@@ -99497,7 +101409,7 @@ function registerTaskMetaTools(server, ctx) {
99497
101409
  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
101410
  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
101411
  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",
101412
+ 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
101413
  delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
99502
101414
  start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
99503
101415
  complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
@@ -111312,8 +113224,8 @@ function defaultSnapshotDir() {
111312
113224
  return join15(dirname8(resolve16(dbPath)), "environment-snapshots");
111313
113225
  }
111314
113226
  function snapshotWithId(snapshot) {
111315
- const digest2 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
111316
- return { id: `env_${digest2}`, ...snapshot };
113227
+ const digest3 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
113228
+ return { id: `env_${digest3}`, ...snapshot };
111317
113229
  }
111318
113230
  function captureEnvironmentSnapshot(input = {}) {
111319
113231
  const root = resolve16(input.root || process.cwd());