@hasna/todos 0.15.28 → 0.15.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cli/cloud-router.d.ts +15 -0
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-registration-commands.d.ts +5 -0
  4. package/dist/cli/commands/project-registration-commands.d.ts.map +1 -0
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +13040 -10368
  8. package/dist/contracts.js +64 -2
  9. package/dist/db/task-crud.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1489 -149
  13. package/dist/lib/task-parent-integrity.d.ts +14 -0
  14. package/dist/lib/task-parent-integrity.d.ts.map +1 -0
  15. package/dist/mcp/index.js +2073 -140
  16. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/project-registration/adoption-validation.d.ts +3 -0
  19. package/dist/project-registration/adoption-validation.d.ts.map +1 -0
  20. package/dist/project-registration/authority.d.ts +3 -1
  21. package/dist/project-registration/authority.d.ts.map +1 -1
  22. package/dist/project-registration/backend.d.ts +26 -1
  23. package/dist/project-registration/backend.d.ts.map +1 -1
  24. package/dist/project-registration/http.d.ts +3 -1
  25. package/dist/project-registration/http.d.ts.map +1 -1
  26. package/dist/project-registration/index.d.ts +2 -1
  27. package/dist/project-registration/index.d.ts.map +1 -1
  28. package/dist/project-registration/page-validation.d.ts +5 -0
  29. package/dist/project-registration/page-validation.d.ts.map +1 -0
  30. package/dist/project-registration/postgres.d.ts +13 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration/schema.d.ts.map +1 -1
  33. package/dist/project-registration/sqlite.d.ts +13 -1
  34. package/dist/project-registration/sqlite.d.ts.map +1 -1
  35. package/dist/project-registration/types.d.ts +68 -1
  36. package/dist/project-registration/types.d.ts.map +1 -1
  37. package/dist/project-registration.js +873 -63
  38. package/dist/registry.js +64 -2
  39. package/dist/release-provenance.json +5 -5
  40. package/dist/sdk/index.d.ts +1 -1
  41. package/dist/sdk/index.d.ts.map +1 -1
  42. package/dist/sdk/index.js +151 -0
  43. package/dist/sdk/v1.generated.d.ts +282 -0
  44. package/dist/sdk/v1.generated.d.ts.map +1 -1
  45. package/dist/server/index.js +2064 -131
  46. package/dist/server/openapi.d.ts +2856 -1357
  47. package/dist/server/openapi.d.ts.map +1 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  50. package/dist/storage/postgres-sync.d.ts +8 -0
  51. package/dist/storage/postgres-sync.d.ts.map +1 -1
  52. package/dist/storage.js +244 -19
  53. package/dist/task-manifest/authority.d.ts +13 -1
  54. package/dist/task-manifest/authority.d.ts.map +1 -1
  55. package/dist/task-manifest/backend.d.ts +5 -0
  56. package/dist/task-manifest/backend.d.ts.map +1 -1
  57. package/dist/task-manifest/index.d.ts +2 -2
  58. package/dist/task-manifest/index.d.ts.map +1 -1
  59. package/dist/task-manifest/plan-slug.d.ts +20 -0
  60. package/dist/task-manifest/plan-slug.d.ts.map +1 -1
  61. package/dist/task-manifest/postgres.d.ts +1 -0
  62. package/dist/task-manifest/postgres.d.ts.map +1 -1
  63. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  64. package/dist/task-manifest/schema.d.ts.map +1 -1
  65. package/dist/task-manifest/sqlite.d.ts +1 -0
  66. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  67. package/dist/task-manifest/types.d.ts +21 -1
  68. package/dist/task-manifest/types.d.ts.map +1 -1
  69. package/dist/task-manifest.js +592 -61
  70. package/dist/types/index.d.ts +4 -2
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/package.json +1 -1
@@ -1015,6 +1015,46 @@ var init_stale_lock_handoff = __esm(() => {
1015
1015
  CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
1016
1016
  });
1017
1017
 
1018
+ // src/lib/task-parent-integrity.ts
1019
+ function parentCycleError(taskId, parentId) {
1020
+ return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
1021
+ }
1022
+ function assertTaskParentIntegrity(taskId, parentId, getTask) {
1023
+ if (parentId === undefined || parentId === null)
1024
+ return;
1025
+ const visited = new Set;
1026
+ let cursor = parentId;
1027
+ while (cursor) {
1028
+ if (cursor === taskId || visited.has(cursor)) {
1029
+ throw parentCycleError(taskId, parentId);
1030
+ }
1031
+ visited.add(cursor);
1032
+ const parent = getTask(cursor);
1033
+ if (!parent)
1034
+ throw new TaskNotFoundError(cursor);
1035
+ cursor = parent.parent_id;
1036
+ }
1037
+ }
1038
+ async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
1039
+ if (parentId === undefined || parentId === null)
1040
+ return;
1041
+ const visited = new Set;
1042
+ let cursor = parentId;
1043
+ while (cursor) {
1044
+ if (cursor === taskId || visited.has(cursor)) {
1045
+ throw parentCycleError(taskId, parentId);
1046
+ }
1047
+ visited.add(cursor);
1048
+ const parent = await getTask(cursor);
1049
+ if (!parent)
1050
+ throw new TaskNotFoundError(cursor);
1051
+ cursor = parent.parent_id;
1052
+ }
1053
+ }
1054
+ var init_task_parent_integrity = __esm(() => {
1055
+ init_types();
1056
+ });
1057
+
1018
1058
  // src/project-registration/schema.ts
