@hasna/todos 0.15.29 → 0.15.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2835 -334
- 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 +1447 -144
- 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 +2031 -135
- 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 +831 -58
- 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 +2022 -126
- 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 +232 -19
- 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 +592 -61
- 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.32",
|
|
74
74
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
75
75
|
type: "module",
|
|
76
76
|
main: "dist/index.js",
|
|
@@ -2373,6 +2373,46 @@ var init_integrity = __esm(() => {
|
|
|
2373
2373
|
};
|
|
2374
2374
|
});
|
|
2375
2375
|
|
|
2376
|
+
// src/lib/task-parent-integrity.ts
|
|
2377
|
+
function parentCycleError(taskId, parentId) {
|
|
2378
|
+
return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
|
|
2379
|
+
}
|
|
2380
|
+
function assertTaskParentIntegrity(taskId, parentId, getTask) {
|
|
2381
|
+
if (parentId === undefined || parentId === null)
|
|
2382
|
+
return;
|
|
2383
|
+
const visited = new Set;
|
|
2384
|
+
let cursor = parentId;
|
|
2385
|
+
while (cursor) {
|
|
2386
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
2387
|
+
throw parentCycleError(taskId, parentId);
|
|
2388
|
+
}
|
|
2389
|
+
visited.add(cursor);
|
|
2390
|
+
const parent = getTask(cursor);
|
|
2391
|
+
if (!parent)
|
|
2392
|
+
throw new TaskNotFoundError(cursor);
|
|
2393
|
+
cursor = parent.parent_id;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
|
|
2397
|
+
if (parentId === undefined || parentId === null)
|
|
2398
|
+
return;
|
|
2399
|
+
const visited = new Set;
|
|
2400
|
+
let cursor = parentId;
|
|
2401
|
+
while (cursor) {
|
|
2402
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
2403
|
+
throw parentCycleError(taskId, parentId);
|
|
2404
|
+
}
|
|
2405
|
+
visited.add(cursor);
|
|
2406
|
+
const parent = await getTask(cursor);
|
|
2407
|
+
if (!parent)
|
|
2408
|
+
throw new TaskNotFoundError(cursor);
|
|
2409
|
+
cursor = parent.parent_id;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
var init_task_parent_integrity = __esm(() => {
|
|
2413
|
+
init_types();
|
|
2414
|
+
});
|
|
2415
|
+
|
|
2376
2416
|
// src/storage/audit-history-import.ts
|
|
2377
2417
|
function auditHistoryRowsAreFieldIdentical(left, right) {
|
|
2378
2418
|
return AUDIT_HISTORY_FIELDS.every((field) => {
|
|
@@ -2469,8 +2509,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
2469
2509
|
resolveRef: (ref) => store.resolveTaskRef(ref),
|
|
2470
2510
|
list: (filter = {}) => store.listTasks(filter),
|
|
2471
2511
|
count: (filter = {}) => store.countTasks(filter),
|
|
2472
|
-
update: (id, input) => updateTask(id, input, store),
|
|
2473
|
-
delete: (id, context) => store.
|
|
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
|
|
21158
21468
|
})
|
|
21159
21469
|
};
|
|
21160
21470
|
}
|
|
21471
|
+
function boundExistingTaskListRecord(taskList) {
|
|
21472
|
+
return {
|
|
21473
|
+
target_id: taskList.id,
|
|
21474
|
+
revision: taskList.created_at,
|
|
21475
|
+
digest: taskListRegistrationDigest({
|
|
21476
|
+
...taskList,
|
|
21477
|
+
updated_at: taskList.created_at
|
|
21478
|
+
})
|
|
21479
|
+
};
|
|
21480
|
+
}
|
|
21481
|
+
function projectRegistrationDigest(project) {
|
|
21482
|
+
return digestProjectRegistrationValue({
|
|
21483
|
+
id: project.id,
|
|
21484
|
+
name: project.name,
|
|
21485
|
+
path: project.path,
|
|
21486
|
+
description: project.description,
|
|
21487
|
+
task_list_id: project.task_list_id,
|
|
21488
|
+
task_prefix: project.task_prefix,
|
|
21489
|
+
task_counter: project.task_counter,
|
|
21490
|
+
created_at: project.created_at,
|
|
21491
|
+
updated_at: project.updated_at
|
|
21492
|
+
});
|
|
21493
|
+
}
|
|
21494
|
+
function taskListRegistrationDigest(taskList) {
|
|
21495
|
+
return digestProjectRegistrationValue({
|
|
21496
|
+
id: taskList.id,
|
|
21497
|
+
project_id: taskList.project_id,
|
|
21498
|
+
slug: taskList.slug,
|
|
21499
|
+
name: taskList.name,
|
|
21500
|
+
description: taskList.description,
|
|
21501
|
+
metadata: taskList.metadata,
|
|
21502
|
+
created_at: taskList.created_at,
|
|
21503
|
+
updated_at: taskList.updated_at
|
|
21504
|
+
});
|
|
21505
|
+
}
|
|
21506
|
+
function canonicalValuesEqual(left, right) {
|
|
21507
|
+
try {
|
|
21508
|
+
return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
|
|
21509
|
+
} catch {
|
|
21510
|
+
return false;
|
|
21511
|
+
}
|
|
21512
|
+
}
|
|
21161
21513
|
function receiptId(input) {
|
|
21162
21514
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
21163
21515
|
}
|
|
@@ -21177,6 +21529,29 @@ function assertCapabilityRequest(request, capability) {
|
|
|
21177
21529
|
}
|
|
21178
21530
|
}
|
|
21179
21531
|
function normalizedCallDigest(request) {
|
|
21532
|
+
return digestProjectRegistrationValue({
|
|
21533
|
+
authority_route: request.authority_route,
|
|
21534
|
+
package_version: request.package_version,
|
|
21535
|
+
authority_id: request.authority_id,
|
|
21536
|
+
tenant_id: request.tenant_id,
|
|
21537
|
+
corpus_id: request.corpus_id,
|
|
21538
|
+
operation_id: request.operation_id,
|
|
21539
|
+
step_id: request.step_id,
|
|
21540
|
+
resource_kind: request.resource_kind,
|
|
21541
|
+
direction: request.direction,
|
|
21542
|
+
target_selector: request.target_selector,
|
|
21543
|
+
idempotency_key: request.idempotency_key,
|
|
21544
|
+
request_digest: request.request_digest,
|
|
21545
|
+
precondition_digest: request.precondition_digest,
|
|
21546
|
+
project_id: request.project_id,
|
|
21547
|
+
project_slug: request.project_slug,
|
|
21548
|
+
project_name: request.project_name,
|
|
21549
|
+
desired: request.desired,
|
|
21550
|
+
bind_existing: request.bind_existing === true,
|
|
21551
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
21552
|
+
});
|
|
21553
|
+
}
|
|
21554
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
21180
21555
|
return digestProjectRegistrationValue({
|
|
21181
21556
|
authority_route: request.authority_route,
|
|
21182
21557
|
package_version: request.package_version,
|
|
@@ -21198,6 +21573,11 @@ function normalizedCallDigest(request) {
|
|
|
21198
21573
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
21199
21574
|
});
|
|
21200
21575
|
}
|
|
21576
|
+
function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
|
|
21577
|
+
if (accepted.normalized_call_digest === callDigest)
|
|
21578
|
+
return true;
|
|
21579
|
+
return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
|
|
21580
|
+
}
|
|
21201
21581
|
function assertCommonRequest(request, capability) {
|
|
21202
21582
|
assertBounds(request);
|
|
21203
21583
|
assertResourceKind(request.resource_kind);
|
|
@@ -21239,6 +21619,9 @@ function assertCommonRequest(request, capability) {
|
|
|
21239
21619
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
21240
21620
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
21241
21621
|
}
|
|
21622
|
+
if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
|
|
21623
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
|
|
21624
|
+
}
|
|
21242
21625
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
21243
21626
|
operation_id: request.operation_id,
|
|
21244
21627
|
step_id: request.step_id,
|
|
@@ -21260,7 +21643,7 @@ function assertForwardRequest(request, capability) {
|
|
|
21260
21643
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
21261
21644
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
21262
21645
|
target_selector: request.target_selector,
|
|
21263
|
-
expected: "absent"
|
|
21646
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
21264
21647
|
});
|
|
21265
21648
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
21266
21649
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -21339,7 +21722,7 @@ function receiptBase(request, callDigest, capability) {
|
|
|
21339
21722
|
normalized_call_digest: callDigest
|
|
21340
21723
|
};
|
|
21341
21724
|
}
|
|
21342
|
-
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt) {
|
|
21725
|
+
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt, createdByOperation = true) {
|
|
21343
21726
|
return makeReceipt({
|
|
21344
21727
|
...receiptBase(request, callDigest, capability),
|
|
21345
21728
|
outcome: "accepted",
|
|
@@ -21349,7 +21732,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt)
|
|
|
21349
21732
|
result_digest: record.digest,
|
|
21350
21733
|
duplicate_of_receipt_id: null,
|
|
21351
21734
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
21352
|
-
created_by_operation:
|
|
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));
|
|
@@ -21729,6 +22181,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21729
22181
|
}
|
|
21730
22182
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
21731
22183
|
}
|
|
22184
|
+
async listProjectResources(request) {
|
|
22185
|
+
const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
|
|
22186
|
+
if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
|
|
22187
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
|
|
22188
|
+
}
|
|
22189
|
+
if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
|
|
22190
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
|
|
22191
|
+
}
|
|
22192
|
+
const includeAnchors = request.include_anchors === true;
|
|
22193
|
+
const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
|
|
22194
|
+
if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
|
|
22195
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted Todos project binding exists for this exact Projects workspace id", { source_project_id: sourceProjectId });
|
|
22196
|
+
}
|
|
22197
|
+
const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
|
|
22198
|
+
if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
|
|
22199
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
|
|
22200
|
+
source_project_id: sourceProjectId,
|
|
22201
|
+
todos_project_id: projectBinding.target_id
|
|
22202
|
+
});
|
|
22203
|
+
}
|
|
22204
|
+
const cursor = decodeProjectResourceCursor(request.cursor, {
|
|
22205
|
+
source_project_id: sourceProjectId,
|
|
22206
|
+
include_anchors: includeAnchors
|
|
22207
|
+
});
|
|
22208
|
+
const collectionInput = {
|
|
22209
|
+
todos_project_id: projectBinding.target_id,
|
|
22210
|
+
task_list_id: taskListBinding.target_id,
|
|
22211
|
+
include_anchors: includeAnchors
|
|
22212
|
+
};
|
|
22213
|
+
const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
22214
|
+
if (cursor && cursor.collection_revision !== collectionRevision) {
|
|
22215
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
|
|
22216
|
+
source_project_id: sourceProjectId,
|
|
22217
|
+
expected_collection_revision: cursor.collection_revision,
|
|
22218
|
+
current_collection_revision: collectionRevision
|
|
22219
|
+
});
|
|
22220
|
+
}
|
|
22221
|
+
const candidates = await this.backend.listProjectResourceCandidates({
|
|
22222
|
+
...collectionInput,
|
|
22223
|
+
after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
|
|
22224
|
+
limit: request.limit + 1
|
|
22225
|
+
});
|
|
22226
|
+
const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
22227
|
+
if (verifiedCollectionRevision !== collectionRevision) {
|
|
22228
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
|
|
22229
|
+
source_project_id: sourceProjectId,
|
|
22230
|
+
expected_collection_revision: collectionRevision,
|
|
22231
|
+
current_collection_revision: verifiedCollectionRevision
|
|
22232
|
+
});
|
|
22233
|
+
}
|
|
22234
|
+
const hasMore = candidates.length > request.limit;
|
|
22235
|
+
const pageCandidates = candidates.slice(0, request.limit);
|
|
22236
|
+
const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
|
|
22237
|
+
const last = pageCandidates.at(-1);
|
|
22238
|
+
return {
|
|
22239
|
+
authority: "todos",
|
|
22240
|
+
route: this.capabilityValue.route,
|
|
22241
|
+
package_version: this.capabilityValue.package_version,
|
|
22242
|
+
authority_id: this.capabilityValue.authority_id,
|
|
22243
|
+
tenant_id: this.capabilityValue.tenant_id,
|
|
22244
|
+
corpus_id: this.capabilityValue.corpus_id,
|
|
22245
|
+
source_project_id: sourceProjectId,
|
|
22246
|
+
todos_project_id: projectBinding.target_id,
|
|
22247
|
+
task_list_id: taskListBinding.target_id,
|
|
22248
|
+
include_anchors: includeAnchors,
|
|
22249
|
+
collection_revision: collectionRevision,
|
|
22250
|
+
limit: request.limit,
|
|
22251
|
+
count: resources.length,
|
|
22252
|
+
resources,
|
|
22253
|
+
has_more: hasMore,
|
|
22254
|
+
next_cursor: hasMore && last ? encodeProjectResourceCursor({
|
|
22255
|
+
source_project_id: sourceProjectId,
|
|
22256
|
+
include_anchors: includeAnchors,
|
|
22257
|
+
collection_revision: collectionRevision,
|
|
22258
|
+
kind_rank: last.kind_rank,
|
|
22259
|
+
target_id: last.target_id
|
|
22260
|
+
}) : null,
|
|
22261
|
+
complete: !hasMore,
|
|
22262
|
+
truncated: false
|
|
22263
|
+
};
|
|
22264
|
+
}
|
|
22265
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
22266
|
+
const startedAt = Date.now();
|
|
22267
|
+
if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
|
|
22268
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
|
|
22269
|
+
}
|
|
22270
|
+
requireString(sourceRequest.package_version, "package_version", {
|
|
22271
|
+
max: 128,
|
|
22272
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
22273
|
+
});
|
|
22274
|
+
assertForwardRequest(sourceRequest, {
|
|
22275
|
+
...this.capabilityValue,
|
|
22276
|
+
package_version: sourceRequest.package_version
|
|
22277
|
+
});
|
|
22278
|
+
const validation = await this.backend.transaction(async (transaction) => {
|
|
22279
|
+
const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
|
|
22280
|
+
if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
|
|
22281
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
22282
|
+
}
|
|
22283
|
+
const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
|
|
22284
|
+
if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
|
|
22285
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
22286
|
+
}
|
|
22287
|
+
const receiptLineageMatches = (receipt) => receipt.authority === "todos" && receipt.route === sourceRequest.authority_route && receipt.package_version === sourceRequest.package_version && receipt.authority_id === sourceRequest.authority_id && receipt.tenant_id === sourceRequest.tenant_id && receipt.corpus_id === sourceRequest.corpus_id && receipt.operation_id === sourceRequest.operation_id && receipt.step_id === sourceRequest.step_id && receipt.resource_kind === sourceRequest.resource_kind && receipt.direction === "forward" && receipt.target_selector === sourceRequest.target_selector && receipt.idempotency_key === sourceRequest.idempotency_key && receipt.request_digest === sourceRequest.request_digest && receipt.precondition_digest === sourceRequest.precondition_digest && acceptedCallMatches(sourceRequest, receipt);
|
|
22288
|
+
if (!receiptLineageMatches(storedSource) || !receiptLineageMatches(accepted) || storedSource.outcome === "duplicate_of_accepted" && (storedSource.duplicate_of_receipt_id !== accepted.receipt_id || storedSource.target_id !== accepted.target_id || storedSource.result_revision !== accepted.result_revision || storedSource.result_digest !== accepted.result_digest)) {
|
|
22289
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
|
|
22290
|
+
}
|
|
22291
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
|
|
22292
|
+
if (!binding || binding.state !== "accepted" || binding.operation_id !== sourceRequest.operation_id || binding.step_id !== sourceRequest.step_id || binding.direction !== "forward" || binding.idempotency_key !== sourceRequest.idempotency_key || binding.request_digest !== sourceRequest.request_digest || binding.precondition_digest !== sourceRequest.precondition_digest || binding.normalized_call_digest !== accepted.normalized_call_digest || binding.target_id !== accepted.target_id || binding.accepted_receipt_id !== accepted.receipt_id || binding.result_revision !== accepted.result_revision || binding.result_digest !== accepted.result_digest) {
|
|
22293
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
|
|
22294
|
+
}
|
|
22295
|
+
const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
22296
|
+
if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
|
|
22297
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
|
|
22298
|
+
}
|
|
22299
|
+
let stableMatch = false;
|
|
22300
|
+
if (sourceRequest.resource_kind === "task_list") {
|
|
22301
|
+
stableMatch = taskListRegistrationDigest({
|
|
22302
|
+
...current,
|
|
22303
|
+
updated_at: accepted.result_revision
|
|
22304
|
+
}) === accepted.result_digest;
|
|
22305
|
+
} else {
|
|
22306
|
+
const project = current;
|
|
22307
|
+
if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
|
|
22308
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
|
|
22309
|
+
}
|
|
22310
|
+
for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
|
|
22311
|
+
if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
|
|
22312
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
|
|
22313
|
+
}
|
|
22314
|
+
if (projectRegistrationDigest({
|
|
22315
|
+
...project,
|
|
22316
|
+
task_counter: priorTaskCounter,
|
|
22317
|
+
updated_at: accepted.result_revision
|
|
22318
|
+
}) === accepted.result_digest) {
|
|
22319
|
+
stableMatch = true;
|
|
22320
|
+
break;
|
|
22321
|
+
}
|
|
22322
|
+
}
|
|
22323
|
+
}
|
|
22324
|
+
if (!stableMatch) {
|
|
22325
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
|
|
22326
|
+
}
|
|
22327
|
+
return {
|
|
22328
|
+
valid: true,
|
|
22329
|
+
resource_kind: sourceRequest.resource_kind,
|
|
22330
|
+
target_id: accepted.target_id,
|
|
22331
|
+
source_receipt_id: storedSource.receipt_id,
|
|
22332
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
22333
|
+
source_outcome: storedSource.outcome,
|
|
22334
|
+
created_at: current.created_at,
|
|
22335
|
+
current_revision: current.updated_at,
|
|
22336
|
+
accepted_result_digest: accepted.result_digest
|
|
22337
|
+
};
|
|
22338
|
+
});
|
|
22339
|
+
assertWithinBounds(validation, sourceRequest, startedAt);
|
|
22340
|
+
return validation;
|
|
22341
|
+
}
|
|
21732
22342
|
async storedAcceptedReceipt(request, supplied) {
|
|
21733
22343
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
21734
22344
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -21897,7 +22507,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
21897
22507
|
cursorTableName
|
|
21898
22508
|
}), authorityOptions);
|
|
21899
22509
|
}
|
|
21900
|
-
var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
|
|
22510
|
+
var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, PROJECT_RESOURCE_PAGE_LIMIT = 500, PROJECT_RESOURCE_CURSOR_VERSION = 1, WriteBoundaryError;
|
|
21901
22511
|
var init_authority = __esm(() => {
|
|
21902
22512
|
init_package_version();
|
|
21903
22513
|
init_postgres2();
|
|
@@ -21922,6 +22532,11 @@ var init_authority = __esm(() => {
|
|
|
21922
22532
|
};
|
|
21923
22533
|
});
|
|
21924
22534
|
|
|
22535
|
+
// src/project-registration/adoption-validation.ts
|
|
22536
|
+
var init_adoption_validation = __esm(() => {
|
|
22537
|
+
init_types3();
|
|
22538
|
+
});
|
|
22539
|
+
|
|
21925
22540
|
// src/project-registration/http.ts
|
|
21926
22541
|
function json(body, status = 200) {
|
|
21927
22542
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
@@ -21967,6 +22582,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
21967
22582
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
21968
22583
|
return json({ capability: await authority.capability() });
|
|
21969
22584
|
}
|
|
22585
|
+
if (action === "resources" && method === "GET") {
|
|
22586
|
+
const sourceProjectId = url.searchParams.get("source_project_id");
|
|
22587
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
22588
|
+
const includeAnchorsRaw = url.searchParams.get("include_anchors");
|
|
22589
|
+
const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
|
|
22590
|
+
return json({
|
|
22591
|
+
page: await authority.listProjectResources({
|
|
22592
|
+
source_project_id: sourceProjectId,
|
|
22593
|
+
limit,
|
|
22594
|
+
include_anchors: includeAnchors,
|
|
22595
|
+
cursor: url.searchParams.get("cursor") ?? undefined
|
|
22596
|
+
})
|
|
22597
|
+
});
|
|
22598
|
+
}
|
|
21970
22599
|
if (method !== "POST")
|
|
21971
22600
|
return json({ error: "method not allowed" }, 405);
|
|
21972
22601
|
const body = await readJson(req);
|
|
@@ -21989,6 +22618,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
21989
22618
|
record: await authority.readExact(body)
|
|
21990
22619
|
});
|
|
21991
22620
|
}
|
|
22621
|
+
if (action === "validate-prior-adoption") {
|
|
22622
|
+
const input = body;
|
|
22623
|
+
return json({
|
|
22624
|
+
validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
|
|
22625
|
+
});
|
|
22626
|
+
}
|
|
21992
22627
|
if (action === "compensate") {
|
|
21993
22628
|
return json({
|
|
21994
22629
|
receipt: await authority.compensate(body)
|
|
@@ -22021,6 +22656,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
22021
22656
|
var JSON_HEADERS;
|
|
22022
22657
|
var init_http = __esm(() => {
|
|
22023
22658
|
init_types3();
|
|
22659
|
+
init_adoption_validation();
|
|
22024
22660
|
JSON_HEADERS = { "Content-Type": "application/json" };
|
|
22025
22661
|
});
|
|
22026
22662
|
|
|
@@ -22028,6 +22664,7 @@ var init_http = __esm(() => {
|
|
|
22028
22664
|
var init_project_registration = __esm(() => {
|
|
22029
22665
|
init_authority();
|
|
22030
22666
|
init_http();
|
|
22667
|
+
init_adoption_validation();
|
|
22031
22668
|
init_postgres2();
|
|
22032
22669
|
init_sqlite();
|
|
22033
22670
|
init_types3();
|
|
@@ -26000,7 +26637,7 @@ var init_zod = __esm(() => {
|
|
|
26000
26637
|
});
|
|
26001
26638
|
|
|
26002
26639
|
// src/task-manifest/types.ts
|
|
26003
|
-
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
|
|
26640
|
+
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1", TodosTaskManifestError;
|
|
26004
26641
|
var init_types5 = __esm(() => {
|
|
26005
26642
|
TodosTaskManifestError = class TodosTaskManifestError extends Error {
|
|
26006
26643
|
code;
|
|
@@ -26087,7 +26724,7 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
26087
26724
|
}
|
|
26088
26725
|
return parsed.data;
|
|
26089
26726
|
}
|
|
26090
|
-
var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
26727
|
+
var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, digest, idempotencyKey, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
26091
26728
|
if (Object.keys(value).length > limit) {
|
|
26092
26729
|
context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
|
|
26093
26730
|
}
|
|
@@ -26109,6 +26746,8 @@ var init_schema2 = __esm(() => {
|
|
|
26109
26746
|
};
|
|
26110
26747
|
key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
26111
26748
|
identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
26749
|
+
digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
|
|
26750
|
+
idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
|
|
26112
26751
|
uuid2 = exports_external.string().uuid();
|
|
26113
26752
|
scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
26114
26753
|
comment = exports_external.object({
|
|
@@ -26146,7 +26785,9 @@ var init_schema2 = __esm(() => {
|
|
|
26146
26785
|
schema = exports_external.object({
|
|
26147
26786
|
version: exports_external.literal(1),
|
|
26148
26787
|
operation_id: identifier,
|
|
26149
|
-
|
|
26788
|
+
step_id: identifier,
|
|
26789
|
+
idempotency_key: idempotencyKey,
|
|
26790
|
+
precondition_digest: digest,
|
|
26150
26791
|
project_id: uuid2,
|
|
26151
26792
|
task_list_id: uuid2.optional(),
|
|
26152
26793
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -26162,7 +26803,10 @@ var init_schema2 = __esm(() => {
|
|
|
26162
26803
|
}).strict();
|
|
26163
26804
|
compensationSchema = exports_external.object({
|
|
26164
26805
|
receipt_id: uuid2,
|
|
26165
|
-
|
|
26806
|
+
operation_id: identifier,
|
|
26807
|
+
step_id: identifier,
|
|
26808
|
+
idempotency_key: idempotencyKey,
|
|
26809
|
+
precondition_digest: digest,
|
|
26166
26810
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
26167
26811
|
}).strict();
|
|
26168
26812
|
bindingLookupSchema = exports_external.object({
|
|
@@ -26180,6 +26824,7 @@ function taskManifestPlanSlug(manifest, planId) {
|
|
|
26180
26824
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
26181
26825
|
return `${base}-${planId}`;
|
|
26182
26826
|
}
|
|
26827
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
26183
26828
|
var init_plan_slug = () => {};
|
|
26184
26829
|
|
|
26185
26830
|
// src/task-manifest/backend.ts
|
|
@@ -26193,11 +26838,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
26193
26838
|
const row = rows[0];
|
|
26194
26839
|
const bindingVersion = Number(row.binding_version);
|
|
26195
26840
|
const state = row.state;
|
|
26196
|
-
if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
|
|
26841
|
+
if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || row.binding_step_id !== row.receipt_step_id || typeof row.binding_operation_id !== "string" || typeof row.binding_step_id !== "string" || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
|
|
26197
26842
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
26198
26843
|
}
|
|
26199
26844
|
return {
|
|
26200
26845
|
plan_id: planId,
|
|
26846
|
+
operation_id: row.binding_operation_id,
|
|
26847
|
+
step_id: row.binding_step_id,
|
|
26201
26848
|
apply_receipt_id: row.apply_receipt_id,
|
|
26202
26849
|
binding_version: bindingVersion,
|
|
26203
26850
|
state
|
|
@@ -26250,9 +26897,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26250
26897
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
26251
26898
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
26252
26899
|
operation_id text NOT NULL,
|
|
26900
|
+
step_id text NOT NULL,
|
|
26253
26901
|
idempotency_key text NOT NULL,
|
|
26254
26902
|
request_digest text NOT NULL,
|
|
26903
|
+
precondition_digest text NOT NULL,
|
|
26255
26904
|
result_digest text NOT NULL,
|
|
26905
|
+
slug_provenance text,
|
|
26906
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
26907
|
+
reason text,
|
|
26908
|
+
duplicate_of_receipt_id text,
|
|
26256
26909
|
binding_version integer NOT NULL,
|
|
26257
26910
|
apply_receipt_id text,
|
|
26258
26911
|
manifest_json jsonb,
|
|
@@ -26264,12 +26917,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26264
26917
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
26265
26918
|
`ALTER TABLE todos_task_manifest_receipts
|
|
26266
26919
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
26920
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
26921
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
26922
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
26923
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
26924
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
26925
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
26926
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
26927
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
26928
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
26929
|
+
ADD COLUMN IF NOT EXISTS reason text`,
|
|
26930
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
26931
|
+
ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
|
|
26267
26932
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
26268
26933
|
operation_id text PRIMARY KEY,
|
|
26269
26934
|
tenant_id text NOT NULL,
|
|
26935
|
+
step_id text NOT NULL,
|
|
26270
26936
|
idempotency_key text NOT NULL UNIQUE,
|
|
26271
26937
|
request_digest text NOT NULL,
|
|
26938
|
+
precondition_digest text NOT NULL,
|
|
26272
26939
|
result_digest text NOT NULL,
|
|
26940
|
+
slug_provenance text,
|
|
26941
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
26273
26942
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
26274
26943
|
manifest_json jsonb NOT NULL,
|
|
26275
26944
|
result_json jsonb NOT NULL,
|
|
@@ -26283,6 +26952,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26283
26952
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
26284
26953
|
`ALTER TABLE todos_task_manifest_bindings
|
|
26285
26954
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
26955
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
26956
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
26957
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
26958
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
26959
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
26960
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
26961
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
26962
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
26286
26963
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
26287
26964
|
id text PRIMARY KEY,
|
|
26288
26965
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -26294,10 +26971,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26294
26971
|
created_at timestamptz NOT NULL,
|
|
26295
26972
|
delivered_at timestamptz
|
|
26296
26973
|
)`,
|
|
26974
|
+
`CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
26975
|
+
receipt_id text PRIMARY KEY,
|
|
26976
|
+
tenant_id text NOT NULL,
|
|
26977
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
26978
|
+
route text NOT NULL,
|
|
26979
|
+
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
26980
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
26981
|
+
operation_id text NOT NULL,
|
|
26982
|
+
step_id text NOT NULL,
|
|
26983
|
+
idempotency_key text NOT NULL,
|
|
26984
|
+
request_digest text NOT NULL,
|
|
26985
|
+
precondition_digest text NOT NULL,
|
|
26986
|
+
result_digest text NOT NULL,
|
|
26987
|
+
outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
26988
|
+
reason text NOT NULL,
|
|
26989
|
+
binding_version integer NOT NULL,
|
|
26990
|
+
apply_receipt_id text,
|
|
26991
|
+
manifest_json jsonb,
|
|
26992
|
+
result_json jsonb NOT NULL,
|
|
26993
|
+
created_at timestamptz NOT NULL
|
|
26994
|
+
)`,
|
|
26297
26995
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
26298
26996
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
26299
26997
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
26300
26998
|
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
26999
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
|
|
27000
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
|
|
27001
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
|
|
27002
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
27003
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
|
|
27004
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
26301
27005
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
26302
27006
|
ON todos_task_manifest_bindings(
|
|
26303
27007
|
tenant_id,
|
|
@@ -26310,6 +27014,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26310
27014
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
26311
27015
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
26312
27016
|
BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
|
|
27017
|
+
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
|
|
27018
|
+
`DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
|
|
27019
|
+
`CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
|
|
27020
|
+
BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
|
|
26313
27021
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
26314
27022
|
];
|
|
26315
27023
|
}
|
|
@@ -26334,6 +27042,39 @@ function safeIdentifier2(value, field) {
|
|
|
26334
27042
|
function parseJson(value) {
|
|
26335
27043
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
26336
27044
|
}
|
|
27045
|
+
function parseApplyResult(value, duplicate) {
|
|
27046
|
+
const parsed = parseJson(value);
|
|
27047
|
+
return {
|
|
27048
|
+
...parsed,
|
|
27049
|
+
duplicate,
|
|
27050
|
+
receipt: {
|
|
27051
|
+
...parsed.receipt,
|
|
27052
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
27053
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
27054
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
27055
|
+
reason: parsed.receipt.reason ?? null,
|
|
27056
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
27057
|
+
}
|
|
27058
|
+
};
|
|
27059
|
+
}
|
|
27060
|
+
function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
|
|
27061
|
+
if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
27062
|
+
const expected = taskManifestPlanSlug(manifest, planId);
|
|
27063
|
+
if (slug !== expected) {
|
|
27064
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
|
|
27065
|
+
}
|
|
27066
|
+
return expected;
|
|
27067
|
+
}
|
|
27068
|
+
if (provenance !== null && provenance !== undefined) {
|
|
27069
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
27070
|
+
}
|
|
27071
|
+
if (slug === null || slug === undefined)
|
|
27072
|
+
return null;
|
|
27073
|
+
if (slug !== null && slug !== undefined) {
|
|
27074
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
|
|
27075
|
+
}
|
|
27076
|
+
return null;
|
|
27077
|
+
}
|
|
26337
27078
|
function timestamp2(value) {
|
|
26338
27079
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
26339
27080
|
}
|
|
@@ -26341,6 +27082,41 @@ function fault(faults, point) {
|
|
|
26341
27082
|
if (faults.points.has(point))
|
|
26342
27083
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
26343
27084
|
}
|
|
27085
|
+
function terminalApplyResult(input, reason) {
|
|
27086
|
+
const receipt = {
|
|
27087
|
+
receipt_id: input.terminal_receipt_id,
|
|
27088
|
+
authority: "todos",
|
|
27089
|
+
route: "todos.task-manifest.v1",
|
|
27090
|
+
schema_version: 1,
|
|
27091
|
+
kind: "apply",
|
|
27092
|
+
operation_id: input.manifest.operation_id,
|
|
27093
|
+
step_id: input.manifest.step_id,
|
|
27094
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
27095
|
+
request_digest: input.request_digest,
|
|
27096
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
27097
|
+
result_digest: canonicalDigest({
|
|
27098
|
+
outcome: "terminal_nonacceptance",
|
|
27099
|
+
reason,
|
|
27100
|
+
operation_id: input.manifest.operation_id,
|
|
27101
|
+
step_id: input.manifest.step_id,
|
|
27102
|
+
request_digest: input.request_digest
|
|
27103
|
+
}),
|
|
27104
|
+
outcome: "terminal_nonacceptance",
|
|
27105
|
+
reason,
|
|
27106
|
+
duplicate_of_receipt_id: null,
|
|
27107
|
+
binding_version: 0,
|
|
27108
|
+
apply_receipt_id: null,
|
|
27109
|
+
created_at: input.now
|
|
27110
|
+
};
|
|
27111
|
+
return {
|
|
27112
|
+
duplicate: false,
|
|
27113
|
+
receipt,
|
|
27114
|
+
graph: input.graph,
|
|
27115
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
27116
|
+
outbox_ids: [],
|
|
27117
|
+
result_digest: receipt.result_digest
|
|
27118
|
+
};
|
|
27119
|
+
}
|
|
26344
27120
|
function receiptFromRow3(row) {
|
|
26345
27121
|
return {
|
|
26346
27122
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -26349,9 +27125,14 @@ function receiptFromRow3(row) {
|
|
|
26349
27125
|
schema_version: 1,
|
|
26350
27126
|
kind: row["kind"],
|
|
26351
27127
|
operation_id: String(row["operation_id"]),
|
|
27128
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
26352
27129
|
idempotency_key: String(row["idempotency_key"]),
|
|
26353
27130
|
request_digest: String(row["request_digest"]),
|
|
27131
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
26354
27132
|
result_digest: String(row["result_digest"]),
|
|
27133
|
+
outcome: row["outcome"] ?? "accepted",
|
|
27134
|
+
reason: row["reason"] == null ? null : row["reason"],
|
|
27135
|
+
duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
|
|
26355
27136
|
binding_version: Number(row["binding_version"]),
|
|
26356
27137
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
26357
27138
|
created_at: timestamp2(row["created_at"])
|
|
@@ -26469,46 +27250,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26469
27250
|
now3
|
|
26470
27251
|
]);
|
|
26471
27252
|
}
|
|
27253
|
+
async persistTerminal(tx, input, reason) {
|
|
27254
|
+
const result = terminalApplyResult(input, reason);
|
|
27255
|
+
const resultJson = canonicalJson(result);
|
|
27256
|
+
await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
|
|
27257
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
27258
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
27259
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
27260
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
|
|
27261
|
+
'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
|
|
27262
|
+
ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
|
|
27263
|
+
result.receipt.receipt_id,
|
|
27264
|
+
this.tenantId,
|
|
27265
|
+
input.manifest.operation_id,
|
|
27266
|
+
input.manifest.step_id,
|
|
27267
|
+
input.manifest.idempotency_key,
|
|
27268
|
+
input.request_digest,
|
|
27269
|
+
input.manifest.precondition_digest,
|
|
27270
|
+
result.receipt.result_digest,
|
|
27271
|
+
reason,
|
|
27272
|
+
canonicalJson(input.manifest),
|
|
27273
|
+
resultJson,
|
|
27274
|
+
input.now
|
|
27275
|
+
]);
|
|
27276
|
+
const stored = await tx.query(`SELECT receipt_id, result_json
|
|
27277
|
+
FROM todos_task_manifest_terminal_receipts
|
|
27278
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
27279
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
27280
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
27281
|
+
LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
|
|
27282
|
+
return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
|
|
27283
|
+
}
|
|
26472
27284
|
async apply(input, faults) {
|
|
26473
27285
|
await this.ensureSchema();
|
|
26474
27286
|
return this.client.transaction(async (tx) => {
|
|
26475
27287
|
const { manifest } = input;
|
|
26476
27288
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
26477
27289
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
27290
|
+
const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
|
|
27291
|
+
WHERE tenant_id = $1
|
|
27292
|
+
AND kind = 'apply'
|
|
27293
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
27294
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
27295
|
+
LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
|
|
27296
|
+
if (terminal.rows[0]) {
|
|
27297
|
+
return parseApplyResult(terminal.rows[0]["result_json"], true);
|
|
27298
|
+
}
|
|
26478
27299
|
const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND operation_id = $2 LIMIT 1 FOR UPDATE", [this.tenantId, manifest.operation_id]);
|
|
26479
27300
|
if (existing.rows[0]) {
|
|
26480
27301
|
const binding = existing.rows[0];
|
|
26481
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
26482
|
-
|
|
27302
|
+
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest || binding["step_id"] !== manifest.step_id || binding["precondition_digest"] !== manifest.precondition_digest) {
|
|
27303
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
26483
27304
|
}
|
|
26484
27305
|
if (binding["state"] !== "applied") {
|
|
26485
|
-
|
|
27306
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
26486
27307
|
}
|
|
26487
|
-
return
|
|
27308
|
+
return parseApplyResult(binding["result_json"], true);
|
|
26488
27309
|
}
|
|
26489
27310
|
const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND idempotency_key = $2 LIMIT 1", [this.tenantId, manifest.idempotency_key]);
|
|
26490
27311
|
if (reused.rows[0])
|
|
26491
|
-
|
|
27312
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
27313
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
27314
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
27315
|
+
}
|
|
26492
27316
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
26493
|
-
|
|
27317
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
26494
27318
|
}
|
|
26495
27319
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
26496
27320
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
26497
27321
|
if (!project.rows[0])
|
|
26498
|
-
|
|
27322
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
26499
27323
|
if (manifest.task_list_id) {
|
|
26500
27324
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
26501
27325
|
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.task_list_id]);
|
|
26502
27326
|
const payload = taskList.rows[0] ? parseJson(taskList.rows[0]["payload"]) : null;
|
|
26503
27327
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
26504
|
-
|
|
27328
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
26505
27329
|
}
|
|
26506
27330
|
}
|
|
26507
27331
|
const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
|
|
26508
27332
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
26509
27333
|
WHERE service = $1 AND object_id IN (${placeholders(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
26510
27334
|
if (conflict.rows[0])
|
|
26511
|
-
|
|
27335
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
26512
27336
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
26513
27337
|
fault(faults, "after_plan_write");
|
|
26514
27338
|
for (const task2 of manifest.tasks) {
|
|
@@ -26578,9 +27402,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26578
27402
|
schema_version: 1,
|
|
26579
27403
|
kind: "apply",
|
|
26580
27404
|
operation_id: manifest.operation_id,
|
|
27405
|
+
step_id: manifest.step_id,
|
|
26581
27406
|
idempotency_key: manifest.idempotency_key,
|
|
26582
27407
|
request_digest: input.request_digest,
|
|
27408
|
+
precondition_digest: manifest.precondition_digest,
|
|
26583
27409
|
result_digest: input.result_digest,
|
|
27410
|
+
outcome: "accepted",
|
|
27411
|
+
reason: null,
|
|
27412
|
+
duplicate_of_receipt_id: null,
|
|
26584
27413
|
binding_version: 1,
|
|
26585
27414
|
apply_receipt_id: null,
|
|
26586
27415
|
created_at: input.now
|
|
@@ -26597,14 +27426,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26597
27426
|
const resultJson = canonicalJson(result);
|
|
26598
27427
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
26599
27428
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
26600
|
-
|
|
26601
|
-
|
|
27429
|
+
step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
|
|
27430
|
+
reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
27431
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8, $9, 'accepted', NULL, NULL, 1, NULL, $10::jsonb, $11::jsonb, $12)`, [
|
|
26602
27432
|
input.receipt_id,
|
|
26603
27433
|
this.tenantId,
|
|
26604
27434
|
manifest.operation_id,
|
|
26605
27435
|
manifest.idempotency_key,
|
|
27436
|
+
manifest.step_id,
|
|
26606
27437
|
input.request_digest,
|
|
27438
|
+
manifest.precondition_digest,
|
|
26607
27439
|
input.result_digest,
|
|
27440
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
26608
27441
|
manifestJson,
|
|
26609
27442
|
resultJson,
|
|
26610
27443
|
input.now
|
|
@@ -26623,14 +27456,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26623
27456
|
}
|
|
26624
27457
|
fault(faults, "after_outbox_write");
|
|
26625
27458
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
26626
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
26627
|
-
|
|
26628
|
-
|
|
27459
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
|
|
27460
|
+
result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
|
|
27461
|
+
state, version, created_at, updated_at
|
|
27462
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
|
|
26629
27463
|
manifest.operation_id,
|
|
26630
27464
|
this.tenantId,
|
|
27465
|
+
manifest.step_id,
|
|
26631
27466
|
manifest.idempotency_key,
|
|
26632
27467
|
input.request_digest,
|
|
27468
|
+
manifest.precondition_digest,
|
|
26633
27469
|
input.result_digest,
|
|
27470
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
26634
27471
|
input.receipt_id,
|
|
26635
27472
|
manifestJson,
|
|
26636
27473
|
resultJson,
|
|
@@ -26643,9 +27480,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26643
27480
|
async readExact(receiptId2) {
|
|
26644
27481
|
await this.ensureSchema();
|
|
26645
27482
|
const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
|
|
26646
|
-
if (
|
|
27483
|
+
if (result.rows[0])
|
|
27484
|
+
return parseApplyResult(result.rows[0]["result_json"], false);
|
|
27485
|
+
const terminal = await this.client.query("SELECT result_json FROM todos_task_manifest_terminal_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
|
|
27486
|
+
if (!terminal.rows[0])
|
|
26647
27487
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
26648
|
-
return
|
|
27488
|
+
return parseApplyResult(terminal.rows[0]["result_json"], false);
|
|
26649
27489
|
}
|
|
26650
27490
|
async lookupBindingByPlanId(planId) {
|
|
26651
27491
|
await this.ensureSchema();
|
|
@@ -26656,6 +27496,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26656
27496
|
b.version AS binding_version,
|
|
26657
27497
|
b.tenant_id AS binding_tenant_id,
|
|
26658
27498
|
b.operation_id AS binding_operation_id,
|
|
27499
|
+
b.step_id AS binding_step_id,
|
|
26659
27500
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
26660
27501
|
r.tenant_id AS receipt_tenant_id,
|
|
26661
27502
|
r.authority AS receipt_authority,
|
|
@@ -26663,6 +27504,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26663
27504
|
r.schema_version AS receipt_schema_version,
|
|
26664
27505
|
r.kind AS receipt_kind,
|
|
26665
27506
|
r.operation_id AS receipt_operation_id,
|
|
27507
|
+
r.step_id AS receipt_step_id,
|
|
26666
27508
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
26667
27509
|
FROM todos_task_manifest_bindings b
|
|
26668
27510
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -26749,6 +27591,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26749
27591
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
26750
27592
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
26751
27593
|
}
|
|
27594
|
+
const appliedReceipt = receiptFromRow3(applyRow);
|
|
27595
|
+
if (appliedReceipt.receipt_id !== input.receipt_id || appliedReceipt.operation_id !== input.operation_id || String(binding["operation_id"]) !== appliedReceipt.operation_id || String(binding["step_id"] ?? "legacy-apply") !== appliedReceipt.step_id || String(binding["idempotency_key"]) !== appliedReceipt.idempotency_key || String(binding["request_digest"]) !== appliedReceipt.request_digest || String(binding["precondition_digest"] ?? "0".repeat(64)) !== appliedReceipt.precondition_digest || String(binding["apply_receipt_id"]) !== input.receipt_id || binding["slug_provenance"] !== applyRow["slug_provenance"]) {
|
|
27596
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
27597
|
+
}
|
|
26752
27598
|
if (binding["state"] !== "applied")
|
|
26753
27599
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
26754
27600
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -26763,12 +27609,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26763
27609
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
26764
27610
|
if (delivered.rows[0])
|
|
26765
27611
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
26766
|
-
const applyResult =
|
|
27612
|
+
const applyResult = parseApplyResult(applyRow["result_json"], false);
|
|
26767
27613
|
const manifest = parseJson(applyRow["manifest_json"]);
|
|
27614
|
+
const manifestRecord = manifest;
|
|
27615
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
26768
27616
|
const expectedEffects = [
|
|
26769
27617
|
{
|
|
26770
27618
|
topic: "todos.task-manifest.applied",
|
|
26771
|
-
payload: {
|
|
27619
|
+
payload: {
|
|
27620
|
+
operation_id: manifest.operation_id,
|
|
27621
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
27622
|
+
project_id: manifest.project_id
|
|
27623
|
+
}
|
|
26772
27624
|
},
|
|
26773
27625
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
26774
27626
|
];
|
|
@@ -26816,9 +27668,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26816
27668
|
}
|
|
26817
27669
|
const appliedAt = receiptFromRow3(applyRow).created_at;
|
|
26818
27670
|
const expectedPayloads = new Map;
|
|
27671
|
+
const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
27672
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
27673
|
+
LIMIT 1`, [this.service, applyResult.graph.plan_id]);
|
|
27674
|
+
const actualPlan = planRow.rows[0] ? parseJson(planRow.rows[0]["payload"]) : null;
|
|
27675
|
+
const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
|
|
27676
|
+
planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
|
|
26819
27677
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
26820
27678
|
type: "plans",
|
|
26821
|
-
payload: canonicalJson(
|
|
27679
|
+
payload: canonicalJson(planExpected)
|
|
26822
27680
|
});
|
|
26823
27681
|
for (const task2 of manifest.tasks)
|
|
26824
27682
|
expectedPayloads.set(applyResult.graph.task_ids[task2.key], {
|
|
@@ -26918,14 +27776,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26918
27776
|
const readback = await this.readback(tx, applyResult.graph);
|
|
26919
27777
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
26920
27778
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
26921
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
26922
|
-
request_digest,
|
|
26923
|
-
|
|
27779
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
27780
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
27781
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
27782
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, 'accepted', NULL, NULL, $9, $10, NULL, $11::jsonb, $12)`, [
|
|
26924
27783
|
compensationReceiptId,
|
|
26925
27784
|
this.tenantId,
|
|
26926
27785
|
receipt.operation_id,
|
|
27786
|
+
receipt.step_id,
|
|
26927
27787
|
input.idempotency_key,
|
|
26928
27788
|
requestDigest,
|
|
27789
|
+
input.precondition_digest,
|
|
26929
27790
|
receipt.result_digest,
|
|
26930
27791
|
receipt.binding_version,
|
|
26931
27792
|
input.receipt_id,
|
|
@@ -26982,36 +27843,83 @@ function resolveTenantId(value) {
|
|
|
26982
27843
|
}
|
|
26983
27844
|
return tenantId;
|
|
26984
27845
|
}
|
|
27846
|
+
function taskManifestRequestDigest(manifest) {
|
|
27847
|
+
const { idempotency_key: _idempotencyKey, ...request } = manifest;
|
|
27848
|
+
return canonicalDigest(request);
|
|
27849
|
+
}
|
|
27850
|
+
function taskManifestCompensationRequestDigest(request) {
|
|
27851
|
+
return canonicalDigest(request);
|
|
27852
|
+
}
|
|
27853
|
+
function deriveTodosTaskManifestApplyPreconditionDigest(input) {
|
|
27854
|
+
return canonicalDigest({
|
|
27855
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
27856
|
+
direction: "apply",
|
|
27857
|
+
operation_id: input.operation_id,
|
|
27858
|
+
step_id: input.step_id,
|
|
27859
|
+
project_id: input.project_id,
|
|
27860
|
+
task_list_id: input.task_list_id ?? null,
|
|
27861
|
+
expected_binding_version: input.if_binding_version ?? 0
|
|
27862
|
+
});
|
|
27863
|
+
}
|
|
27864
|
+
function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
|
|
27865
|
+
return canonicalDigest({
|
|
27866
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
27867
|
+
direction: "compensate",
|
|
27868
|
+
operation_id: input.operation_id,
|
|
27869
|
+
step_id: input.step_id,
|
|
27870
|
+
apply_receipt_id: input.receipt_id,
|
|
27871
|
+
expected_binding_version: input.if_binding_version
|
|
27872
|
+
});
|
|
27873
|
+
}
|
|
27874
|
+
function deriveTodosTaskManifestIdempotencyKey(input) {
|
|
27875
|
+
return `tmk_${canonicalDigest({
|
|
27876
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
27877
|
+
...input
|
|
27878
|
+
}).slice(0, 48)}`;
|
|
27879
|
+
}
|
|
26985
27880
|
function normalize(input, now3) {
|
|
26986
27881
|
const parsed = parseTodosTaskManifest(input);
|
|
26987
27882
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
26988
27883
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
26989
27884
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BOUNDS_EXCEEDED", `Task manifest requires ${requestBytes} bytes but the bound is ${TODOS_TASK_MANIFEST_BOUNDS.request_bytes}`, { request_bytes: requestBytes, request_byte_limit: TODOS_TASK_MANIFEST_BOUNDS.request_bytes });
|
|
26990
27885
|
}
|
|
27886
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
27887
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
26991
27888
|
const manifest = sanitizeManifest(parsed);
|
|
27889
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
|
|
27890
|
+
if (manifest.precondition_digest !== expectedPreconditionDigest) {
|
|
27891
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
27892
|
+
}
|
|
27893
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
27894
|
+
operation_id: manifest.operation_id,
|
|
27895
|
+
step_id: manifest.step_id,
|
|
27896
|
+
direction: "apply",
|
|
27897
|
+
target_selector: manifest.project_id,
|
|
27898
|
+
request_digest,
|
|
27899
|
+
precondition_digest: manifest.precondition_digest
|
|
27900
|
+
});
|
|
26992
27901
|
const task_ids = Object.fromEntries(manifest.tasks.map((task2) => [
|
|
26993
27902
|
task2.key,
|
|
26994
|
-
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task2.key)
|
|
27903
|
+
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task2.key)
|
|
26995
27904
|
]));
|
|
26996
27905
|
const graph = {
|
|
26997
|
-
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
|
|
27906
|
+
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
|
|
26998
27907
|
task_ids,
|
|
26999
|
-
comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task2.key, String(index)))),
|
|
27000
|
-
verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task2.key, String(index)))),
|
|
27908
|
+
comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "comment", task2.key, String(index)))),
|
|
27909
|
+
verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "verification", task2.key, String(index)))),
|
|
27001
27910
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
27002
27911
|
};
|
|
27003
|
-
const request_digest = canonicalDigest(parsed);
|
|
27004
27912
|
const effectInputs = [
|
|
27005
27913
|
{
|
|
27006
27914
|
topic: "todos.task-manifest.applied",
|
|
27007
|
-
payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
|
|
27915
|
+
payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
|
|
27008
27916
|
},
|
|
27009
27917
|
...manifest.effects ?? []
|
|
27010
27918
|
];
|
|
27011
27919
|
const outbox = effectInputs.map((effect2, index) => {
|
|
27012
27920
|
const payload = { ...effect2.payload };
|
|
27013
27921
|
return {
|
|
27014
|
-
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
|
|
27922
|
+
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
|
|
27015
27923
|
topic: effect2.topic,
|
|
27016
27924
|
payload,
|
|
27017
27925
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -27021,11 +27929,14 @@ function normalize(input, now3) {
|
|
|
27021
27929
|
return {
|
|
27022
27930
|
manifest,
|
|
27023
27931
|
request_digest,
|
|
27932
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
27024
27933
|
result_digest,
|
|
27025
|
-
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
|
|
27934
|
+
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
27935
|
+
terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
27026
27936
|
graph,
|
|
27027
27937
|
outbox,
|
|
27028
|
-
now: now3
|
|
27938
|
+
now: now3,
|
|
27939
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
27029
27940
|
};
|
|
27030
27941
|
}
|
|
27031
27942
|
function sanitizeManifest(manifest) {
|
|
@@ -27083,6 +27994,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
27083
27994
|
tenant_id: this.tenantId,
|
|
27084
27995
|
backend: this.backend.kind,
|
|
27085
27996
|
deterministic_ids: true,
|
|
27997
|
+
operation_step_identity: true,
|
|
27998
|
+
deterministic_idempotency_keys: true,
|
|
27999
|
+
terminal_nonacceptance_receipts: true,
|
|
28000
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
27086
28001
|
immutable_receipts: true,
|
|
27087
28002
|
transactional_outbox: true,
|
|
27088
28003
|
idempotent_outbox_delivery: true,
|
|
@@ -27112,7 +28027,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
27112
28027
|
async apply(input) {
|
|
27113
28028
|
const normalized = normalize(input, this.now());
|
|
27114
28029
|
const faults = await this.prepareFaults();
|
|
27115
|
-
|
|
28030
|
+
const result = this.bounded(await this.backend.apply(normalized, faults));
|
|
28031
|
+
if (result.receipt.outcome === "terminal_nonacceptance") {
|
|
28032
|
+
throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
|
|
28033
|
+
}
|
|
28034
|
+
return result;
|
|
27116
28035
|
}
|
|
27117
28036
|
readExact(receiptId2) {
|
|
27118
28037
|
if (!receiptId2 || receiptId2.length > 200) {
|
|
@@ -27145,18 +28064,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
27145
28064
|
async compensate(input) {
|
|
27146
28065
|
const request = parseTodosTaskManifestCompensation(input);
|
|
27147
28066
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
27148
|
-
|
|
27149
|
-
|
|
28067
|
+
if (applied.receipt.outcome !== "accepted") {
|
|
28068
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
|
|
28069
|
+
}
|
|
28070
|
+
if (request.operation_id !== applied.receipt.operation_id) {
|
|
28071
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
|
|
28072
|
+
}
|
|
28073
|
+
if (request.step_id === applied.receipt.step_id) {
|
|
28074
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
|
|
28075
|
+
}
|
|
28076
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
|
|
28077
|
+
if (request.precondition_digest !== expectedPreconditionDigest) {
|
|
28078
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
28079
|
+
}
|
|
28080
|
+
const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
|
|
28081
|
+
const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
|
|
28082
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
28083
|
+
operation_id: request.operation_id,
|
|
28084
|
+
step_id: request.step_id,
|
|
28085
|
+
direction: "compensate",
|
|
28086
|
+
target_selector: request.receipt_id,
|
|
28087
|
+
request_digest: requestDigest,
|
|
28088
|
+
precondition_digest: request.precondition_digest
|
|
28089
|
+
});
|
|
28090
|
+
if (request.idempotency_key !== expectedIdempotencyKey) {
|
|
28091
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
|
|
28092
|
+
}
|
|
28093
|
+
const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
|
|
27150
28094
|
const receipt = {
|
|
27151
28095
|
receipt_id: compensationReceiptId,
|
|
27152
28096
|
authority: "todos",
|
|
27153
28097
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
27154
28098
|
schema_version: 1,
|
|
27155
28099
|
kind: "compensate",
|
|
27156
|
-
operation_id:
|
|
28100
|
+
operation_id: request.operation_id,
|
|
28101
|
+
step_id: request.step_id,
|
|
27157
28102
|
idempotency_key: request.idempotency_key,
|
|
27158
28103
|
request_digest: requestDigest,
|
|
28104
|
+
precondition_digest: request.precondition_digest,
|
|
27159
28105
|
result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
|
|
28106
|
+
outcome: "accepted",
|
|
28107
|
+
reason: null,
|
|
28108
|
+
duplicate_of_receipt_id: null,
|
|
27160
28109
|
binding_version: request.if_binding_version + 1,
|
|
27161
28110
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
27162
28111
|
created_at: this.now()
|
|
@@ -29661,9 +30610,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29661
30610
|
TaskManifestBounds: taskManifestBoundsSchema,
|
|
29662
30611
|
TaskManifestCapability: taskManifestCapabilitySchema,
|
|
29663
30612
|
TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
|
|
30613
|
+
TaskManifest: taskManifestSchema,
|
|
30614
|
+
TaskManifestReceipt: taskManifestReceiptSchema,
|
|
30615
|
+
TaskManifestApplyResult: taskManifestApplyResultSchema,
|
|
30616
|
+
TaskManifestApplyResponse: taskManifestApplyResponseSchema,
|
|
30617
|
+
TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
|
|
30618
|
+
TaskManifestCompensationResult: taskManifestCompensationResultSchema,
|
|
30619
|
+
TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
|
|
30620
|
+
TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
|
|
29664
30621
|
TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
|
|
29665
30622
|
TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
|
|
29666
30623
|
TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
|
|
30624
|
+
ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
|
|
30625
|
+
ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
|
|
30626
|
+
ProjectRegistrationRequest: projectRegistrationRequestSchema,
|
|
30627
|
+
ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
|
|
30628
|
+
PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
|
|
30629
|
+
ProjectResource: projectResourceSchema,
|
|
30630
|
+
ProjectResourcePage: projectResourcePageSchema,
|
|
29667
30631
|
TaskList: taskListSchema,
|
|
29668
30632
|
ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
|
|
29669
30633
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
@@ -29706,6 +30670,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29706
30670
|
priority: { type: "string", enum: [...TASK_PRIORITIES] },
|
|
29707
30671
|
assigned_to: { type: "string" },
|
|
29708
30672
|
project_id: { type: "string", nullable: true },
|
|
30673
|
+
parent_id: { type: "string", nullable: true },
|
|
29709
30674
|
plan_id: { type: "string", nullable: true },
|
|
29710
30675
|
task_list_id: { type: "string", nullable: true },
|
|
29711
30676
|
version: { type: "number" }
|
|
@@ -30660,6 +31625,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30660
31625
|
},
|
|
30661
31626
|
security: [{ apiKey: [] }],
|
|
30662
31627
|
paths: {
|
|
31628
|
+
"/v1/project-registration/capability": {
|
|
31629
|
+
get: {
|
|
31630
|
+
operationId: "getProjectRegistrationCapability",
|
|
31631
|
+
summary: "Read the live package-owned Projects to Todos registration capability",
|
|
31632
|
+
responses: {
|
|
31633
|
+
"200": {
|
|
31634
|
+
content: {
|
|
31635
|
+
"application/json": {
|
|
31636
|
+
schema: {
|
|
31637
|
+
type: "object",
|
|
31638
|
+
additionalProperties: false,
|
|
31639
|
+
required: ["capability"],
|
|
31640
|
+
properties: {
|
|
31641
|
+
capability: {
|
|
31642
|
+
$ref: "#/components/schemas/ProjectRegistrationCapability"
|
|
31643
|
+
}
|
|
31644
|
+
}
|
|
31645
|
+
}
|
|
31646
|
+
}
|
|
31647
|
+
}
|
|
31648
|
+
}
|
|
31649
|
+
}
|
|
31650
|
+
}
|
|
31651
|
+
},
|
|
31652
|
+
"/v1/project-registration/resources": {
|
|
31653
|
+
get: {
|
|
31654
|
+
operationId: "listProjectRegistrationResources",
|
|
31655
|
+
summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
|
|
31656
|
+
parameters: [
|
|
31657
|
+
{
|
|
31658
|
+
name: "source_project_id",
|
|
31659
|
+
in: "query",
|
|
31660
|
+
required: true,
|
|
31661
|
+
schema: { type: "string" }
|
|
31662
|
+
},
|
|
31663
|
+
{
|
|
31664
|
+
name: "include_anchors",
|
|
31665
|
+
in: "query",
|
|
31666
|
+
schema: { type: "boolean", default: false }
|
|
31667
|
+
},
|
|
31668
|
+
{
|
|
31669
|
+
name: "limit",
|
|
31670
|
+
in: "query",
|
|
31671
|
+
schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
|
|
31672
|
+
},
|
|
31673
|
+
{
|
|
31674
|
+
name: "cursor",
|
|
31675
|
+
in: "query",
|
|
31676
|
+
schema: { type: "string" }
|
|
31677
|
+
}
|
|
31678
|
+
],
|
|
31679
|
+
responses: {
|
|
31680
|
+
"200": {
|
|
31681
|
+
content: {
|
|
31682
|
+
"application/json": {
|
|
31683
|
+
schema: {
|
|
31684
|
+
type: "object",
|
|
31685
|
+
additionalProperties: false,
|
|
31686
|
+
required: ["page"],
|
|
31687
|
+
properties: {
|
|
31688
|
+
page: { $ref: "#/components/schemas/ProjectResourcePage" }
|
|
31689
|
+
}
|
|
31690
|
+
}
|
|
31691
|
+
}
|
|
31692
|
+
}
|
|
31693
|
+
},
|
|
31694
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31695
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31696
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
31697
|
+
}
|
|
31698
|
+
}
|
|
31699
|
+
},
|
|
31700
|
+
"/v1/project-registration/create": {
|
|
31701
|
+
post: {
|
|
31702
|
+
operationId: "createProjectRegistrationResource",
|
|
31703
|
+
summary: "Create or deterministically bind one Projects to Todos resource",
|
|
31704
|
+
requestBody: {
|
|
31705
|
+
required: true,
|
|
31706
|
+
content: {
|
|
31707
|
+
"application/json": {
|
|
31708
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
31709
|
+
}
|
|
31710
|
+
}
|
|
31711
|
+
},
|
|
31712
|
+
responses: {
|
|
31713
|
+
"201": {
|
|
31714
|
+
content: {
|
|
31715
|
+
"application/json": {
|
|
31716
|
+
schema: {
|
|
31717
|
+
type: "object",
|
|
31718
|
+
additionalProperties: false,
|
|
31719
|
+
required: ["receipt"],
|
|
31720
|
+
properties: {
|
|
31721
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
|
|
31722
|
+
}
|
|
31723
|
+
}
|
|
31724
|
+
}
|
|
31725
|
+
}
|
|
31726
|
+
},
|
|
31727
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31728
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
31729
|
+
}
|
|
31730
|
+
}
|
|
31731
|
+
},
|
|
31732
|
+
"/v1/project-registration/read-exact": {
|
|
31733
|
+
post: {
|
|
31734
|
+
operationId: "readExactProjectRegistrationResource",
|
|
31735
|
+
summary: "Read one registered project or task list by exact full UUID",
|
|
31736
|
+
requestBody: {
|
|
31737
|
+
required: true,
|
|
31738
|
+
content: {
|
|
31739
|
+
"application/json": {
|
|
31740
|
+
schema: {
|
|
31741
|
+
type: "object",
|
|
31742
|
+
additionalProperties: false,
|
|
31743
|
+
required: [
|
|
31744
|
+
"resource_kind",
|
|
31745
|
+
"target_id",
|
|
31746
|
+
"response_byte_limit",
|
|
31747
|
+
"time_budget_ms"
|
|
31748
|
+
],
|
|
31749
|
+
properties: {
|
|
31750
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
31751
|
+
target_id: { type: "string", format: "uuid" },
|
|
31752
|
+
...projectRegistrationBoundsProperties
|
|
31753
|
+
}
|
|
31754
|
+
}
|
|
31755
|
+
}
|
|
31756
|
+
}
|
|
31757
|
+
},
|
|
31758
|
+
responses: {
|
|
31759
|
+
"200": {
|
|
31760
|
+
content: {
|
|
31761
|
+
"application/json": {
|
|
31762
|
+
schema: {
|
|
31763
|
+
type: "object",
|
|
31764
|
+
additionalProperties: false,
|
|
31765
|
+
required: ["record"],
|
|
31766
|
+
properties: {
|
|
31767
|
+
record: {
|
|
31768
|
+
type: "object",
|
|
31769
|
+
additionalProperties: false,
|
|
31770
|
+
required: ["target_id", "revision", "digest"],
|
|
31771
|
+
properties: {
|
|
31772
|
+
target_id: { type: "string", format: "uuid" },
|
|
31773
|
+
revision: { type: "string" },
|
|
31774
|
+
digest: { type: "string" }
|
|
31775
|
+
}
|
|
31776
|
+
}
|
|
31777
|
+
}
|
|
31778
|
+
}
|
|
31779
|
+
}
|
|
31780
|
+
}
|
|
31781
|
+
},
|
|
31782
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31783
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
31784
|
+
}
|
|
31785
|
+
}
|
|
31786
|
+
},
|
|
31787
|
+
"/v1/project-registration/receipts/lookup": {
|
|
31788
|
+
post: {
|
|
31789
|
+
operationId: "lookupProjectRegistrationReceipt",
|
|
31790
|
+
summary: "Recover one exact immutable terminal registration receipt",
|
|
31791
|
+
requestBody: {
|
|
31792
|
+
required: true,
|
|
31793
|
+
content: {
|
|
31794
|
+
"application/json": {
|
|
31795
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
|
|
31796
|
+
}
|
|
31797
|
+
}
|
|
31798
|
+
},
|
|
31799
|
+
responses: {
|
|
31800
|
+
"200": {
|
|
31801
|
+
content: {
|
|
31802
|
+
"application/json": {
|
|
31803
|
+
schema: {
|
|
31804
|
+
type: "object",
|
|
31805
|
+
additionalProperties: false,
|
|
31806
|
+
required: ["receipt", "response_control"],
|
|
31807
|
+
properties: {
|
|
31808
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
31809
|
+
response_control: {
|
|
31810
|
+
type: "object",
|
|
31811
|
+
additionalProperties: false,
|
|
31812
|
+
required: [
|
|
31813
|
+
"response_byte_limit",
|
|
31814
|
+
"time_budget_ms",
|
|
31815
|
+
"response_bytes",
|
|
31816
|
+
"elapsed_ms",
|
|
31817
|
+
"complete",
|
|
31818
|
+
"truncated"
|
|
31819
|
+
],
|
|
31820
|
+
properties: {
|
|
31821
|
+
...projectRegistrationBoundsProperties,
|
|
31822
|
+
response_bytes: { type: "integer", minimum: 0 },
|
|
31823
|
+
elapsed_ms: { type: "integer", minimum: 0 },
|
|
31824
|
+
complete: { type: "boolean", enum: [true] },
|
|
31825
|
+
truncated: { type: "boolean", enum: [false] }
|
|
31826
|
+
}
|
|
31827
|
+
}
|
|
31828
|
+
}
|
|
31829
|
+
}
|
|
31830
|
+
}
|
|
31831
|
+
}
|
|
31832
|
+
},
|
|
31833
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31834
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
31835
|
+
}
|
|
31836
|
+
}
|
|
31837
|
+
},
|
|
31838
|
+
"/v1/project-registration/validate-prior-adoption": {
|
|
31839
|
+
post: {
|
|
31840
|
+
operationId: "validatePriorRegistrationAdoption",
|
|
31841
|
+
summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
|
|
31842
|
+
requestBody: {
|
|
31843
|
+
required: true,
|
|
31844
|
+
content: {
|
|
31845
|
+
"application/json": {
|
|
31846
|
+
schema: {
|
|
31847
|
+
type: "object",
|
|
31848
|
+
additionalProperties: false,
|
|
31849
|
+
required: ["source_request", "source_receipt", "current_record"],
|
|
31850
|
+
properties: {
|
|
31851
|
+
source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
|
|
31852
|
+
source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
31853
|
+
current_record: {
|
|
31854
|
+
oneOf: [
|
|
31855
|
+
{ $ref: "#/components/schemas/Project" },
|
|
31856
|
+
{ $ref: "#/components/schemas/TaskList" }
|
|
31857
|
+
]
|
|
31858
|
+
}
|
|
31859
|
+
}
|
|
31860
|
+
}
|
|
31861
|
+
}
|
|
31862
|
+
}
|
|
31863
|
+
},
|
|
31864
|
+
responses: {
|
|
31865
|
+
"200": {
|
|
31866
|
+
content: {
|
|
31867
|
+
"application/json": {
|
|
31868
|
+
schema: {
|
|
31869
|
+
type: "object",
|
|
31870
|
+
additionalProperties: false,
|
|
31871
|
+
required: ["validation"],
|
|
31872
|
+
properties: {
|
|
31873
|
+
validation: {
|
|
31874
|
+
$ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
|
|
31875
|
+
}
|
|
31876
|
+
}
|
|
31877
|
+
}
|
|
31878
|
+
}
|
|
31879
|
+
}
|
|
31880
|
+
},
|
|
31881
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31882
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31883
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
31884
|
+
}
|
|
31885
|
+
}
|
|
31886
|
+
},
|
|
31887
|
+
"/v1/project-registration/compensate": {
|
|
31888
|
+
post: {
|
|
31889
|
+
operationId: "compensateProjectRegistrationResource",
|
|
31890
|
+
summary: "Conditionally remove an unchanged receipt-owned registration resource",
|
|
31891
|
+
requestBody: {
|
|
31892
|
+
required: true,
|
|
31893
|
+
content: {
|
|
31894
|
+
"application/json": {
|
|
31895
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
31896
|
+
}
|
|
31897
|
+
}
|
|
31898
|
+
},
|
|
31899
|
+
responses: {
|
|
31900
|
+
"201": {
|
|
31901
|
+
content: {
|
|
31902
|
+
"application/json": {
|
|
31903
|
+
schema: {
|
|
31904
|
+
type: "object",
|
|
31905
|
+
additionalProperties: false,
|
|
31906
|
+
required: ["receipt"],
|
|
31907
|
+
properties: {
|
|
31908
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
|
|
31909
|
+
}
|
|
31910
|
+
}
|
|
31911
|
+
}
|
|
31912
|
+
}
|
|
31913
|
+
},
|
|
31914
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31915
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31916
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
31917
|
+
}
|
|
31918
|
+
}
|
|
31919
|
+
},
|
|
31920
|
+
"/v1/project-registration/verify-inverse": {
|
|
31921
|
+
post: {
|
|
31922
|
+
operationId: "verifyInverseProjectRegistrationResource",
|
|
31923
|
+
summary: "Verify exact absence after conditional registration compensation",
|
|
31924
|
+
requestBody: {
|
|
31925
|
+
required: true,
|
|
31926
|
+
content: {
|
|
31927
|
+
"application/json": {
|
|
31928
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
31929
|
+
}
|
|
31930
|
+
}
|
|
31931
|
+
},
|
|
31932
|
+
responses: {
|
|
31933
|
+
"200": {
|
|
31934
|
+
content: {
|
|
31935
|
+
"application/json": {
|
|
31936
|
+
schema: {
|
|
31937
|
+
type: "object",
|
|
31938
|
+
additionalProperties: false,
|
|
31939
|
+
required: ["verification"],
|
|
31940
|
+
properties: {
|
|
31941
|
+
verification: {
|
|
31942
|
+
type: "object",
|
|
31943
|
+
additionalProperties: false,
|
|
31944
|
+
required: ["target_id", "accepted_receipt_id", "absent", "digest"],
|
|
31945
|
+
properties: {
|
|
31946
|
+
target_id: { type: "string", format: "uuid" },
|
|
31947
|
+
accepted_receipt_id: { type: "string" },
|
|
31948
|
+
absent: { type: "boolean", enum: [true] },
|
|
31949
|
+
digest: { type: "string" }
|
|
31950
|
+
}
|
|
31951
|
+
}
|
|
31952
|
+
}
|
|
31953
|
+
}
|
|
31954
|
+
}
|
|
31955
|
+
}
|
|
31956
|
+
},
|
|
31957
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31958
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
31959
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
31960
|
+
}
|
|
31961
|
+
}
|
|
31962
|
+
},
|
|
30663
31963
|
"/v1/task-manifest/capability": {
|
|
30664
31964
|
get: {
|
|
30665
31965
|
operationId: "getTaskManifestCapability",
|
|
@@ -30677,6 +31977,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30677
31977
|
}
|
|
30678
31978
|
}
|
|
30679
31979
|
},
|
|
31980
|
+
"/v1/task-manifest/apply": {
|
|
31981
|
+
post: {
|
|
31982
|
+
operationId: "applyTaskManifest",
|
|
31983
|
+
summary: "Apply one exact task-manifest graph through the Todos authority",
|
|
31984
|
+
requestBody: {
|
|
31985
|
+
required: true,
|
|
31986
|
+
content: {
|
|
31987
|
+
"application/json": {
|
|
31988
|
+
schema: { $ref: "#/components/schemas/TaskManifest" }
|
|
31989
|
+
}
|
|
31990
|
+
}
|
|
31991
|
+
},
|
|
31992
|
+
responses: {
|
|
31993
|
+
"201": {
|
|
31994
|
+
content: {
|
|
31995
|
+
"application/json": {
|
|
31996
|
+
schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
|
|
31997
|
+
}
|
|
31998
|
+
}
|
|
31999
|
+
},
|
|
32000
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
32001
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
32002
|
+
"503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
32003
|
+
}
|
|
32004
|
+
}
|
|
32005
|
+
},
|
|
32006
|
+
"/v1/task-manifest/read-exact": {
|
|
32007
|
+
post: {
|
|
32008
|
+
operationId: "readExactTaskManifest",
|
|
32009
|
+
summary: "Read one exact immutable task-manifest apply receipt",
|
|
32010
|
+
requestBody: {
|
|
32011
|
+
required: true,
|
|
32012
|
+
content: {
|
|
32013
|
+
"application/json": {
|
|
32014
|
+
schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
|
|
32015
|
+
}
|
|
32016
|
+
}
|
|
32017
|
+
},
|
|
32018
|
+
responses: {
|
|
32019
|
+
"200": {
|
|
32020
|
+
content: {
|
|
32021
|
+
"application/json": {
|
|
32022
|
+
schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
|
|
32023
|
+
}
|
|
32024
|
+
}
|
|
32025
|
+
},
|
|
32026
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
32027
|
+
}
|
|
32028
|
+
}
|
|
32029
|
+
},
|
|
32030
|
+
"/v1/task-manifest/compensate": {
|
|
32031
|
+
post: {
|
|
32032
|
+
operationId: "compensateTaskManifest",
|
|
32033
|
+
summary: "Compensate one exact untouched task-manifest graph with CAS protection",
|
|
32034
|
+
requestBody: {
|
|
32035
|
+
required: true,
|
|
32036
|
+
content: {
|
|
32037
|
+
"application/json": {
|
|
32038
|
+
schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
|
|
32039
|
+
}
|
|
32040
|
+
}
|
|
32041
|
+
},
|
|
32042
|
+
responses: {
|
|
32043
|
+
"201": {
|
|
32044
|
+
content: {
|
|
32045
|
+
"application/json": {
|
|
32046
|
+
schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
|
|
32047
|
+
}
|
|
32048
|
+
}
|
|
32049
|
+
},
|
|
32050
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
32051
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
32052
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
32053
|
+
}
|
|
32054
|
+
}
|
|
32055
|
+
},
|
|
30680
32056
|
"/v1/task-manifest/bindings/lookup": {
|
|
30681
32057
|
post: {
|
|
30682
32058
|
operationId: "lookupTaskManifestBinding",
|
|
@@ -31537,7 +32913,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
31537
32913
|
}
|
|
31538
32914
|
});
|
|
31539
32915
|
}
|
|
31540
|
-
var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
32916
|
+
var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
|
|
31541
32917
|
var init_openapi = __esm(() => {
|
|
31542
32918
|
init_package_version();
|
|
31543
32919
|
init_types();
|
|
@@ -31599,6 +32975,10 @@ var init_openapi = __esm(() => {
|
|
|
31599
32975
|
"tenant_id",
|
|
31600
32976
|
"backend",
|
|
31601
32977
|
"deterministic_ids",
|
|
32978
|
+
"operation_step_identity",
|
|
32979
|
+
"deterministic_idempotency_keys",
|
|
32980
|
+
"terminal_nonacceptance_receipts",
|
|
32981
|
+
"plan_slug_provenance",
|
|
31602
32982
|
"immutable_receipts",
|
|
31603
32983
|
"transactional_outbox",
|
|
31604
32984
|
"idempotent_outbox_delivery",
|
|
@@ -31614,6 +32994,10 @@ var init_openapi = __esm(() => {
|
|
|
31614
32994
|
tenant_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
31615
32995
|
backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
|
|
31616
32996
|
deterministic_ids: { type: "boolean", enum: [true] },
|
|
32997
|
+
operation_step_identity: { type: "boolean", enum: [true] },
|
|
32998
|
+
deterministic_idempotency_keys: { type: "boolean", enum: [true] },
|
|
32999
|
+
terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
|
|
33000
|
+
plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
|
|
31617
33001
|
immutable_receipts: { type: "boolean", enum: [true] },
|
|
31618
33002
|
transactional_outbox: { type: "boolean", enum: [true] },
|
|
31619
33003
|
idempotent_outbox_delivery: { type: "boolean", enum: [true] },
|
|
@@ -31667,6 +33051,8 @@ var init_openapi = __esm(() => {
|
|
|
31667
33051
|
"schema_version",
|
|
31668
33052
|
"tenant_id",
|
|
31669
33053
|
"plan_id",
|
|
33054
|
+
"operation_id",
|
|
33055
|
+
"step_id",
|
|
31670
33056
|
"apply_receipt_id",
|
|
31671
33057
|
"binding_version",
|
|
31672
33058
|
"state"
|
|
@@ -31677,11 +33063,169 @@ var init_openapi = __esm(() => {
|
|
|
31677
33063
|
schema_version: { type: "integer", enum: [1] },
|
|
31678
33064
|
tenant_id: { type: "string" },
|
|
31679
33065
|
plan_id: { type: "string", format: "uuid" },
|
|
33066
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
33067
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
31680
33068
|
apply_receipt_id: { type: "string", format: "uuid" },
|
|
31681
33069
|
binding_version: { type: "integer", minimum: 1 },
|
|
31682
33070
|
state: { type: "string", enum: ["applied", "compensated"] }
|
|
31683
33071
|
}
|
|
31684
33072
|
};
|
|
33073
|
+
taskManifestSchema = {
|
|
33074
|
+
type: "object",
|
|
33075
|
+
additionalProperties: false,
|
|
33076
|
+
required: [
|
|
33077
|
+
"version",
|
|
33078
|
+
"operation_id",
|
|
33079
|
+
"step_id",
|
|
33080
|
+
"idempotency_key",
|
|
33081
|
+
"precondition_digest",
|
|
33082
|
+
"project_id",
|
|
33083
|
+
"plan",
|
|
33084
|
+
"tasks"
|
|
33085
|
+
],
|
|
33086
|
+
properties: {
|
|
33087
|
+
version: { type: "integer", enum: [1] },
|
|
33088
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
33089
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
33090
|
+
idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
|
|
33091
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
33092
|
+
project_id: { type: "string", format: "uuid" },
|
|
33093
|
+
task_list_id: { type: "string", format: "uuid" },
|
|
33094
|
+
if_binding_version: { type: "integer", minimum: 0 },
|
|
33095
|
+
plan: {
|
|
33096
|
+
type: "object",
|
|
33097
|
+
additionalProperties: false,
|
|
33098
|
+
required: ["key", "name"],
|
|
33099
|
+
properties: {
|
|
33100
|
+
key: { type: "string", minLength: 1, maxLength: 200 },
|
|
33101
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
33102
|
+
description: { type: "string" },
|
|
33103
|
+
status: { type: "string", enum: ["active", "completed", "archived"] }
|
|
33104
|
+
}
|
|
33105
|
+
},
|
|
33106
|
+
tasks: {
|
|
33107
|
+
type: "array",
|
|
33108
|
+
minItems: 1,
|
|
33109
|
+
items: {
|
|
33110
|
+
type: "object",
|
|
33111
|
+
additionalProperties: false,
|
|
33112
|
+
required: ["key", "title"],
|
|
33113
|
+
properties: {
|
|
33114
|
+
key: { type: "string", minLength: 1, maxLength: 200 },
|
|
33115
|
+
title: { type: "string", minLength: 1, maxLength: 200 },
|
|
33116
|
+
description: { type: "string" },
|
|
33117
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
|
|
33118
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
33119
|
+
assigned_to: { type: "string" },
|
|
33120
|
+
created_by: { type: "string" },
|
|
33121
|
+
tags: { type: "array", items: { type: "string" } },
|
|
33122
|
+
metadata: { type: "object", additionalProperties: true },
|
|
33123
|
+
comments: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
33124
|
+
verifications: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
33125
|
+
}
|
|
33126
|
+
}
|
|
33127
|
+
},
|
|
33128
|
+
dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
33129
|
+
effects: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
33130
|
+
}
|
|
33131
|
+
};
|
|
33132
|
+
taskManifestReceiptSchema = {
|
|
33133
|
+
type: "object",
|
|
33134
|
+
additionalProperties: false,
|
|
33135
|
+
required: [
|
|
33136
|
+
"receipt_id",
|
|
33137
|
+
"authority",
|
|
33138
|
+
"route",
|
|
33139
|
+
"schema_version",
|
|
33140
|
+
"kind",
|
|
33141
|
+
"operation_id",
|
|
33142
|
+
"step_id",
|
|
33143
|
+
"idempotency_key",
|
|
33144
|
+
"request_digest",
|
|
33145
|
+
"precondition_digest",
|
|
33146
|
+
"result_digest",
|
|
33147
|
+
"outcome",
|
|
33148
|
+
"reason",
|
|
33149
|
+
"duplicate_of_receipt_id",
|
|
33150
|
+
"binding_version",
|
|
33151
|
+
"apply_receipt_id",
|
|
33152
|
+
"created_at"
|
|
33153
|
+
],
|
|
33154
|
+
properties: {
|
|
33155
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
33156
|
+
authority: { type: "string", enum: ["todos"] },
|
|
33157
|
+
route: { type: "string", enum: ["todos.task-manifest.v1"] },
|
|
33158
|
+
schema_version: { type: "integer", enum: [1] },
|
|
33159
|
+
kind: { type: "string", enum: ["apply", "compensate"] },
|
|
33160
|
+
operation_id: { type: "string" },
|
|
33161
|
+
step_id: { type: "string" },
|
|
33162
|
+
idempotency_key: { type: "string" },
|
|
33163
|
+
request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
33164
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
33165
|
+
result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
33166
|
+
outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
|
|
33167
|
+
reason: { type: "string", nullable: true },
|
|
33168
|
+
duplicate_of_receipt_id: { type: "string", nullable: true },
|
|
33169
|
+
binding_version: { type: "integer", minimum: 0 },
|
|
33170
|
+
apply_receipt_id: { type: "string", nullable: true },
|
|
33171
|
+
created_at: { type: "string", format: "date-time" }
|
|
33172
|
+
}
|
|
33173
|
+
};
|
|
33174
|
+
taskManifestApplyResultSchema = {
|
|
33175
|
+
type: "object",
|
|
33176
|
+
additionalProperties: false,
|
|
33177
|
+
required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
|
|
33178
|
+
properties: {
|
|
33179
|
+
duplicate: { type: "boolean" },
|
|
33180
|
+
receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
|
|
33181
|
+
graph: { type: "object", additionalProperties: true },
|
|
33182
|
+
readback: { type: "object", additionalProperties: true },
|
|
33183
|
+
outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
|
|
33184
|
+
result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
|
|
33185
|
+
}
|
|
33186
|
+
};
|
|
33187
|
+
taskManifestApplyResponseSchema = {
|
|
33188
|
+
type: "object",
|
|
33189
|
+
additionalProperties: false,
|
|
33190
|
+
required: ["result"],
|
|
33191
|
+
properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
|
|
33192
|
+
};
|
|
33193
|
+
taskManifestCompensateRequestSchema = {
|
|
33194
|
+
type: "object",
|
|
33195
|
+
additionalProperties: false,
|
|
33196
|
+
required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
|
|
33197
|
+
properties: {
|
|
33198
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
33199
|
+
operation_id: { type: "string" },
|
|
33200
|
+
step_id: { type: "string" },
|
|
33201
|
+
idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
|
|
33202
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
33203
|
+
if_binding_version: { type: "integer", minimum: 1 }
|
|
33204
|
+
}
|
|
33205
|
+
};
|
|
33206
|
+
taskManifestCompensationResultSchema = {
|
|
33207
|
+
type: "object",
|
|
33208
|
+
additionalProperties: false,
|
|
33209
|
+
required: ["duplicate", "receipt", "absent", "readback"],
|
|
33210
|
+
properties: {
|
|
33211
|
+
duplicate: { type: "boolean" },
|
|
33212
|
+
receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
|
|
33213
|
+
absent: { type: "boolean", enum: [true] },
|
|
33214
|
+
readback: { type: "object", additionalProperties: true }
|
|
33215
|
+
}
|
|
33216
|
+
};
|
|
33217
|
+
taskManifestCompensateResponseSchema = {
|
|
33218
|
+
type: "object",
|
|
33219
|
+
additionalProperties: false,
|
|
33220
|
+
required: ["result"],
|
|
33221
|
+
properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
|
|
33222
|
+
};
|
|
33223
|
+
taskManifestReadExactRequestSchema = {
|
|
33224
|
+
type: "object",
|
|
33225
|
+
additionalProperties: false,
|
|
33226
|
+
required: ["receipt_id"],
|
|
33227
|
+
properties: { receipt_id: { type: "string", format: "uuid" } }
|
|
33228
|
+
};
|
|
31685
33229
|
taskManifestBindingLookupResponseSchema = {
|
|
31686
33230
|
type: "object",
|
|
31687
33231
|
additionalProperties: false,
|
|
@@ -32031,6 +33575,292 @@ var init_openapi = __esm(() => {
|
|
|
32031
33575
|
metadata: { type: "object", additionalProperties: true }
|
|
32032
33576
|
}
|
|
32033
33577
|
};
|
|
33578
|
+
projectRegistrationBoundsProperties = {
|
|
33579
|
+
response_byte_limit: { type: "integer", minimum: 1 },
|
|
33580
|
+
time_budget_ms: { type: "integer", minimum: 1 }
|
|
33581
|
+
};
|
|
33582
|
+
projectRegistrationReceiptSchema = {
|
|
33583
|
+
type: "object",
|
|
33584
|
+
additionalProperties: false,
|
|
33585
|
+
required: [
|
|
33586
|
+
"receipt_id",
|
|
33587
|
+
"authority",
|
|
33588
|
+
"route",
|
|
33589
|
+
"package_version",
|
|
33590
|
+
"authority_id",
|
|
33591
|
+
"tenant_id",
|
|
33592
|
+
"corpus_id",
|
|
33593
|
+
"operation_id",
|
|
33594
|
+
"step_id",
|
|
33595
|
+
"resource_kind",
|
|
33596
|
+
"direction",
|
|
33597
|
+
"idempotency_key",
|
|
33598
|
+
"request_digest",
|
|
33599
|
+
"precondition_digest",
|
|
33600
|
+
"outcome",
|
|
33601
|
+
"reason",
|
|
33602
|
+
"target_id",
|
|
33603
|
+
"result_revision",
|
|
33604
|
+
"result_digest",
|
|
33605
|
+
"duplicate_of_receipt_id",
|
|
33606
|
+
"accepted_receipt_id",
|
|
33607
|
+
"created_by_operation",
|
|
33608
|
+
"created_at"
|
|
33609
|
+
],
|
|
33610
|
+
properties: {
|
|
33611
|
+
receipt_id: { type: "string" },
|
|
33612
|
+
authority: { type: "string", enum: ["todos"] },
|
|
33613
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
33614
|
+
package_version: { type: "string" },
|
|
33615
|
+
authority_id: { type: "string" },
|
|
33616
|
+
tenant_id: { type: "string" },
|
|
33617
|
+
corpus_id: { type: "string" },
|
|
33618
|
+
operation_id: { type: "string" },
|
|
33619
|
+
step_id: { type: "string" },
|
|
33620
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
33621
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
33622
|
+
idempotency_key: { type: "string" },
|
|
33623
|
+
request_digest: { type: "string" },
|
|
33624
|
+
precondition_digest: { type: "string" },
|
|
33625
|
+
outcome: {
|
|
33626
|
+
type: "string",
|
|
33627
|
+
enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
|
|
33628
|
+
},
|
|
33629
|
+
reason: { type: "string", nullable: true },
|
|
33630
|
+
target_id: { type: "string", format: "uuid", nullable: true },
|
|
33631
|
+
result_revision: { type: "string", nullable: true },
|
|
33632
|
+
result_digest: { type: "string", nullable: true },
|
|
33633
|
+
duplicate_of_receipt_id: { type: "string", nullable: true },
|
|
33634
|
+
accepted_receipt_id: { type: "string", nullable: true },
|
|
33635
|
+
created_by_operation: { type: "boolean" },
|
|
33636
|
+
created_at: { type: "string", format: "date-time" }
|
|
33637
|
+
}
|
|
33638
|
+
};
|
|
33639
|
+
projectRegistrationCapabilitySchema = {
|
|
33640
|
+
type: "object",
|
|
33641
|
+
additionalProperties: false,
|
|
33642
|
+
required: [
|
|
33643
|
+
"authority",
|
|
33644
|
+
"route",
|
|
33645
|
+
"package_version",
|
|
33646
|
+
"authority_id",
|
|
33647
|
+
"tenant_id",
|
|
33648
|
+
"corpus_id",
|
|
33649
|
+
"supported_resources",
|
|
33650
|
+
"conditional_create",
|
|
33651
|
+
"immutable_receipts",
|
|
33652
|
+
"exact_terminal_lookup",
|
|
33653
|
+
"exact_readback",
|
|
33654
|
+
"bind_existing_adoption",
|
|
33655
|
+
"prior_registration_adoption_validation",
|
|
33656
|
+
"project_resource_enumeration",
|
|
33657
|
+
"project_resource_page_limit",
|
|
33658
|
+
"conditional_inverse",
|
|
33659
|
+
"ambiguous_outcome_reconciliation"
|
|
33660
|
+
],
|
|
33661
|
+
properties: {
|
|
33662
|
+
authority: { type: "string", enum: ["todos"] },
|
|
33663
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
33664
|
+
package_version: { type: "string" },
|
|
33665
|
+
authority_id: { type: "string" },
|
|
33666
|
+
tenant_id: { type: "string" },
|
|
33667
|
+
corpus_id: { type: "string" },
|
|
33668
|
+
supported_resources: {
|
|
33669
|
+
type: "array",
|
|
33670
|
+
items: { type: "string", enum: ["project", "task_list"] }
|
|
33671
|
+
},
|
|
33672
|
+
conditional_create: { type: "boolean", enum: [true] },
|
|
33673
|
+
immutable_receipts: { type: "boolean", enum: [true] },
|
|
33674
|
+
exact_terminal_lookup: { type: "boolean", enum: [true] },
|
|
33675
|
+
exact_readback: { type: "boolean", enum: [true] },
|
|
33676
|
+
bind_existing_adoption: { type: "boolean", enum: [true] },
|
|
33677
|
+
prior_registration_adoption_validation: { type: "boolean", enum: [true] },
|
|
33678
|
+
project_resource_enumeration: { type: "boolean", enum: [true] },
|
|
33679
|
+
project_resource_page_limit: { type: "integer", minimum: 1 },
|
|
33680
|
+
conditional_inverse: { type: "boolean", enum: [true] },
|
|
33681
|
+
ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
|
|
33682
|
+
}
|
|
33683
|
+
};
|
|
33684
|
+
projectRegistrationRequestSchema = {
|
|
33685
|
+
type: "object",
|
|
33686
|
+
additionalProperties: false,
|
|
33687
|
+
required: [
|
|
33688
|
+
"operation_id",
|
|
33689
|
+
"step_id",
|
|
33690
|
+
"resource_kind",
|
|
33691
|
+
"direction",
|
|
33692
|
+
"authority_route",
|
|
33693
|
+
"package_version",
|
|
33694
|
+
"authority_id",
|
|
33695
|
+
"tenant_id",
|
|
33696
|
+
"corpus_id",
|
|
33697
|
+
"target_selector",
|
|
33698
|
+
"idempotency_key",
|
|
33699
|
+
"request_digest",
|
|
33700
|
+
"precondition_digest",
|
|
33701
|
+
"project_id",
|
|
33702
|
+
"project_slug",
|
|
33703
|
+
"project_name",
|
|
33704
|
+
"desired",
|
|
33705
|
+
"response_byte_limit",
|
|
33706
|
+
"time_budget_ms"
|
|
33707
|
+
],
|
|
33708
|
+
properties: {
|
|
33709
|
+
operation_id: { type: "string" },
|
|
33710
|
+
step_id: { type: "string" },
|
|
33711
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
33712
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
33713
|
+
authority_route: { type: "string" },
|
|
33714
|
+
package_version: { type: "string" },
|
|
33715
|
+
authority_id: { type: "string" },
|
|
33716
|
+
tenant_id: { type: "string" },
|
|
33717
|
+
corpus_id: { type: "string" },
|
|
33718
|
+
target_selector: { type: "string" },
|
|
33719
|
+
idempotency_key: { type: "string" },
|
|
33720
|
+
request_digest: { type: "string" },
|
|
33721
|
+
precondition_digest: { type: "string" },
|
|
33722
|
+
project_id: { type: "string" },
|
|
33723
|
+
project_slug: { type: "string" },
|
|
33724
|
+
project_name: { type: "string" },
|
|
33725
|
+
desired: { type: "object", additionalProperties: true },
|
|
33726
|
+
bind_existing: { type: "boolean" },
|
|
33727
|
+
accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
33728
|
+
...projectRegistrationBoundsProperties
|
|
33729
|
+
}
|
|
33730
|
+
};
|
|
33731
|
+
projectRegistrationLookupRequestSchema = {
|
|
33732
|
+
type: "object",
|
|
33733
|
+
additionalProperties: false,
|
|
33734
|
+
required: [
|
|
33735
|
+
"operation_id",
|
|
33736
|
+
"step_id",
|
|
33737
|
+
"resource_kind",
|
|
33738
|
+
"direction",
|
|
33739
|
+
"authority",
|
|
33740
|
+
"authority_route",
|
|
33741
|
+
"package_version",
|
|
33742
|
+
"authority_id",
|
|
33743
|
+
"tenant_id",
|
|
33744
|
+
"corpus_id",
|
|
33745
|
+
"target_selector",
|
|
33746
|
+
"idempotency_key",
|
|
33747
|
+
"max_items",
|
|
33748
|
+
"response_byte_limit",
|
|
33749
|
+
"time_budget_ms"
|
|
33750
|
+
],
|
|
33751
|
+
properties: {
|
|
33752
|
+
operation_id: { type: "string" },
|
|
33753
|
+
step_id: { type: "string" },
|
|
33754
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
33755
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
33756
|
+
authority: { type: "string", enum: ["todos"] },
|
|
33757
|
+
authority_route: { type: "string" },
|
|
33758
|
+
package_version: { type: "string" },
|
|
33759
|
+
authority_id: { type: "string" },
|
|
33760
|
+
tenant_id: { type: "string" },
|
|
33761
|
+
corpus_id: { type: "string" },
|
|
33762
|
+
target_selector: { type: "string" },
|
|
33763
|
+
idempotency_key: { type: "string" },
|
|
33764
|
+
target_id: { type: "string", format: "uuid" },
|
|
33765
|
+
max_items: { type: "integer", enum: [1] },
|
|
33766
|
+
...projectRegistrationBoundsProperties
|
|
33767
|
+
}
|
|
33768
|
+
};
|
|
33769
|
+
priorRegistrationAdoptionValidationSchema = {
|
|
33770
|
+
type: "object",
|
|
33771
|
+
additionalProperties: false,
|
|
33772
|
+
required: [
|
|
33773
|
+
"valid",
|
|
33774
|
+
"resource_kind",
|
|
33775
|
+
"target_id",
|
|
33776
|
+
"source_receipt_id",
|
|
33777
|
+
"accepted_receipt_id",
|
|
33778
|
+
"source_outcome",
|
|
33779
|
+
"created_at",
|
|
33780
|
+
"current_revision",
|
|
33781
|
+
"accepted_result_digest"
|
|
33782
|
+
],
|
|
33783
|
+
properties: {
|
|
33784
|
+
valid: { type: "boolean", enum: [true] },
|
|
33785
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
33786
|
+
target_id: { type: "string", format: "uuid" },
|
|
33787
|
+
source_receipt_id: { type: "string" },
|
|
33788
|
+
accepted_receipt_id: { type: "string" },
|
|
33789
|
+
source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
|
|
33790
|
+
created_at: { type: "string", format: "date-time" },
|
|
33791
|
+
current_revision: { type: "string", format: "date-time" },
|
|
33792
|
+
accepted_result_digest: { type: "string" }
|
|
33793
|
+
}
|
|
33794
|
+
};
|
|
33795
|
+
projectResourceSchema = {
|
|
33796
|
+
type: "object",
|
|
33797
|
+
additionalProperties: false,
|
|
33798
|
+
required: [
|
|
33799
|
+
"source_project_id",
|
|
33800
|
+
"kind",
|
|
33801
|
+
"scope",
|
|
33802
|
+
"target_id",
|
|
33803
|
+
"parent_id",
|
|
33804
|
+
"revision",
|
|
33805
|
+
"digest"
|
|
33806
|
+
],
|
|
33807
|
+
properties: {
|
|
33808
|
+
source_project_id: { type: "string" },
|
|
33809
|
+
kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
|
|
33810
|
+
scope: { type: "string", enum: ["collection", "resource"] },
|
|
33811
|
+
target_id: { type: "string", format: "uuid" },
|
|
33812
|
+
parent_id: { type: "string", format: "uuid", nullable: true },
|
|
33813
|
+
revision: { type: "string" },
|
|
33814
|
+
digest: { type: "string" }
|
|
33815
|
+
}
|
|
33816
|
+
};
|
|
33817
|
+
projectResourcePageSchema = {
|
|
33818
|
+
type: "object",
|
|
33819
|
+
additionalProperties: false,
|
|
33820
|
+
required: [
|
|
33821
|
+
"authority",
|
|
33822
|
+
"route",
|
|
33823
|
+
"package_version",
|
|
33824
|
+
"authority_id",
|
|
33825
|
+
"tenant_id",
|
|
33826
|
+
"corpus_id",
|
|
33827
|
+
"source_project_id",
|
|
33828
|
+
"todos_project_id",
|
|
33829
|
+
"task_list_id",
|
|
33830
|
+
"include_anchors",
|
|
33831
|
+
"collection_revision",
|
|
33832
|
+
"limit",
|
|
33833
|
+
"count",
|
|
33834
|
+
"resources",
|
|
33835
|
+
"has_more",
|
|
33836
|
+
"next_cursor",
|
|
33837
|
+
"complete",
|
|
33838
|
+
"truncated"
|
|
33839
|
+
],
|
|
33840
|
+
properties: {
|
|
33841
|
+
authority: { type: "string", enum: ["todos"] },
|
|
33842
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
33843
|
+
package_version: { type: "string" },
|
|
33844
|
+
authority_id: { type: "string" },
|
|
33845
|
+
tenant_id: { type: "string" },
|
|
33846
|
+
corpus_id: { type: "string" },
|
|
33847
|
+
source_project_id: { type: "string" },
|
|
33848
|
+
todos_project_id: { type: "string", format: "uuid" },
|
|
33849
|
+
task_list_id: { type: "string", format: "uuid" },
|
|
33850
|
+
include_anchors: { type: "boolean" },
|
|
33851
|
+
collection_revision: { type: "string" },
|
|
33852
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
33853
|
+
count: { type: "integer", minimum: 0 },
|
|
33854
|
+
resources: {
|
|
33855
|
+
type: "array",
|
|
33856
|
+
items: { $ref: "#/components/schemas/ProjectResource" }
|
|
33857
|
+
},
|
|
33858
|
+
has_more: { type: "boolean" },
|
|
33859
|
+
next_cursor: { type: "string", nullable: true },
|
|
33860
|
+
complete: { type: "boolean" },
|
|
33861
|
+
truncated: { type: "boolean", enum: [false] }
|
|
33862
|
+
}
|
|
33863
|
+
};
|
|
32034
33864
|
});
|
|
32035
33865
|
|
|
32036
33866
|
// src/server/pr-groups.ts
|
|
@@ -32177,11 +34007,11 @@ function canonicalJson2(value) {
|
|
|
32177
34007
|
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
32178
34008
|
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
|
|
32179
34009
|
}
|
|
32180
|
-
function
|
|
34010
|
+
function digest2(value) {
|
|
32181
34011
|
return createHash9("sha256").update(canonicalJson2(value)).digest("hex");
|
|
32182
34012
|
}
|
|
32183
34013
|
function deriveIdempotencyKey(projectId, slug) {
|
|
32184
|
-
return `ptlk_${
|
|
34014
|
+
return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
32185
34015
|
}
|
|
32186
34016
|
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
32187
34017
|
const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
@@ -32190,13 +34020,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
|
|
|
32190
34020
|
}
|
|
32191
34021
|
return key2;
|
|
32192
34022
|
}
|
|
32193
|
-
function receiptId2(projectId, slug,
|
|
32194
|
-
return `ptlr_${
|
|
34023
|
+
function receiptId2(projectId, slug, idempotencyKey2) {
|
|
34024
|
+
return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
|
|
32195
34025
|
}
|
|
32196
34026
|
function semanticListDigest(list) {
|
|
32197
34027
|
const metadata = { ...list.metadata ?? {} };
|
|
32198
34028
|
delete metadata[RECEIPT_METADATA_KEY];
|
|
32199
|
-
return
|
|
34029
|
+
return digest2({
|
|
32200
34030
|
project_id: list.project_id,
|
|
32201
34031
|
slug: list.slug,
|
|
32202
34032
|
name: list.name,
|
|
@@ -32213,10 +34043,10 @@ function storedMarker(list) {
|
|
|
32213
34043
|
return null;
|
|
32214
34044
|
return marker;
|
|
32215
34045
|
}
|
|
32216
|
-
function receiptFor(store, project, list,
|
|
34046
|
+
function receiptFor(store, project, list, idempotencyKey2) {
|
|
32217
34047
|
const marker = storedMarker(list);
|
|
32218
34048
|
const owned = marker?.project_id === project.id && marker.slug === list.slug;
|
|
32219
|
-
if (owned && marker.idempotency_key !==
|
|
34049
|
+
if (owned && marker.idempotency_key !== idempotencyKey2) {
|
|
32220
34050
|
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
|
|
32221
34051
|
project_id: project.id,
|
|
32222
34052
|
task_list_id: list.id,
|
|
@@ -32225,8 +34055,8 @@ function receiptFor(store, project, list, idempotencyKey) {
|
|
|
32225
34055
|
}
|
|
32226
34056
|
return {
|
|
32227
34057
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
32228
|
-
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${
|
|
32229
|
-
idempotency_key: owned ? marker.idempotency_key :
|
|
34058
|
+
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
|
|
34059
|
+
idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
|
|
32230
34060
|
project_id: project.id,
|
|
32231
34061
|
task_list_id: list.id,
|
|
32232
34062
|
slug: list.slug,
|
|
@@ -32278,20 +34108,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
32278
34108
|
});
|
|
32279
34109
|
}
|
|
32280
34110
|
const slug = project.task_list_id;
|
|
32281
|
-
const
|
|
34111
|
+
const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
|
|
32282
34112
|
if (state.scoped) {
|
|
32283
34113
|
return {
|
|
32284
34114
|
mode: "apply",
|
|
32285
34115
|
action: "already_present",
|
|
32286
34116
|
project,
|
|
32287
34117
|
task_list: state.scoped,
|
|
32288
|
-
receipt: receiptFor(store, project, state.scoped,
|
|
34118
|
+
receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
|
|
32289
34119
|
};
|
|
32290
34120
|
}
|
|
32291
34121
|
const marker = {
|
|
32292
34122
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
32293
|
-
receipt_id: receiptId2(project.id, slug,
|
|
32294
|
-
idempotency_key:
|
|
34123
|
+
receipt_id: receiptId2(project.id, slug, idempotencyKey2),
|
|
34124
|
+
idempotency_key: idempotencyKey2,
|
|
32295
34125
|
project_id: project.id,
|
|
32296
34126
|
slug,
|
|
32297
34127
|
result_digest: semanticListDigest({
|
|
@@ -32329,7 +34159,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
32329
34159
|
action: "already_present",
|
|
32330
34160
|
project: raced.project,
|
|
32331
34161
|
task_list: raced.scoped,
|
|
32332
|
-
receipt: receiptFor(store, raced.project, raced.scoped,
|
|
34162
|
+
receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
|
|
32333
34163
|
};
|
|
32334
34164
|
}
|
|
32335
34165
|
const projectReadback = await store.projects.get(project.id);
|
|
@@ -32359,7 +34189,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
32359
34189
|
action: "created",
|
|
32360
34190
|
project: projectReadback,
|
|
32361
34191
|
task_list: readback,
|
|
32362
|
-
receipt: receiptFor(store, projectReadback, readback,
|
|
34192
|
+
receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
|
|
32363
34193
|
};
|
|
32364
34194
|
}
|
|
32365
34195
|
async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
@@ -32404,7 +34234,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
|
32404
34234
|
project_id: project.id,
|
|
32405
34235
|
task_list_id: list.id,
|
|
32406
34236
|
accepted_receipt_id: options.receipt_id,
|
|
32407
|
-
rollback_receipt_id: `ptlr_inverse_${
|
|
34237
|
+
rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
32408
34238
|
removed_at: new Date().toISOString()
|
|
32409
34239
|
};
|
|
32410
34240
|
}
|
|
@@ -32603,6 +34433,9 @@ function validateTaskPatchVocabulary(value) {
|
|
|
32603
34433
|
if (!parsed.ok)
|
|
32604
34434
|
return { ok: false, message: parsed.message };
|
|
32605
34435
|
}
|
|
34436
|
+
if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
|
|
34437
|
+
return { ok: false, message: "parent_id must be a non-empty task id or null" };
|
|
34438
|
+
}
|
|
32606
34439
|
return { ok: true, patch: body2 };
|
|
32607
34440
|
}
|
|
32608
34441
|
function validateProjectPatch(value) {
|
|
@@ -33087,8 +34920,8 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
33087
34920
|
}
|
|
33088
34921
|
const created = await store.tasks.create(body2, storageContext);
|
|
33089
34922
|
const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
|
|
33090
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
|
|
33091
|
-
return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id and
|
|
34923
|
+
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null)) {
|
|
34924
|
+
return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, and plan_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
|
|
33092
34925
|
}
|
|
33093
34926
|
return json5({ task: persisted }, 201);
|
|
33094
34927
|
}
|
|
@@ -33962,6 +35795,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
33962
35795
|
if (e instanceof TaskNotFoundError) {
|
|
33963
35796
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
33964
35797
|
}
|
|
35798
|
+
if (e instanceof VersionConflictError) {
|
|
35799
|
+
return error(409, e.message, {
|
|
35800
|
+
code: VersionConflictError.code,
|
|
35801
|
+
conflict: true,
|
|
35802
|
+
task_id: e.taskId,
|
|
35803
|
+
expected_version: e.expectedVersion,
|
|
35804
|
+
current_version: e.actualVersion
|
|
35805
|
+
});
|
|
35806
|
+
}
|
|
33965
35807
|
if (e instanceof StaleLockHandoffError) {
|
|
33966
35808
|
const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
|
|
33967
35809
|
return error(status2, e.message, {
|
|
@@ -52577,16 +54419,16 @@ function createHasnaStorageClient(name, transport) {
|
|
|
52577
54419
|
}
|
|
52578
54420
|
},
|
|
52579
54421
|
async create(resource, body2, options = {}) {
|
|
52580
|
-
const { idempotencyKey, ...rest } = options;
|
|
54422
|
+
const { idempotencyKey: idempotencyKey2, ...rest } = options;
|
|
52581
54423
|
return transport.post(resourcePath(resource), body2, {
|
|
52582
54424
|
...rest,
|
|
52583
|
-
idempotencyKey:
|
|
54425
|
+
idempotencyKey: idempotencyKey2 ?? newIdempotencyKey()
|
|
52584
54426
|
});
|
|
52585
54427
|
},
|
|
52586
54428
|
async update(resource, id, patch, options = {}) {
|
|
52587
|
-
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
54429
|
+
const { method = "PATCH", idempotencyKey: idempotencyKey2, ...rest } = options;
|
|
52588
54430
|
const call = method === "PUT" ? transport.put : transport.patch;
|
|
52589
|
-
return call(entityPath(resource, id), patch, { ...rest, ...
|
|
54431
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey2 ? { idempotencyKey: idempotencyKey2 } : {} });
|
|
52590
54432
|
},
|
|
52591
54433
|
async delete(resource, id, options = {}) {
|
|
52592
54434
|
try {
|
|
@@ -64770,6 +66612,11 @@ var init_http_client = __esm(() => {
|
|
|
64770
66612
|
]);
|
|
64771
66613
|
});
|
|
64772
66614
|
|
|
66615
|
+
// src/project-registration/page-validation.ts
|
|
66616
|
+
var init_page_validation = __esm(() => {
|
|
66617
|
+
init_types3();
|
|
66618
|
+
});
|
|
66619
|
+
|
|
64773
66620
|
// src/cli/cloud-router.ts
|
|
64774
66621
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
64775
66622
|
import { resolve as resolvePath } from "path";
|
|
@@ -65168,12 +67015,39 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
65168
67015
|
union3.sort(compareCloudTaskOrder);
|
|
65169
67016
|
return union3.slice(start, windowEnd);
|
|
65170
67017
|
}
|
|
67018
|
+
async function cloudResolveTaskRef(client, ref) {
|
|
67019
|
+
const input = ref.trim().toLowerCase();
|
|
67020
|
+
if (!input)
|
|
67021
|
+
throw new Error("Task reference must not be empty");
|
|
67022
|
+
if (UUID_RE.test(input))
|
|
67023
|
+
return input;
|
|
67024
|
+
let task2;
|
|
67025
|
+
try {
|
|
67026
|
+
task2 = await cloudGetTask(client, input);
|
|
67027
|
+
} catch (error3) {
|
|
67028
|
+
const status2 = error3 && typeof error3 === "object" ? error3.status : undefined;
|
|
67029
|
+
if (status2 === 409) {
|
|
67030
|
+
const body2 = error3 && typeof error3 === "object" ? error3.body : undefined;
|
|
67031
|
+
const authorityMessage = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.error : undefined;
|
|
67032
|
+
throw new Error(typeof authorityMessage === "string" && authorityMessage.trim() ? authorityMessage : `Task reference is ambiguous: "${ref}"`);
|
|
67033
|
+
}
|
|
67034
|
+
throw error3;
|
|
67035
|
+
}
|
|
67036
|
+
if (task2 && typeof task2.id === "string" && (task2.short_id?.toLowerCase() === input || task2.id.toLowerCase().startsWith(input))) {
|
|
67037
|
+
return task2.id;
|
|
67038
|
+
}
|
|
67039
|
+
if (task2) {
|
|
67040
|
+
throw new Error(`Task not found: ${ref} \u2014 the authority returned a task carrying neither this short id ` + "nor this id prefix, so the reference was not resolved.");
|
|
67041
|
+
}
|
|
67042
|
+
throw new Error(`Task not found: ${ref} \u2014 the authority resolved no task for this short reference. ` + "That is not proof the task is absent: an authority predating server-side " + "short-reference resolution answers EVERY short id and id prefix with the same 404. " + "Retry with the full task UUID to tell the two apart, or deploy the current " + "@hasna/todos /v1 server.");
|
|
67043
|
+
}
|
|
65171
67044
|
async function cloudGetTask(client, id) {
|
|
65172
67045
|
const raw = await client.get("tasks", id);
|
|
65173
67046
|
return raw == null ? null : unwrapTask(raw);
|
|
65174
67047
|
}
|
|
65175
67048
|
async function cloudCreateTask(client, input, verification2 = {}) {
|
|
65176
67049
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
67050
|
+
const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
|
|
65177
67051
|
const expectedCreatedBy = typeof verification2.expectedCreatedBy === "string" && verification2.expectedCreatedBy.trim() ? verification2.expectedCreatedBy : null;
|
|
65178
67052
|
if (expectedCreatedBy !== null) {
|
|
65179
67053
|
await requireTaskCreatorCapability(client);
|
|
@@ -65186,9 +67060,9 @@ async function cloudCreateTask(client, input, verification2 = {}) {
|
|
|
65186
67060
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
|
|
65187
67061
|
}
|
|
65188
67062
|
const persisted = await cloudGetTask(client, created.id);
|
|
65189
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
|
|
67063
|
+
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
|
|
65190
67064
|
const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
|
|
65191
|
-
throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id and
|
|
67065
|
+
throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id, parent_id, and plan_id ` + `(requested plan_id=${JSON.stringify(expectedPlanId)}, readback=${JSON.stringify(persisted?.plan_id ?? null)})` + `${creatorDetail}; no success row or local SQLite fallback is permitted`);
|
|
65192
67066
|
}
|
|
65193
67067
|
return persisted;
|
|
65194
67068
|
}
|
|
@@ -65457,6 +67331,8 @@ var init_cloud_router = __esm(() => {
|
|
|
65457
67331
|
init_redaction();
|
|
65458
67332
|
init_plan_project_link_contract();
|
|
65459
67333
|
init_http_client();
|
|
67334
|
+
init_adoption_validation();
|
|
67335
|
+
init_page_validation();
|
|
65460
67336
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
65461
67337
|
TRANSPORT_TOKENS = {
|
|
65462
67338
|
sqlite: "sqlite",
|
|
@@ -65493,6 +67369,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
65493
67369
|
compact["version"] = task2.version;
|
|
65494
67370
|
compact["created_at"] = task2.created_at;
|
|
65495
67371
|
compact["task_list_id"] = task2.task_list_id;
|
|
67372
|
+
compact["parent_id"] = task2.parent_id;
|
|
65496
67373
|
return compactJson(compact);
|
|
65497
67374
|
}
|
|
65498
67375
|
function versionFor(taskId, version2) {
|
|
@@ -65762,6 +67639,7 @@ ${task2.description}` : null
|
|
|
65762
67639
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
65763
67640
|
assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
|
|
65764
67641
|
project_id: exports_external.string().nullable().optional(),
|
|
67642
|
+
parent_id: exports_external.string().nullable().optional().describe("Existing parent task ID/reference, null to detach"),
|
|
65765
67643
|
task_list_id: exports_external.string().nullable().optional(),
|
|
65766
67644
|
depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
|
|
65767
67645
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
@@ -65793,6 +67671,9 @@ ${task2.description}` : null
|
|
|
65793
67671
|
if (typeof patch.project_id === "string" && patch.project_id) {
|
|
65794
67672
|
patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
|
|
65795
67673
|
}
|
|
67674
|
+
if (typeof patch.parent_id === "string" && patch.parent_id) {
|
|
67675
|
+
patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
|
|
67676
|
+
}
|
|
65796
67677
|
if (typeof patch.task_list_id === "string" && patch.task_list_id) {
|
|
65797
67678
|
let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
|
|
65798
67679
|
if (!scope) {
|
|
@@ -65801,9 +67682,22 @@ ${task2.description}` : null
|
|
|
65801
67682
|
}
|
|
65802
67683
|
patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
|
|
65803
67684
|
}
|
|
67685
|
+
if (patch.parent_id !== undefined && version3 === undefined) {
|
|
67686
|
+
const current = await cloudGetTask(cloud, task_id2);
|
|
67687
|
+
if (!current)
|
|
67688
|
+
throw new TaskNotFoundError(task_id2);
|
|
67689
|
+
patch.version = current.version;
|
|
67690
|
+
}
|
|
65804
67691
|
if (version3 !== undefined)
|
|
65805
67692
|
patch.version = version3;
|
|
65806
|
-
|
|
67693
|
+
let updated = await cloudUpdateTask(cloud, task_id2, patch);
|
|
67694
|
+
if (patch.parent_id !== undefined) {
|
|
67695
|
+
const persisted = await cloudGetTask(cloud, task_id2);
|
|
67696
|
+
if (!persisted || (persisted.parent_id ?? null) !== patch.parent_id) {
|
|
67697
|
+
throw new Error(`TASK_REPARENT_PERSISTENCE_UNVERIFIED: parent_id expected ${patch.parent_id ?? "null"}, ` + `received ${persisted?.parent_id ?? "missing task"}`);
|
|
67698
|
+
}
|
|
67699
|
+
updated = persisted;
|
|
67700
|
+
}
|
|
65807
67701
|
return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
|
|
65808
67702
|
}
|
|
65809
67703
|
const resolvedId = resolveId(params.task_id);
|
|
@@ -65815,6 +67709,8 @@ ${task2.description}` : null
|
|
|
65815
67709
|
resolved.assigned_to = resolveAssignee(resolved.assigned_to);
|
|
65816
67710
|
if (resolved.project_id && typeof resolved.project_id === "string")
|
|
65817
67711
|
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
67712
|
+
if (resolved.parent_id && typeof resolved.parent_id === "string")
|
|
67713
|
+
resolved.parent_id = resolveId(resolved.parent_id);
|
|
65818
67714
|
if (resolved.task_list_id && typeof resolved.task_list_id === "string")
|
|
65819
67715
|
resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
|
|
65820
67716
|
if (resolved.depends_on && Array.isArray(resolved.depends_on))
|
|
@@ -99497,7 +101393,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
99497
101393
|
create_task: "create_task \u2014 Create a new task. Params: title (required), description, status, priority, project_id, task_list_id, assigned_to, depends_on, short_id (null to disable), tags, estimate (minutes), confidence (0.0-1.0), deadline (ISO), retry_count",
|
|
99498
101394
|
list_tasks: "list_tasks \u2014 List tasks with filters. Params: status, priority, project_id, task_list_id, assigned_to, tags[], created_after, created_before, limit, offset",
|
|
99499
101395
|
get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
|
|
99500
|
-
update_task: "update_task \u2014 Update task fields (optimistic locking). Params: task_id (required), title, description, status, priority, assigned_to (null to unassign), project_id, task_list_id, depends_on[], tags[], estimate, actual_minutes, confidence, approved_by, completed_at, deadline, retry_count, version",
|
|
101396
|
+
update_task: "update_task \u2014 Update task fields (optimistic locking). Params: task_id (required), title, description, status, priority, assigned_to (null to unassign), project_id, parent_id (null to detach), task_list_id, depends_on[], tags[], estimate, actual_minutes, confidence, approved_by, completed_at, deadline, retry_count, version",
|
|
99501
101397
|
delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
|
|
99502
101398
|
start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
|
|
99503
101399
|
complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
|
|
@@ -111312,8 +113208,8 @@ function defaultSnapshotDir() {
|
|
|
111312
113208
|
return join15(dirname8(resolve16(dbPath)), "environment-snapshots");
|
|
111313
113209
|
}
|
|
111314
113210
|
function snapshotWithId(snapshot) {
|
|
111315
|
-
const
|
|
111316
|
-
return { id: `env_${
|
|
113211
|
+
const digest3 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
|
|
113212
|
+
return { id: `env_${digest3}`, ...snapshot };
|
|
111317
113213
|
}
|
|
111318
113214
|
function captureEnvironmentSnapshot(input = {}) {
|
|
111319
113215
|
const root = resolve16(input.root || process.cwd());
|