@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.
- package/dist/cli/cloud-router.d.ts +13 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +2 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2861 -342
- package/dist/contracts.js +52 -2
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1466 -147
- package/dist/lib/task-parent-integrity.d.ts +14 -0
- package/dist/lib/task-parent-integrity.d.ts.map +1 -0
- package/dist/mcp/index.js +2050 -138
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/project-registration/adoption-validation.d.ts +3 -0
- package/dist/project-registration/adoption-validation.d.ts.map +1 -0
- package/dist/project-registration/authority.d.ts +3 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/backend.d.ts +24 -1
- package/dist/project-registration/backend.d.ts.map +1 -1
- package/dist/project-registration/http.d.ts +3 -1
- package/dist/project-registration/http.d.ts.map +1 -1
- package/dist/project-registration/index.d.ts +2 -1
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/page-validation.d.ts +5 -0
- package/dist/project-registration/page-validation.d.ts.map +1 -0
- package/dist/project-registration/postgres.d.ts +13 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts +13 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration/types.d.ts +68 -1
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +849 -60
- package/dist/registry.js +52 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +151 -0
- package/dist/sdk/v1.generated.d.ts +282 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +2041 -129
- package/dist/server/openapi.d.ts +2856 -1357
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +8 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +233 -20
- package/dist/task-manifest/authority.d.ts +13 -1
- package/dist/task-manifest/authority.d.ts.map +1 -1
- package/dist/task-manifest/backend.d.ts +5 -0
- package/dist/task-manifest/backend.d.ts.map +1 -1
- package/dist/task-manifest/index.d.ts +2 -2
- package/dist/task-manifest/index.d.ts.map +1 -1
- package/dist/task-manifest/plan-slug.d.ts +20 -0
- package/dist/task-manifest/plan-slug.d.ts.map +1 -1
- package/dist/task-manifest/postgres.d.ts +1 -0
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/schema-sql.d.ts.map +1 -1
- package/dist/task-manifest/schema.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -0
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest/types.d.ts +21 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +593 -62
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
3056
|
-
|
|
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
|
-
|
|
3822
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
21150
|
-
|
|
21151
|
-
|
|
21152
|
-
|
|
21153
|
-
|
|
21154
|
-
|
|
21155
|
-
|
|
21156
|
-
|
|
21157
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27322
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
26486
27323
|
}
|
|
26487
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26601
|
-
|
|
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,
|
|
26627
|
-
|
|
26628
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
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: {
|
|
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(
|
|
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,
|
|
26923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27149
|
-
|
|
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:
|
|
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
|
|
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_${
|
|
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,
|
|
32194
|
-
return `ptlr_${
|
|
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
|
|
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,
|
|
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 !==
|
|
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_${
|
|
32229
|
-
idempotency_key: owned ? marker.idempotency_key :
|
|
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
|
|
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,
|
|
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,
|
|
32294
|
-
idempotency_key:
|
|
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,
|
|
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,
|
|
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_${
|
|
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
|
|
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:
|
|
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, ...
|
|
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
|
|
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
|
-
|
|
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
|
|
111316
|
-
return { id: `env_${
|
|
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());
|