1019
1059
  function sqliteTodosProjectRegistrationSchemaSql() {
1020
1060
  return `
@@ -1053,6 +1093,12 @@ function sqliteTodosProjectRegistrationSchemaSql() {
1053
1093
  authority_id, tenant_id, corpus_id, operation_id, step_id,
1054
1094
  resource_kind, direction, idempotency_key
1055
1095
  );
1096
+ CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_source_identity
1097
+ ON todos_project_registration_receipts (
1098
+ authority_id, tenant_id, corpus_id, route, package_version,
1099
+ operation_id, step_id, resource_kind, direction, idempotency_key,
1100
+ target_selector
1101
+ );
1056
1102
  CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
1057
1103
  ON todos_project_registration_receipts (
1058
1104
  authority_id, tenant_id, corpus_id, operation_id, step_id,
@@ -1152,6 +1198,12 @@ function postgresTodosProjectRegistrationSchemaSql() {
1152
1198
  authority_id, tenant_id, corpus_id, operation_id, step_id,
1153
1199
  resource_kind, direction, idempotency_key
1154
1200
  )`,
1201
+ `CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_source_identity_idx
1202
+ ON todos_project_registration_receipts (
1203
+ authority_id, tenant_id, corpus_id, route, package_version,
1204
+ operation_id, step_id, resource_kind, direction, idempotency_key,
1205
+ target_selector
1206
+ )`,
1155
1207
  `CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
1156
1208
  ON todos_project_registration_receipts (
1157
1209
  authority_id, tenant_id, corpus_id, operation_id, step_id,
@@ -9428,6 +9480,7 @@ function createTaskStored(input, d) {
9428
9480
  let id = uuid();
9429
9481
  for (let attempt = 0;attempt < 3; attempt++) {
9430
9482
  try {
9483
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
9431
9484
  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)
9432
9485
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
9433
9486
  id,
@@ -9806,6 +9859,7 @@ function updateTaskStored(id, input, db) {
9806
9859
  throw new VersionConflictError(id, input.version, task.version);
9807
9860
  }
9808
9861
  input = sanitizeUpdateTaskInput(input);
9862
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
9809
9863
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
9810
9864
  const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
9811
9865
  if (linkedProjectId) {
@@ -9859,6 +9913,10 @@ function updateTaskStored(id, input, db) {
9859
9913
  sets.push("project_id = ?");
9860
9914
  params.push(input.project_id);
9861
9915
  }
9916
+ if (input.parent_id !== undefined) {
9917
+ sets.push("parent_id = ?");
9918
+ params.push(input.parent_id);
9919
+ }
9862
9920
  if (input.assigned_to !== undefined) {
9863
9921
  sets.push("assigned_to = ?");
9864
9922
  params.push(input.assigned_to);
@@ -9974,6 +10032,8 @@ function updateTaskStored(id, input, db) {
9974
10032
  logTaskChange2(id, "update", "priority", task.priority, input.priority, agentId, d);
9975
10033
  if (input.title !== undefined && input.title !== task.title)
9976
10034
  logTaskChange2(id, "update", "title", task.title, input.title, agentId, d);
10035
+ if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
10036
+ logTaskChange2(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
9977
10037
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
9978
10038
  logTaskChange2(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
9979
10039
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
@@ -10031,7 +10091,8 @@ function updateTask2(id, input, db) {
10031
10091
  if (!before)
10032
10092
  throw new TaskNotFoundError(id);
10033
10093
  const guardedPlanIds = [before.plan_id, input.plan_id];
10034
- if (!guardedPlanIds.some(Boolean))
10094
+ const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
10095
+ if (!needsSerializedWrite)
10035
10096
  return updateTaskStored(id, input, d);
10036
10097
  return d.transaction(() => {
10037
10098
  guardPlanRowsSqlite(guardedPlanIds, d);
@@ -10067,6 +10128,7 @@ var init_task_crud = __esm(() => {
10067
10128
  init_checklists();
10068
10129
  init_storage_tombstones();
10069
10130
  init_prewrite_secrets();
10131
+ init_task_parent_integrity();
10070
10132
  });
10071
10133
 
10072
10134
  // src/db/task-status.ts
@@ -12657,11 +12719,11 @@ var init_tasks = __esm(() => {
12657
12719
  });
12658
12720
 
12659
12721
  // src/project-registration/authority.ts
12660
- import { createHash as createHash6 } from "crypto";
12722
+ import { createHash as createHash7 } from "crypto";
12661
12723
  // package.json
12662
12724
  var package_default = {
12663
12725
  name: "@hasna/todos",
12664
- version: "0.15.28",
12726
+ version: "0.15.32",
12665
12727
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12666
12728
  type: "module",
12667
12729
  main: "dist/index.js",
@@ -13514,6 +13576,7 @@ function measureIntegrityRows(spec, sets) {
13514
13576
 
13515
13577
  // src/storage/postgres-adapter.ts
13516
13578
  init_redaction();
13579
+ init_task_parent_integrity();
13517
13580
 
13518
13581
  // src/storage/audit-history-import.ts
13519
13582
  var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY";
@@ -13585,8 +13648,8 @@ function createPostgresTodosStorageAdapter(options) {
13585
13648
  resolveRef: (ref) => store.resolveTaskRef(ref),
13586
13649
  list: (filter = {}) => store.listTasks(filter),
13587
13650
  count: (filter = {}) => store.countTasks(filter),
13588
- update: (id, input) => updateTask(id, input, store),
13589
- delete: (id, context) => store.delete("tasks", id, context),
13651
+ update: (id, input, context) => updateTask(id, input, store, context),
13652
+ delete: (id, context) => store.deleteTaskHierarchy(id, context),
13590
13653
  start: (id, agentId) => startTask(id, agentId, store),
13591
13654
  complete: (id, agentId, options2) => completeTask(id, agentId, options2, store),
13592
13655
  fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, store),
@@ -14119,22 +14182,70 @@ class PostgresJsonRecordStore {
14119
14182
  return "identical";
14120
14183
  throw new Error(divergentAuditHistoryReplayError(value.id));
14121
14184
  }
14122
- async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
14185
+ async withTaskParentIntegrityTransaction(fn) {
14186
+ if (typeof this.options.client.transaction !== "function") {
14187
+ throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
14188
+ }
14189
+ return this.options.client.transaction(async (client) => {
14190
+ await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
14191
+ return fn(client);
14192
+ });
14193
+ }
14194
+ async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
14123
14195
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
14124
- if (planIds.length === 0)
14196
+ if (planIds.length === 0 && !parentGuard)
14125
14197
  return this.upsert("tasks", value, context);
14126
14198
  await this.ensureSchema();
14199
+ if (parentGuard && !queryClient) {
14200
+ return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
14201
+ }
14202
+ const client = queryClient ?? this.options.client;
14127
14203
  const updatedAt = value.updated_at;
14128
14204
  const targetPlanId = value.plan_id;
14129
- const result = await this.options.client.query(`/* todos:task-plan-membership-guard */ WITH
14205
+ const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
14206
+ locked_task AS MATERIALIZED (
14207
+ SELECT payload FROM ${this.tableName}
14208
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
14209
+ FOR UPDATE
14210
+ ),
14130
14211
  locked_plans AS MATERIALIZED (
14131
14212
  SELECT object_id, payload FROM ${this.tableName}
14132
14213
  WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
14133
14214
  AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
14134
14215
  ORDER BY object_id
14135
14216
  FOR UPDATE
14217
+ ), parent_chain(object_id, payload, path, cycle) AS (
14218
+ SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
14219
+ FROM ${this.tableName} AS parent
14220
+ WHERE $10::boolean
14221
+ AND $11::text IS NOT NULL
14222
+ AND parent.service = $1
14223
+ AND parent.object_type = 'tasks'
14224
+ AND parent.object_id = $11
14225
+ AND parent.deleted_at IS NULL
14226
+ UNION ALL
14227
+ SELECT ancestor.object_id,
14228
+ ancestor.payload,
14229
+ chain.path || ancestor.object_id,
14230
+ ancestor.object_id = ANY(chain.path)
14231
+ FROM parent_chain AS chain
14232
+ JOIN ${this.tableName} AS ancestor
14233
+ ON ancestor.service = $1
14234
+ AND ancestor.object_type = 'tasks'
14235
+ AND ancestor.object_id = chain.payload->>'parent_id'
14236
+ AND ancestor.deleted_at IS NULL
14237
+ WHERE NOT chain.cycle
14136
14238
  ), validation AS (
14137
14239
  SELECT
14240
+ (NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
14241
+ (NOT $10::boolean OR NOT $13::boolean
14242
+ OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
14243
+ (NOT $10::boolean OR $11::text IS NULL
14244
+ OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
14245
+ (NOT $10::boolean OR $11::text IS NULL
14246
+ OR ($11::text <> $2
14247
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
14248
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
14138
14249
  (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
14139
14250
  ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
14140
14251
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
@@ -14155,7 +14266,13 @@ class PostgresJsonRecordStore {
14155
14266
  )
14156
14267
  SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
14157
14268
  FROM guarded
14158
- WHERE guarded.all_plans_found AND guarded.target_plan_found AND NOT guarded.project_conflict
14269
+ WHERE guarded.task_found
14270
+ AND guarded.version_matches
14271
+ AND guarded.parent_found
14272
+ AND guarded.parent_acyclic
14273
+ AND guarded.all_plans_found
14274
+ AND guarded.target_plan_found
14275
+ AND NOT guarded.project_conflict
14159
14276
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
14160
14277
  payload = EXCLUDED.payload,
14161
14278
  updated_at = EXCLUDED.updated_at,
@@ -14168,8 +14285,10 @@ class PostgresJsonRecordStore {
14168
14285
  AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
14169
14286
  RETURNING payload
14170
14287
  )
14171
- SELECT guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
14172
- (SELECT payload FROM stored) AS payload
14288
+ SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
14289
+ guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
14290
+ (SELECT payload FROM stored) AS payload,
14291
+ (SELECT payload FROM locked_task) AS current_payload
14173
14292
  FROM guarded`, [
14174
14293
  this.service,
14175
14294
  value.id,
@@ -14179,9 +14298,26 @@ class PostgresJsonRecordStore {
14179
14298
  numberValue2(value.version),
14180
14299
  jsonbParam(planIds),
14181
14300
  targetPlanId,
14182
- explicitProject
14301
+ explicitProject,
14302
+ Boolean(parentGuard),
14303
+ parentGuard?.parentId ?? null,
14304
+ parentGuard?.expectedVersion ?? null,
14305
+ parentGuard?.operation === "update"
14183
14306
  ]);
14184
14307
  const row = result.rows[0];
14308
+ if (parentGuard && !row?.task_found) {
14309
+ throw new TaskNotFoundError(value.id);
14310
+ }
14311
+ if (parentGuard && !row?.version_matches) {
14312
+ const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
14313
+ throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
14314
+ }
14315
+ if (parentGuard && !row?.parent_found && parentGuard.parentId) {
14316
+ throw new TaskNotFoundError(parentGuard.parentId);
14317
+ }
14318
+ if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
14319
+ throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
14320
+ }
14185
14321
  if (!row?.all_plans_found || !row.target_plan_found) {
14186
14322
  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 });
14187
14323
  }
@@ -14189,6 +14325,8 @@ class PostgresJsonRecordStore {
14189
14325
  throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
14190
14326
  }
14191
14327
  if (!row.payload) {
14328
+ if (row.current_payload)
14329
+ return payloadRecord2(row.current_payload);
14192
14330
  return await requireRecord("tasks", value.id, this);
14193
14331
  }
14194
14332
  return payloadRecord2(row.payload);
@@ -14514,6 +14652,80 @@ class PostgresJsonRecordStore {
14514
14652
  version: numberValue2(existing["version"])
14515
14653
  }, context);
14516
14654
  }
14655
+ async deleteTaskHierarchy(id, context = {}) {
14656
+ await this.ensureSchema();
14657
+ return this.withTaskParentIntegrityTransaction(async (client) => {
14658
+ const timestamp = new Date().toISOString();
14659
+ const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
14660
+ task_tree(object_id, path, cycle) AS (
14661
+ SELECT task.object_id, ARRAY[task.object_id], false
14662
+ FROM ${this.tableName} AS task
14663
+ WHERE task.service = $1
14664
+ AND task.object_type = 'tasks'
14665
+ AND task.object_id = $2
14666
+ AND task.deleted_at IS NULL
14667
+ UNION ALL
14668
+ SELECT child.object_id,
14669
+ tree.path || child.object_id,
14670
+ child.object_id = ANY(tree.path)
14671
+ FROM task_tree AS tree
14672
+ JOIN ${this.tableName} AS child
14673
+ ON child.service = $1
14674
+ AND child.object_type = 'tasks'
14675
+ AND child.payload->>'parent_id' = tree.object_id
14676
+ AND child.deleted_at IS NULL
14677
+ WHERE NOT tree.cycle
14678
+ ), tombstoned AS (
14679
+ UPDATE ${this.tableName} AS task
14680
+ SET deleted_at = $3::timestamptz,
14681
+ updated_at = $3::timestamptz,
14682
+ source_machine_id = $4
14683
+ WHERE task.service = $1
14684
+ AND task.object_type = 'tasks'
14685
+ AND task.deleted_at IS NULL
14686
+ AND task.object_id IN (
14687
+ SELECT object_id FROM task_tree WHERE NOT cycle
14688
+ )
14689
+ RETURNING task.object_id
14690
+ ), tombstoned_related AS (
14691
+ UPDATE ${this.tableName} AS related
14692
+ SET deleted_at = $3::timestamptz,
14693
+ updated_at = $3::timestamptz,
14694
+ source_machine_id = $4
14695
+ WHERE related.service = $1
14696
+ AND related.deleted_at IS NULL
14697
+ AND (
14698
+ (
14699
+ related.object_type = 'dependencies'
14700
+ AND (
14701
+ related.payload->>'task_id' IN (
14702
+ SELECT object_id FROM task_tree WHERE NOT cycle
14703
+ )
14704
+ OR related.payload->>'depends_on' IN (
14705
+ SELECT object_id FROM task_tree WHERE NOT cycle
14706
+ )
14707
+ )
14708
+ )
14709
+ OR (
14710
+ related.object_type IN ('comments', 'verifications', 'commits', 'refs')
14711
+ AND related.payload->>'task_id' IN (
14712
+ SELECT object_id FROM task_tree WHERE NOT cycle
14713
+ )
14714
+ )
14715
+ )
14716
+ RETURNING related.object_id
14717
+ )
14718
+ SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
14719
+ (SELECT count(*) FROM tombstoned) AS deleted_count,
14720
+ (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
14721
+ this.service,
14722
+ id,
14723
+ timestamp,
14724
+ context.requestId ?? this.sourceMachineId ?? null
14725
+ ]);
14726
+ return Boolean(result.rows[0]?.found);
14727
+ });
14728
+ }
14517
14729
  async getPlanProjectLinkReceipt(receiptId) {
14518
14730
  const value = await this.get("plan_project_link_receipts", receiptId);
14519
14731
  return value ? assertPlanProjectLinkReceipt(value) : null;
@@ -14934,9 +15146,8 @@ class PostgresJsonRecordStore {
14934
15146
  }
14935
15147
  async function createTask(input, store, context) {
14936
15148
  const timestamp = new Date().toISOString();
14937
- if (input.parent_id && !await store.get("tasks", input.parent_id)) {
14938
- throw new TaskNotFoundError(input.parent_id);
14939
- }
15149
+ const taskId = randomUUID();
15150
+ await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
14940
15151
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
14941
15152
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
14942
15153
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -14945,7 +15156,7 @@ async function createTask(input, store, context) {
14945
15156
  const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
14946
15157
  const shortId = effectiveProjectId ? await nextTaskShortId(effectiveProjectId, store, context) : null;
14947
15158
  const task = {
14948
- id: randomUUID(),
15159
+ id: taskId,
14949
15160
  short_id: shortId,
14950
15161
  project_id: effectiveProjectId,
14951
15162
  parent_id: input.parent_id ?? null,
@@ -15001,15 +15212,16 @@ async function createTask(input, store, context) {
15001
15212
  synced_at: null,
15002
15213
  archived_at: null
15003
15214
  };
15004
- const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, task.plan_id ? [task.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
15215
+ 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);
15005
15216
  await logTaskChange(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
15006
15217
  return storedTask;
15007
15218
  }
15008
- async function updateTask(id, input, store) {
15219
+ async function updateTask(id, input, store, context) {
15009
15220
  const existing = await requireRecord("tasks", id, store);
15010
15221
  if (existing.version !== input.version) {
15011
- throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
15222
+ throw new VersionConflictError(id, input.version, existing.version);
15012
15223
  }
15224
+ await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
15013
15225
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
15014
15226
  const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
15015
15227
  if (linkedPlan?.project_id) {
@@ -15034,10 +15246,19 @@ async function updateTask(id, input, store) {
15034
15246
  metadata: input.metadata ?? existing.metadata,
15035
15247
  requires_approval: input.requires_approval ?? existing.requires_approval,
15036
15248
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
15249
+ parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
15037
15250
  created_by: existing.created_by,
15038
15251
  completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
15039
15252
  };
15040
- return store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined);
15253
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
15254
+ operation: "update",
15255
+ expectedVersion: input.version,
15256
+ parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
15257
+ });
15258
+ if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
15259
+ await logTaskChange(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
15260
+ }
15261
+ return storedTask;
15041
15262
  }
15042
15263
  async function startTask(id, agentId, store) {
15043
15264
  const task = await requireRecord("tasks", id, store);
@@ -15101,7 +15322,11 @@ async function patchTask(task, patch, store) {
15101
15322
  version: task.version + 1,
15102
15323
  updated_at: new Date().toISOString()
15103
15324
  };
15104
- return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"));
15325
+ return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
15326
+ operation: "update",
15327
+ expectedVersion: task.version,
15328
+ parentId: updated.parent_id
15329
+ });
15105
15330
  }
15106
15331
  var CLOUD_LOCK_EXPIRY_MINUTES = 30;
15107
15332
  function sameCloudLockHolder(stored, incoming) {
@@ -15877,8 +16102,9 @@ class PostgresTodosProjectRegistrationTransaction {
15877
16102
  const result = await this.client.query(`
15878
16103
  SELECT * FROM todos_project_registration_receipts
15879
16104
  WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
15880
- AND operation_id = $4 AND step_id = $5 AND resource_kind = $6
15881
- AND direction = $7 AND idempotency_key = $8 AND target_selector = $9
16105
+ AND route = $4 AND package_version = $5
16106
+ AND operation_id = $6 AND step_id = $7 AND resource_kind = $8
16107
+ AND direction = $9 AND idempotency_key = $10 AND target_selector = $11
15882
16108
  ORDER BY CASE outcome
15883
16109
  WHEN 'terminal_nonacceptance' THEN 0
15884
16110
  WHEN 'duplicate_of_accepted' THEN 1
@@ -15889,6 +16115,8 @@ class PostgresTodosProjectRegistrationTransaction {
15889
16115
  identity.authority_id,
15890
16116
  identity.tenant_id,
15891
16117
  identity.corpus_id,
16118
+ identity.route,
16119
+ identity.package_version,
15892
16120
  identity.operation_id,
15893
16121
  identity.step_id,
15894
16122
  identity.resource_kind,
@@ -16088,6 +16316,7 @@ class PostgresTodosProjectRegistrationTransaction {
16088
16316
  AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
16089
16317
  ORDER BY payload->>'created_at' ASC, object_id ASC
16090
16318
  LIMIT 1
16319
+ FOR UPDATE
16091
16320
  `, [this.service, path, taskListSlug]);
16092
16321
  return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
16093
16322
  }
@@ -16098,6 +16327,7 @@ class PostgresTodosProjectRegistrationTransaction {
16098
16327
  AND payload->>'project_id' = $2 AND payload->>'slug' = $3
16099
16328
  ORDER BY payload->>'created_at' ASC, object_id ASC
16100
16329
  LIMIT 1
16330
+ FOR UPDATE
16101
16331
  `, [this.service, projectId, slug]);
16102
16332
  return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
16103
16333
  }
@@ -16108,10 +16338,24 @@ class PostgresTodosProjectRegistrationTransaction {
16108
16338
  return await this.storage.taskLists.create(input);
16109
16339
  }
16110
16340
  async getProject(id) {
16111
- return await this.storage.projects.get(id);
16341
+ const result = await this.client.query(`
16342
+ SELECT payload FROM ${this.tableName}
16343
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2
16344
+ AND deleted_at IS NULL
16345
+ LIMIT 1
16346
+ FOR SHARE
16347
+ `, [this.service, id]);
16348
+ return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
16112
16349
  }
16113
16350
  async getTaskList(id) {
16114
- return await this.storage.taskLists.get(id);
16351
+ const result = await this.client.query(`
16352
+ SELECT payload FROM ${this.tableName}
16353
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
16354
+ AND deleted_at IS NULL
16355
+ LIMIT 1
16356
+ FOR SHARE
16357
+ `, [this.service, id]);
16358
+ return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
16115
16359
  }
16116
16360
  async lockCompensationWrites() {
16117
16361
  await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
@@ -16191,11 +16435,102 @@ class PostgresTodosProjectRegistrationBackend {
16191
16435
  async getTaskList(id) {
16192
16436
  return (await this.direct()).getTaskList(id);
16193
16437
  }
16438
+ async getProjectResourceCollectionRevision(input) {
16439
+ await this.ensureSchema();
16440
+ const result = await this.client.query(`
16441
+ WITH resources(kind_rank, target_id, revision) AS (
16442
+ SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
16443
+ FROM ${this.tableName}
16444
+ WHERE service = $1 AND object_type = 'projects'
16445
+ AND deleted_at IS NULL AND object_id = $2
16446
+ UNION ALL
16447
+ SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
16448
+ FROM ${this.tableName}
16449
+ WHERE service = $1 AND object_type = 'task_lists'
16450
+ AND deleted_at IS NULL AND object_id = $3
16451
+ AND payload->>'project_id' = $2
16452
+ UNION ALL
16453
+ SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
16454
+ FROM ${this.tableName}
16455
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
16456
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
16457
+ UNION ALL
16458
+ SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
16459
+ FROM ${this.tableName}
16460
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
16461
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
16462
+ )
16463
+ SELECT 'md5:' || md5(COALESCE(string_agg(
16464
+ kind_rank::text || chr(31) || target_id || chr(31) || revision,
16465
+ chr(30) ORDER BY kind_rank ASC, target_id ASC
16466
+ ), '')) AS revision
16467
+ FROM resources
16468
+ `, [
16469
+ this.service,
16470
+ input.todos_project_id,
16471
+ input.task_list_id,
16472
+ input.include_anchors
16473
+ ]);
16474
+ const revision = result.rows[0]?.revision;
16475
+ if (!revision) {
16476
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
16477
+ }
16478
+ return revision;
16479
+ }
16480
+ async listProjectResourceCandidates(input) {
16481
+ await this.ensureSchema();
16482
+ const afterRank = input.after?.kind_rank ?? -1;
16483
+ const afterId = input.after?.target_id ?? "";
16484
+ const result = await this.client.query(`
16485
+ WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
16486
+ SELECT 'project'::text, 0, object_id, NULL::text,
16487
+ COALESCE(payload->>'updated_at', updated_at::text)
16488
+ FROM ${this.tableName}
16489
+ WHERE service = $1 AND object_type = 'projects'
16490
+ AND deleted_at IS NULL AND object_id = $2
16491
+ UNION ALL
16492
+ SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
16493
+ COALESCE(payload->>'updated_at', updated_at::text)
16494
+ FROM ${this.tableName}
16495
+ WHERE service = $1 AND object_type = 'task_lists'
16496
+ AND deleted_at IS NULL AND object_id = $3
16497
+ AND payload->>'project_id' = $2
16498
+ UNION ALL
16499
+ SELECT 'plan'::text, 2, object_id, payload->>'project_id',
16500
+ COALESCE(payload->>'updated_at', updated_at::text)
16501
+ FROM ${this.tableName}
16502
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
16503
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
16504
+ UNION ALL
16505
+ SELECT 'task'::text, 3, object_id,
16506
+ COALESCE(payload->>'plan_id', payload->>'project_id'),
16507
+ COALESCE(payload->>'updated_at', updated_at::text)
16508
+ FROM ${this.tableName}
16509
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
16510
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
16511
+ )
16512
+ SELECT kind, kind_rank, target_id, parent_id, revision
16513
+ FROM resources
16514
+ WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
16515
+ ORDER BY kind_rank ASC, target_id ASC
16516
+ LIMIT $7
16517
+ `, [
16518
+ this.service,
16519
+ input.todos_project_id,
16520
+ input.task_list_id,
16521
+ input.include_anchors,
16522
+ afterRank,
16523
+ afterId,
16524
+ input.limit
16525
+ ]);
16526
+ return result.rows;
16527
+ }
16194
16528
  }
16195
16529
 
16196
16530
  // src/project-registration/sqlite.ts
16197
16531
  init_database();
16198
16532
  init_storage_tombstones();
16533
+ import { createHash as createHash6 } from "crypto";
16199
16534
 
16200
16535
  // src/storage/local-sqlite.ts
16201
16536
  init_types();
@@ -17950,6 +18285,7 @@ class SqliteTodosProjectRegistrationTransaction {
17950
18285
  const row = this.db.query(`
17951
18286
  SELECT * FROM todos_project_registration_receipts
17952
18287
  WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
18288
+ AND route = ? AND package_version = ?
17953
18289
  AND operation_id = ? AND step_id = ? AND resource_kind = ?
17954
18290
  AND direction = ? AND idempotency_key = ? AND target_selector = ?
17955
18291
  ORDER BY CASE outcome
@@ -17958,7 +18294,7 @@ class SqliteTodosProjectRegistrationTransaction {
17958
18294
  ELSE 2
17959
18295
  END, created_at DESC, receipt_id DESC
17960
18296
  LIMIT 1
17961
- `).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
18297
+ `).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.route, identity.package_version, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
17962
18298
  return row ? receiptFromRow2(row) : null;
17963
18299
  }
17964
18300
  async getReceiptById(receiptId) {
@@ -18112,7 +18448,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
18112
18448
  }
18113
18449
  async lockStep(_identity) {}
18114
18450
  async getReceiptForLookup(identity) {
18115
- const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
18451
+ const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.route === identity.route && receipt.package_version === identity.package_version && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
18116
18452
  const stored = await this.direct.getReceiptForLookup(identity);
18117
18453
  if (stored)
18118
18454
  staged.push(stored);
@@ -18502,6 +18838,64 @@ class SqliteTodosProjectRegistrationBackend {
18502
18838
  getTaskList(id) {
18503
18839
  return this.direct.getTaskList(id);
18504
18840
  }
18841
+ async getProjectResourceCollectionRevision(input) {
18842
+ const digest = createHash6("sha256");
18843
+ const rows = this.db.query(`
18844
+ WITH resources(kind_rank, target_id, revision) AS (
18845
+ SELECT 0, id, updated_at
18846
+ FROM projects
18847
+ WHERE id = ?
18848
+ UNION ALL
18849
+ SELECT 1, id, updated_at
18850
+ FROM task_lists
18851
+ WHERE id = ? AND project_id = ?
18852
+ UNION ALL
18853
+ SELECT 2, id, updated_at
18854
+ FROM plans
18855
+ WHERE ? = 1 AND project_id = ?
18856
+ UNION ALL
18857
+ SELECT 3, id, updated_at
18858
+ FROM tasks
18859
+ WHERE ? = 1 AND project_id = ?
18860
+ )
18861
+ SELECT kind_rank, target_id, revision
18862
+ FROM resources
18863
+ ORDER BY kind_rank ASC, target_id ASC
18864
+ `).iterate(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id);
18865
+ for (const row of rows) {
18866
+ digest.update(`${row.kind_rank}\x00${row.target_id}\x00${row.revision}
18867
+ `);
18868
+ }
18869
+ return `sha256:${digest.digest("hex")}`;
18870
+ }
18871
+ async listProjectResourceCandidates(input) {
18872
+ const afterRank = input.after?.kind_rank ?? -1;
18873
+ const afterId = input.after?.target_id ?? "";
18874
+ return this.db.query(`
18875
+ WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
18876
+ SELECT 'project', 0, id, NULL, updated_at
18877
+ FROM projects
18878
+ WHERE id = ?
18879
+ UNION ALL
18880
+ SELECT 'task_list', 1, id, project_id, updated_at
18881
+ FROM task_lists
18882
+ WHERE id = ? AND project_id = ?
18883
+ UNION ALL
18884
+ SELECT 'plan', 2, id, project_id, updated_at
18885
+ FROM plans
18886
+ WHERE ? = 1 AND project_id = ?
18887
+ UNION ALL
18888
+ SELECT 'task', 3, id, COALESCE(plan_id, project_id), updated_at
18889
+ FROM tasks
18890
+ WHERE ? = 1 AND project_id = ?
18891
+ )
18892
+ SELECT kind, kind_rank, target_id, parent_id, revision
18893
+ FROM resources
18894
+ WHERE kind_rank > ? OR (kind_rank = ? AND target_id > ?)
18895
+ ORDER BY kind_rank ASC, target_id ASC
18896
+ LIMIT ?
18897
+ `).all(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, afterRank, afterRank, afterId, input.limit);
18898
+ }
18505
18899
  }
18506
18900
 
18507
18901
  // src/project-registration/authority.ts
@@ -18509,8 +18903,12 @@ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-
18509
18903
  var WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
18510
18904
  var OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
18511
18905
  var STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
18906
+ var AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
18907
+ var PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
18512
18908
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
18513
18909
  var IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
18910
+ var PROJECT_RESOURCE_PAGE_LIMIT = 500;
18911
+ var PROJECT_RESOURCE_CURSOR_VERSION = 1;
18514
18912
 
18515
18913
  class WriteBoundaryError extends Error {
18516
18914
  point;
@@ -18538,7 +18936,7 @@ function canonicalize3(value) {
18538
18936
  return out;
18539
18937
  }
18540
18938
  function digestProjectRegistrationValue(value) {
18541
- return createHash6("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
18939
+ return createHash7("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
18542
18940
  }
18543
18941
  function deriveTodosProjectRegistrationIdempotencyKey(input) {
18544
18942
  return `prk_${digestProjectRegistrationValue({
@@ -18650,35 +19048,68 @@ function projectRecord(project) {
18650
19048
  return {
18651
19049
  target_id: project.id,
18652
19050
  revision: project.updated_at,
18653
- digest: digestProjectRegistrationValue({
18654
- id: project.id,
18655
- name: project.name,
18656
- path: project.path,
18657
- description: project.description,
18658
- task_list_id: project.task_list_id,
18659
- task_prefix: project.task_prefix,
18660
- task_counter: project.task_counter,
18661
- created_at: project.created_at,
18662
- updated_at: project.updated_at
18663
- })
19051
+ digest: projectRegistrationDigest(project)
18664
19052
  };
18665
19053
  }
18666
19054
  function taskListRecord(taskList) {
18667
19055
  return {
18668
19056
  target_id: taskList.id,
18669
19057
  revision: taskList.updated_at,
18670
- digest: digestProjectRegistrationValue({
18671
- id: taskList.id,
18672
- project_id: taskList.project_id,
18673
- slug: taskList.slug,
18674
- name: taskList.name,
18675
- description: taskList.description,
18676
- metadata: taskList.metadata,
18677
- created_at: taskList.created_at,
18678
- updated_at: taskList.updated_at
19058
+ digest: taskListRegistrationDigest(taskList)
19059
+ };
19060
+ }
19061
+ function boundExistingProjectRecord(project) {
19062
+ return {
19063
+ target_id: project.id,
19064
+ revision: project.created_at,
19065
+ digest: projectRegistrationDigest({
19066
+ ...project,
19067
+ updated_at: project.created_at
19068
+ })
19069
+ };
19070
+ }
19071
+ function boundExistingTaskListRecord(taskList) {
19072
+ return {
19073
+ target_id: taskList.id,
19074
+ revision: taskList.created_at,
19075
+ digest: taskListRegistrationDigest({
19076
+ ...taskList,
19077
+ updated_at: taskList.created_at
18679
19078
  })
18680
19079
  };
18681
19080
  }
19081
+ function projectRegistrationDigest(project) {
19082
+ return digestProjectRegistrationValue({
19083
+ id: project.id,
19084
+ name: project.name,
19085
+ path: project.path,
19086
+ description: project.description,
19087
+ task_list_id: project.task_list_id,
19088
+ task_prefix: project.task_prefix,
19089
+ task_counter: project.task_counter,
19090
+ created_at: project.created_at,
19091
+ updated_at: project.updated_at
19092
+ });
19093
+ }
19094
+ function taskListRegistrationDigest(taskList) {
19095
+ return digestProjectRegistrationValue({
19096
+ id: taskList.id,
19097
+ project_id: taskList.project_id,
19098
+ slug: taskList.slug,
19099
+ name: taskList.name,
19100
+ description: taskList.description,
19101
+ metadata: taskList.metadata,
19102
+ created_at: taskList.created_at,
19103
+ updated_at: taskList.updated_at
19104
+ });
19105
+ }
19106
+ function canonicalValuesEqual(left, right) {
19107
+ try {
19108
+ return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
19109
+ } catch {
19110
+ return false;
19111
+ }
19112
+ }
18682
19113
  function receiptId(input) {
18683
19114
  return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
18684
19115
  }
@@ -18698,6 +19129,29 @@ function assertCapabilityRequest(request, capability) {
18698
19129
  }
18699
19130
  }
18700
19131
  function normalizedCallDigest(request) {
19132
+ return digestProjectRegistrationValue({
19133
+ authority_route: request.authority_route,
19134
+ package_version: request.package_version,
19135
+ authority_id: request.authority_id,
19136
+ tenant_id: request.tenant_id,
19137
+ corpus_id: request.corpus_id,
19138
+ operation_id: request.operation_id,
19139
+ step_id: request.step_id,
19140
+ resource_kind: request.resource_kind,
19141
+ direction: request.direction,
19142
+ target_selector: request.target_selector,
19143
+ idempotency_key: request.idempotency_key,
19144
+ request_digest: request.request_digest,
19145
+ precondition_digest: request.precondition_digest,
19146
+ project_id: request.project_id,
19147
+ project_slug: request.project_slug,
19148
+ project_name: request.project_name,
19149
+ desired: request.desired,
19150
+ bind_existing: request.bind_existing === true,
19151
+ accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
19152
+ });
19153
+ }
19154
+ function legacyNormalizedCallDigestBeforeBindExisting(request) {
18701
19155
  return digestProjectRegistrationValue({
18702
19156
  authority_route: request.authority_route,
18703
19157
  package_version: request.package_version,
@@ -18719,6 +19173,11 @@ function normalizedCallDigest(request) {
18719
19173
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
18720
19174
  });
18721
19175
  }
19176
+ function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
19177
+ if (accepted.normalized_call_digest === callDigest)
19178
+ return true;
19179
+ return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
19180
+ }
18722
19181
  function assertCommonRequest(request, capability) {
18723
19182
  assertBounds(request);
18724
19183
  assertResourceKind(request.resource_kind);
@@ -18760,6 +19219,9 @@ function assertCommonRequest(request, capability) {
18760
19219
  if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
18761
19220
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
18762
19221
  }
19222
+ if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
19223
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
19224
+ }
18763
19225
  const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
18764
19226
  operation_id: request.operation_id,
18765
19227
  step_id: request.step_id,
@@ -18781,7 +19243,7 @@ function assertForwardRequest(request, capability) {
18781
19243
  const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
18782
19244
  const expectedPreconditionDigest = digestProjectRegistrationValue({
18783
19245
  target_selector: request.target_selector,
18784
- expected: "absent"
19246
+ expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
18785
19247
  });
18786
19248
  if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
18787
19249
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
@@ -18860,7 +19322,7 @@ function receiptBase(request, callDigest, capability) {
18860
19322
  normalized_call_digest: callDigest
18861
19323
  };
18862
19324
  }
18863
- function makeAcceptedReceipt(request, callDigest, capability, record, createdAt) {
19325
+ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt, createdByOperation = true) {
18864
19326
  return makeReceipt({
18865
19327
  ...receiptBase(request, callDigest, capability),
18866
19328
  outcome: "accepted",
@@ -18870,7 +19332,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt)
18870
19332
  result_digest: record.digest,
18871
19333
  duplicate_of_receipt_id: null,
18872
19334
  accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
18873
- created_by_operation: true
19335
+ created_by_operation: createdByOperation
18874
19336
  }, createdAt);
18875
19337
  }
18876
19338
  function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt) {
@@ -18932,6 +19394,53 @@ function bindingFor(request, callDigest, timestamp2, capability) {
18932
19394
  updated_at: timestamp2
18933
19395
  };
18934
19396
  }
19397
+ function encodeProjectResourceCursor(input) {
19398
+ return Buffer.from(JSON.stringify({
19399
+ version: PROJECT_RESOURCE_CURSOR_VERSION,
19400
+ ...input
19401
+ }), "utf8").toString("base64url");
19402
+ }
19403
+ function decodeProjectResourceCursor(cursor, expected) {
19404
+ if (!cursor)
19405
+ return null;
19406
+ let parsed;
19407
+ try {
19408
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
19409
+ } catch {
19410
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
19411
+ }
19412
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
19413
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
19414
+ }
19415
+ const value = parsed;
19416
+ 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) {
19417
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
19418
+ }
19419
+ return {
19420
+ kind_rank: Number(value["kind_rank"]),
19421
+ target_id: value["target_id"],
19422
+ collection_revision: value["collection_revision"]
19423
+ };
19424
+ }
19425
+ function projectResourceFromCandidate(sourceProjectId, candidate) {
19426
+ const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
19427
+ return {
19428
+ source_project_id: sourceProjectId,
19429
+ kind: candidate.kind,
19430
+ scope,
19431
+ target_id: candidate.target_id,
19432
+ parent_id: candidate.parent_id,
19433
+ revision: candidate.revision,
19434
+ digest: digestProjectRegistrationValue({
19435
+ source_project_id: sourceProjectId,
19436
+ kind: candidate.kind,
19437
+ scope,
19438
+ target_id: candidate.target_id,
19439
+ parent_id: candidate.parent_id,
19440
+ revision: candidate.revision
19441
+ })
19442
+ };
19443
+ }
18935
19444
 
18936
19445
  class PackageOwnedTodosProjectRegistrationAuthority {
18937
19446
  backend;
@@ -18953,6 +19462,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
18953
19462
  immutable_receipts: true,
18954
19463
  exact_terminal_lookup: true,
18955
19464
  exact_readback: true,
19465
+ bind_existing_adoption: true,
19466
+ prior_registration_adoption_validation: true,
19467
+ project_resource_enumeration: true,
19468
+ project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
18956
19469
  conditional_inverse: true,
18957
19470
  ambiguous_outcome_reconciliation: true
18958
19471
  };
@@ -18997,6 +19510,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
18997
19510
  async existingForwardResolution(transaction, request, callDigest) {
18998
19511
  const exact = await transaction.getReceiptForLookup({
18999
19512
  ...authorityScope(this.capabilityValue),
19513
+ route: this.capabilityValue.route,
19514
+ package_version: this.capabilityValue.package_version,
19000
19515
  operation_id: request.operation_id,
19001
19516
  step_id: request.step_id,
19002
19517
  resource_kind: request.resource_kind,
@@ -19011,7 +19526,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19011
19526
  if (!accepted2) {
19012
19527
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
19013
19528
  }
19014
- if (accepted2.normalized_call_digest !== callDigest) {
19529
+ if (!acceptedCallMatches(request, accepted2, callDigest)) {
19015
19530
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
19016
19531
  }
19017
19532
  return this.duplicateFor(transaction, request, callDigest, accepted2);
@@ -19025,7 +19540,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19025
19540
  });
19026
19541
  if (!accepted)
19027
19542
  return null;
19028
- if (accepted.normalized_call_digest === callDigest) {
19543
+ if (acceptedCallMatches(request, accepted, callDigest)) {
19029
19544
  return this.duplicateFor(transaction, request, callDigest, accepted);
19030
19545
  }
19031
19546
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
@@ -19036,6 +19551,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19036
19551
  const slug2 = taskListSlug(request.project_slug);
19037
19552
  const conflict2 = await transaction.findProjectConflict(path, slug2);
19038
19553
  if (conflict2) {
19554
+ if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
19555
+ return {
19556
+ record: boundExistingProjectRecord(conflict2),
19557
+ created_by_operation: false
19558
+ };
19559
+ }
19039
19560
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
19040
19561
  }
19041
19562
  await this.fault("before_object_write", request);
@@ -19047,7 +19568,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19047
19568
  task_prefix: deterministicTaskPrefix(request.project_slug)
19048
19569
  });
19049
19570
  await this.fault("after_object_write", request);
19050
- return projectRecord(project);
19571
+ return {
19572
+ record: projectRecord(project),
19573
+ created_by_operation: true
19574
+ };
19051
19575
  }
19052
19576
  const todosProjectId = String(request.desired["todos_project_id"]);
19053
19577
  const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
@@ -19061,6 +19585,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19061
19585
  const slug = taskListSlug(request.project_slug);
19062
19586
  const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
19063
19587
  if (conflict) {
19588
+ if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
19589
+ return {
19590
+ record: boundExistingTaskListRecord(conflict),
19591
+ created_by_operation: false
19592
+ };
19593
+ }
19064
19594
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
19065
19595
  }
19066
19596
  await this.fault("before_object_write", request);
@@ -19077,7 +19607,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19077
19607
  if (taskList.project_id !== todosProjectId) {
19078
19608
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
19079
19609
  }
19080
- return taskListRecord(taskList);
19610
+ return {
19611
+ record: taskListRecord(taskList),
19612
+ created_by_operation: true
19613
+ };
19081
19614
  }
19082
19615
  async create(request) {
19083
19616
  const startedAt = Date.now();
@@ -19099,9 +19632,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19099
19632
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
19100
19633
  if (!claimed) {
19101
19634
  const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
19102
- if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
19635
+ if (binding?.state === "accepted" && binding.accepted_receipt_id) {
19103
19636
  const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
19104
- if (accepted2) {
19637
+ if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
19105
19638
  return this.duplicateFor(transaction, request, callDigest, accepted2);
19106
19639
  }
19107
19640
  }
@@ -19112,15 +19645,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19112
19645
  await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
19113
19646
  return recordOrTerminal;
19114
19647
  }
19115
- const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
19648
+ const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
19116
19649
  await this.fault("before_receipt_write", request);
19117
19650
  const stored = await insertDeterministicReceipt(transaction, accepted);
19118
19651
  await this.fault("after_receipt_write", request);
19119
19652
  await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
19120
- target_id: recordOrTerminal.target_id,
19653
+ target_id: recordOrTerminal.record.target_id,
19121
19654
  accepted_receipt_id: stored.receipt_id,
19122
- result_revision: recordOrTerminal.revision,
19123
- result_digest: recordOrTerminal.digest,
19655
+ result_revision: recordOrTerminal.record.revision,
19656
+ result_digest: recordOrTerminal.record.digest,
19124
19657
  updated_at: this.now()
19125
19658
  });
19126
19659
  return stored;
@@ -19149,6 +19682,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19149
19682
  });
19150
19683
  const exact = await transaction.getReceiptForLookup({
19151
19684
  ...authorityScope(this.capabilityValue),
19685
+ route: this.capabilityValue.route,
19686
+ package_version: this.capabilityValue.package_version,
19152
19687
  operation_id: request.operation_id,
19153
19688
  step_id: request.step_id,
19154
19689
  resource_kind: request.resource_kind,
@@ -19166,7 +19701,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19166
19701
  direction: request.direction
19167
19702
  });
19168
19703
  if (accepted) {
19169
- 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 });
19704
+ return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
19170
19705
  }
19171
19706
  const timestamp2 = this.now();
19172
19707
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
@@ -19199,9 +19734,18 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19199
19734
  if (request.max_items !== 1) {
19200
19735
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
19201
19736
  }
19202
- if (request.authority !== "todos" || request.authority_route !== this.capabilityValue.route || request.package_version !== this.capabilityValue.package_version || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
19737
+ 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) {
19203
19738
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
19204
19739
  }
19740
+ requireString(request.authority_route, "authority_route", {
19741
+ min: 3,
19742
+ max: 128,
19743
+ pattern: AUTHORITY_ROUTE_PATTERN
19744
+ });
19745
+ requireString(request.package_version, "package_version", {
19746
+ max: 128,
19747
+ pattern: PACKAGE_VERSION_PATTERN
19748
+ });
19205
19749
  requireString(request.operation_id, "operation_id", {
19206
19750
  min: 8,
19207
19751
  max: 128,
@@ -19223,6 +19767,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19223
19767
  }
19224
19768
  const receipt = await this.backend.getReceiptForLookup({
19225
19769
  ...authorityScope(this.capabilityValue),
19770
+ route: request.authority_route,
19771
+ package_version: request.package_version,
19226
19772
  operation_id: request.operation_id,
19227
19773
  step_id: request.step_id,
19228
19774
  resource_kind: request.resource_kind,
@@ -19235,6 +19781,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19235
19781
  }
19236
19782
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
19237
19783
  }
19784
+ async listProjectResources(request) {
19785
+ const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
19786
+ if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
19787
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
19788
+ }
19789
+ if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
19790
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
19791
+ }
19792
+ const includeAnchors = request.include_anchors === true;
19793
+ const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
19794
+ if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
19795
+ 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 });
19796
+ }
19797
+ const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
19798
+ if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
19799
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
19800
+ source_project_id: sourceProjectId,
19801
+ todos_project_id: projectBinding.target_id
19802
+ });
19803
+ }
19804
+ const cursor = decodeProjectResourceCursor(request.cursor, {
19805
+ source_project_id: sourceProjectId,
19806
+ include_anchors: includeAnchors
19807
+ });
19808
+ const collectionInput = {
19809
+ todos_project_id: projectBinding.target_id,
19810
+ task_list_id: taskListBinding.target_id,
19811
+ include_anchors: includeAnchors
19812
+ };
19813
+ const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
19814
+ if (cursor && cursor.collection_revision !== collectionRevision) {
19815
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
19816
+ source_project_id: sourceProjectId,
19817
+ expected_collection_revision: cursor.collection_revision,
19818
+ current_collection_revision: collectionRevision
19819
+ });
19820
+ }
19821
+ const candidates = await this.backend.listProjectResourceCandidates({
19822
+ ...collectionInput,
19823
+ after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
19824
+ limit: request.limit + 1
19825
+ });
19826
+ const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
19827
+ if (verifiedCollectionRevision !== collectionRevision) {
19828
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
19829
+ source_project_id: sourceProjectId,
19830
+ expected_collection_revision: collectionRevision,
19831
+ current_collection_revision: verifiedCollectionRevision
19832
+ });
19833
+ }
19834
+ const hasMore = candidates.length > request.limit;
19835
+ const pageCandidates = candidates.slice(0, request.limit);
19836
+ const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
19837
+ const last = pageCandidates.at(-1);
19838
+ return {
19839
+ authority: "todos",
19840
+ route: this.capabilityValue.route,
19841
+ package_version: this.capabilityValue.package_version,
19842
+ authority_id: this.capabilityValue.authority_id,
19843
+ tenant_id: this.capabilityValue.tenant_id,
19844
+ corpus_id: this.capabilityValue.corpus_id,
19845
+ source_project_id: sourceProjectId,
19846
+ todos_project_id: projectBinding.target_id,
19847
+ task_list_id: taskListBinding.target_id,
19848
+ include_anchors: includeAnchors,
19849
+ collection_revision: collectionRevision,
19850
+ limit: request.limit,
19851
+ count: resources.length,
19852
+ resources,
19853
+ has_more: hasMore,
19854
+ next_cursor: hasMore && last ? encodeProjectResourceCursor({
19855
+ source_project_id: sourceProjectId,
19856
+ include_anchors: includeAnchors,
19857
+ collection_revision: collectionRevision,
19858
+ kind_rank: last.kind_rank,
19859
+ target_id: last.target_id
19860
+ }) : null,
19861
+ complete: !hasMore,
19862
+ truncated: false
19863
+ };
19864
+ }
19865
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
19866
+ const startedAt = Date.now();
19867
+ if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
19868
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
19869
+ }
19870
+ requireString(sourceRequest.package_version, "package_version", {
19871
+ max: 128,
19872
+ pattern: PACKAGE_VERSION_PATTERN
19873
+ });
19874
+ assertForwardRequest(sourceRequest, {
19875
+ ...this.capabilityValue,
19876
+ package_version: sourceRequest.package_version
19877
+ });
19878
+ const validation = await this.backend.transaction(async (transaction) => {
19879
+ const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
19880
+ if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
19881
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
19882
+ }
19883
+ const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
19884
+ if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
19885
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
19886
+ }
19887
+ 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);
19888
+ 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)) {
19889
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
19890
+ }
19891
+ const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
19892
+ 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) {
19893
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
19894
+ }
19895
+ const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
19896
+ if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
19897
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
19898
+ }
19899
+ let stableMatch = false;
19900
+ if (sourceRequest.resource_kind === "task_list") {
19901
+ stableMatch = taskListRegistrationDigest({
19902
+ ...current,
19903
+ updated_at: accepted.result_revision
19904
+ }) === accepted.result_digest;
19905
+ } else {
19906
+ const project = current;
19907
+ if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
19908
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
19909
+ }
19910
+ for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
19911
+ if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
19912
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
19913
+ }
19914
+ if (projectRegistrationDigest({
19915
+ ...project,
19916
+ task_counter: priorTaskCounter,
19917
+ updated_at: accepted.result_revision
19918
+ }) === accepted.result_digest) {
19919
+ stableMatch = true;
19920
+ break;
19921
+ }
19922
+ }
19923
+ }
19924
+ if (!stableMatch) {
19925
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
19926
+ }
19927
+ return {
19928
+ valid: true,
19929
+ resource_kind: sourceRequest.resource_kind,
19930
+ target_id: accepted.target_id,
19931
+ source_receipt_id: storedSource.receipt_id,
19932
+ accepted_receipt_id: accepted.receipt_id,
19933
+ source_outcome: storedSource.outcome,
19934
+ created_at: current.created_at,
19935
+ current_revision: current.updated_at,
19936
+ accepted_result_digest: accepted.result_digest
19937
+ };
19938
+ });
19939
+ assertWithinBounds(validation, sourceRequest, startedAt);
19940
+ return validation;
19941
+ }
19238
19942
  async storedAcceptedReceipt(request, supplied) {
19239
19943
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
19240
19944
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -19261,6 +19965,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19261
19965
  });
