@hasna/todos 0.15.28 → 0.15.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +15 -0
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +5 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -0
- 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 +13040 -10368
- package/dist/contracts.js +64 -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 +1489 -149
- 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 +2073 -140
- 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 +26 -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/schema.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 +873 -63
- package/dist/registry.js +64 -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 +2064 -131
- 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 +244 -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}`;
|
|
@@ -6612,6 +6815,12 @@ function sqliteTodosProjectRegistrationSchemaSql() {
|
|
|
6612
6815
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
6613
6816
|
resource_kind, direction, idempotency_key
|
|
6614
6817
|
);
|
|
6818
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_source_identity
|
|
6819
|
+
ON todos_project_registration_receipts (
|
|
6820
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
6821
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
6822
|
+
target_selector
|
|
6823
|
+
);
|
|
6615
6824
|
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
6616
6825
|
ON todos_project_registration_receipts (
|
|
6617
6826
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -6711,6 +6920,12 @@ function postgresTodosProjectRegistrationSchemaSql() {
|
|
|
6711
6920
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
6712
6921
|
resource_kind, direction, idempotency_key
|
|
6713
6922
|
)`,
|
|
6923
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_source_identity_idx
|
|
6924
|
+
ON todos_project_registration_receipts (
|
|
6925
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
6926
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
6927
|
+
target_selector
|
|
6928
|
+
)`,
|
|
6714
6929
|
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
6715
6930
|
ON todos_project_registration_receipts (
|
|
6716
6931
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -6852,8 +7067,9 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
6852
7067
|
const result = await this.client.query(`
|
|
6853
7068
|
SELECT * FROM todos_project_registration_receipts
|
|
6854
7069
|
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
6855
|
-
AND
|
|
6856
|
-
AND
|
|
7070
|
+
AND route = $4 AND package_version = $5
|
|
7071
|
+
AND operation_id = $6 AND step_id = $7 AND resource_kind = $8
|
|
7072
|
+
AND direction = $9 AND idempotency_key = $10 AND target_selector = $11
|
|
6857
7073
|
ORDER BY CASE outcome
|
|
6858
7074
|
WHEN 'terminal_nonacceptance' THEN 0
|
|
6859
7075
|
WHEN 'duplicate_of_accepted' THEN 1
|
|
@@ -6864,6 +7080,8 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
6864
7080
|
identity.authority_id,
|
|
6865
7081
|
identity.tenant_id,
|
|
6866
7082
|
identity.corpus_id,
|
|
7083
|
+
identity.route,
|
|
7084
|
+
identity.package_version,
|
|
6867
7085
|
identity.operation_id,
|
|
6868
7086
|
identity.step_id,
|
|
6869
7087
|
identity.resource_kind,
|
|
@@ -7063,6 +7281,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
7063
7281
|
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
7064
7282
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
7065
7283
|
LIMIT 1
|
|
7284
|
+
FOR UPDATE
|
|
7066
7285
|
`, [this.service, path, taskListSlug]);
|
|
7067
7286
|
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
7068
7287
|
}
|
|
@@ -7073,6 +7292,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
7073
7292
|
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
7074
7293
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
7075
7294
|
LIMIT 1
|
|
7295
|
+
FOR UPDATE
|
|
7076
7296
|
`, [this.service, projectId, slug]);
|
|
7077
7297
|
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
7078
7298
|
}
|
|
@@ -7083,10 +7303,24 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
7083
7303
|
return await this.storage.taskLists.create(input);
|
|
7084
7304
|
}
|
|
7085
7305
|
async getProject(id) {
|
|
7086
|
-
|
|
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;
|
|
7087
7314
|
}
|
|
7088
7315
|
async getTaskList(id) {
|
|
7089
|
-
|
|
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;
|
|
7090
7324
|
}
|
|
7091
7325
|
async lockCompensationWrites() {
|
|
7092
7326
|
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
@@ -7166,6 +7400,96 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
7166
7400
|
async getTaskList(id) {
|
|
7167
7401
|
return (await this.direct()).getTaskList(id);
|
|
7168
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
|
+
}
|
|
7169
7493
|
}
|
|
7170
7494
|
var init_postgres2 = __esm(() => {
|
|
7171
7495
|
init_postgres_adapter();
|
|
@@ -15289,6 +15613,7 @@ function createTaskStored(input, d) {
|
|
|
15289
15613
|
let id = uuid();
|
|
15290
15614
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
15291
15615
|
try {
|
|
15616
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
15292
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)
|
|
15293
15618
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
15294
15619
|
id,
|
|
@@ -15667,6 +15992,7 @@ function updateTaskStored(id, input, db) {
|
|
|
15667
15992
|
throw new VersionConflictError(id, input.version, task.version);
|
|
15668
15993
|
}
|
|
15669
15994
|
input = sanitizeUpdateTaskInput(input);
|
|
15995
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
15670
15996
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
|
|
15671
15997
|
const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
|
|
15672
15998
|
if (linkedProjectId) {
|
|
@@ -15720,6 +16046,10 @@ function updateTaskStored(id, input, db) {
|
|
|
15720
16046
|
sets.push("project_id = ?");
|
|
15721
16047
|
params.push(input.project_id);
|
|
15722
16048
|
}
|
|
16049
|
+
if (input.parent_id !== undefined) {
|
|
16050
|
+
sets.push("parent_id = ?");
|
|
16051
|
+
params.push(input.parent_id);
|
|
16052
|
+
}
|
|
15723
16053
|
if (input.assigned_to !== undefined) {
|
|
15724
16054
|
sets.push("assigned_to = ?");
|
|
15725
16055
|
params.push(input.assigned_to);
|
|
@@ -15835,6 +16165,8 @@ function updateTaskStored(id, input, db) {
|
|
|
15835
16165
|
logTaskChange2(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
15836
16166
|
if (input.title !== undefined && input.title !== task.title)
|
|
15837
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);
|
|
15838
16170
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
15839
16171
|
logTaskChange2(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
15840
16172
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
@@ -15892,7 +16224,8 @@ function updateTask2(id, input, db) {
|
|
|
15892
16224
|
if (!before)
|
|
15893
16225
|
throw new TaskNotFoundError(id);
|
|
15894
16226
|
const guardedPlanIds = [before.plan_id, input.plan_id];
|
|
15895
|
-
|
|
16227
|
+
const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
|
|
16228
|
+
if (!needsSerializedWrite)
|
|
15896
16229
|
return updateTaskStored(id, input, d);
|
|
15897
16230
|
return d.transaction(() => {
|
|
15898
16231
|
guardPlanRowsSqlite(guardedPlanIds, d);
|
|
@@ -15928,6 +16261,7 @@ var init_task_crud = __esm(() => {
|
|
|
15928
16261
|
init_checklists();
|
|
15929
16262
|
init_storage_tombstones();
|
|
15930
16263
|
init_prewrite_secrets();
|
|
16264
|
+
init_task_parent_integrity();
|
|
15931
16265
|
});
|
|
15932
16266
|
|
|
15933
16267
|
// src/db/task-status.ts
|
|
@@ -20463,6 +20797,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
20463
20797
|
const row = this.db.query(`
|
|
20464
20798
|
SELECT * FROM todos_project_registration_receipts
|
|
20465
20799
|
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
20800
|
+
AND route = ? AND package_version = ?
|
|
20466
20801
|
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
20467
20802
|
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
20468
20803
|
ORDER BY CASE outcome
|
|
@@ -20471,7 +20806,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
20471
20806
|
ELSE 2
|
|
20472
20807
|
END, created_at DESC, receipt_id DESC
|
|
20473
20808
|
LIMIT 1
|
|
20474
|
-
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
20809
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.route, identity.package_version, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
20475
20810
|
return row ? receiptFromRow2(row) : null;
|
|
20476
20811
|
}
|
|
20477
20812
|
async getReceiptById(receiptId) {
|
|
@@ -20625,7 +20960,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
20625
20960
|
}
|
|
20626
20961
|
async lockStep(_identity) {}
|
|
20627
20962
|
async getReceiptForLookup(identity) {
|
|
20628
|
-
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
20963
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.route === identity.route && receipt.package_version === identity.package_version && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
20629
20964
|
const stored = await this.direct.getReceiptForLookup(identity);
|
|
20630
20965
|
if (stored)
|
|
20631
20966
|
staged.push(stored);
|
|
@@ -21113,35 +21448,68 @@ function projectRecord(project) {
|
|
|
21113
21448
|
return {
|
|
21114
21449
|
target_id: project.id,
|
|
21115
21450
|
revision: project.updated_at,
|
|
21116
|
-
digest:
|
|
21117
|
-
id: project.id,
|
|
21118
|
-
name: project.name,
|
|
21119
|
-
path: project.path,
|
|
21120
|
-
description: project.description,
|
|
21121
|
-
task_list_id: project.task_list_id,
|
|
21122
|
-
task_prefix: project.task_prefix,
|
|
21123
|
-
task_counter: project.task_counter,
|
|
21124
|
-
created_at: project.created_at,
|
|
21125
|
-
updated_at: project.updated_at
|
|
21126
|
-
})
|
|
21451
|
+
digest: projectRegistrationDigest(project)
|
|
21127
21452
|
};
|
|
21128
21453
|
}
|
|
21129
21454
|
function taskListRecord(taskList) {
|
|
21130
21455
|
return {
|
|
21131
21456
|
target_id: taskList.id,
|
|
21132
21457
|
revision: taskList.updated_at,
|
|
21133
|
-
digest:
|
|
21134
|
-
|
|
21135
|
-
|
|
21136
|
-
|
|
21137
|
-
|
|
21138
|
-
|
|
21139
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
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
|
|
21142
21468
|
})
|
|
21143
21469
|
};
|
|
21144
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
|
+
}
|
|
21145
21513
|
function receiptId(input) {
|
|
21146
21514
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
21147
21515
|
}
|
|
@@ -21179,9 +21547,37 @@ function normalizedCallDigest(request) {
|
|
|
21179
21547
|
project_slug: request.project_slug,
|
|
21180
21548
|
project_name: request.project_name,
|
|
21181
21549
|
desired: request.desired,
|
|
21550
|
+
bind_existing: request.bind_existing === true,
|
|
21182
21551
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
21183
21552
|
});
|
|
21184
21553
|
}
|
|
21554
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
21555
|
+
return digestProjectRegistrationValue({
|
|
21556
|
+
authority_route: request.authority_route,
|
|
21557
|
+
package_version: request.package_version,
|
|
21558
|
+
authority_id: request.authority_id,
|
|
21559
|
+
tenant_id: request.tenant_id,
|
|
21560
|
+
corpus_id: request.corpus_id,
|
|
21561
|
+
operation_id: request.operation_id,
|
|
21562
|
+
step_id: request.step_id,
|
|
21563
|
+
resource_kind: request.resource_kind,
|
|
21564
|
+
direction: request.direction,
|
|
21565
|
+
target_selector: request.target_selector,
|
|
21566
|
+
idempotency_key: request.idempotency_key,
|
|
21567
|
+
request_digest: request.request_digest,
|
|
21568
|
+
precondition_digest: request.precondition_digest,
|
|
21569
|
+
project_id: request.project_id,
|
|
21570
|
+
project_slug: request.project_slug,
|
|
21571
|
+
project_name: request.project_name,
|
|
21572
|
+
desired: request.desired,
|
|
21573
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
21574
|
+
});
|
|
21575
|
+
}
|
|
21576
|
+
function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
|
|
21577
|
+
if (accepted.normalized_call_digest === callDigest)
|
|
21578
|
+
return true;
|
|
21579
|
+
return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
|
|
21580
|
+
}
|
|
21185
21581
|
function assertCommonRequest(request, capability) {
|
|
21186
21582
|
assertBounds(request);
|
|
21187
21583
|
assertResourceKind(request.resource_kind);
|
|
@@ -21223,6 +21619,9 @@ function assertCommonRequest(request, capability) {
|
|
|
21223
21619
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
21224
21620
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
21225
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
|
+
}
|
|
21226
21625
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
21227
21626
|
operation_id: request.operation_id,
|
|
21228
21627
|
step_id: request.step_id,
|
|
@@ -21244,7 +21643,7 @@ function assertForwardRequest(request, capability) {
|
|
|
21244
21643
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
21245
21644
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
21246
21645
|
target_selector: request.target_selector,
|
|
21247
|
-
expected: "absent"
|
|
21646
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
21248
21647
|
});
|
|
21249
21648
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
21250
21649
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -21323,7 +21722,7 @@ function receiptBase(request, callDigest, capability) {
|
|
|
21323
21722
|
normalized_call_digest: callDigest
|
|
21324
21723
|
};
|
|
21325
21724
|
}
|
|
21326
|
-
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt) {
|
|
21725
|
+
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt, createdByOperation = true) {
|
|
21327
21726
|
return makeReceipt({
|
|
21328
21727
|
...receiptBase(request, callDigest, capability),
|
|
21329
21728
|
outcome: "accepted",
|
|
@@ -21333,7 +21732,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt)
|
|
|
21333
21732
|
result_digest: record.digest,
|
|
21334
21733
|
duplicate_of_receipt_id: null,
|
|
21335
21734
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
21336
|
-
created_by_operation:
|
|
21735
|
+
created_by_operation: createdByOperation
|
|
21337
21736
|
}, createdAt);
|
|
21338
21737
|
}
|
|
21339
21738
|
function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt) {
|
|
@@ -21395,6 +21794,53 @@ function bindingFor(request, callDigest, timestamp2, capability) {
|
|
|
21395
21794
|
updated_at: timestamp2
|
|
21396
21795
|
};
|
|
21397
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
|
+
}
|
|
21398
21844
|
|
|
21399
21845
|
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
21400
21846
|
backend;
|
|
@@ -21416,6 +21862,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21416
21862
|
immutable_receipts: true,
|
|
21417
21863
|
exact_terminal_lookup: true,
|
|
21418
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,
|
|
21419
21869
|
conditional_inverse: true,
|
|
21420
21870
|
ambiguous_outcome_reconciliation: true
|
|
21421
21871
|
};
|
|
@@ -21460,6 +21910,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21460
21910
|
async existingForwardResolution(transaction, request, callDigest) {
|
|
21461
21911
|
const exact = await transaction.getReceiptForLookup({
|
|
21462
21912
|
...authorityScope(this.capabilityValue),
|
|
21913
|
+
route: this.capabilityValue.route,
|
|
21914
|
+
package_version: this.capabilityValue.package_version,
|
|
21463
21915
|
operation_id: request.operation_id,
|
|
21464
21916
|
step_id: request.step_id,
|
|
21465
21917
|
resource_kind: request.resource_kind,
|
|
@@ -21474,7 +21926,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21474
21926
|
if (!accepted2) {
|
|
21475
21927
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
21476
21928
|
}
|
|
21477
|
-
if (accepted2
|
|
21929
|
+
if (!acceptedCallMatches(request, accepted2, callDigest)) {
|
|
21478
21930
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
21479
21931
|
}
|
|
21480
21932
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
@@ -21488,7 +21940,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21488
21940
|
});
|
|
21489
21941
|
if (!accepted)
|
|
21490
21942
|
return null;
|
|
21491
|
-
if (accepted
|
|
21943
|
+
if (acceptedCallMatches(request, accepted, callDigest)) {
|
|
21492
21944
|
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
21493
21945
|
}
|
|
21494
21946
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
@@ -21499,6 +21951,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21499
21951
|
const slug2 = taskListSlug(request.project_slug);
|
|
21500
21952
|
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
21501
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
|
+
}
|
|
21502
21960
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
21503
21961
|
}
|
|
21504
21962
|
await this.fault("before_object_write", request);
|
|
@@ -21510,7 +21968,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21510
21968
|
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
21511
21969
|
});
|
|
21512
21970
|
await this.fault("after_object_write", request);
|
|
21513
|
-
return
|
|
21971
|
+
return {
|
|
21972
|
+
record: projectRecord(project),
|
|
21973
|
+
created_by_operation: true
|
|
21974
|
+
};
|
|
21514
21975
|
}
|
|
21515
21976
|
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
21516
21977
|
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
@@ -21524,6 +21985,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21524
21985
|
const slug = taskListSlug(request.project_slug);
|
|
21525
21986
|
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
21526
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
|
+
}
|
|
21527
21994
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
21528
21995
|
}
|
|
21529
21996
|
await this.fault("before_object_write", request);
|
|
@@ -21540,7 +22007,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21540
22007
|
if (taskList.project_id !== todosProjectId) {
|
|
21541
22008
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
21542
22009
|
}
|
|
21543
|
-
return
|
|
22010
|
+
return {
|
|
22011
|
+
record: taskListRecord(taskList),
|
|
22012
|
+
created_by_operation: true
|
|
22013
|
+
};
|
|
21544
22014
|
}
|
|
21545
22015
|
async create(request) {
|
|
21546
22016
|
const startedAt = Date.now();
|
|
@@ -21562,9 +22032,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21562
22032
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
|
|
21563
22033
|
if (!claimed) {
|
|
21564
22034
|
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
21565
|
-
if (binding?.state === "accepted" && binding.
|
|
22035
|
+
if (binding?.state === "accepted" && binding.accepted_receipt_id) {
|
|
21566
22036
|
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
21567
|
-
if (accepted2) {
|
|
22037
|
+
if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
|
|
21568
22038
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
21569
22039
|
}
|
|
21570
22040
|
}
|
|
@@ -21575,15 +22045,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21575
22045
|
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
21576
22046
|
return recordOrTerminal;
|
|
21577
22047
|
}
|
|
21578
|
-
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);
|
|
21579
22049
|
await this.fault("before_receipt_write", request);
|
|
21580
22050
|
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
21581
22051
|
await this.fault("after_receipt_write", request);
|
|
21582
22052
|
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
21583
|
-
target_id: recordOrTerminal.target_id,
|
|
22053
|
+
target_id: recordOrTerminal.record.target_id,
|
|
21584
22054
|
accepted_receipt_id: stored.receipt_id,
|
|
21585
|
-
result_revision: recordOrTerminal.revision,
|
|
21586
|
-
result_digest: recordOrTerminal.digest,
|
|
22055
|
+
result_revision: recordOrTerminal.record.revision,
|
|
22056
|
+
result_digest: recordOrTerminal.record.digest,
|
|
21587
22057
|
updated_at: this.now()
|
|
21588
22058
|
});
|
|
21589
22059
|
return stored;
|
|
@@ -21612,6 +22082,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21612
22082
|
});
|
|
21613
22083
|
const exact = await transaction.getReceiptForLookup({
|
|
21614
22084
|
...authorityScope(this.capabilityValue),
|
|
22085
|
+
route: this.capabilityValue.route,
|
|
22086
|
+
package_version: this.capabilityValue.package_version,
|
|
21615
22087
|
operation_id: request.operation_id,
|
|
21616
22088
|
step_id: request.step_id,
|
|
21617
22089
|
resource_kind: request.resource_kind,
|
|
@@ -21629,7 +22101,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21629
22101
|
direction: request.direction
|
|
21630
22102
|
});
|
|
21631
22103
|
if (accepted) {
|
|
21632
|
-
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 });
|
|
21633
22105
|
}
|
|
21634
22106
|
const timestamp2 = this.now();
|
|
21635
22107
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
|
|
@@ -21662,9 +22134,18 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21662
22134
|
if (request.max_items !== 1) {
|
|
21663
22135
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
21664
22136
|
}
|
|
21665
|
-
if (request.authority !== "todos" || request.
|
|
22137
|
+
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
|
|
21666
22138
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
21667
22139
|
}
|
|
22140
|
+
requireString(request.authority_route, "authority_route", {
|
|
22141
|
+
min: 3,
|
|
22142
|
+
max: 128,
|
|
22143
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
22144
|
+
});
|
|
22145
|
+
requireString(request.package_version, "package_version", {
|
|
22146
|
+
max: 128,
|
|
22147
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
22148
|
+
});
|
|
21668
22149
|
requireString(request.operation_id, "operation_id", {
|
|
21669
22150
|
min: 8,
|
|
21670
22151
|
max: 128,
|
|
@@ -21686,6 +22167,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21686
22167
|
}
|
|
21687
22168
|
const receipt = await this.backend.getReceiptForLookup({
|
|
21688
22169
|
...authorityScope(this.capabilityValue),
|
|
22170
|
+
route: request.authority_route,
|
|
22171
|
+
package_version: request.package_version,
|
|
21689
22172
|
operation_id: request.operation_id,
|
|
21690
22173
|
step_id: request.step_id,
|
|
21691
22174
|
resource_kind: request.resource_kind,
|
|
@@ -21698,6 +22181,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21698
22181
|
}
|
|
21699
22182
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
21700
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
|
+
}
|
|
21701
22342
|
async storedAcceptedReceipt(request, supplied) {
|
|
21702
22343
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
21703
22344
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -21724,6 +22365,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21724
22365
|
});
|
|
21725
22366
|
const exact = await transaction.getReceiptForLookup({
|
|
21726
22367
|
...authorityScope(this.capabilityValue),
|
|
22368
|
+
route: this.capabilityValue.route,
|
|
22369
|
+
package_version: this.capabilityValue.package_version,
|
|
21727
22370
|
operation_id: request.operation_id,
|
|
21728
22371
|
step_id: request.step_id,
|
|
21729
22372
|
resource_kind: request.resource_kind,
|
|
@@ -21818,6 +22461,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
21818
22461
|
await this.storedAcceptedReceipt(request, accepted);
|
|
21819
22462
|
const receipt = await this.backend.getReceiptForLookup({
|
|
21820
22463
|
...authorityScope(this.capabilityValue),
|
|
22464
|
+
route: this.capabilityValue.route,
|
|
22465
|
+
package_version: this.capabilityValue.package_version,
|
|
21821
22466
|
operation_id: request.operation_id,
|
|
21822
22467
|
step_id: request.step_id,
|
|
21823
22468
|
resource_kind: request.resource_kind,
|
|
@@ -21862,7 +22507,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
21862
22507
|
cursorTableName
|
|
21863
22508
|
}), authorityOptions);
|
|
21864
22509
|
}
|
|
21865
|
-
var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_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;
|
|
21866
22511
|
var init_authority = __esm(() => {
|
|
21867
22512
|
init_package_version();
|
|
21868
22513
|
init_postgres2();
|
|
@@ -21872,6 +22517,8 @@ var init_authority = __esm(() => {
|
|
|
21872
22517
|
WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
|
|
21873
22518
|
OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
21874
22519
|
STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
22520
|
+
AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
22521
|
+
PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
21875
22522
|
SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
21876
22523
|
IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
21877
22524
|
WriteBoundaryError = class WriteBoundaryError extends Error {
|
|
@@ -21885,6 +22532,11 @@ var init_authority = __esm(() => {
|
|
|
21885
22532
|
};
|
|
21886
22533
|
});
|
|
21887
22534
|
|
|
22535
|
+
// src/project-registration/adoption-validation.ts
|
|
22536
|
+
var init_adoption_validation = __esm(() => {
|
|
22537
|
+
init_types3();
|
|
22538
|
+
});
|
|
22539
|
+
|
|
21888
22540
|
// src/project-registration/http.ts
|
|
21889
22541
|
function json(body, status = 200) {
|
|
21890
22542
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
@@ -21930,6 +22582,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
21930
22582
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
21931
22583
|
return json({ capability: await authority.capability() });
|
|
21932
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
|
+
}
|
|
21933
22599
|
if (method !== "POST")
|
|
21934
22600
|
return json({ error: "method not allowed" }, 405);
|
|
21935
22601
|
const body = await readJson(req);
|
|
@@ -21952,6 +22618,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
21952
22618
|
record: await authority.readExact(body)
|
|
21953
22619
|
});
|
|
21954
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
|
+
}
|
|
21955
22627
|
if (action === "compensate") {
|
|
21956
22628
|
return json({
|
|
21957
22629
|
receipt: await authority.compensate(body)
|
|
@@ -21984,6 +22656,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
21984
22656
|
var JSON_HEADERS;
|
|
21985
22657
|
var init_http = __esm(() => {
|
|
21986
22658
|
init_types3();
|
|
22659
|
+
init_adoption_validation();
|
|
21987
22660
|
JSON_HEADERS = { "Content-Type": "application/json" };
|
|
21988
22661
|
});
|
|
21989
22662
|
|
|
@@ -21991,6 +22664,7 @@ var init_http = __esm(() => {
|
|
|
21991
22664
|
var init_project_registration = __esm(() => {
|
|
21992
22665
|
init_authority();
|
|
21993
22666
|
init_http();
|
|
22667
|
+
init_adoption_validation();
|
|
21994
22668
|
init_postgres2();
|
|
21995
22669
|
init_sqlite();
|
|
21996
22670
|
init_types3();
|
|
@@ -25963,7 +26637,7 @@ var init_zod = __esm(() => {
|
|
|
25963
26637
|
});
|
|
25964
26638
|
|
|
25965
26639
|
// src/task-manifest/types.ts
|
|
25966
|
-
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;
|
|
25967
26641
|
var init_types5 = __esm(() => {
|
|
25968
26642
|
TodosTaskManifestError = class TodosTaskManifestError extends Error {
|
|
25969
26643
|
code;
|
|
@@ -26050,7 +26724,7 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
26050
26724
|
}
|
|
26051
26725
|
return parsed.data;
|
|
26052
26726
|
}
|
|
26053
|
-
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) => {
|
|
26054
26728
|
if (Object.keys(value).length > limit) {
|
|
26055
26729
|
context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
|
|
26056
26730
|
}
|
|
@@ -26072,6 +26746,8 @@ var init_schema2 = __esm(() => {
|
|
|
26072
26746
|
};
|
|
26073
26747
|
key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
26074
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}$/);
|
|
26075
26751
|
uuid2 = exports_external.string().uuid();
|
|
26076
26752
|
scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
26077
26753
|
comment = exports_external.object({
|
|
@@ -26109,7 +26785,9 @@ var init_schema2 = __esm(() => {
|
|
|
26109
26785
|
schema = exports_external.object({
|
|
26110
26786
|
version: exports_external.literal(1),
|
|
26111
26787
|
operation_id: identifier,
|
|
26112
|
-
|
|
26788
|
+
step_id: identifier,
|
|
26789
|
+
idempotency_key: idempotencyKey,
|
|
26790
|
+
precondition_digest: digest,
|
|
26113
26791
|
project_id: uuid2,
|
|
26114
26792
|
task_list_id: uuid2.optional(),
|
|
26115
26793
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -26125,7 +26803,10 @@ var init_schema2 = __esm(() => {
|
|
|
26125
26803
|
}).strict();
|
|
26126
26804
|
compensationSchema = exports_external.object({
|
|
26127
26805
|
receipt_id: uuid2,
|
|
26128
|
-
|
|
26806
|
+
operation_id: identifier,
|
|
26807
|
+
step_id: identifier,
|
|
26808
|
+
idempotency_key: idempotencyKey,
|
|
26809
|
+
precondition_digest: digest,
|
|
26129
26810
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
26130
26811
|
}).strict();
|
|
26131
26812
|
bindingLookupSchema = exports_external.object({
|
|
@@ -26143,6 +26824,7 @@ function taskManifestPlanSlug(manifest, planId) {
|
|
|
26143
26824
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
26144
26825
|
return `${base}-${planId}`;
|
|
26145
26826
|
}
|
|
26827
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
26146
26828
|
var init_plan_slug = () => {};
|
|
26147
26829
|
|
|
26148
26830
|
// src/task-manifest/backend.ts
|
|
@@ -26156,11 +26838,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
26156
26838
|
const row = rows[0];
|
|
26157
26839
|
const bindingVersion = Number(row.binding_version);
|
|
26158
26840
|
const state = row.state;
|
|
26159
|
-
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") {
|
|
26160
26842
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
26161
26843
|
}
|
|
26162
26844
|
return {
|
|
26163
26845
|
plan_id: planId,
|
|
26846
|
+
operation_id: row.binding_operation_id,
|
|
26847
|
+
step_id: row.binding_step_id,
|
|
26164
26848
|
apply_receipt_id: row.apply_receipt_id,
|
|
26165
26849
|
binding_version: bindingVersion,
|
|
26166
26850
|
state
|
|
@@ -26213,9 +26897,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26213
26897
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
26214
26898
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
26215
26899
|
operation_id text NOT NULL,
|
|
26900
|
+
step_id text NOT NULL,
|
|
26216
26901
|
idempotency_key text NOT NULL,
|
|
26217
26902
|
request_digest text NOT NULL,
|
|
26903
|
+
precondition_digest text NOT NULL,
|
|
26218
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,
|
|
26219
26909
|
binding_version integer NOT NULL,
|
|
26220
26910
|
apply_receipt_id text,
|
|
26221
26911
|
manifest_json jsonb,
|
|
@@ -26227,12 +26917,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26227
26917
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
26228
26918
|
`ALTER TABLE todos_task_manifest_receipts
|
|
26229
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`,
|
|
26230
26932
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
26231
26933
|
operation_id text PRIMARY KEY,
|
|
26232
26934
|
tenant_id text NOT NULL,
|
|
26935
|
+
step_id text NOT NULL,
|
|
26233
26936
|
idempotency_key text NOT NULL UNIQUE,
|
|
26234
26937
|
request_digest text NOT NULL,
|
|
26938
|
+
precondition_digest text NOT NULL,
|
|
26235
26939
|
result_digest text NOT NULL,
|
|
26940
|
+
slug_provenance text,
|
|
26941
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
26236
26942
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
26237
26943
|
manifest_json jsonb NOT NULL,
|
|
26238
26944
|
result_json jsonb NOT NULL,
|
|
@@ -26246,6 +26952,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26246
26952
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
26247
26953
|
`ALTER TABLE todos_task_manifest_bindings
|
|
26248
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'`,
|
|
26249
26963
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
26250
26964
|
id text PRIMARY KEY,
|
|
26251
26965
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -26257,10 +26971,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26257
26971
|
created_at timestamptz NOT NULL,
|
|
26258
26972
|
delivered_at timestamptz
|
|
26259
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
|
+
)`,
|
|
26260
26995
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
26261
26996
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
26262
26997
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
26263
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)`,
|
|
26264
27005
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
26265
27006
|
ON todos_task_manifest_bindings(
|
|
26266
27007
|
tenant_id,
|
|
@@ -26273,6 +27014,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
26273
27014
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
26274
27015
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
26275
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
|
|
26276
27021
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
26277
27022
|
];
|
|
26278
27023
|
}
|
|
@@ -26297,6 +27042,39 @@ function safeIdentifier2(value, field) {
|
|
|
26297
27042
|
function parseJson(value) {
|
|
26298
27043
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
26299
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
|
+
}
|
|
26300
27078
|
function timestamp2(value) {
|
|
26301
27079
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
26302
27080
|
}
|
|
@@ -26304,6 +27082,41 @@ function fault(faults, point) {
|
|
|
26304
27082
|
if (faults.points.has(point))
|
|
26305
27083
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
26306
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
|
+
}
|
|
26307
27120
|
function receiptFromRow3(row) {
|
|
26308
27121
|
return {
|
|
26309
27122
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -26312,9 +27125,14 @@ function receiptFromRow3(row) {
|
|
|
26312
27125
|
schema_version: 1,
|
|
26313
27126
|
kind: row["kind"],
|
|
26314
27127
|
operation_id: String(row["operation_id"]),
|
|
27128
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
26315
27129
|
idempotency_key: String(row["idempotency_key"]),
|
|
26316
27130
|
request_digest: String(row["request_digest"]),
|
|
27131
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
26317
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"]),
|
|
26318
27136
|
binding_version: Number(row["binding_version"]),
|
|
26319
27137
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
26320
27138
|
created_at: timestamp2(row["created_at"])
|
|
@@ -26432,46 +27250,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26432
27250
|
now3
|
|
26433
27251
|
]);
|
|
26434
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
|
+
}
|
|
26435
27284
|
async apply(input, faults) {
|
|
26436
27285
|
await this.ensureSchema();
|
|
26437
27286
|
return this.client.transaction(async (tx) => {
|
|
26438
27287
|
const { manifest } = input;
|
|
26439
27288
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
26440
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
|
+
}
|
|
26441
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]);
|
|
26442
27300
|
if (existing.rows[0]) {
|
|
26443
27301
|
const binding = existing.rows[0];
|
|
26444
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
26445
|
-
|
|
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");
|
|
26446
27304
|
}
|
|
26447
27305
|
if (binding["state"] !== "applied") {
|
|
26448
|
-
|
|
27306
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
26449
27307
|
}
|
|
26450
|
-
return
|
|
27308
|
+
return parseApplyResult(binding["result_json"], true);
|
|
26451
27309
|
}
|
|
26452
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]);
|
|
26453
27311
|
if (reused.rows[0])
|
|
26454
|
-
|
|
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
|
+
}
|
|
26455
27316
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
26456
|
-
|
|
27317
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
26457
27318
|
}
|
|
26458
27319
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
26459
27320
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
26460
27321
|
if (!project.rows[0])
|
|
26461
|
-
|
|
27322
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
26462
27323
|
if (manifest.task_list_id) {
|
|
26463
27324
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
26464
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]);
|
|
26465
27326
|
const payload = taskList.rows[0] ? parseJson(taskList.rows[0]["payload"]) : null;
|
|
26466
27327
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
26467
|
-
|
|
27328
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
26468
27329
|
}
|
|
26469
27330
|
}
|
|
26470
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];
|
|
26471
27332
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
26472
27333
|
WHERE service = $1 AND object_id IN (${placeholders(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
26473
27334
|
if (conflict.rows[0])
|
|
26474
|
-
|
|
27335
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
26475
27336
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
26476
27337
|
fault(faults, "after_plan_write");
|
|
26477
27338
|
for (const task2 of manifest.tasks) {
|
|
@@ -26541,9 +27402,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26541
27402
|
schema_version: 1,
|
|
26542
27403
|
kind: "apply",
|
|
26543
27404
|
operation_id: manifest.operation_id,
|
|
27405
|
+
step_id: manifest.step_id,
|
|
26544
27406
|
idempotency_key: manifest.idempotency_key,
|
|
26545
27407
|
request_digest: input.request_digest,
|
|
27408
|
+
precondition_digest: manifest.precondition_digest,
|
|
26546
27409
|
result_digest: input.result_digest,
|
|
27410
|
+
outcome: "accepted",
|
|
27411
|
+
reason: null,
|
|
27412
|
+
duplicate_of_receipt_id: null,
|
|
26547
27413
|
binding_version: 1,
|
|
26548
27414
|
apply_receipt_id: null,
|
|
26549
27415
|
created_at: input.now
|
|
@@ -26560,14 +27426,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26560
27426
|
const resultJson = canonicalJson(result);
|
|
26561
27427
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
26562
27428
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
26563
|
-
|
|
26564
|
-
|
|
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)`, [
|
|
26565
27432
|
input.receipt_id,
|
|
26566
27433
|
this.tenantId,
|
|
26567
27434
|
manifest.operation_id,
|
|
26568
27435
|
manifest.idempotency_key,
|
|
27436
|
+
manifest.step_id,
|
|
26569
27437
|
input.request_digest,
|
|
27438
|
+
manifest.precondition_digest,
|
|
26570
27439
|
input.result_digest,
|
|
27440
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
26571
27441
|
manifestJson,
|
|
26572
27442
|
resultJson,
|
|
26573
27443
|
input.now
|
|
@@ -26586,14 +27456,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26586
27456
|
}
|
|
26587
27457
|
fault(faults, "after_outbox_write");
|
|
26588
27458
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
26589
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
26590
|
-
|
|
26591
|
-
|
|
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)`, [
|
|
26592
27463
|
manifest.operation_id,
|
|
26593
27464
|
this.tenantId,
|
|
27465
|
+
manifest.step_id,
|
|
26594
27466
|
manifest.idempotency_key,
|
|
26595
27467
|
input.request_digest,
|
|
27468
|
+
manifest.precondition_digest,
|
|
26596
27469
|
input.result_digest,
|
|
27470
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
26597
27471
|
input.receipt_id,
|
|
26598
27472
|
manifestJson,
|
|
26599
27473
|
resultJson,
|
|
@@ -26606,9 +27480,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26606
27480
|
async readExact(receiptId2) {
|
|
26607
27481
|
await this.ensureSchema();
|
|
26608
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]);
|
|
26609
|
-
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])
|
|
26610
27487
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
26611
|
-
return
|
|
27488
|
+
return parseApplyResult(terminal.rows[0]["result_json"], false);
|
|
26612
27489
|
}
|
|
26613
27490
|
async lookupBindingByPlanId(planId) {
|
|
26614
27491
|
await this.ensureSchema();
|
|
@@ -26619,6 +27496,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26619
27496
|
b.version AS binding_version,
|
|
26620
27497
|
b.tenant_id AS binding_tenant_id,
|
|
26621
27498
|
b.operation_id AS binding_operation_id,
|
|
27499
|
+
b.step_id AS binding_step_id,
|
|
26622
27500
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
26623
27501
|
r.tenant_id AS receipt_tenant_id,
|
|
26624
27502
|
r.authority AS receipt_authority,
|
|
@@ -26626,6 +27504,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26626
27504
|
r.schema_version AS receipt_schema_version,
|
|
26627
27505
|
r.kind AS receipt_kind,
|
|
26628
27506
|
r.operation_id AS receipt_operation_id,
|
|
27507
|
+
r.step_id AS receipt_step_id,
|
|
26629
27508
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
26630
27509
|
FROM todos_task_manifest_bindings b
|
|
26631
27510
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -26712,6 +27591,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26712
27591
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
26713
27592
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
26714
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
|
+
}
|
|
26715
27598
|
if (binding["state"] !== "applied")
|
|
26716
27599
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
26717
27600
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -26726,12 +27609,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26726
27609
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
26727
27610
|
if (delivered.rows[0])
|
|
26728
27611
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
26729
|
-
const applyResult =
|
|
27612
|
+
const applyResult = parseApplyResult(applyRow["result_json"], false);
|
|
26730
27613
|
const manifest = parseJson(applyRow["manifest_json"]);
|
|
27614
|
+
const manifestRecord = manifest;
|
|
27615
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
26731
27616
|
const expectedEffects = [
|
|
26732
27617
|
{
|
|
26733
27618
|
topic: "todos.task-manifest.applied",
|
|
26734
|
-
payload: {
|
|
27619
|
+
payload: {
|
|
27620
|
+
operation_id: manifest.operation_id,
|
|
27621
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
27622
|
+
project_id: manifest.project_id
|
|
27623
|
+
}
|
|
26735
27624
|
},
|
|
26736
27625
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
26737
27626
|
];
|
|
@@ -26779,9 +27668,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26779
27668
|
}
|
|
26780
27669
|
const appliedAt = receiptFromRow3(applyRow).created_at;
|
|
26781
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"]);
|
|
26782
27677
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
26783
27678
|
type: "plans",
|
|
26784
|
-
payload: canonicalJson(
|
|
27679
|
+
payload: canonicalJson(planExpected)
|
|
26785
27680
|
});
|
|
26786
27681
|
for (const task2 of manifest.tasks)
|
|
26787
27682
|
expectedPayloads.set(applyResult.graph.task_ids[task2.key], {
|
|
@@ -26881,14 +27776,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
26881
27776
|
const readback = await this.readback(tx, applyResult.graph);
|
|
26882
27777
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
26883
27778
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
26884
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
26885
|
-
request_digest,
|
|
26886
|
-
|
|
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)`, [
|
|
26887
27783
|
compensationReceiptId,
|
|
26888
27784
|
this.tenantId,
|
|
26889
27785
|
receipt.operation_id,
|
|
27786
|
+
receipt.step_id,
|
|
26890
27787
|
input.idempotency_key,
|
|
26891
27788
|
requestDigest,
|
|
27789
|
+
input.precondition_digest,
|
|
26892
27790
|
receipt.result_digest,
|
|
26893
27791
|
receipt.binding_version,
|
|
26894
27792
|
input.receipt_id,
|
|
@@ -26945,36 +27843,83 @@ function resolveTenantId(value) {
|
|
|
26945
27843
|
}
|
|
26946
27844
|
return tenantId;
|
|
26947
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
|
+
}
|
|
26948
27880
|
function normalize(input, now3) {
|
|
26949
27881
|
const parsed = parseTodosTaskManifest(input);
|
|
26950
27882
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
26951
27883
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
26952
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 });
|
|
26953
27885
|
}
|
|
27886
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
27887
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
26954
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
|
+
});
|
|
26955
27901
|
const task_ids = Object.fromEntries(manifest.tasks.map((task2) => [
|
|
26956
27902
|
task2.key,
|
|
26957
|
-
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)
|
|
26958
27904
|
]));
|
|
26959
27905
|
const graph = {
|
|
26960
|
-
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),
|
|
26961
27907
|
task_ids,
|
|
26962
|
-
comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task2.key, String(index)))),
|
|
26963
|
-
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)))),
|
|
26964
27910
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
26965
27911
|
};
|
|
26966
|
-
const request_digest = canonicalDigest(parsed);
|
|
26967
27912
|
const effectInputs = [
|
|
26968
27913
|
{
|
|
26969
27914
|
topic: "todos.task-manifest.applied",
|
|
26970
|
-
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 }
|
|
26971
27916
|
},
|
|
26972
27917
|
...manifest.effects ?? []
|
|
26973
27918
|
];
|
|
26974
27919
|
const outbox = effectInputs.map((effect2, index) => {
|
|
26975
27920
|
const payload = { ...effect2.payload };
|
|
26976
27921
|
return {
|
|
26977
|
-
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)),
|
|
26978
27923
|
topic: effect2.topic,
|
|
26979
27924
|
payload,
|
|
26980
27925
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -26984,11 +27929,14 @@ function normalize(input, now3) {
|
|
|
26984
27929
|
return {
|
|
26985
27930
|
manifest,
|
|
26986
27931
|
request_digest,
|
|
27932
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
26987
27933
|
result_digest,
|
|
26988
|
-
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),
|
|
26989
27936
|
graph,
|
|
26990
27937
|
outbox,
|
|
26991
|
-
now: now3
|
|
27938
|
+
now: now3,
|
|
27939
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
26992
27940
|
};
|
|
26993
27941
|
}
|
|
26994
27942
|
function sanitizeManifest(manifest) {
|
|
@@ -27046,6 +27994,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
27046
27994
|
tenant_id: this.tenantId,
|
|
27047
27995
|
backend: this.backend.kind,
|
|
27048
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,
|
|
27049
28001
|
immutable_receipts: true,
|
|
27050
28002
|
transactional_outbox: true,
|
|
27051
28003
|
idempotent_outbox_delivery: true,
|
|
@@ -27075,7 +28027,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
27075
28027
|
async apply(input) {
|
|
27076
28028
|
const normalized = normalize(input, this.now());
|
|
27077
28029
|
const faults = await this.prepareFaults();
|
|
27078
|
-
|
|
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;
|
|
27079
28035
|
}
|
|
27080
28036
|
readExact(receiptId2) {
|
|
27081
28037
|
if (!receiptId2 || receiptId2.length > 200) {
|
|
@@ -27108,18 +28064,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
27108
28064
|
async compensate(input) {
|
|
27109
28065
|
const request = parseTodosTaskManifestCompensation(input);
|
|
27110
28066
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
27111
|
-
|
|
27112
|
-
|
|
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);
|
|
27113
28094
|
const receipt = {
|
|
27114
28095
|
receipt_id: compensationReceiptId,
|
|
27115
28096
|
authority: "todos",
|
|
27116
28097
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
27117
28098
|
schema_version: 1,
|
|
27118
28099
|
kind: "compensate",
|
|
27119
|
-
operation_id:
|
|
28100
|
+
operation_id: request.operation_id,
|
|
28101
|
+
step_id: request.step_id,
|
|
27120
28102
|
idempotency_key: request.idempotency_key,
|
|
27121
28103
|
request_digest: requestDigest,
|
|
28104
|
+
precondition_digest: request.precondition_digest,
|
|
27122
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,
|
|
27123
28109
|
binding_version: request.if_binding_version + 1,
|
|
27124
28110
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
27125
28111
|
created_at: this.now()
|
|
@@ -29624,9 +30610,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29624
30610
|
TaskManifestBounds: taskManifestBoundsSchema,
|
|
29625
30611
|
TaskManifestCapability: taskManifestCapabilitySchema,
|
|
29626
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,
|
|
29627
30621
|
TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
|
|
29628
30622
|
TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
|
|
29629
30623
|
TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
|
|
30624
|
+
ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
|
|
30625
|
+
ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
|
|
30626
|
+
ProjectRegistrationRequest: projectRegistrationRequestSchema,
|
|
30627
|
+
ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
|
|
30628
|
+
PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
|
|
30629
|
+
ProjectResource: projectResourceSchema,
|
|
30630
|
+
ProjectResourcePage: projectResourcePageSchema,
|
|
29630
30631
|
TaskList: taskListSchema,
|
|
29631
30632
|
ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
|
|
29632
30633
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
@@ -29669,6 +30670,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29669
30670
|
priority: { type: "string", enum: [...TASK_PRIORITIES] },
|
|
29670
30671
|
assigned_to: { type: "string" },
|
|
29671
30672
|
project_id: { type: "string", nullable: true },
|
|
30673
|
+
parent_id: { type: "string", nullable: true },
|
|
29672
30674
|
plan_id: { type: "string", nullable: true },
|
|
29673
30675
|
task_list_id: { type: "string", nullable: true },
|
|
29674
30676
|
version: { type: "number" }
|
|
@@ -30623,6 +31625,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30623
31625
|
},
|
|
30624
31626
|
security: [{ apiKey: [] }],
|
|
30625
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
|
+
},
|
|
30626
31963
|
"/v1/task-manifest/capability": {
|
|
30627
31964
|
get: {
|
|
30628
31965
|
operationId: "getTaskManifestCapability",
|
|
@@ -30640,6 +31977,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30640
31977
|
}
|
|
30641
31978
|
}
|
|
30642
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
|
+
},
|
|
30643
32056
|
"/v1/task-manifest/bindings/lookup": {
|
|
30644
32057
|
post: {
|
|
30645
32058
|
operationId: "lookupTaskManifestBinding",
|
|
@@ -31500,7 +32913,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
31500
32913
|
}
|
|
31501
32914
|
});
|
|
31502
32915
|
}
|
|
31503
|
-
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;
|
|
31504
32917
|
var init_openapi = __esm(() => {
|
|
31505
32918
|
init_package_version();
|
|
31506
32919
|
init_types();
|
|
@@ -31562,6 +32975,10 @@ var init_openapi = __esm(() => {
|
|
|
31562
32975
|
"tenant_id",
|
|
31563
32976
|
"backend",
|
|
31564
32977
|
"deterministic_ids",
|
|
32978
|
+
"operation_step_identity",
|
|
32979
|
+
"deterministic_idempotency_keys",
|
|
32980
|
+
"terminal_nonacceptance_receipts",
|
|
32981
|
+
"plan_slug_provenance",
|
|
31565
32982
|
"immutable_receipts",
|
|
31566
32983
|
"transactional_outbox",
|
|
31567
32984
|
"idempotent_outbox_delivery",
|
|
@@ -31577,6 +32994,10 @@ var init_openapi = __esm(() => {
|
|
|
31577
32994
|
tenant_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
31578
32995
|
backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
|
|
31579
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"] },
|
|
31580
33001
|
immutable_receipts: { type: "boolean", enum: [true] },
|
|
31581
33002
|
transactional_outbox: { type: "boolean", enum: [true] },
|
|
31582
33003
|
idempotent_outbox_delivery: { type: "boolean", enum: [true] },
|
|
@@ -31630,6 +33051,8 @@ var init_openapi = __esm(() => {
|
|
|
31630
33051
|
"schema_version",
|
|
31631
33052
|
"tenant_id",
|
|
31632
33053
|
"plan_id",
|
|
33054
|
+
"operation_id",
|
|
33055
|
+
"step_id",
|
|
31633
33056
|
"apply_receipt_id",
|
|
31634
33057
|
"binding_version",
|
|
31635
33058
|
"state"
|
|
@@ -31640,11 +33063,169 @@ var init_openapi = __esm(() => {
|
|
|
31640
33063
|
schema_version: { type: "integer", enum: [1] },
|
|
31641
33064
|
tenant_id: { type: "string" },
|
|
31642
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 },
|
|
31643
33068
|
apply_receipt_id: { type: "string", format: "uuid" },
|
|
31644
33069
|
binding_version: { type: "integer", minimum: 1 },
|
|
31645
33070
|
state: { type: "string", enum: ["applied", "compensated"] }
|
|
31646
33071
|
}
|
|
31647
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
|
+
};
|
|
31648
33229
|
taskManifestBindingLookupResponseSchema = {
|
|
31649
33230
|
type: "object",
|
|
31650
33231
|
additionalProperties: false,
|
|
@@ -31994,6 +33575,292 @@ var init_openapi = __esm(() => {
|
|
|
31994
33575
|
metadata: { type: "object", additionalProperties: true }
|
|
31995
33576
|
}
|
|
31996
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
|
+
};
|
|
31997
33864
|
});
|
|
31998
33865
|
|
|
31999
33866
|
// src/server/pr-groups.ts
|
|
@@ -32140,11 +34007,11 @@ function canonicalJson2(value) {
|
|
|
32140
34007
|
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
32141
34008
|
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
|
|
32142
34009
|
}
|
|
32143
|
-
function
|
|
34010
|
+
function digest2(value) {
|
|
32144
34011
|
return createHash9("sha256").update(canonicalJson2(value)).digest("hex");
|
|
32145
34012
|
}
|
|
32146
34013
|
function deriveIdempotencyKey(projectId, slug) {
|
|
32147
|
-
return `ptlk_${
|
|
34014
|
+
return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
32148
34015
|
}
|
|
32149
34016
|
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
32150
34017
|
const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
@@ -32153,13 +34020,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
|
|
|
32153
34020
|
}
|
|
32154
34021
|
return key2;
|
|
32155
34022
|
}
|
|
32156
|
-
function receiptId2(projectId, slug,
|
|
32157
|
-
return `ptlr_${
|
|
34023
|
+
function receiptId2(projectId, slug, idempotencyKey2) {
|
|
34024
|
+
return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
|
|
32158
34025
|
}
|
|
32159
34026
|
function semanticListDigest(list) {
|
|
32160
34027
|
const metadata = { ...list.metadata ?? {} };
|
|
32161
34028
|
delete metadata[RECEIPT_METADATA_KEY];
|
|
32162
|
-
return
|
|
34029
|
+
return digest2({
|
|
32163
34030
|
project_id: list.project_id,
|
|
32164
34031
|
slug: list.slug,
|
|
32165
34032
|
name: list.name,
|
|
@@ -32176,10 +34043,10 @@ function storedMarker(list) {
|
|
|
32176
34043
|
return null;
|
|
32177
34044
|
return marker;
|
|
32178
34045
|
}
|
|
32179
|
-
function receiptFor(store, project, list,
|
|
34046
|
+
function receiptFor(store, project, list, idempotencyKey2) {
|
|
32180
34047
|
const marker = storedMarker(list);
|
|
32181
34048
|
const owned = marker?.project_id === project.id && marker.slug === list.slug;
|
|
32182
|
-
if (owned && marker.idempotency_key !==
|
|
34049
|
+
if (owned && marker.idempotency_key !== idempotencyKey2) {
|
|
32183
34050
|
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
|
|
32184
34051
|
project_id: project.id,
|
|
32185
34052
|
task_list_id: list.id,
|
|
@@ -32188,8 +34055,8 @@ function receiptFor(store, project, list, idempotencyKey) {
|
|
|
32188
34055
|
}
|
|
32189
34056
|
return {
|
|
32190
34057
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
32191
|
-
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${
|
|
32192
|
-
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,
|
|
32193
34060
|
project_id: project.id,
|
|
32194
34061
|
task_list_id: list.id,
|
|
32195
34062
|
slug: list.slug,
|
|
@@ -32241,20 +34108,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
32241
34108
|
});
|
|
32242
34109
|
}
|
|
32243
34110
|
const slug = project.task_list_id;
|
|
32244
|
-
const
|
|
34111
|
+
const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
|
|
32245
34112
|
if (state.scoped) {
|
|
32246
34113
|
return {
|
|
32247
34114
|
mode: "apply",
|
|
32248
34115
|
action: "already_present",
|
|
32249
34116
|
project,
|
|
32250
34117
|
task_list: state.scoped,
|
|
32251
|
-
receipt: receiptFor(store, project, state.scoped,
|
|
34118
|
+
receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
|
|
32252
34119
|
};
|
|
32253
34120
|
}
|
|
32254
34121
|
const marker = {
|
|
32255
34122
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
32256
|
-
receipt_id: receiptId2(project.id, slug,
|
|
32257
|
-
idempotency_key:
|
|
34123
|
+
receipt_id: receiptId2(project.id, slug, idempotencyKey2),
|
|
34124
|
+
idempotency_key: idempotencyKey2,
|
|
32258
34125
|
project_id: project.id,
|
|
32259
34126
|
slug,
|
|
32260
34127
|
result_digest: semanticListDigest({
|
|
@@ -32292,7 +34159,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
32292
34159
|
action: "already_present",
|
|
32293
34160
|
project: raced.project,
|
|
32294
34161
|
task_list: raced.scoped,
|
|
32295
|
-
receipt: receiptFor(store, raced.project, raced.scoped,
|
|
34162
|
+
receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
|
|
32296
34163
|
};
|
|
32297
34164
|
}
|
|
32298
34165
|
const projectReadback = await store.projects.get(project.id);
|
|
@@ -32322,7 +34189,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
32322
34189
|
action: "created",
|
|
32323
34190
|
project: projectReadback,
|
|
32324
34191
|
task_list: readback,
|
|
32325
|
-
receipt: receiptFor(store, projectReadback, readback,
|
|
34192
|
+
receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
|
|
32326
34193
|
};
|
|
32327
34194
|
}
|
|
32328
34195
|
async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
@@ -32367,7 +34234,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
|
32367
34234
|
project_id: project.id,
|
|
32368
34235
|
task_list_id: list.id,
|
|
32369
34236
|
accepted_receipt_id: options.receipt_id,
|
|
32370
|
-
rollback_receipt_id: `ptlr_inverse_${
|
|
34237
|
+
rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
32371
34238
|
removed_at: new Date().toISOString()
|
|
32372
34239
|
};
|
|
32373
34240
|
}
|
|
@@ -32566,6 +34433,9 @@ function validateTaskPatchVocabulary(value) {
|
|
|
32566
34433
|
if (!parsed.ok)
|
|
32567
34434
|
return { ok: false, message: parsed.message };
|
|
32568
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
|
+
}
|
|
32569
34439
|
return { ok: true, patch: body2 };
|
|
32570
34440
|
}
|
|
32571
34441
|
function validateProjectPatch(value) {
|
|
@@ -33050,8 +34920,8 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
33050
34920
|
}
|
|
33051
34921
|
const created = await store.tasks.create(body2, storageContext);
|
|
33052
34922
|
const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
|
|
33053
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
|
|
33054
|
-
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" });
|
|
33055
34925
|
}
|
|
33056
34926
|
return json5({ task: persisted }, 201);
|
|
33057
34927
|
}
|
|
@@ -33925,6 +35795,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
33925
35795
|
if (e instanceof TaskNotFoundError) {
|
|
33926
35796
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
33927
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
|
+
}
|
|
33928
35807
|
if (e instanceof StaleLockHandoffError) {
|
|
33929
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;
|
|
33930
35809
|
return error(status2, e.message, {
|
|
@@ -52540,16 +54419,16 @@ function createHasnaStorageClient(name, transport) {
|
|
|
52540
54419
|
}
|
|
52541
54420
|
},
|
|
52542
54421
|
async create(resource, body2, options = {}) {
|
|
52543
|
-
const { idempotencyKey, ...rest } = options;
|
|
54422
|
+
const { idempotencyKey: idempotencyKey2, ...rest } = options;
|
|
52544
54423
|
return transport.post(resourcePath(resource), body2, {
|
|
52545
54424
|
...rest,
|
|
52546
|
-
idempotencyKey:
|
|
54425
|
+
idempotencyKey: idempotencyKey2 ?? newIdempotencyKey()
|
|
52547
54426
|
});
|
|
52548
54427
|
},
|
|
52549
54428
|
async update(resource, id, patch, options = {}) {
|
|
52550
|
-
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
54429
|
+
const { method = "PATCH", idempotencyKey: idempotencyKey2, ...rest } = options;
|
|
52551
54430
|
const call = method === "PUT" ? transport.put : transport.patch;
|
|
52552
|
-
return call(entityPath(resource, id), patch, { ...rest, ...
|
|
54431
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey2 ? { idempotencyKey: idempotencyKey2 } : {} });
|
|
52553
54432
|
},
|
|
52554
54433
|
async delete(resource, id, options = {}) {
|
|
52555
54434
|
try {
|
|
@@ -64733,6 +66612,11 @@ var init_http_client = __esm(() => {
|
|
|
64733
66612
|
]);
|
|
64734
66613
|
});
|
|
64735
66614
|
|
|
66615
|
+
// src/project-registration/page-validation.ts
|
|
66616
|
+
var init_page_validation = __esm(() => {
|
|
66617
|
+
init_types3();
|
|
66618
|
+
});
|
|
66619
|
+
|
|
64736
66620
|
// src/cli/cloud-router.ts
|
|
64737
66621
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
64738
66622
|
import { resolve as resolvePath } from "path";
|
|
@@ -65131,12 +67015,39 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
65131
67015
|
union3.sort(compareCloudTaskOrder);
|
|
65132
67016
|
return union3.slice(start, windowEnd);
|
|
65133
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
|
+
}
|
|
65134
67044
|
async function cloudGetTask(client, id) {
|
|
65135
67045
|
const raw = await client.get("tasks", id);
|
|
65136
67046
|
return raw == null ? null : unwrapTask(raw);
|
|
65137
67047
|
}
|
|
65138
67048
|
async function cloudCreateTask(client, input, verification2 = {}) {
|
|
65139
67049
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
67050
|
+
const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
|
|
65140
67051
|
const expectedCreatedBy = typeof verification2.expectedCreatedBy === "string" && verification2.expectedCreatedBy.trim() ? verification2.expectedCreatedBy : null;
|
|
65141
67052
|
if (expectedCreatedBy !== null) {
|
|
65142
67053
|
await requireTaskCreatorCapability(client);
|
|
@@ -65149,9 +67060,9 @@ async function cloudCreateTask(client, input, verification2 = {}) {
|
|
|
65149
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");
|
|
65150
67061
|
}
|
|
65151
67062
|
const persisted = await cloudGetTask(client, created.id);
|
|
65152
|
-
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) {
|
|
65153
67064
|
const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
|
|
65154
|
-
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`);
|
|
65155
67066
|
}
|
|
65156
67067
|
return persisted;
|
|
65157
67068
|
}
|
|
@@ -65420,6 +67331,8 @@ var init_cloud_router = __esm(() => {
|
|
|
65420
67331
|
init_redaction();
|
|
65421
67332
|
init_plan_project_link_contract();
|
|
65422
67333
|
init_http_client();
|
|
67334
|
+
init_adoption_validation();
|
|
67335
|
+
init_page_validation();
|
|
65423
67336
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
65424
67337
|
TRANSPORT_TOKENS = {
|
|
65425
67338
|
sqlite: "sqlite",
|
|
@@ -65456,6 +67369,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
65456
67369
|
compact["version"] = task2.version;
|
|
65457
67370
|
compact["created_at"] = task2.created_at;
|
|
65458
67371
|
compact["task_list_id"] = task2.task_list_id;
|
|
67372
|
+
compact["parent_id"] = task2.parent_id;
|
|
65459
67373
|
return compactJson(compact);
|
|
65460
67374
|
}
|
|
65461
67375
|
function versionFor(taskId, version2) {
|
|
@@ -65725,6 +67639,7 @@ ${task2.description}` : null
|
|
|
65725
67639
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
65726
67640
|
assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
|
|
65727
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"),
|
|
65728
67643
|
task_list_id: exports_external.string().nullable().optional(),
|
|
65729
67644
|
depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
|
|
65730
67645
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
@@ -65756,6 +67671,9 @@ ${task2.description}` : null
|
|
|
65756
67671
|
if (typeof patch.project_id === "string" && patch.project_id) {
|
|
65757
67672
|
patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
|
|
65758
67673
|
}
|
|
67674
|
+
if (typeof patch.parent_id === "string" && patch.parent_id) {
|
|
67675
|
+
patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
|
|
67676
|
+
}
|
|
65759
67677
|
if (typeof patch.task_list_id === "string" && patch.task_list_id) {
|
|
65760
67678
|
let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
|
|
65761
67679
|
if (!scope) {
|
|
@@ -65764,9 +67682,22 @@ ${task2.description}` : null
|
|
|
65764
67682
|
}
|
|
65765
67683
|
patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
|
|
65766
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
|
+
}
|
|
65767
67691
|
if (version3 !== undefined)
|
|
65768
67692
|
patch.version = version3;
|
|
65769
|
-
|
|
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
|
+
}
|
|
65770
67701
|
return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
|
|
65771
67702
|
}
|
|
65772
67703
|
const resolvedId = resolveId(params.task_id);
|
|
@@ -65778,6 +67709,8 @@ ${task2.description}` : null
|
|
|
65778
67709
|
resolved.assigned_to = resolveAssignee(resolved.assigned_to);
|
|
65779
67710
|
if (resolved.project_id && typeof resolved.project_id === "string")
|
|
65780
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);
|
|
65781
67714
|
if (resolved.task_list_id && typeof resolved.task_list_id === "string")
|
|
65782
67715
|
resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
|
|
65783
67716
|
if (resolved.depends_on && Array.isArray(resolved.depends_on))
|
|
@@ -99460,7 +101393,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
99460
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",
|
|
99461
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",
|
|
99462
101395
|
get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
|
|
99463
|
-
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",
|
|
99464
101397
|
delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
|
|
99465
101398
|
start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
|
|
99466
101399
|
complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
|
|
@@ -111275,8 +113208,8 @@ function defaultSnapshotDir() {
|
|
|
111275
113208
|
return join15(dirname8(resolve16(dbPath)), "environment-snapshots");
|
|
111276
113209
|
}
|
|
111277
113210
|
function snapshotWithId(snapshot) {
|
|
111278
|
-
const
|
|
111279
|
-
return { id: `env_${
|
|
113211
|
+
const digest3 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
|
|
113212
|
+
return { id: `env_${digest3}`, ...snapshot };
|
|
111280
113213
|
}
|
|
111281
113214
|
function captureEnvironmentSnapshot(input = {}) {
|
|
111282
113215
|
const root = resolve16(input.root || process.cwd());
|