19262
19966
  const exact = await transaction.getReceiptForLookup({
19263
19967
  ...authorityScope(this.capabilityValue),
19968
+ route: this.capabilityValue.route,
19969
+ package_version: this.capabilityValue.package_version,
19264
19970
  operation_id: request.operation_id,
19265
19971
  step_id: request.step_id,
19266
19972
  resource_kind: request.resource_kind,
@@ -19355,6 +20061,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19355
20061
  await this.storedAcceptedReceipt(request, accepted);
19356
20062
  const receipt = await this.backend.getReceiptForLookup({
19357
20063
  ...authorityScope(this.capabilityValue),
20064
+ route: this.capabilityValue.route,
20065
+ package_version: this.capabilityValue.package_version,
19358
20066
  operation_id: request.operation_id,
19359
20067
  step_id: request.step_id,
19360
20068
  resource_kind: request.resource_kind,
@@ -19402,6 +20110,65 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
19402
20110
  cursorTableName
19403
20111
  }), authorityOptions);
19404
20112
  }
20113
+ // src/project-registration/adoption-validation.ts
20114
+ var VALIDATION_KEYS = [
20115
+ "valid",
20116
+ "resource_kind",
20117
+ "target_id",
20118
+ "source_receipt_id",
20119
+ "accepted_receipt_id",
20120
+ "source_outcome",
20121
+ "created_at",
20122
+ "current_revision",
20123
+ "accepted_result_digest"
20124
+ ];
20125
+ function isRecord(value) {
20126
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
20127
+ }
20128
+ function isNonEmptyString(value) {
20129
+ return typeof value === "string" && value.length > 0;
20130
+ }
20131
+ function hasExactKeys(value, expected) {
20132
+ const actual = Object.keys(value).sort();
20133
+ const wanted = [...expected].sort();
20134
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
20135
+ }
20136
+ function adoptionRejected(message) {
20137
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", `TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED: ${message}`);
20138
+ }
20139
+ function assertTodosPriorRegistrationAdoptionValidationEnvelope(value, input) {
20140
+ if (!isRecord(input) || !hasExactKeys(input, [
20141
+ "source_request",
20142
+ "source_receipt",
20143
+ "current_record"
20144
+ ])) {
20145
+ adoptionRejected("prior-adoption validation input is incomplete");
20146
+ }
20147
+ const request = input["source_request"];
20148
+ const receipt = input["source_receipt"];
20149
+ const current = input["current_record"];
20150
+ if (!isRecord(request) || !isRecord(receipt) || !isRecord(current)) {
20151
+ adoptionRejected("prior-adoption validation input records are incomplete");
20152
+ }
20153
+ const resourceKind = request["resource_kind"];
20154
+ const sourceOutcome = receipt["outcome"];
20155
+ const acceptedReceiptId = sourceOutcome === "accepted" ? receipt["receipt_id"] : sourceOutcome === "duplicate_of_accepted" ? receipt["duplicate_of_receipt_id"] : null;
20156
+ if (resourceKind !== "project" && resourceKind !== "task_list" || request["direction"] !== "forward" || sourceOutcome !== "accepted" && sourceOutcome !== "duplicate_of_accepted" || !isNonEmptyString(acceptedReceiptId) || !isNonEmptyString(receipt["receipt_id"]) || !isNonEmptyString(receipt["target_id"]) || !isNonEmptyString(receipt["result_revision"]) || !isNonEmptyString(receipt["result_digest"]) || !isNonEmptyString(current["id"]) || !isNonEmptyString(current["created_at"]) || !isNonEmptyString(current["updated_at"]) || receipt["authority"] !== "todos" || receipt["route"] !== request["authority_route"] || receipt["package_version"] !== request["package_version"] || receipt["authority_id"] !== request["authority_id"] || receipt["tenant_id"] !== request["tenant_id"] || receipt["corpus_id"] !== request["corpus_id"] || receipt["operation_id"] !== request["operation_id"] || receipt["step_id"] !== request["step_id"] || receipt["resource_kind"] !== resourceKind || receipt["direction"] !== "forward" || receipt["idempotency_key"] !== request["idempotency_key"] || receipt["request_digest"] !== request["request_digest"] || receipt["precondition_digest"] !== request["precondition_digest"] || receipt["accepted_receipt_id"] !== null || receipt["target_id"] !== current["id"] || receipt["result_revision"] !== current["created_at"]) {
20157
+ adoptionRejected("prior-adoption validation input does not carry one complete accepted receipt and current target incarnation");
20158
+ }
20159
+ if (sourceOutcome === "accepted" && receipt["duplicate_of_receipt_id"] !== null || sourceOutcome === "duplicate_of_accepted" && receipt["duplicate_of_receipt_id"] !== acceptedReceiptId) {
20160
+ adoptionRejected("prior-adoption validation source receipt lineage is incomplete");
20161
+ }
20162
+ if (!isRecord(value) || !hasExactKeys(value, ["validation"]) || !isRecord(value["validation"]) || !hasExactKeys(value["validation"], VALIDATION_KEYS)) {
20163
+ adoptionRejected("prior-adoption validation response envelope is incomplete");
20164
+ }
20165
+ const validation = value["validation"];
20166
+ if (validation["valid"] !== true || validation["resource_kind"] !== resourceKind || validation["target_id"] !== current["id"] || validation["source_receipt_id"] !== receipt["receipt_id"] || validation["accepted_receipt_id"] !== acceptedReceiptId || validation["source_outcome"] !== sourceOutcome || validation["created_at"] !== current["created_at"] || validation["current_revision"] !== current["updated_at"] || validation["accepted_result_digest"] !== receipt["result_digest"]) {
20167
+ adoptionRejected("prior-adoption validation response does not prove the exact accepted receipt and current target");
20168
+ }
20169
+ return validation;
20170
+ }
20171
+
19405
20172
  // src/project-registration/http.ts
19406
20173
  var JSON_HEADERS = { "Content-Type": "application/json" };
19407
20174
  function json(body, status = 200) {
@@ -19448,6 +20215,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
19448
20215
  if ((action === "" || action === "capability") && method === "GET") {
19449
20216
  return json({ capability: await authority.capability() });
19450
20217
  }
20218
+ if (action === "resources" && method === "GET") {
20219
+ const sourceProjectId = url.searchParams.get("source_project_id");
20220
+ const limit = Number(url.searchParams.get("limit") ?? "100");
20221
+ const includeAnchorsRaw = url.searchParams.get("include_anchors");
20222
+ const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
20223
+ return json({
20224
+ page: await authority.listProjectResources({
20225
+ source_project_id: sourceProjectId,
20226
+ limit,
20227
+ include_anchors: includeAnchors,
20228
+ cursor: url.searchParams.get("cursor") ?? undefined
20229
+ })
20230
+ });
20231
+ }
19451
20232
  if (method !== "POST")
19452
20233
  return json({ error: "method not allowed" }, 405);
19453
20234
  const body = await readJson(req);
@@ -19470,6 +20251,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
19470
20251
  record: await authority.readExact(body)
19471
20252
  });
19472
20253
  }
20254
+ if (action === "validate-prior-adoption") {
20255
+ const input = body;
20256
+ return json({
20257
+ validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
20258
+ });
20259
+ }
19473
20260
  if (action === "compensate") {
19474
20261
  return json({
19475
20262
  receipt: await authority.compensate(body)
@@ -19544,6 +20331,28 @@ class TodosProjectRegistrationHttpClient {
19544
20331
  async lookupReceipt(request) {
19545
20332
  return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
19546
20333
  }
20334
+ async listProjectResources(request) {
20335
+ const query = new URLSearchParams({
20336
+ source_project_id: request.source_project_id,
20337
+ limit: String(request.limit),
20338
+ include_anchors: String(request.include_anchors === true),
20339
+ ...request.cursor ? { cursor: request.cursor } : {}
20340
+ });
20341
+ const body = await this.request(`/resources?${query.toString()}`);
20342
+ return body.page;
20343
+ }
20344
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
20345
+ const input = {
20346
+ source_request: withoutTarget(sourceRequest),
20347
+ source_receipt: sourceReceipt,
20348
+ current_record: currentRecord
20349
+ };
20350
+ const body = await this.request("/validate-prior-adoption", {
20351
+ method: "POST",
20352
+ body: JSON.stringify(input)
20353
+ });
20354
+ return assertTodosPriorRegistrationAdoptionValidationEnvelope(body, input);
20355
+ }
19547
20356
  async compensate(request) {
19548
20357
  const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
19549
20358
  return body.receipt;
@@ -19569,6 +20378,7 @@ export {
19569
20378
  createPostgresTodosProjectRegistrationAuthority,
19570
20379
  createLocalTodosProjectRegistrationAuthority,
19571
20380
  canonicalProjectRegistrationJson,
20381
+ assertTodosPriorRegistrationAdoptionValidationEnvelope,
19572
20382
  TodosProjectRegistrationHttpClient,
19573
20383
  TodosProjectRegistrationError,
19574
20384
  TODOS_PROJECT_REGISTRATION_SCHEMA_VERSION,