@hasna/todos 0.15.29 → 0.15.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +13 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +2 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2861 -342
- package/dist/contracts.js +52 -2
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1466 -147
- package/dist/lib/task-parent-integrity.d.ts +14 -0
- package/dist/lib/task-parent-integrity.d.ts.map +1 -0
- package/dist/mcp/index.js +2050 -138
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/project-registration/adoption-validation.d.ts +3 -0
- package/dist/project-registration/adoption-validation.d.ts.map +1 -0
- package/dist/project-registration/authority.d.ts +3 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/backend.d.ts +24 -1
- package/dist/project-registration/backend.d.ts.map +1 -1
- package/dist/project-registration/http.d.ts +3 -1
- package/dist/project-registration/http.d.ts.map +1 -1
- package/dist/project-registration/index.d.ts +2 -1
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/page-validation.d.ts +5 -0
- package/dist/project-registration/page-validation.d.ts.map +1 -0
- package/dist/project-registration/postgres.d.ts +13 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts +13 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration/types.d.ts +68 -1
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +849 -60
- package/dist/registry.js +52 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +151 -0
- package/dist/sdk/v1.generated.d.ts +282 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +2041 -129
- package/dist/server/openapi.d.ts +2856 -1357
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +8 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +233 -20
- package/dist/task-manifest/authority.d.ts +13 -1
- package/dist/task-manifest/authority.d.ts.map +1 -1
- package/dist/task-manifest/backend.d.ts +5 -0
- package/dist/task-manifest/backend.d.ts.map +1 -1
- package/dist/task-manifest/index.d.ts +2 -2
- package/dist/task-manifest/index.d.ts.map +1 -1
- package/dist/task-manifest/plan-slug.d.ts +20 -0
- package/dist/task-manifest/plan-slug.d.ts.map +1 -1
- package/dist/task-manifest/postgres.d.ts +1 -0
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/schema-sql.d.ts.map +1 -1
- package/dist/task-manifest/schema.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -0
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest/types.d.ts +21 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +593 -62
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -12492,6 +12492,46 @@ function guardPlanRowsSqlite(planIds, db) {
|
|
|
12492
12492
|
}
|
|
12493
12493
|
}
|
|
12494
12494
|
|
|
12495
|
+
// src/lib/task-parent-integrity.ts
|
|
12496
|
+
function parentCycleError(taskId, parentId) {
|
|
12497
|
+
return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
|
|
12498
|
+
}
|
|
12499
|
+
function assertTaskParentIntegrity(taskId, parentId, getTask) {
|
|
12500
|
+
if (parentId === undefined || parentId === null)
|
|
12501
|
+
return;
|
|
12502
|
+
const visited = new Set;
|
|
12503
|
+
let cursor = parentId;
|
|
12504
|
+
while (cursor) {
|
|
12505
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
12506
|
+
throw parentCycleError(taskId, parentId);
|
|
12507
|
+
}
|
|
12508
|
+
visited.add(cursor);
|
|
12509
|
+
const parent = getTask(cursor);
|
|
12510
|
+
if (!parent)
|
|
12511
|
+
throw new TaskNotFoundError(cursor);
|
|
12512
|
+
cursor = parent.parent_id;
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
|
|
12516
|
+
if (parentId === undefined || parentId === null)
|
|
12517
|
+
return;
|
|
12518
|
+
const visited = new Set;
|
|
12519
|
+
let cursor = parentId;
|
|
12520
|
+
while (cursor) {
|
|
12521
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
12522
|
+
throw parentCycleError(taskId, parentId);
|
|
12523
|
+
}
|
|
12524
|
+
visited.add(cursor);
|
|
12525
|
+
const parent = await getTask(cursor);
|
|
12526
|
+
if (!parent)
|
|
12527
|
+
throw new TaskNotFoundError(cursor);
|
|
12528
|
+
cursor = parent.parent_id;
|
|
12529
|
+
}
|
|
12530
|
+
}
|
|
12531
|
+
var init_task_parent_integrity = __esm(() => {
|
|
12532
|
+
init_types();
|
|
12533
|
+
});
|
|
12534
|
+
|
|
12495
12535
|
// src/lib/creator-identity.ts
|
|
12496
12536
|
import { existsSync as existsSync6, rmSync } from "fs";
|
|
12497
12537
|
import { join as join5 } from "path";
|
|
@@ -14071,6 +14111,7 @@ function createTaskStored(input, d) {
|
|
|
14071
14111
|
let id = uuid();
|
|
14072
14112
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
14073
14113
|
try {
|
|
14114
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
14074
14115
|
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)
|
|
14075
14116
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
14076
14117
|
id,
|
|
@@ -14449,6 +14490,7 @@ function updateTaskStored(id, input, db) {
|
|
|
14449
14490
|
throw new VersionConflictError(id, input.version, task.version);
|
|
14450
14491
|
}
|
|
14451
14492
|
input = sanitizeUpdateTaskInput(input);
|
|
14493
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
14452
14494
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
|
|
14453
14495
|
const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
|
|
14454
14496
|
if (linkedProjectId) {
|
|
@@ -14502,6 +14544,10 @@ function updateTaskStored(id, input, db) {
|
|
|
14502
14544
|
sets.push("project_id = ?");
|
|
14503
14545
|
params.push(input.project_id);
|
|
14504
14546
|
}
|
|
14547
|
+
if (input.parent_id !== undefined) {
|
|
14548
|
+
sets.push("parent_id = ?");
|
|
14549
|
+
params.push(input.parent_id);
|
|
14550
|
+
}
|
|
14505
14551
|
if (input.assigned_to !== undefined) {
|
|
14506
14552
|
sets.push("assigned_to = ?");
|
|
14507
14553
|
params.push(input.assigned_to);
|
|
@@ -14617,6 +14663,8 @@ function updateTaskStored(id, input, db) {
|
|
|
14617
14663
|
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
14618
14664
|
if (input.title !== undefined && input.title !== task.title)
|
|
14619
14665
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
14666
|
+
if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
|
|
14667
|
+
logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
|
|
14620
14668
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
14621
14669
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
14622
14670
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
@@ -14674,7 +14722,8 @@ function updateTask(id, input, db) {
|
|
|
14674
14722
|
if (!before)
|
|
14675
14723
|
throw new TaskNotFoundError(id);
|
|
14676
14724
|
const guardedPlanIds = [before.plan_id, input.plan_id];
|
|
14677
|
-
|
|
14725
|
+
const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
|
|
14726
|
+
if (!needsSerializedWrite)
|
|
14678
14727
|
return updateTaskStored(id, input, d);
|
|
14679
14728
|
return d.transaction(() => {
|
|
14680
14729
|
guardPlanRowsSqlite(guardedPlanIds, d);
|
|
@@ -14710,6 +14759,7 @@ var init_task_crud = __esm(() => {
|
|
|
14710
14759
|
init_checklists();
|
|
14711
14760
|
init_storage_tombstones();
|
|
14712
14761
|
init_prewrite_secrets();
|
|
14762
|
+
init_task_parent_integrity();
|
|
14713
14763
|
});
|
|
14714
14764
|
|
|
14715
14765
|
// src/db/task-status.ts
|
|
@@ -21016,6 +21066,31 @@ var init_http_client = __esm(() => {
|
|
|
21016
21066
|
]);
|
|
21017
21067
|
});
|
|
21018
21068
|
|
|
21069
|
+
// src/project-registration/types.ts
|
|
21070
|
+
var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1", TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1", TodosProjectRegistrationError;
|
|
21071
|
+
var init_types4 = __esm(() => {
|
|
21072
|
+
TodosProjectRegistrationError = class TodosProjectRegistrationError extends Error {
|
|
21073
|
+
code;
|
|
21074
|
+
details;
|
|
21075
|
+
constructor(code, message, details = {}) {
|
|
21076
|
+
super(message);
|
|
21077
|
+
this.code = code;
|
|
21078
|
+
this.details = details;
|
|
21079
|
+
this.name = "TodosProjectRegistrationError";
|
|
21080
|
+
}
|
|
21081
|
+
};
|
|
21082
|
+
});
|
|
21083
|
+
|
|
21084
|
+
// src/project-registration/adoption-validation.ts
|
|
21085
|
+
var init_adoption_validation = __esm(() => {
|
|
21086
|
+
init_types4();
|
|
21087
|
+
});
|
|
21088
|
+
|
|
21089
|
+
// src/project-registration/page-validation.ts
|
|
21090
|
+
var init_page_validation = __esm(() => {
|
|
21091
|
+
init_types4();
|
|
21092
|
+
});
|
|
21093
|
+
|
|
21019
21094
|
// src/cli/cloud-router.ts
|
|
21020
21095
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
21021
21096
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
@@ -21237,7 +21312,7 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
21237
21312
|
return await request();
|
|
21238
21313
|
} catch (error) {
|
|
21239
21314
|
const status = error && typeof error === "object" ? error.status : undefined;
|
|
21240
|
-
if (status === 404) {
|
|
21315
|
+
if (status === 404 || status === 405) {
|
|
21241
21316
|
const body = error && typeof error === "object" ? error.body : undefined;
|
|
21242
21317
|
const code = body && typeof body === "object" && !Array.isArray(body) ? body.code : undefined;
|
|
21243
21318
|
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
@@ -21416,12 +21491,39 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
21416
21491
|
union.sort(compareCloudTaskOrder);
|
|
21417
21492
|
return union.slice(start, windowEnd);
|
|
21418
21493
|
}
|
|
21494
|
+
async function cloudResolveTaskRef(client, ref) {
|
|
21495
|
+
const input = ref.trim().toLowerCase();
|
|
21496
|
+
if (!input)
|
|
21497
|
+
throw new Error("Task reference must not be empty");
|
|
21498
|
+
if (UUID_RE.test(input))
|
|
21499
|
+
return input;
|
|
21500
|
+
let task;
|
|
21501
|
+
try {
|
|
21502
|
+
task = await cloudGetTask(client, input);
|
|
21503
|
+
} catch (error) {
|
|
21504
|
+
const status = error && typeof error === "object" ? error.status : undefined;
|
|
21505
|
+
if (status === 409) {
|
|
21506
|
+
const body = error && typeof error === "object" ? error.body : undefined;
|
|
21507
|
+
const authorityMessage = body && typeof body === "object" && !Array.isArray(body) ? body.error : undefined;
|
|
21508
|
+
throw new Error(typeof authorityMessage === "string" && authorityMessage.trim() ? authorityMessage : `Task reference is ambiguous: "${ref}"`);
|
|
21509
|
+
}
|
|
21510
|
+
throw error;
|
|
21511
|
+
}
|
|
21512
|
+
if (task && typeof task.id === "string" && (task.short_id?.toLowerCase() === input || task.id.toLowerCase().startsWith(input))) {
|
|
21513
|
+
return task.id;
|
|
21514
|
+
}
|
|
21515
|
+
if (task) {
|
|
21516
|
+
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.");
|
|
21517
|
+
}
|
|
21518
|
+
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.");
|
|
21519
|
+
}
|
|
21419
21520
|
async function cloudGetTask(client, id) {
|
|
21420
21521
|
const raw = await client.get("tasks", id);
|
|
21421
21522
|
return raw == null ? null : unwrapTask(raw);
|
|
21422
21523
|
}
|
|
21423
21524
|
async function cloudCreateTask(client, input, verification = {}) {
|
|
21424
21525
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
21526
|
+
const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
|
|
21425
21527
|
const expectedCreatedBy = typeof verification.expectedCreatedBy === "string" && verification.expectedCreatedBy.trim() ? verification.expectedCreatedBy : null;
|
|
21426
21528
|
if (expectedCreatedBy !== null) {
|
|
21427
21529
|
await requireTaskCreatorCapability(client);
|
|
@@ -21434,9 +21536,9 @@ async function cloudCreateTask(client, input, verification = {}) {
|
|
|
21434
21536
|
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");
|
|
21435
21537
|
}
|
|
21436
21538
|
const persisted = await cloudGetTask(client, created.id);
|
|
21437
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
|
|
21539
|
+
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
|
|
21438
21540
|
const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
|
|
21439
|
-
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
|
|
21541
|
+
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`);
|
|
21440
21542
|
}
|
|
21441
21543
|
return persisted;
|
|
21442
21544
|
}
|
|
@@ -21703,6 +21805,8 @@ var init_cloud_router = __esm(() => {
|
|
|
21703
21805
|
init_redaction();
|
|
21704
21806
|
init_plan_project_link_contract();
|
|
21705
21807
|
init_http_client();
|
|
21808
|
+
init_adoption_validation();
|
|
21809
|
+
init_page_validation();
|
|
21706
21810
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21707
21811
|
TRANSPORT_TOKENS = {
|
|
21708
21812
|
sqlite: "sqlite",
|
|
@@ -21739,6 +21843,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
21739
21843
|
compact["version"] = task.version;
|
|
21740
21844
|
compact["created_at"] = task.created_at;
|
|
21741
21845
|
compact["task_list_id"] = task.task_list_id;
|
|
21846
|
+
compact["parent_id"] = task.parent_id;
|
|
21742
21847
|
return compactJson(compact);
|
|
21743
21848
|
}
|
|
21744
21849
|
function versionFor(taskId, version) {
|
|
@@ -22008,6 +22113,7 @@ ${task.description}` : null
|
|
|
22008
22113
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
22009
22114
|
assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
|
|
22010
22115
|
project_id: exports_external.string().nullable().optional(),
|
|
22116
|
+
parent_id: exports_external.string().nullable().optional().describe("Existing parent task ID/reference, null to detach"),
|
|
22011
22117
|
task_list_id: exports_external.string().nullable().optional(),
|
|
22012
22118
|
depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
|
|
22013
22119
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
@@ -22039,6 +22145,9 @@ ${task.description}` : null
|
|
|
22039
22145
|
if (typeof patch.project_id === "string" && patch.project_id) {
|
|
22040
22146
|
patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
|
|
22041
22147
|
}
|
|
22148
|
+
if (typeof patch.parent_id === "string" && patch.parent_id) {
|
|
22149
|
+
patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
|
|
22150
|
+
}
|
|
22042
22151
|
if (typeof patch.task_list_id === "string" && patch.task_list_id) {
|
|
22043
22152
|
let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
|
|
22044
22153
|
if (!scope) {
|
|
@@ -22047,9 +22156,22 @@ ${task.description}` : null
|
|
|
22047
22156
|
}
|
|
22048
22157
|
patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
|
|
22049
22158
|
}
|
|
22159
|
+
if (patch.parent_id !== undefined && version2 === undefined) {
|
|
22160
|
+
const current = await cloudGetTask(cloud, task_id2);
|
|
22161
|
+
if (!current)
|
|
22162
|
+
throw new TaskNotFoundError(task_id2);
|
|
22163
|
+
patch.version = current.version;
|
|
22164
|
+
}
|
|
22050
22165
|
if (version2 !== undefined)
|
|
22051
22166
|
patch.version = version2;
|
|
22052
|
-
|
|
22167
|
+
let updated = await cloudUpdateTask(cloud, task_id2, patch);
|
|
22168
|
+
if (patch.parent_id !== undefined) {
|
|
22169
|
+
const persisted = await cloudGetTask(cloud, task_id2);
|
|
22170
|
+
if (!persisted || (persisted.parent_id ?? null) !== patch.parent_id) {
|
|
22171
|
+
throw new Error(`TASK_REPARENT_PERSISTENCE_UNVERIFIED: parent_id expected ${patch.parent_id ?? "null"}, ` + `received ${persisted?.parent_id ?? "missing task"}`);
|
|
22172
|
+
}
|
|
22173
|
+
updated = persisted;
|
|
22174
|
+
}
|
|
22053
22175
|
return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
|
|
22054
22176
|
}
|
|
22055
22177
|
const resolvedId = resolveId(params.task_id);
|
|
@@ -22061,6 +22183,8 @@ ${task.description}` : null
|
|
|
22061
22183
|
resolved.assigned_to = resolveAssignee(resolved.assigned_to);
|
|
22062
22184
|
if (resolved.project_id && typeof resolved.project_id === "string")
|
|
22063
22185
|
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
22186
|
+
if (resolved.parent_id && typeof resolved.parent_id === "string")
|
|
22187
|
+
resolved.parent_id = resolveId(resolved.parent_id);
|
|
22064
22188
|
if (resolved.task_list_id && typeof resolved.task_list_id === "string")
|
|
22065
22189
|
resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
|
|
22066
22190
|
if (resolved.depends_on && Array.isArray(resolved.depends_on))
|
|
@@ -32760,7 +32884,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
32760
32884
|
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",
|
|
32761
32885
|
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",
|
|
32762
32886
|
get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
|
|
32763
|
-
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",
|
|
32887
|
+
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",
|
|
32764
32888
|
delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
|
|
32765
32889
|
start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
|
|
32766
32890
|
complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
|
|
@@ -35815,7 +35939,7 @@ var package_default;
|
|
|
35815
35939
|
var init_package = __esm(() => {
|
|
35816
35940
|
package_default = {
|
|
35817
35941
|
name: "@hasna/todos",
|
|
35818
|
-
version: "0.15.
|
|
35942
|
+
version: "0.15.33",
|
|
35819
35943
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35820
35944
|
type: "module",
|
|
35821
35945
|
main: "dist/index.js",
|
|
@@ -46124,7 +46248,7 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
|
|
|
46124
46248
|
AS $$ SELECT unaccent('unaccent', $1) $$`,
|
|
46125
46249
|
`CREATE OR REPLACE FUNCTION todos_try_timestamptz(text)
|
|
46126
46250
|
RETURNS timestamptz
|
|
46127
|
-
LANGUAGE plpgsql IMMUTABLE PARALLEL
|
|
46251
|
+
LANGUAGE plpgsql IMMUTABLE PARALLEL UNSAFE
|
|
46128
46252
|
SET DateStyle TO 'ISO, YMD'
|
|
46129
46253
|
AS $$
|
|
46130
46254
|
BEGIN
|
|
@@ -46947,8 +47071,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
46947
47071
|
resolveRef: (ref) => store.resolveTaskRef(ref),
|
|
46948
47072
|
list: (filter = {}) => store.listTasks(filter),
|
|
46949
47073
|
count: (filter = {}) => store.countTasks(filter),
|
|
46950
|
-
update: (id, input) => updateTask2(id, input, store),
|
|
46951
|
-
delete: (id, context) => store.
|
|
47074
|
+
update: (id, input, context) => updateTask2(id, input, store, context),
|
|
47075
|
+
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
46952
47076
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
46953
47077
|
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
46954
47078
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
@@ -47481,22 +47605,70 @@ class PostgresJsonRecordStore {
|
|
|
47481
47605
|
return "identical";
|
|
47482
47606
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
47483
47607
|
}
|
|
47484
|
-
async
|
|
47608
|
+
async withTaskParentIntegrityTransaction(fn) {
|
|
47609
|
+
if (typeof this.options.client.transaction !== "function") {
|
|
47610
|
+
throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
|
|
47611
|
+
}
|
|
47612
|
+
return this.options.client.transaction(async (client) => {
|
|
47613
|
+
await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
|
|
47614
|
+
return fn(client);
|
|
47615
|
+
});
|
|
47616
|
+
}
|
|
47617
|
+
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
|
|
47485
47618
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
47486
|
-
if (planIds.length === 0)
|
|
47619
|
+
if (planIds.length === 0 && !parentGuard)
|
|
47487
47620
|
return this.upsert("tasks", value, context);
|
|
47488
47621
|
await this.ensureSchema();
|
|
47622
|
+
if (parentGuard && !queryClient) {
|
|
47623
|
+
return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
|
|
47624
|
+
}
|
|
47625
|
+
const client = queryClient ?? this.options.client;
|
|
47489
47626
|
const updatedAt = value.updated_at;
|
|
47490
47627
|
const targetPlanId = value.plan_id;
|
|
47491
|
-
const result = await
|
|
47628
|
+
const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
|
|
47629
|
+
locked_task AS MATERIALIZED (
|
|
47630
|
+
SELECT payload FROM ${this.tableName}
|
|
47631
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
|
|
47632
|
+
FOR UPDATE
|
|
47633
|
+
),
|
|
47492
47634
|
locked_plans AS MATERIALIZED (
|
|
47493
47635
|
SELECT object_id, payload FROM ${this.tableName}
|
|
47494
47636
|
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
|
|
47495
47637
|
AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
|
|
47496
47638
|
ORDER BY object_id
|
|
47497
47639
|
FOR UPDATE
|
|
47640
|
+
), parent_chain(object_id, payload, path, cycle) AS (
|
|
47641
|
+
SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
|
|
47642
|
+
FROM ${this.tableName} AS parent
|
|
47643
|
+
WHERE $10::boolean
|
|
47644
|
+
AND $11::text IS NOT NULL
|
|
47645
|
+
AND parent.service = $1
|
|
47646
|
+
AND parent.object_type = 'tasks'
|
|
47647
|
+
AND parent.object_id = $11
|
|
47648
|
+
AND parent.deleted_at IS NULL
|
|
47649
|
+
UNION ALL
|
|
47650
|
+
SELECT ancestor.object_id,
|
|
47651
|
+
ancestor.payload,
|
|
47652
|
+
chain.path || ancestor.object_id,
|
|
47653
|
+
ancestor.object_id = ANY(chain.path)
|
|
47654
|
+
FROM parent_chain AS chain
|
|
47655
|
+
JOIN ${this.tableName} AS ancestor
|
|
47656
|
+
ON ancestor.service = $1
|
|
47657
|
+
AND ancestor.object_type = 'tasks'
|
|
47658
|
+
AND ancestor.object_id = chain.payload->>'parent_id'
|
|
47659
|
+
AND ancestor.deleted_at IS NULL
|
|
47660
|
+
WHERE NOT chain.cycle
|
|
47498
47661
|
), validation AS (
|
|
47499
47662
|
SELECT
|
|
47663
|
+
(NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
|
|
47664
|
+
(NOT $10::boolean OR NOT $13::boolean
|
|
47665
|
+
OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
|
|
47666
|
+
(NOT $10::boolean OR $11::text IS NULL
|
|
47667
|
+
OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
|
|
47668
|
+
(NOT $10::boolean OR $11::text IS NULL
|
|
47669
|
+
OR ($11::text <> $2
|
|
47670
|
+
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
47671
|
+
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
47500
47672
|
(SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
|
|
47501
47673
|
($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
47502
47674
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
@@ -47517,7 +47689,13 @@ class PostgresJsonRecordStore {
|
|
|
47517
47689
|
)
|
|
47518
47690
|
SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
|
|
47519
47691
|
FROM guarded
|
|
47520
|
-
WHERE guarded.
|
|
47692
|
+
WHERE guarded.task_found
|
|
47693
|
+
AND guarded.version_matches
|
|
47694
|
+
AND guarded.parent_found
|
|
47695
|
+
AND guarded.parent_acyclic
|
|
47696
|
+
AND guarded.all_plans_found
|
|
47697
|
+
AND guarded.target_plan_found
|
|
47698
|
+
AND NOT guarded.project_conflict
|
|
47521
47699
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
47522
47700
|
payload = EXCLUDED.payload,
|
|
47523
47701
|
updated_at = EXCLUDED.updated_at,
|
|
@@ -47530,8 +47708,10 @@ class PostgresJsonRecordStore {
|
|
|
47530
47708
|
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
47531
47709
|
RETURNING payload
|
|
47532
47710
|
)
|
|
47533
|
-
SELECT guarded.
|
|
47534
|
-
|
|
47711
|
+
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
47712
|
+
guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
|
|
47713
|
+
(SELECT payload FROM stored) AS payload,
|
|
47714
|
+
(SELECT payload FROM locked_task) AS current_payload
|
|
47535
47715
|
FROM guarded`, [
|
|
47536
47716
|
this.service,
|
|
47537
47717
|
value.id,
|
|
@@ -47541,9 +47721,26 @@ class PostgresJsonRecordStore {
|
|
|
47541
47721
|
numberValue3(value.version),
|
|
47542
47722
|
jsonbParam(planIds),
|
|
47543
47723
|
targetPlanId,
|
|
47544
|
-
explicitProject
|
|
47724
|
+
explicitProject,
|
|
47725
|
+
Boolean(parentGuard),
|
|
47726
|
+
parentGuard?.parentId ?? null,
|
|
47727
|
+
parentGuard?.expectedVersion ?? null,
|
|
47728
|
+
parentGuard?.operation === "update"
|
|
47545
47729
|
]);
|
|
47546
47730
|
const row = result.rows[0];
|
|
47731
|
+
if (parentGuard && !row?.task_found) {
|
|
47732
|
+
throw new TaskNotFoundError(value.id);
|
|
47733
|
+
}
|
|
47734
|
+
if (parentGuard && !row?.version_matches) {
|
|
47735
|
+
const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
|
|
47736
|
+
throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
|
|
47737
|
+
}
|
|
47738
|
+
if (parentGuard && !row?.parent_found && parentGuard.parentId) {
|
|
47739
|
+
throw new TaskNotFoundError(parentGuard.parentId);
|
|
47740
|
+
}
|
|
47741
|
+
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
47742
|
+
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
47743
|
+
}
|
|
47547
47744
|
if (!row?.all_plans_found || !row.target_plan_found) {
|
|
47548
47745
|
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 });
|
|
47549
47746
|
}
|
|
@@ -47551,6 +47748,8 @@ class PostgresJsonRecordStore {
|
|
|
47551
47748
|
throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
|
|
47552
47749
|
}
|
|
47553
47750
|
if (!row.payload) {
|
|
47751
|
+
if (row.current_payload)
|
|
47752
|
+
return payloadRecord2(row.current_payload);
|
|
47554
47753
|
return await requireRecord("tasks", value.id, this);
|
|
47555
47754
|
}
|
|
47556
47755
|
return payloadRecord2(row.payload);
|
|
@@ -47876,6 +48075,80 @@ class PostgresJsonRecordStore {
|
|
|
47876
48075
|
version: numberValue3(existing["version"])
|
|
47877
48076
|
}, context);
|
|
47878
48077
|
}
|
|
48078
|
+
async deleteTaskHierarchy(id, context = {}) {
|
|
48079
|
+
await this.ensureSchema();
|
|
48080
|
+
return this.withTaskParentIntegrityTransaction(async (client) => {
|
|
48081
|
+
const timestamp4 = new Date().toISOString();
|
|
48082
|
+
const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
|
|
48083
|
+
task_tree(object_id, path, cycle) AS (
|
|
48084
|
+
SELECT task.object_id, ARRAY[task.object_id], false
|
|
48085
|
+
FROM ${this.tableName} AS task
|
|
48086
|
+
WHERE task.service = $1
|
|
48087
|
+
AND task.object_type = 'tasks'
|
|
48088
|
+
AND task.object_id = $2
|
|
48089
|
+
AND task.deleted_at IS NULL
|
|
48090
|
+
UNION ALL
|
|
48091
|
+
SELECT child.object_id,
|
|
48092
|
+
tree.path || child.object_id,
|
|
48093
|
+
child.object_id = ANY(tree.path)
|
|
48094
|
+
FROM task_tree AS tree
|
|
48095
|
+
JOIN ${this.tableName} AS child
|
|
48096
|
+
ON child.service = $1
|
|
48097
|
+
AND child.object_type = 'tasks'
|
|
48098
|
+
AND child.payload->>'parent_id' = tree.object_id
|
|
48099
|
+
AND child.deleted_at IS NULL
|
|
48100
|
+
WHERE NOT tree.cycle
|
|
48101
|
+
), tombstoned AS (
|
|
48102
|
+
UPDATE ${this.tableName} AS task
|
|
48103
|
+
SET deleted_at = $3::timestamptz,
|
|
48104
|
+
updated_at = $3::timestamptz,
|
|
48105
|
+
source_machine_id = $4
|
|
48106
|
+
WHERE task.service = $1
|
|
48107
|
+
AND task.object_type = 'tasks'
|
|
48108
|
+
AND task.deleted_at IS NULL
|
|
48109
|
+
AND task.object_id IN (
|
|
48110
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
48111
|
+
)
|
|
48112
|
+
RETURNING task.object_id
|
|
48113
|
+
), tombstoned_related AS (
|
|
48114
|
+
UPDATE ${this.tableName} AS related
|
|
48115
|
+
SET deleted_at = $3::timestamptz,
|
|
48116
|
+
updated_at = $3::timestamptz,
|
|
48117
|
+
source_machine_id = $4
|
|
48118
|
+
WHERE related.service = $1
|
|
48119
|
+
AND related.deleted_at IS NULL
|
|
48120
|
+
AND (
|
|
48121
|
+
(
|
|
48122
|
+
related.object_type = 'dependencies'
|
|
48123
|
+
AND (
|
|
48124
|
+
related.payload->>'task_id' IN (
|
|
48125
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
48126
|
+
)
|
|
48127
|
+
OR related.payload->>'depends_on' IN (
|
|
48128
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
48129
|
+
)
|
|
48130
|
+
)
|
|
48131
|
+
)
|
|
48132
|
+
OR (
|
|
48133
|
+
related.object_type IN ('comments', 'verifications', 'commits', 'refs')
|
|
48134
|
+
AND related.payload->>'task_id' IN (
|
|
48135
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
48136
|
+
)
|
|
48137
|
+
)
|
|
48138
|
+
)
|
|
48139
|
+
RETURNING related.object_id
|
|
48140
|
+
)
|
|
48141
|
+
SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
|
|
48142
|
+
(SELECT count(*) FROM tombstoned) AS deleted_count,
|
|
48143
|
+
(SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
|
|
48144
|
+
this.service,
|
|
48145
|
+
id,
|
|
48146
|
+
timestamp4,
|
|
48147
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
48148
|
+
]);
|
|
48149
|
+
return Boolean(result.rows[0]?.found);
|
|
48150
|
+
});
|
|
48151
|
+
}
|
|
47879
48152
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
47880
48153
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
47881
48154
|
return value ? assertPlanProjectLinkReceipt(value) : null;
|
|
@@ -48296,9 +48569,8 @@ class PostgresJsonRecordStore {
|
|
|
48296
48569
|
}
|
|
48297
48570
|
async function createTask3(input, store, context) {
|
|
48298
48571
|
const timestamp4 = new Date().toISOString();
|
|
48299
|
-
|
|
48300
|
-
|
|
48301
|
-
}
|
|
48572
|
+
const taskId = randomUUID4();
|
|
48573
|
+
await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
|
|
48302
48574
|
const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
|
|
48303
48575
|
const requestedProjectId = input.project_id ?? context?.projectId ?? null;
|
|
48304
48576
|
if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
|
|
@@ -48307,7 +48579,7 @@ async function createTask3(input, store, context) {
|
|
|
48307
48579
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
48308
48580
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
48309
48581
|
const task2 = {
|
|
48310
|
-
id:
|
|
48582
|
+
id: taskId,
|
|
48311
48583
|
short_id: shortId,
|
|
48312
48584
|
project_id: effectiveProjectId,
|
|
48313
48585
|
parent_id: input.parent_id ?? null,
|
|
@@ -48363,15 +48635,16 @@ async function createTask3(input, store, context) {
|
|
|
48363
48635
|
synced_at: null,
|
|
48364
48636
|
archived_at: null
|
|
48365
48637
|
};
|
|
48366
|
-
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, task2.plan_id ? [task2.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
|
|
48638
|
+
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, task2.plan_id ? [task2.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context, input.parent_id ? { operation: "create", expectedVersion: 0, parentId: input.parent_id } : undefined);
|
|
48367
48639
|
await logTaskChange2(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
|
|
48368
48640
|
return storedTask;
|
|
48369
48641
|
}
|
|
48370
|
-
async function updateTask2(id, input, store) {
|
|
48642
|
+
async function updateTask2(id, input, store, context) {
|
|
48371
48643
|
const existing = await requireRecord("tasks", id, store);
|
|
48372
48644
|
if (existing.version !== input.version) {
|
|
48373
|
-
throw new
|
|
48645
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
48374
48646
|
}
|
|
48647
|
+
await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
|
|
48375
48648
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
|
|
48376
48649
|
const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
|
|
48377
48650
|
if (linkedPlan?.project_id) {
|
|
@@ -48396,10 +48669,19 @@ async function updateTask2(id, input, store) {
|
|
|
48396
48669
|
metadata: input.metadata ?? existing.metadata,
|
|
48397
48670
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
48398
48671
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
48672
|
+
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
48399
48673
|
created_by: existing.created_by,
|
|
48400
48674
|
completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
|
|
48401
48675
|
};
|
|
48402
|
-
|
|
48676
|
+
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
48677
|
+
operation: "update",
|
|
48678
|
+
expectedVersion: input.version,
|
|
48679
|
+
parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
|
|
48680
|
+
});
|
|
48681
|
+
if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
|
|
48682
|
+
await logTaskChange2(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
|
|
48683
|
+
}
|
|
48684
|
+
return storedTask;
|
|
48403
48685
|
}
|
|
48404
48686
|
async function startTask2(id, agentId, store) {
|
|
48405
48687
|
const task2 = await requireRecord("tasks", id, store);
|
|
@@ -48463,7 +48745,11 @@ async function patchTask(task2, patch, store) {
|
|
|
48463
48745
|
version: task2.version + 1,
|
|
48464
48746
|
updated_at: new Date().toISOString()
|
|
48465
48747
|
};
|
|
48466
|
-
return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id")
|
|
48748
|
+
return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
|
|
48749
|
+
operation: "update",
|
|
48750
|
+
expectedVersion: task2.version,
|
|
48751
|
+
parentId: updated.parent_id
|
|
48752
|
+
});
|
|
48467
48753
|
}
|
|
48468
48754
|
function sameCloudLockHolder(stored, incoming) {
|
|
48469
48755
|
if (!stored || !incoming)
|
|
@@ -49166,6 +49452,7 @@ var init_postgres_adapter = __esm(() => {
|
|
|
49166
49452
|
init_postgres_sync();
|
|
49167
49453
|
init_integrity();
|
|
49168
49454
|
init_redaction();
|
|
49455
|
+
init_task_parent_integrity();
|
|
49169
49456
|
init_audit_history_import();
|
|
49170
49457
|
init_canonical();
|
|
49171
49458
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
@@ -49651,21 +49938,6 @@ var init_postgres = __esm(() => {
|
|
|
49651
49938
|
init_types3();
|
|
49652
49939
|
});
|
|
49653
49940
|
|
|
49654
|
-
// src/project-registration/types.ts
|
|
49655
|
-
var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1", TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1", TodosProjectRegistrationError;
|
|
49656
|
-
var init_types4 = __esm(() => {
|
|
49657
|
-
TodosProjectRegistrationError = class TodosProjectRegistrationError extends Error {
|
|
49658
|
-
code;
|
|
49659
|
-
details;
|
|
49660
|
-
constructor(code, message, details = {}) {
|
|
49661
|
-
super(message);
|
|
49662
|
-
this.code = code;
|
|
49663
|
-
this.details = details;
|
|
49664
|
-
this.name = "TodosProjectRegistrationError";
|
|
49665
|
-
}
|
|
49666
|
-
};
|
|
49667
|
-
});
|
|
49668
|
-
|
|
49669
49941
|
// src/project-registration/postgres.ts
|
|
49670
49942
|
function safeIdentifier(value, field) {
|
|
49671
49943
|
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
|
|
@@ -49943,6 +50215,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49943
50215
|
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
49944
50216
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
49945
50217
|
LIMIT 1
|
|
50218
|
+
FOR UPDATE
|
|
49946
50219
|
`, [this.service, path, taskListSlug]);
|
|
49947
50220
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
49948
50221
|
}
|
|
@@ -49953,6 +50226,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49953
50226
|
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
49954
50227
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
49955
50228
|
LIMIT 1
|
|
50229
|
+
FOR UPDATE
|
|
49956
50230
|
`, [this.service, projectId, slug]);
|
|
49957
50231
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
49958
50232
|
}
|
|
@@ -49963,10 +50237,24 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49963
50237
|
return await this.storage.taskLists.create(input);
|
|
49964
50238
|
}
|
|
49965
50239
|
async getProject(id) {
|
|
49966
|
-
|
|
50240
|
+
const result = await this.client.query(`
|
|
50241
|
+
SELECT payload FROM ${this.tableName}
|
|
50242
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2
|
|
50243
|
+
AND deleted_at IS NULL
|
|
50244
|
+
LIMIT 1
|
|
50245
|
+
FOR SHARE
|
|
50246
|
+
`, [this.service, id]);
|
|
50247
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
49967
50248
|
}
|
|
49968
50249
|
async getTaskList(id) {
|
|
49969
|
-
|
|
50250
|
+
const result = await this.client.query(`
|
|
50251
|
+
SELECT payload FROM ${this.tableName}
|
|
50252
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
|
|
50253
|
+
AND deleted_at IS NULL
|
|
50254
|
+
LIMIT 1
|
|
50255
|
+
FOR SHARE
|
|
50256
|
+
`, [this.service, id]);
|
|
50257
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
49970
50258
|
}
|
|
49971
50259
|
async lockCompensationWrites() {
|
|
49972
50260
|
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
@@ -50046,6 +50334,96 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
50046
50334
|
async getTaskList(id) {
|
|
50047
50335
|
return (await this.direct()).getTaskList(id);
|
|
50048
50336
|
}
|
|
50337
|
+
async getProjectResourceCollectionRevision(input) {
|
|
50338
|
+
await this.ensureSchema();
|
|
50339
|
+
const result = await this.client.query(`
|
|
50340
|
+
WITH resources(kind_rank, target_id, revision) AS (
|
|
50341
|
+
SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
50342
|
+
FROM ${this.tableName}
|
|
50343
|
+
WHERE service = $1 AND object_type = 'projects'
|
|
50344
|
+
AND deleted_at IS NULL AND object_id = $2
|
|
50345
|
+
UNION ALL
|
|
50346
|
+
SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
50347
|
+
FROM ${this.tableName}
|
|
50348
|
+
WHERE service = $1 AND object_type = 'task_lists'
|
|
50349
|
+
AND deleted_at IS NULL AND object_id = $3
|
|
50350
|
+
AND payload->>'project_id' = $2
|
|
50351
|
+
UNION ALL
|
|
50352
|
+
SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
50353
|
+
FROM ${this.tableName}
|
|
50354
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'plans'
|
|
50355
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
50356
|
+
UNION ALL
|
|
50357
|
+
SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
50358
|
+
FROM ${this.tableName}
|
|
50359
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
|
|
50360
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
50361
|
+
)
|
|
50362
|
+
SELECT 'md5:' || md5(COALESCE(string_agg(
|
|
50363
|
+
kind_rank::text || chr(31) || target_id || chr(31) || revision,
|
|
50364
|
+
chr(30) ORDER BY kind_rank ASC, target_id ASC
|
|
50365
|
+
), '')) AS revision
|
|
50366
|
+
FROM resources
|
|
50367
|
+
`, [
|
|
50368
|
+
this.service,
|
|
50369
|
+
input.todos_project_id,
|
|
50370
|
+
input.task_list_id,
|
|
50371
|
+
input.include_anchors
|
|
50372
|
+
]);
|
|
50373
|
+
const revision = result.rows[0]?.revision;
|
|
50374
|
+
if (!revision) {
|
|
50375
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
|
|
50376
|
+
}
|
|
50377
|
+
return revision;
|
|
50378
|
+
}
|
|
50379
|
+
async listProjectResourceCandidates(input) {
|
|
50380
|
+
await this.ensureSchema();
|
|
50381
|
+
const afterRank = input.after?.kind_rank ?? -1;
|
|
50382
|
+
const afterId = input.after?.target_id ?? "";
|
|
50383
|
+
const result = await this.client.query(`
|
|
50384
|
+
WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
|
|
50385
|
+
SELECT 'project'::text, 0, object_id, NULL::text,
|
|
50386
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
50387
|
+
FROM ${this.tableName}
|
|
50388
|
+
WHERE service = $1 AND object_type = 'projects'
|
|
50389
|
+
AND deleted_at IS NULL AND object_id = $2
|
|
50390
|
+
UNION ALL
|
|
50391
|
+
SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
|
|
50392
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
50393
|
+
FROM ${this.tableName}
|
|
50394
|
+
WHERE service = $1 AND object_type = 'task_lists'
|
|
50395
|
+
AND deleted_at IS NULL AND object_id = $3
|
|
50396
|
+
AND payload->>'project_id' = $2
|
|
50397
|
+
UNION ALL
|
|
50398
|
+
SELECT 'plan'::text, 2, object_id, payload->>'project_id',
|
|
50399
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
50400
|
+
FROM ${this.tableName}
|
|
50401
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'plans'
|
|
50402
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
50403
|
+
UNION ALL
|
|
50404
|
+
SELECT 'task'::text, 3, object_id,
|
|
50405
|
+
COALESCE(payload->>'plan_id', payload->>'project_id'),
|
|
50406
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
50407
|
+
FROM ${this.tableName}
|
|
50408
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
|
|
50409
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
50410
|
+
)
|
|
50411
|
+
SELECT kind, kind_rank, target_id, parent_id, revision
|
|
50412
|
+
FROM resources
|
|
50413
|
+
WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
|
|
50414
|
+
ORDER BY kind_rank ASC, target_id ASC
|
|
50415
|
+
LIMIT $7
|
|
50416
|
+
`, [
|
|
50417
|
+
this.service,
|
|
50418
|
+
input.todos_project_id,
|
|
50419
|
+
input.task_list_id,
|
|
50420
|
+
input.include_anchors,
|
|
50421
|
+
afterRank,
|
|
50422
|
+
afterId,
|
|
50423
|
+
input.limit
|
|
50424
|
+
]);
|
|
50425
|
+
return result.rows;
|
|
50426
|
+
}
|
|
50049
50427
|
}
|
|
50050
50428
|
var init_postgres2 = __esm(() => {
|
|
50051
50429
|
init_postgres_adapter();
|
|
@@ -50789,35 +51167,68 @@ function projectRecord(project) {
|
|
|
50789
51167
|
return {
|
|
50790
51168
|
target_id: project.id,
|
|
50791
51169
|
revision: project.updated_at,
|
|
50792
|
-
digest:
|
|
50793
|
-
id: project.id,
|
|
50794
|
-
name: project.name,
|
|
50795
|
-
path: project.path,
|
|
50796
|
-
description: project.description,
|
|
50797
|
-
task_list_id: project.task_list_id,
|
|
50798
|
-
task_prefix: project.task_prefix,
|
|
50799
|
-
task_counter: project.task_counter,
|
|
50800
|
-
created_at: project.created_at,
|
|
50801
|
-
updated_at: project.updated_at
|
|
50802
|
-
})
|
|
51170
|
+
digest: projectRegistrationDigest(project)
|
|
50803
51171
|
};
|
|
50804
51172
|
}
|
|
50805
51173
|
function taskListRecord(taskList) {
|
|
50806
51174
|
return {
|
|
50807
51175
|
target_id: taskList.id,
|
|
50808
51176
|
revision: taskList.updated_at,
|
|
50809
|
-
digest:
|
|
50810
|
-
|
|
50811
|
-
|
|
50812
|
-
|
|
50813
|
-
|
|
50814
|
-
|
|
50815
|
-
|
|
50816
|
-
|
|
50817
|
-
|
|
51177
|
+
digest: taskListRegistrationDigest(taskList)
|
|
51178
|
+
};
|
|
51179
|
+
}
|
|
51180
|
+
function boundExistingProjectRecord(project) {
|
|
51181
|
+
return {
|
|
51182
|
+
target_id: project.id,
|
|
51183
|
+
revision: project.created_at,
|
|
51184
|
+
digest: projectRegistrationDigest({
|
|
51185
|
+
...project,
|
|
51186
|
+
updated_at: project.created_at
|
|
51187
|
+
})
|
|
51188
|
+
};
|
|
51189
|
+
}
|
|
51190
|
+
function boundExistingTaskListRecord(taskList) {
|
|
51191
|
+
return {
|
|
51192
|
+
target_id: taskList.id,
|
|
51193
|
+
revision: taskList.created_at,
|
|
51194
|
+
digest: taskListRegistrationDigest({
|
|
51195
|
+
...taskList,
|
|
51196
|
+
updated_at: taskList.created_at
|
|
50818
51197
|
})
|
|
50819
51198
|
};
|
|
50820
51199
|
}
|
|
51200
|
+
function projectRegistrationDigest(project) {
|
|
51201
|
+
return digestProjectRegistrationValue({
|
|
51202
|
+
id: project.id,
|
|
51203
|
+
name: project.name,
|
|
51204
|
+
path: project.path,
|
|
51205
|
+
description: project.description,
|
|
51206
|
+
task_list_id: project.task_list_id,
|
|
51207
|
+
task_prefix: project.task_prefix,
|
|
51208
|
+
task_counter: project.task_counter,
|
|
51209
|
+
created_at: project.created_at,
|
|
51210
|
+
updated_at: project.updated_at
|
|
51211
|
+
});
|
|
51212
|
+
}
|
|
51213
|
+
function taskListRegistrationDigest(taskList) {
|
|
51214
|
+
return digestProjectRegistrationValue({
|
|
51215
|
+
id: taskList.id,
|
|
51216
|
+
project_id: taskList.project_id,
|
|
51217
|
+
slug: taskList.slug,
|
|
51218
|
+
name: taskList.name,
|
|
51219
|
+
description: taskList.description,
|
|
51220
|
+
metadata: taskList.metadata,
|
|
51221
|
+
created_at: taskList.created_at,
|
|
51222
|
+
updated_at: taskList.updated_at
|
|
51223
|
+
});
|
|
51224
|
+
}
|
|
51225
|
+
function canonicalValuesEqual(left, right) {
|
|
51226
|
+
try {
|
|
51227
|
+
return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
|
|
51228
|
+
} catch {
|
|
51229
|
+
return false;
|
|
51230
|
+
}
|
|
51231
|
+
}
|
|
50821
51232
|
function receiptId(input) {
|
|
50822
51233
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
50823
51234
|
}
|
|
@@ -50855,9 +51266,37 @@ function normalizedCallDigest(request) {
|
|
|
50855
51266
|
project_slug: request.project_slug,
|
|
50856
51267
|
project_name: request.project_name,
|
|
50857
51268
|
desired: request.desired,
|
|
51269
|
+
bind_existing: request.bind_existing === true,
|
|
50858
51270
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
50859
51271
|
});
|
|
50860
51272
|
}
|
|
51273
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
51274
|
+
return digestProjectRegistrationValue({
|
|
51275
|
+
authority_route: request.authority_route,
|
|
51276
|
+
package_version: request.package_version,
|
|
51277
|
+
authority_id: request.authority_id,
|
|
51278
|
+
tenant_id: request.tenant_id,
|
|
51279
|
+
corpus_id: request.corpus_id,
|
|
51280
|
+
operation_id: request.operation_id,
|
|
51281
|
+
step_id: request.step_id,
|
|
51282
|
+
resource_kind: request.resource_kind,
|
|
51283
|
+
direction: request.direction,
|
|
51284
|
+
target_selector: request.target_selector,
|
|
51285
|
+
idempotency_key: request.idempotency_key,
|
|
51286
|
+
request_digest: request.request_digest,
|
|
51287
|
+
precondition_digest: request.precondition_digest,
|
|
51288
|
+
project_id: request.project_id,
|
|
51289
|
+
project_slug: request.project_slug,
|
|
51290
|
+
project_name: request.project_name,
|
|
51291
|
+
desired: request.desired,
|
|
51292
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
51293
|
+
});
|
|
51294
|
+
}
|
|
51295
|
+
function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
|
|
51296
|
+
if (accepted.normalized_call_digest === callDigest)
|
|
51297
|
+
return true;
|
|
51298
|
+
return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
|
|
51299
|
+
}
|
|
50861
51300
|
function assertCommonRequest(request, capability) {
|
|
50862
51301
|
assertBounds(request);
|
|
50863
51302
|
assertResourceKind(request.resource_kind);
|
|
@@ -50899,6 +51338,9 @@ function assertCommonRequest(request, capability) {
|
|
|
50899
51338
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
50900
51339
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
50901
51340
|
}
|
|
51341
|
+
if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
|
|
51342
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
|
|
51343
|
+
}
|
|
50902
51344
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
50903
51345
|
operation_id: request.operation_id,
|
|
50904
51346
|
step_id: request.step_id,
|
|
@@ -50920,7 +51362,7 @@ function assertForwardRequest(request, capability) {
|
|
|
50920
51362
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
50921
51363
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
50922
51364
|
target_selector: request.target_selector,
|
|
50923
|
-
expected: "absent"
|
|
51365
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
50924
51366
|
});
|
|
50925
51367
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
50926
51368
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -50999,7 +51441,7 @@ function receiptBase(request, callDigest, capability) {
|
|
|
50999
51441
|
normalized_call_digest: callDigest
|
|
51000
51442
|
};
|
|
51001
51443
|
}
|
|
51002
|
-
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2) {
|
|
51444
|
+
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2, createdByOperation = true) {
|
|
51003
51445
|
return makeReceipt({
|
|
51004
51446
|
...receiptBase(request, callDigest, capability),
|
|
51005
51447
|
outcome: "accepted",
|
|
@@ -51009,7 +51451,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2
|
|
|
51009
51451
|
result_digest: record.digest,
|
|
51010
51452
|
duplicate_of_receipt_id: null,
|
|
51011
51453
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
51012
|
-
created_by_operation:
|
|
51454
|
+
created_by_operation: createdByOperation
|
|
51013
51455
|
}, createdAt2);
|
|
51014
51456
|
}
|
|
51015
51457
|
function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt2) {
|
|
@@ -51071,6 +51513,53 @@ function bindingFor(request, callDigest, timestamp4, capability) {
|
|
|
51071
51513
|
updated_at: timestamp4
|
|
51072
51514
|
};
|
|
51073
51515
|
}
|
|
51516
|
+
function encodeProjectResourceCursor(input) {
|
|
51517
|
+
return Buffer.from(JSON.stringify({
|
|
51518
|
+
version: PROJECT_RESOURCE_CURSOR_VERSION,
|
|
51519
|
+
...input
|
|
51520
|
+
}), "utf8").toString("base64url");
|
|
51521
|
+
}
|
|
51522
|
+
function decodeProjectResourceCursor(cursor, expected) {
|
|
51523
|
+
if (!cursor)
|
|
51524
|
+
return null;
|
|
51525
|
+
let parsed;
|
|
51526
|
+
try {
|
|
51527
|
+
parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
51528
|
+
} catch {
|
|
51529
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
|
|
51530
|
+
}
|
|
51531
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
51532
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
|
|
51533
|
+
}
|
|
51534
|
+
const value = parsed;
|
|
51535
|
+
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) {
|
|
51536
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
|
|
51537
|
+
}
|
|
51538
|
+
return {
|
|
51539
|
+
kind_rank: Number(value["kind_rank"]),
|
|
51540
|
+
target_id: value["target_id"],
|
|
51541
|
+
collection_revision: value["collection_revision"]
|
|
51542
|
+
};
|
|
51543
|
+
}
|
|
51544
|
+
function projectResourceFromCandidate(sourceProjectId, candidate) {
|
|
51545
|
+
const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
|
|
51546
|
+
return {
|
|
51547
|
+
source_project_id: sourceProjectId,
|
|
51548
|
+
kind: candidate.kind,
|
|
51549
|
+
scope,
|
|
51550
|
+
target_id: candidate.target_id,
|
|
51551
|
+
parent_id: candidate.parent_id,
|
|
51552
|
+
revision: candidate.revision,
|
|
51553
|
+
digest: digestProjectRegistrationValue({
|
|
51554
|
+
source_project_id: sourceProjectId,
|
|
51555
|
+
kind: candidate.kind,
|
|
51556
|
+
scope,
|
|
51557
|
+
target_id: candidate.target_id,
|
|
51558
|
+
parent_id: candidate.parent_id,
|
|
51559
|
+
revision: candidate.revision
|
|
51560
|
+
})
|
|
51561
|
+
};
|
|
51562
|
+
}
|
|
51074
51563
|
|
|
51075
51564
|
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
51076
51565
|
backend;
|
|
@@ -51092,6 +51581,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51092
51581
|
immutable_receipts: true,
|
|
51093
51582
|
exact_terminal_lookup: true,
|
|
51094
51583
|
exact_readback: true,
|
|
51584
|
+
bind_existing_adoption: true,
|
|
51585
|
+
prior_registration_adoption_validation: true,
|
|
51586
|
+
project_resource_enumeration: true,
|
|
51587
|
+
project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
|
|
51095
51588
|
conditional_inverse: true,
|
|
51096
51589
|
ambiguous_outcome_reconciliation: true
|
|
51097
51590
|
};
|
|
@@ -51152,7 +51645,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51152
51645
|
if (!accepted2) {
|
|
51153
51646
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
51154
51647
|
}
|
|
51155
|
-
if (accepted2
|
|
51648
|
+
if (!acceptedCallMatches(request, accepted2, callDigest)) {
|
|
51156
51649
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
51157
51650
|
}
|
|
51158
51651
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
@@ -51166,7 +51659,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51166
51659
|
});
|
|
51167
51660
|
if (!accepted)
|
|
51168
51661
|
return null;
|
|
51169
|
-
if (accepted
|
|
51662
|
+
if (acceptedCallMatches(request, accepted, callDigest)) {
|
|
51170
51663
|
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
51171
51664
|
}
|
|
51172
51665
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
@@ -51177,6 +51670,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51177
51670
|
const slug2 = taskListSlug(request.project_slug);
|
|
51178
51671
|
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
51179
51672
|
if (conflict2) {
|
|
51673
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
|
|
51674
|
+
return {
|
|
51675
|
+
record: boundExistingProjectRecord(conflict2),
|
|
51676
|
+
created_by_operation: false
|
|
51677
|
+
};
|
|
51678
|
+
}
|
|
51180
51679
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
51181
51680
|
}
|
|
51182
51681
|
await this.fault("before_object_write", request);
|
|
@@ -51188,7 +51687,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51188
51687
|
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
51189
51688
|
});
|
|
51190
51689
|
await this.fault("after_object_write", request);
|
|
51191
|
-
return
|
|
51690
|
+
return {
|
|
51691
|
+
record: projectRecord(project),
|
|
51692
|
+
created_by_operation: true
|
|
51693
|
+
};
|
|
51192
51694
|
}
|
|
51193
51695
|
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
51194
51696
|
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
@@ -51202,6 +51704,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51202
51704
|
const slug = taskListSlug(request.project_slug);
|
|
51203
51705
|
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
51204
51706
|
if (conflict) {
|
|
51707
|
+
if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
|
|
51708
|
+
return {
|
|
51709
|
+
record: boundExistingTaskListRecord(conflict),
|
|
51710
|
+
created_by_operation: false
|
|
51711
|
+
};
|
|
51712
|
+
}
|
|
51205
51713
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
51206
51714
|
}
|
|
51207
51715
|
await this.fault("before_object_write", request);
|
|
@@ -51218,7 +51726,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51218
51726
|
if (taskList.project_id !== todosProjectId) {
|
|
51219
51727
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
51220
51728
|
}
|
|
51221
|
-
return
|
|
51729
|
+
return {
|
|
51730
|
+
record: taskListRecord(taskList),
|
|
51731
|
+
created_by_operation: true
|
|
51732
|
+
};
|
|
51222
51733
|
}
|
|
51223
51734
|
async create(request) {
|
|
51224
51735
|
const startedAt = Date.now();
|
|
@@ -51240,9 +51751,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51240
51751
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
|
|
51241
51752
|
if (!claimed) {
|
|
51242
51753
|
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
51243
|
-
if (binding?.state === "accepted" && binding.
|
|
51754
|
+
if (binding?.state === "accepted" && binding.accepted_receipt_id) {
|
|
51244
51755
|
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
51245
|
-
if (accepted2) {
|
|
51756
|
+
if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
|
|
51246
51757
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
51247
51758
|
}
|
|
51248
51759
|
}
|
|
@@ -51253,15 +51764,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51253
51764
|
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
51254
51765
|
return recordOrTerminal;
|
|
51255
51766
|
}
|
|
51256
|
-
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
|
|
51767
|
+
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
|
|
51257
51768
|
await this.fault("before_receipt_write", request);
|
|
51258
51769
|
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
51259
51770
|
await this.fault("after_receipt_write", request);
|
|
51260
51771
|
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
51261
|
-
target_id: recordOrTerminal.target_id,
|
|
51772
|
+
target_id: recordOrTerminal.record.target_id,
|
|
51262
51773
|
accepted_receipt_id: stored.receipt_id,
|
|
51263
|
-
result_revision: recordOrTerminal.revision,
|
|
51264
|
-
result_digest: recordOrTerminal.digest,
|
|
51774
|
+
result_revision: recordOrTerminal.record.revision,
|
|
51775
|
+
result_digest: recordOrTerminal.record.digest,
|
|
51265
51776
|
updated_at: this.now()
|
|
51266
51777
|
});
|
|
51267
51778
|
return stored;
|
|
@@ -51309,7 +51820,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51309
51820
|
direction: request.direction
|
|
51310
51821
|
});
|
|
51311
51822
|
if (accepted) {
|
|
51312
|
-
return accepted
|
|
51823
|
+
return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
51313
51824
|
}
|
|
51314
51825
|
const timestamp4 = this.now();
|
|
51315
51826
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
|
|
@@ -51342,9 +51853,14 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51342
51853
|
if (request.max_items !== 1) {
|
|
51343
51854
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
51344
51855
|
}
|
|
51345
|
-
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id
|
|
51856
|
+
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id) {
|
|
51346
51857
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
51347
51858
|
}
|
|
51859
|
+
requireString(request.corpus_id, "corpus_id", {
|
|
51860
|
+
min: 3,
|
|
51861
|
+
max: 128,
|
|
51862
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
51863
|
+
});
|
|
51348
51864
|
requireString(request.authority_route, "authority_route", {
|
|
51349
51865
|
min: 3,
|
|
51350
51866
|
max: 128,
|
|
@@ -51375,6 +51891,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51375
51891
|
}
|
|
51376
51892
|
const receipt = await this.backend.getReceiptForLookup({
|
|
51377
51893
|
...authorityScope(this.capabilityValue),
|
|
51894
|
+
corpus_id: request.corpus_id,
|
|
51378
51895
|
route: request.authority_route,
|
|
51379
51896
|
package_version: request.package_version,
|
|
51380
51897
|
operation_id: request.operation_id,
|
|
@@ -51389,6 +51906,174 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51389
51906
|
}
|
|
51390
51907
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
51391
51908
|
}
|
|
51909
|
+
async listProjectResources(request) {
|
|
51910
|
+
const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
|
|
51911
|
+
if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
|
|
51912
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
|
|
51913
|
+
}
|
|
51914
|
+
if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
|
|
51915
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
|
|
51916
|
+
}
|
|
51917
|
+
const includeAnchors = request.include_anchors === true;
|
|
51918
|
+
const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
|
|
51919
|
+
if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
|
|
51920
|
+
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 });
|
|
51921
|
+
}
|
|
51922
|
+
const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
|
|
51923
|
+
if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
|
|
51924
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
|
|
51925
|
+
source_project_id: sourceProjectId,
|
|
51926
|
+
todos_project_id: projectBinding.target_id
|
|
51927
|
+
});
|
|
51928
|
+
}
|
|
51929
|
+
const cursor = decodeProjectResourceCursor(request.cursor, {
|
|
51930
|
+
source_project_id: sourceProjectId,
|
|
51931
|
+
include_anchors: includeAnchors
|
|
51932
|
+
});
|
|
51933
|
+
const collectionInput = {
|
|
51934
|
+
todos_project_id: projectBinding.target_id,
|
|
51935
|
+
task_list_id: taskListBinding.target_id,
|
|
51936
|
+
include_anchors: includeAnchors
|
|
51937
|
+
};
|
|
51938
|
+
const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
51939
|
+
if (cursor && cursor.collection_revision !== collectionRevision) {
|
|
51940
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
|
|
51941
|
+
source_project_id: sourceProjectId,
|
|
51942
|
+
expected_collection_revision: cursor.collection_revision,
|
|
51943
|
+
current_collection_revision: collectionRevision
|
|
51944
|
+
});
|
|
51945
|
+
}
|
|
51946
|
+
const candidates = await this.backend.listProjectResourceCandidates({
|
|
51947
|
+
...collectionInput,
|
|
51948
|
+
after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
|
|
51949
|
+
limit: request.limit + 1
|
|
51950
|
+
});
|
|
51951
|
+
const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
51952
|
+
if (verifiedCollectionRevision !== collectionRevision) {
|
|
51953
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
|
|
51954
|
+
source_project_id: sourceProjectId,
|
|
51955
|
+
expected_collection_revision: collectionRevision,
|
|
51956
|
+
current_collection_revision: verifiedCollectionRevision
|
|
51957
|
+
});
|
|
51958
|
+
}
|
|
51959
|
+
const hasMore = candidates.length > request.limit;
|
|
51960
|
+
const pageCandidates = candidates.slice(0, request.limit);
|
|
51961
|
+
const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
|
|
51962
|
+
const last = pageCandidates.at(-1);
|
|
51963
|
+
return {
|
|
51964
|
+
authority: "todos",
|
|
51965
|
+
route: this.capabilityValue.route,
|
|
51966
|
+
package_version: this.capabilityValue.package_version,
|
|
51967
|
+
authority_id: this.capabilityValue.authority_id,
|
|
51968
|
+
tenant_id: this.capabilityValue.tenant_id,
|
|
51969
|
+
corpus_id: this.capabilityValue.corpus_id,
|
|
51970
|
+
source_project_id: sourceProjectId,
|
|
51971
|
+
todos_project_id: projectBinding.target_id,
|
|
51972
|
+
task_list_id: taskListBinding.target_id,
|
|
51973
|
+
include_anchors: includeAnchors,
|
|
51974
|
+
collection_revision: collectionRevision,
|
|
51975
|
+
limit: request.limit,
|
|
51976
|
+
count: resources.length,
|
|
51977
|
+
resources,
|
|
51978
|
+
has_more: hasMore,
|
|
51979
|
+
next_cursor: hasMore && last ? encodeProjectResourceCursor({
|
|
51980
|
+
source_project_id: sourceProjectId,
|
|
51981
|
+
include_anchors: includeAnchors,
|
|
51982
|
+
collection_revision: collectionRevision,
|
|
51983
|
+
kind_rank: last.kind_rank,
|
|
51984
|
+
target_id: last.target_id
|
|
51985
|
+
}) : null,
|
|
51986
|
+
complete: !hasMore,
|
|
51987
|
+
truncated: false
|
|
51988
|
+
};
|
|
51989
|
+
}
|
|
51990
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
51991
|
+
const startedAt = Date.now();
|
|
51992
|
+
if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
|
|
51993
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
|
|
51994
|
+
}
|
|
51995
|
+
requireString(sourceRequest.package_version, "package_version", {
|
|
51996
|
+
max: 128,
|
|
51997
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
51998
|
+
});
|
|
51999
|
+
requireString(sourceRequest.corpus_id, "corpus_id", {
|
|
52000
|
+
min: 3,
|
|
52001
|
+
max: 128,
|
|
52002
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
52003
|
+
});
|
|
52004
|
+
assertForwardRequest(sourceRequest, {
|
|
52005
|
+
...this.capabilityValue,
|
|
52006
|
+
package_version: sourceRequest.package_version,
|
|
52007
|
+
corpus_id: sourceRequest.corpus_id
|
|
52008
|
+
});
|
|
52009
|
+
const validation = await this.backend.transaction(async (transaction) => {
|
|
52010
|
+
const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
|
|
52011
|
+
if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
|
|
52012
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
52013
|
+
}
|
|
52014
|
+
const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
|
|
52015
|
+
if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
|
|
52016
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
52017
|
+
}
|
|
52018
|
+
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);
|
|
52019
|
+
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)) {
|
|
52020
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
|
|
52021
|
+
}
|
|
52022
|
+
const binding = await transaction.getBinding({
|
|
52023
|
+
authority_id: sourceRequest.authority_id,
|
|
52024
|
+
tenant_id: sourceRequest.tenant_id,
|
|
52025
|
+
corpus_id: sourceRequest.corpus_id
|
|
52026
|
+
}, sourceRequest.resource_kind, sourceRequest.target_selector);
|
|
52027
|
+
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) {
|
|
52028
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
|
|
52029
|
+
}
|
|
52030
|
+
const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
52031
|
+
if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
|
|
52032
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
|
|
52033
|
+
}
|
|
52034
|
+
let stableMatch = false;
|
|
52035
|
+
if (sourceRequest.resource_kind === "task_list") {
|
|
52036
|
+
stableMatch = taskListRegistrationDigest({
|
|
52037
|
+
...current,
|
|
52038
|
+
updated_at: accepted.result_revision
|
|
52039
|
+
}) === accepted.result_digest;
|
|
52040
|
+
} else {
|
|
52041
|
+
const project = current;
|
|
52042
|
+
if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
|
|
52043
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
|
|
52044
|
+
}
|
|
52045
|
+
for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
|
|
52046
|
+
if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
|
|
52047
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
|
|
52048
|
+
}
|
|
52049
|
+
if (projectRegistrationDigest({
|
|
52050
|
+
...project,
|
|
52051
|
+
task_counter: priorTaskCounter,
|
|
52052
|
+
updated_at: accepted.result_revision
|
|
52053
|
+
}) === accepted.result_digest) {
|
|
52054
|
+
stableMatch = true;
|
|
52055
|
+
break;
|
|
52056
|
+
}
|
|
52057
|
+
}
|
|
52058
|
+
}
|
|
52059
|
+
if (!stableMatch) {
|
|
52060
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
|
|
52061
|
+
}
|
|
52062
|
+
return {
|
|
52063
|
+
valid: true,
|
|
52064
|
+
resource_kind: sourceRequest.resource_kind,
|
|
52065
|
+
target_id: accepted.target_id,
|
|
52066
|
+
source_receipt_id: storedSource.receipt_id,
|
|
52067
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
52068
|
+
source_outcome: storedSource.outcome,
|
|
52069
|
+
created_at: current.created_at,
|
|
52070
|
+
current_revision: current.updated_at,
|
|
52071
|
+
accepted_result_digest: accepted.result_digest
|
|
52072
|
+
};
|
|
52073
|
+
});
|
|
52074
|
+
assertWithinBounds(validation, sourceRequest, startedAt);
|
|
52075
|
+
return validation;
|
|
52076
|
+
}
|
|
51392
52077
|
async storedAcceptedReceipt(request, supplied) {
|
|
51393
52078
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
51394
52079
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -51557,7 +52242,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
51557
52242
|
cursorTableName
|
|
51558
52243
|
}), authorityOptions);
|
|
51559
52244
|
}
|
|
51560
|
-
var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
|
|
52245
|
+
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;
|
|
51561
52246
|
var init_authority = __esm(() => {
|
|
51562
52247
|
init_package_version();
|
|
51563
52248
|
init_postgres2();
|
|
@@ -51627,6 +52312,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
51627
52312
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
51628
52313
|
return json({ capability: await authority.capability() });
|
|
51629
52314
|
}
|
|
52315
|
+
if (action === "resources" && method === "GET") {
|
|
52316
|
+
const sourceProjectId = url.searchParams.get("source_project_id");
|
|
52317
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
52318
|
+
const includeAnchorsRaw = url.searchParams.get("include_anchors");
|
|
52319
|
+
const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
|
|
52320
|
+
return json({
|
|
52321
|
+
page: await authority.listProjectResources({
|
|
52322
|
+
source_project_id: sourceProjectId,
|
|
52323
|
+
limit,
|
|
52324
|
+
include_anchors: includeAnchors,
|
|
52325
|
+
cursor: url.searchParams.get("cursor") ?? undefined
|
|
52326
|
+
})
|
|
52327
|
+
});
|
|
52328
|
+
}
|
|
51630
52329
|
if (method !== "POST")
|
|
51631
52330
|
return json({ error: "method not allowed" }, 405);
|
|
51632
52331
|
const body = await readJson(req);
|
|
@@ -51649,6 +52348,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
51649
52348
|
record: await authority.readExact(body)
|
|
51650
52349
|
});
|
|
51651
52350
|
}
|
|
52351
|
+
if (action === "validate-prior-adoption") {
|
|
52352
|
+
const input = body;
|
|
52353
|
+
return json({
|
|
52354
|
+
validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
|
|
52355
|
+
});
|
|
52356
|
+
}
|
|
51652
52357
|
if (action === "compensate") {
|
|
51653
52358
|
return json({
|
|
51654
52359
|
receipt: await authority.compensate(body)
|
|
@@ -51681,6 +52386,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
51681
52386
|
var JSON_HEADERS;
|
|
51682
52387
|
var init_http2 = __esm(() => {
|
|
51683
52388
|
init_types4();
|
|
52389
|
+
init_adoption_validation();
|
|
51684
52390
|
JSON_HEADERS = { "Content-Type": "application/json" };
|
|
51685
52391
|
});
|
|
51686
52392
|
|
|
@@ -51688,13 +52394,14 @@ var init_http2 = __esm(() => {
|
|
|
51688
52394
|
var init_project_registration = __esm(() => {
|
|
51689
52395
|
init_authority();
|
|
51690
52396
|
init_http2();
|
|
52397
|
+
init_adoption_validation();
|
|
51691
52398
|
init_postgres2();
|
|
51692
52399
|
init_sqlite();
|
|
51693
52400
|
init_types4();
|
|
51694
52401
|
});
|
|
51695
52402
|
|
|
51696
52403
|
// src/task-manifest/types.ts
|
|
51697
|
-
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
|
|
52404
|
+
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;
|
|
51698
52405
|
var init_types5 = __esm(() => {
|
|
51699
52406
|
TodosTaskManifestError = class TodosTaskManifestError extends Error {
|
|
51700
52407
|
code;
|
|
@@ -51781,7 +52488,7 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
51781
52488
|
}
|
|
51782
52489
|
return parsed.data;
|
|
51783
52490
|
}
|
|
51784
|
-
var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
52491
|
+
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) => {
|
|
51785
52492
|
if (Object.keys(value).length > limit) {
|
|
51786
52493
|
context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
|
|
51787
52494
|
}
|
|
@@ -51803,6 +52510,8 @@ var init_schema2 = __esm(() => {
|
|
|
51803
52510
|
};
|
|
51804
52511
|
key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
51805
52512
|
identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
52513
|
+
digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
|
|
52514
|
+
idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
|
|
51806
52515
|
uuid2 = exports_external.string().uuid();
|
|
51807
52516
|
scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
51808
52517
|
comment = exports_external.object({
|
|
@@ -51840,7 +52549,9 @@ var init_schema2 = __esm(() => {
|
|
|
51840
52549
|
schema = exports_external.object({
|
|
51841
52550
|
version: exports_external.literal(1),
|
|
51842
52551
|
operation_id: identifier,
|
|
51843
|
-
|
|
52552
|
+
step_id: identifier,
|
|
52553
|
+
idempotency_key: idempotencyKey,
|
|
52554
|
+
precondition_digest: digest,
|
|
51844
52555
|
project_id: uuid2,
|
|
51845
52556
|
task_list_id: uuid2.optional(),
|
|
51846
52557
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -51856,7 +52567,10 @@ var init_schema2 = __esm(() => {
|
|
|
51856
52567
|
}).strict();
|
|
51857
52568
|
compensationSchema = exports_external.object({
|
|
51858
52569
|
receipt_id: uuid2,
|
|
51859
|
-
|
|
52570
|
+
operation_id: identifier,
|
|
52571
|
+
step_id: identifier,
|
|
52572
|
+
idempotency_key: idempotencyKey,
|
|
52573
|
+
precondition_digest: digest,
|
|
51860
52574
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
51861
52575
|
}).strict();
|
|
51862
52576
|
bindingLookupSchema = exports_external.object({
|
|
@@ -51874,6 +52588,7 @@ function taskManifestPlanSlug(manifest, planId) {
|
|
|
51874
52588
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
51875
52589
|
return `${base}-${planId}`;
|
|
51876
52590
|
}
|
|
52591
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
51877
52592
|
var init_plan_slug = () => {};
|
|
51878
52593
|
|
|
51879
52594
|
// src/task-manifest/backend.ts
|
|
@@ -51887,11 +52602,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
51887
52602
|
const row = rows[0];
|
|
51888
52603
|
const bindingVersion = Number(row.binding_version);
|
|
51889
52604
|
const state = row.state;
|
|
51890
|
-
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") {
|
|
52605
|
+
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") {
|
|
51891
52606
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
51892
52607
|
}
|
|
51893
52608
|
return {
|
|
51894
52609
|
plan_id: planId,
|
|
52610
|
+
operation_id: row.binding_operation_id,
|
|
52611
|
+
step_id: row.binding_step_id,
|
|
51895
52612
|
apply_receipt_id: row.apply_receipt_id,
|
|
51896
52613
|
binding_version: bindingVersion,
|
|
51897
52614
|
state
|
|
@@ -51944,9 +52661,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51944
52661
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
51945
52662
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
51946
52663
|
operation_id text NOT NULL,
|
|
52664
|
+
step_id text NOT NULL,
|
|
51947
52665
|
idempotency_key text NOT NULL,
|
|
51948
52666
|
request_digest text NOT NULL,
|
|
52667
|
+
precondition_digest text NOT NULL,
|
|
51949
52668
|
result_digest text NOT NULL,
|
|
52669
|
+
slug_provenance text,
|
|
52670
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
52671
|
+
reason text,
|
|
52672
|
+
duplicate_of_receipt_id text,
|
|
51950
52673
|
binding_version integer NOT NULL,
|
|
51951
52674
|
apply_receipt_id text,
|
|
51952
52675
|
manifest_json jsonb,
|
|
@@ -51958,12 +52681,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51958
52681
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
51959
52682
|
`ALTER TABLE todos_task_manifest_receipts
|
|
51960
52683
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
52684
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52685
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
52686
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52687
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
52688
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52689
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
52690
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52691
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
52692
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52693
|
+
ADD COLUMN IF NOT EXISTS reason text`,
|
|
52694
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52695
|
+
ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
|
|
51961
52696
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
51962
52697
|
operation_id text PRIMARY KEY,
|
|
51963
52698
|
tenant_id text NOT NULL,
|
|
52699
|
+
step_id text NOT NULL,
|
|
51964
52700
|
idempotency_key text NOT NULL UNIQUE,
|
|
51965
52701
|
request_digest text NOT NULL,
|
|
52702
|
+
precondition_digest text NOT NULL,
|
|
51966
52703
|
result_digest text NOT NULL,
|
|
52704
|
+
slug_provenance text,
|
|
52705
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
51967
52706
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
51968
52707
|
manifest_json jsonb NOT NULL,
|
|
51969
52708
|
result_json jsonb NOT NULL,
|
|
@@ -51977,6 +52716,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51977
52716
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
51978
52717
|
`ALTER TABLE todos_task_manifest_bindings
|
|
51979
52718
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
52719
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52720
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
52721
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52722
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
52723
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52724
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
52725
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52726
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
51980
52727
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
51981
52728
|
id text PRIMARY KEY,
|
|
51982
52729
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -51988,10 +52735,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51988
52735
|
created_at timestamptz NOT NULL,
|
|
51989
52736
|
delivered_at timestamptz
|
|
51990
52737
|
)`,
|
|
52738
|
+
`CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
52739
|
+
receipt_id text PRIMARY KEY,
|
|
52740
|
+
tenant_id text NOT NULL,
|
|
52741
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
52742
|
+
route text NOT NULL,
|
|
52743
|
+
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
52744
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
52745
|
+
operation_id text NOT NULL,
|
|
52746
|
+
step_id text NOT NULL,
|
|
52747
|
+
idempotency_key text NOT NULL,
|
|
52748
|
+
request_digest text NOT NULL,
|
|
52749
|
+
precondition_digest text NOT NULL,
|
|
52750
|
+
result_digest text NOT NULL,
|
|
52751
|
+
outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
52752
|
+
reason text NOT NULL,
|
|
52753
|
+
binding_version integer NOT NULL,
|
|
52754
|
+
apply_receipt_id text,
|
|
52755
|
+
manifest_json jsonb,
|
|
52756
|
+
result_json jsonb NOT NULL,
|
|
52757
|
+
created_at timestamptz NOT NULL
|
|
52758
|
+
)`,
|
|
51991
52759
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
51992
52760
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
51993
52761
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
51994
52762
|
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
52763
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
|
|
52764
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
|
|
52765
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
|
|
52766
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
52767
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
|
|
52768
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
51995
52769
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
51996
52770
|
ON todos_task_manifest_bindings(
|
|
51997
52771
|
tenant_id,
|
|
@@ -52004,6 +52778,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
52004
52778
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
52005
52779
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
52006
52780
|
BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
|
|
52781
|
+
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
|
|
52782
|
+
`DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
|
|
52783
|
+
`CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
|
|
52784
|
+
BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
|
|
52007
52785
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
52008
52786
|
];
|
|
52009
52787
|
}
|
|
@@ -52028,6 +52806,39 @@ function safeIdentifier2(value, field) {
|
|
|
52028
52806
|
function parseJson2(value) {
|
|
52029
52807
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
52030
52808
|
}
|
|
52809
|
+
function parseApplyResult(value, duplicate) {
|
|
52810
|
+
const parsed = parseJson2(value);
|
|
52811
|
+
return {
|
|
52812
|
+
...parsed,
|
|
52813
|
+
duplicate,
|
|
52814
|
+
receipt: {
|
|
52815
|
+
...parsed.receipt,
|
|
52816
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
52817
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
52818
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
52819
|
+
reason: parsed.receipt.reason ?? null,
|
|
52820
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
52821
|
+
}
|
|
52822
|
+
};
|
|
52823
|
+
}
|
|
52824
|
+
function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
|
|
52825
|
+
if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
52826
|
+
const expected = taskManifestPlanSlug(manifest, planId);
|
|
52827
|
+
if (slug !== expected) {
|
|
52828
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
|
|
52829
|
+
}
|
|
52830
|
+
return expected;
|
|
52831
|
+
}
|
|
52832
|
+
if (provenance !== null && provenance !== undefined) {
|
|
52833
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
52834
|
+
}
|
|
52835
|
+
if (slug === null || slug === undefined)
|
|
52836
|
+
return null;
|
|
52837
|
+
if (slug !== null && slug !== undefined) {
|
|
52838
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
|
|
52839
|
+
}
|
|
52840
|
+
return null;
|
|
52841
|
+
}
|
|
52031
52842
|
function timestamp4(value) {
|
|
52032
52843
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
52033
52844
|
}
|
|
@@ -52035,6 +52846,41 @@ function fault(faults, point) {
|
|
|
52035
52846
|
if (faults.points.has(point))
|
|
52036
52847
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
52037
52848
|
}
|
|
52849
|
+
function terminalApplyResult(input, reason) {
|
|
52850
|
+
const receipt = {
|
|
52851
|
+
receipt_id: input.terminal_receipt_id,
|
|
52852
|
+
authority: "todos",
|
|
52853
|
+
route: "todos.task-manifest.v1",
|
|
52854
|
+
schema_version: 1,
|
|
52855
|
+
kind: "apply",
|
|
52856
|
+
operation_id: input.manifest.operation_id,
|
|
52857
|
+
step_id: input.manifest.step_id,
|
|
52858
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
52859
|
+
request_digest: input.request_digest,
|
|
52860
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
52861
|
+
result_digest: canonicalDigest({
|
|
52862
|
+
outcome: "terminal_nonacceptance",
|
|
52863
|
+
reason,
|
|
52864
|
+
operation_id: input.manifest.operation_id,
|
|
52865
|
+
step_id: input.manifest.step_id,
|
|
52866
|
+
request_digest: input.request_digest
|
|
52867
|
+
}),
|
|
52868
|
+
outcome: "terminal_nonacceptance",
|
|
52869
|
+
reason,
|
|
52870
|
+
duplicate_of_receipt_id: null,
|
|
52871
|
+
binding_version: 0,
|
|
52872
|
+
apply_receipt_id: null,
|
|
52873
|
+
created_at: input.now
|
|
52874
|
+
};
|
|
52875
|
+
return {
|
|
52876
|
+
duplicate: false,
|
|
52877
|
+
receipt,
|
|
52878
|
+
graph: input.graph,
|
|
52879
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
52880
|
+
outbox_ids: [],
|
|
52881
|
+
result_digest: receipt.result_digest
|
|
52882
|
+
};
|
|
52883
|
+
}
|
|
52038
52884
|
function receiptFromRow3(row) {
|
|
52039
52885
|
return {
|
|
52040
52886
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -52043,9 +52889,14 @@ function receiptFromRow3(row) {
|
|
|
52043
52889
|
schema_version: 1,
|
|
52044
52890
|
kind: row["kind"],
|
|
52045
52891
|
operation_id: String(row["operation_id"]),
|
|
52892
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
52046
52893
|
idempotency_key: String(row["idempotency_key"]),
|
|
52047
52894
|
request_digest: String(row["request_digest"]),
|
|
52895
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
52048
52896
|
result_digest: String(row["result_digest"]),
|
|
52897
|
+
outcome: row["outcome"] ?? "accepted",
|
|
52898
|
+
reason: row["reason"] == null ? null : row["reason"],
|
|
52899
|
+
duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
|
|
52049
52900
|
binding_version: Number(row["binding_version"]),
|
|
52050
52901
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
52051
52902
|
created_at: timestamp4(row["created_at"])
|
|
@@ -52163,46 +53014,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52163
53014
|
now4
|
|
52164
53015
|
]);
|
|
52165
53016
|
}
|
|
53017
|
+
async persistTerminal(tx, input, reason) {
|
|
53018
|
+
const result = terminalApplyResult(input, reason);
|
|
53019
|
+
const resultJson = canonicalJson(result);
|
|
53020
|
+
await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
|
|
53021
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
53022
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
53023
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
53024
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
|
|
53025
|
+
'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
|
|
53026
|
+
ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
|
|
53027
|
+
result.receipt.receipt_id,
|
|
53028
|
+
this.tenantId,
|
|
53029
|
+
input.manifest.operation_id,
|
|
53030
|
+
input.manifest.step_id,
|
|
53031
|
+
input.manifest.idempotency_key,
|
|
53032
|
+
input.request_digest,
|
|
53033
|
+
input.manifest.precondition_digest,
|
|
53034
|
+
result.receipt.result_digest,
|
|
53035
|
+
reason,
|
|
53036
|
+
canonicalJson(input.manifest),
|
|
53037
|
+
resultJson,
|
|
53038
|
+
input.now
|
|
53039
|
+
]);
|
|
53040
|
+
const stored = await tx.query(`SELECT receipt_id, result_json
|
|
53041
|
+
FROM todos_task_manifest_terminal_receipts
|
|
53042
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
53043
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
53044
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
53045
|
+
LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
|
|
53046
|
+
return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
|
|
53047
|
+
}
|
|
52166
53048
|
async apply(input, faults) {
|
|
52167
53049
|
await this.ensureSchema();
|
|
52168
53050
|
return this.client.transaction(async (tx) => {
|
|
52169
53051
|
const { manifest } = input;
|
|
52170
53052
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
52171
53053
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
53054
|
+
const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
|
|
53055
|
+
WHERE tenant_id = $1
|
|
53056
|
+
AND kind = 'apply'
|
|
53057
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
53058
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
53059
|
+
LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
|
|
53060
|
+
if (terminal.rows[0]) {
|
|
53061
|
+
return parseApplyResult(terminal.rows[0]["result_json"], true);
|
|
53062
|
+
}
|
|
52172
53063
|
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]);
|
|
52173
53064
|
if (existing.rows[0]) {
|
|
52174
53065
|
const binding = existing.rows[0];
|
|
52175
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
52176
|
-
|
|
53066
|
+
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) {
|
|
53067
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
52177
53068
|
}
|
|
52178
53069
|
if (binding["state"] !== "applied") {
|
|
52179
|
-
|
|
53070
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
52180
53071
|
}
|
|
52181
|
-
return
|
|
53072
|
+
return parseApplyResult(binding["result_json"], true);
|
|
52182
53073
|
}
|
|
52183
53074
|
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]);
|
|
52184
53075
|
if (reused.rows[0])
|
|
52185
|
-
|
|
53076
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
53077
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
53078
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
53079
|
+
}
|
|
52186
53080
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
52187
|
-
|
|
53081
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
52188
53082
|
}
|
|
52189
53083
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
52190
53084
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
52191
53085
|
if (!project.rows[0])
|
|
52192
|
-
|
|
53086
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
52193
53087
|
if (manifest.task_list_id) {
|
|
52194
53088
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
52195
53089
|
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]);
|
|
52196
53090
|
const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
|
|
52197
53091
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
52198
|
-
|
|
53092
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
52199
53093
|
}
|
|
52200
53094
|
}
|
|
52201
53095
|
const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
|
|
52202
53096
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
52203
53097
|
WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
52204
53098
|
if (conflict.rows[0])
|
|
52205
|
-
|
|
53099
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
52206
53100
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
52207
53101
|
fault(faults, "after_plan_write");
|
|
52208
53102
|
for (const task3 of manifest.tasks) {
|
|
@@ -52272,9 +53166,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52272
53166
|
schema_version: 1,
|
|
52273
53167
|
kind: "apply",
|
|
52274
53168
|
operation_id: manifest.operation_id,
|
|
53169
|
+
step_id: manifest.step_id,
|
|
52275
53170
|
idempotency_key: manifest.idempotency_key,
|
|
52276
53171
|
request_digest: input.request_digest,
|
|
53172
|
+
precondition_digest: manifest.precondition_digest,
|
|
52277
53173
|
result_digest: input.result_digest,
|
|
53174
|
+
outcome: "accepted",
|
|
53175
|
+
reason: null,
|
|
53176
|
+
duplicate_of_receipt_id: null,
|
|
52278
53177
|
binding_version: 1,
|
|
52279
53178
|
apply_receipt_id: null,
|
|
52280
53179
|
created_at: input.now
|
|
@@ -52291,14 +53190,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52291
53190
|
const resultJson = canonicalJson(result);
|
|
52292
53191
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
52293
53192
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
52294
|
-
|
|
52295
|
-
|
|
53193
|
+
step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
|
|
53194
|
+
reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
53195
|
+
) 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)`, [
|
|
52296
53196
|
input.receipt_id,
|
|
52297
53197
|
this.tenantId,
|
|
52298
53198
|
manifest.operation_id,
|
|
52299
53199
|
manifest.idempotency_key,
|
|
53200
|
+
manifest.step_id,
|
|
52300
53201
|
input.request_digest,
|
|
53202
|
+
manifest.precondition_digest,
|
|
52301
53203
|
input.result_digest,
|
|
53204
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
52302
53205
|
manifestJson,
|
|
52303
53206
|
resultJson,
|
|
52304
53207
|
input.now
|
|
@@ -52317,14 +53220,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52317
53220
|
}
|
|
52318
53221
|
fault(faults, "after_outbox_write");
|
|
52319
53222
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
52320
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
52321
|
-
|
|
52322
|
-
|
|
53223
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
|
|
53224
|
+
result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
|
|
53225
|
+
state, version, created_at, updated_at
|
|
53226
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
|
|
52323
53227
|
manifest.operation_id,
|
|
52324
53228
|
this.tenantId,
|
|
53229
|
+
manifest.step_id,
|
|
52325
53230
|
manifest.idempotency_key,
|
|
52326
53231
|
input.request_digest,
|
|
53232
|
+
manifest.precondition_digest,
|
|
52327
53233
|
input.result_digest,
|
|
53234
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
52328
53235
|
input.receipt_id,
|
|
52329
53236
|
manifestJson,
|
|
52330
53237
|
resultJson,
|
|
@@ -52337,9 +53244,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52337
53244
|
async readExact(receiptId2) {
|
|
52338
53245
|
await this.ensureSchema();
|
|
52339
53246
|
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]);
|
|
52340
|
-
if (
|
|
53247
|
+
if (result.rows[0])
|
|
53248
|
+
return parseApplyResult(result.rows[0]["result_json"], false);
|
|
53249
|
+
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]);
|
|
53250
|
+
if (!terminal.rows[0])
|
|
52341
53251
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
52342
|
-
return
|
|
53252
|
+
return parseApplyResult(terminal.rows[0]["result_json"], false);
|
|
52343
53253
|
}
|
|
52344
53254
|
async lookupBindingByPlanId(planId) {
|
|
52345
53255
|
await this.ensureSchema();
|
|
@@ -52350,6 +53260,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52350
53260
|
b.version AS binding_version,
|
|
52351
53261
|
b.tenant_id AS binding_tenant_id,
|
|
52352
53262
|
b.operation_id AS binding_operation_id,
|
|
53263
|
+
b.step_id AS binding_step_id,
|
|
52353
53264
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
52354
53265
|
r.tenant_id AS receipt_tenant_id,
|
|
52355
53266
|
r.authority AS receipt_authority,
|
|
@@ -52357,6 +53268,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52357
53268
|
r.schema_version AS receipt_schema_version,
|
|
52358
53269
|
r.kind AS receipt_kind,
|
|
52359
53270
|
r.operation_id AS receipt_operation_id,
|
|
53271
|
+
r.step_id AS receipt_step_id,
|
|
52360
53272
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
52361
53273
|
FROM todos_task_manifest_bindings b
|
|
52362
53274
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -52443,6 +53355,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52443
53355
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
52444
53356
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
52445
53357
|
}
|
|
53358
|
+
const appliedReceipt = receiptFromRow3(applyRow);
|
|
53359
|
+
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"]) {
|
|
53360
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
53361
|
+
}
|
|
52446
53362
|
if (binding["state"] !== "applied")
|
|
52447
53363
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
52448
53364
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -52457,12 +53373,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52457
53373
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
52458
53374
|
if (delivered.rows[0])
|
|
52459
53375
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
52460
|
-
const applyResult =
|
|
53376
|
+
const applyResult = parseApplyResult(applyRow["result_json"], false);
|
|
52461
53377
|
const manifest = parseJson2(applyRow["manifest_json"]);
|
|
53378
|
+
const manifestRecord2 = manifest;
|
|
53379
|
+
const applyStepId = typeof manifestRecord2["step_id"] === "string" ? String(manifestRecord2["step_id"]) : null;
|
|
52462
53380
|
const expectedEffects = [
|
|
52463
53381
|
{
|
|
52464
53382
|
topic: "todos.task-manifest.applied",
|
|
52465
|
-
payload: {
|
|
53383
|
+
payload: {
|
|
53384
|
+
operation_id: manifest.operation_id,
|
|
53385
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
53386
|
+
project_id: manifest.project_id
|
|
53387
|
+
}
|
|
52466
53388
|
},
|
|
52467
53389
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
52468
53390
|
];
|
|
@@ -52510,9 +53432,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52510
53432
|
}
|
|
52511
53433
|
const appliedAt = receiptFromRow3(applyRow).created_at;
|
|
52512
53434
|
const expectedPayloads = new Map;
|
|
53435
|
+
const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
53436
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
53437
|
+
LIMIT 1`, [this.service, applyResult.graph.plan_id]);
|
|
53438
|
+
const actualPlan = planRow.rows[0] ? parseJson2(planRow.rows[0]["payload"]) : null;
|
|
53439
|
+
const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
|
|
53440
|
+
planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
|
|
52513
53441
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
52514
53442
|
type: "plans",
|
|
52515
|
-
payload: canonicalJson(
|
|
53443
|
+
payload: canonicalJson(planExpected)
|
|
52516
53444
|
});
|
|
52517
53445
|
for (const task3 of manifest.tasks)
|
|
52518
53446
|
expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
|
|
@@ -52612,14 +53540,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52612
53540
|
const readback = await this.readback(tx, applyResult.graph);
|
|
52613
53541
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
52614
53542
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
52615
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
52616
|
-
request_digest,
|
|
52617
|
-
|
|
53543
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
53544
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
53545
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
53546
|
+
) 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)`, [
|
|
52618
53547
|
compensationReceiptId,
|
|
52619
53548
|
this.tenantId,
|
|
52620
53549
|
receipt.operation_id,
|
|
53550
|
+
receipt.step_id,
|
|
52621
53551
|
input.idempotency_key,
|
|
52622
53552
|
requestDigest,
|
|
53553
|
+
input.precondition_digest,
|
|
52623
53554
|
receipt.result_digest,
|
|
52624
53555
|
receipt.binding_version,
|
|
52625
53556
|
input.receipt_id,
|
|
@@ -52676,36 +53607,83 @@ function resolveTenantId(value) {
|
|
|
52676
53607
|
}
|
|
52677
53608
|
return tenantId;
|
|
52678
53609
|
}
|
|
53610
|
+
function taskManifestRequestDigest(manifest) {
|
|
53611
|
+
const { idempotency_key: _idempotencyKey, ...request } = manifest;
|
|
53612
|
+
return canonicalDigest(request);
|
|
53613
|
+
}
|
|
53614
|
+
function taskManifestCompensationRequestDigest(request) {
|
|
53615
|
+
return canonicalDigest(request);
|
|
53616
|
+
}
|
|
53617
|
+
function deriveTodosTaskManifestApplyPreconditionDigest(input) {
|
|
53618
|
+
return canonicalDigest({
|
|
53619
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
53620
|
+
direction: "apply",
|
|
53621
|
+
operation_id: input.operation_id,
|
|
53622
|
+
step_id: input.step_id,
|
|
53623
|
+
project_id: input.project_id,
|
|
53624
|
+
task_list_id: input.task_list_id ?? null,
|
|
53625
|
+
expected_binding_version: input.if_binding_version ?? 0
|
|
53626
|
+
});
|
|
53627
|
+
}
|
|
53628
|
+
function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
|
|
53629
|
+
return canonicalDigest({
|
|
53630
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
53631
|
+
direction: "compensate",
|
|
53632
|
+
operation_id: input.operation_id,
|
|
53633
|
+
step_id: input.step_id,
|
|
53634
|
+
apply_receipt_id: input.receipt_id,
|
|
53635
|
+
expected_binding_version: input.if_binding_version
|
|
53636
|
+
});
|
|
53637
|
+
}
|
|
53638
|
+
function deriveTodosTaskManifestIdempotencyKey(input) {
|
|
53639
|
+
return `tmk_${canonicalDigest({
|
|
53640
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
53641
|
+
...input
|
|
53642
|
+
}).slice(0, 48)}`;
|
|
53643
|
+
}
|
|
52679
53644
|
function normalize(input, now4) {
|
|
52680
53645
|
const parsed = parseTodosTaskManifest(input);
|
|
52681
53646
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
52682
53647
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
52683
53648
|
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 });
|
|
52684
53649
|
}
|
|
53650
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
53651
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
52685
53652
|
const manifest = sanitizeManifest(parsed);
|
|
53653
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
|
|
53654
|
+
if (manifest.precondition_digest !== expectedPreconditionDigest) {
|
|
53655
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
53656
|
+
}
|
|
53657
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
53658
|
+
operation_id: manifest.operation_id,
|
|
53659
|
+
step_id: manifest.step_id,
|
|
53660
|
+
direction: "apply",
|
|
53661
|
+
target_selector: manifest.project_id,
|
|
53662
|
+
request_digest,
|
|
53663
|
+
precondition_digest: manifest.precondition_digest
|
|
53664
|
+
});
|
|
52686
53665
|
const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
|
|
52687
53666
|
task3.key,
|
|
52688
|
-
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task3.key)
|
|
53667
|
+
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task3.key)
|
|
52689
53668
|
]));
|
|
52690
53669
|
const graph = {
|
|
52691
|
-
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
|
|
53670
|
+
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
|
|
52692
53671
|
task_ids,
|
|
52693
|
-
comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task3.key, String(index)))),
|
|
52694
|
-
verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task3.key, String(index)))),
|
|
53672
|
+
comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "comment", task3.key, String(index)))),
|
|
53673
|
+
verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "verification", task3.key, String(index)))),
|
|
52695
53674
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
52696
53675
|
};
|
|
52697
|
-
const request_digest = canonicalDigest(parsed);
|
|
52698
53676
|
const effectInputs = [
|
|
52699
53677
|
{
|
|
52700
53678
|
topic: "todos.task-manifest.applied",
|
|
52701
|
-
payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
|
|
53679
|
+
payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
|
|
52702
53680
|
},
|
|
52703
53681
|
...manifest.effects ?? []
|
|
52704
53682
|
];
|
|
52705
53683
|
const outbox = effectInputs.map((effect2, index) => {
|
|
52706
53684
|
const payload = { ...effect2.payload };
|
|
52707
53685
|
return {
|
|
52708
|
-
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
|
|
53686
|
+
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
|
|
52709
53687
|
topic: effect2.topic,
|
|
52710
53688
|
payload,
|
|
52711
53689
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -52715,11 +53693,14 @@ function normalize(input, now4) {
|
|
|
52715
53693
|
return {
|
|
52716
53694
|
manifest,
|
|
52717
53695
|
request_digest,
|
|
53696
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
52718
53697
|
result_digest,
|
|
52719
|
-
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
|
|
53698
|
+
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
53699
|
+
terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
52720
53700
|
graph,
|
|
52721
53701
|
outbox,
|
|
52722
|
-
now: now4
|
|
53702
|
+
now: now4,
|
|
53703
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
52723
53704
|
};
|
|
52724
53705
|
}
|
|
52725
53706
|
function sanitizeManifest(manifest) {
|
|
@@ -52777,6 +53758,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
52777
53758
|
tenant_id: this.tenantId,
|
|
52778
53759
|
backend: this.backend.kind,
|
|
52779
53760
|
deterministic_ids: true,
|
|
53761
|
+
operation_step_identity: true,
|
|
53762
|
+
deterministic_idempotency_keys: true,
|
|
53763
|
+
terminal_nonacceptance_receipts: true,
|
|
53764
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
52780
53765
|
immutable_receipts: true,
|
|
52781
53766
|
transactional_outbox: true,
|
|
52782
53767
|
idempotent_outbox_delivery: true,
|
|
@@ -52806,7 +53791,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
52806
53791
|
async apply(input) {
|
|
52807
53792
|
const normalized = normalize(input, this.now());
|
|
52808
53793
|
const faults = await this.prepareFaults();
|
|
52809
|
-
|
|
53794
|
+
const result = this.bounded(await this.backend.apply(normalized, faults));
|
|
53795
|
+
if (result.receipt.outcome === "terminal_nonacceptance") {
|
|
53796
|
+
throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
|
|
53797
|
+
}
|
|
53798
|
+
return result;
|
|
52810
53799
|
}
|
|
52811
53800
|
readExact(receiptId2) {
|
|
52812
53801
|
if (!receiptId2 || receiptId2.length > 200) {
|
|
@@ -52839,18 +53828,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
52839
53828
|
async compensate(input) {
|
|
52840
53829
|
const request = parseTodosTaskManifestCompensation(input);
|
|
52841
53830
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
52842
|
-
|
|
52843
|
-
|
|
53831
|
+
if (applied.receipt.outcome !== "accepted") {
|
|
53832
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
|
|
53833
|
+
}
|
|
53834
|
+
if (request.operation_id !== applied.receipt.operation_id) {
|
|
53835
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
|
|
53836
|
+
}
|
|
53837
|
+
if (request.step_id === applied.receipt.step_id) {
|
|
53838
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
|
|
53839
|
+
}
|
|
53840
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
|
|
53841
|
+
if (request.precondition_digest !== expectedPreconditionDigest) {
|
|
53842
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
53843
|
+
}
|
|
53844
|
+
const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
|
|
53845
|
+
const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
|
|
53846
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
53847
|
+
operation_id: request.operation_id,
|
|
53848
|
+
step_id: request.step_id,
|
|
53849
|
+
direction: "compensate",
|
|
53850
|
+
target_selector: request.receipt_id,
|
|
53851
|
+
request_digest: requestDigest,
|
|
53852
|
+
precondition_digest: request.precondition_digest
|
|
53853
|
+
});
|
|
53854
|
+
if (request.idempotency_key !== expectedIdempotencyKey) {
|
|
53855
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
|
|
53856
|
+
}
|
|
53857
|
+
const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
|
|
52844
53858
|
const receipt = {
|
|
52845
53859
|
receipt_id: compensationReceiptId,
|
|
52846
53860
|
authority: "todos",
|
|
52847
53861
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
52848
53862
|
schema_version: 1,
|
|
52849
53863
|
kind: "compensate",
|
|
52850
|
-
operation_id:
|
|
53864
|
+
operation_id: request.operation_id,
|
|
53865
|
+
step_id: request.step_id,
|
|
52851
53866
|
idempotency_key: request.idempotency_key,
|
|
52852
53867
|
request_digest: requestDigest,
|
|
53868
|
+
precondition_digest: request.precondition_digest,
|
|
52853
53869
|
result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
|
|
53870
|
+
outcome: "accepted",
|
|
53871
|
+
reason: null,
|
|
53872
|
+
duplicate_of_receipt_id: null,
|
|
52854
53873
|
binding_version: request.if_binding_version + 1,
|
|
52855
53874
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
52856
53875
|
created_at: this.now()
|
|
@@ -54516,9 +55535,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
54516
55535
|
TaskManifestBounds: taskManifestBoundsSchema,
|
|
54517
55536
|
TaskManifestCapability: taskManifestCapabilitySchema,
|
|
54518
55537
|
TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
|
|
55538
|
+
TaskManifest: taskManifestSchema,
|
|
55539
|
+
TaskManifestReceipt: taskManifestReceiptSchema,
|
|
55540
|
+
TaskManifestApplyResult: taskManifestApplyResultSchema,
|
|
55541
|
+
TaskManifestApplyResponse: taskManifestApplyResponseSchema,
|
|
55542
|
+
TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
|
|
55543
|
+
TaskManifestCompensationResult: taskManifestCompensationResultSchema,
|
|
55544
|
+
TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
|
|
55545
|
+
TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
|
|
54519
55546
|
TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
|
|
54520
55547
|
TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
|
|
54521
55548
|
TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
|
|
55549
|
+
ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
|
|
55550
|
+
ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
|
|
55551
|
+
ProjectRegistrationRequest: projectRegistrationRequestSchema,
|
|
55552
|
+
ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
|
|
55553
|
+
PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
|
|
55554
|
+
ProjectResource: projectResourceSchema,
|
|
55555
|
+
ProjectResourcePage: projectResourcePageSchema,
|
|
54522
55556
|
TaskList: taskListSchema,
|
|
54523
55557
|
ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
|
|
54524
55558
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
@@ -54561,6 +55595,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
54561
55595
|
priority: { type: "string", enum: [...TASK_PRIORITIES] },
|
|
54562
55596
|
assigned_to: { type: "string" },
|
|
54563
55597
|
project_id: { type: "string", nullable: true },
|
|
55598
|
+
parent_id: { type: "string", nullable: true },
|
|
54564
55599
|
plan_id: { type: "string", nullable: true },
|
|
54565
55600
|
task_list_id: { type: "string", nullable: true },
|
|
54566
55601
|
version: { type: "number" }
|
|
@@ -55515,6 +56550,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55515
56550
|
},
|
|
55516
56551
|
security: [{ apiKey: [] }],
|
|
55517
56552
|
paths: {
|
|
56553
|
+
"/v1/project-registration/capability": {
|
|
56554
|
+
get: {
|
|
56555
|
+
operationId: "getProjectRegistrationCapability",
|
|
56556
|
+
summary: "Read the live package-owned Projects to Todos registration capability",
|
|
56557
|
+
responses: {
|
|
56558
|
+
"200": {
|
|
56559
|
+
content: {
|
|
56560
|
+
"application/json": {
|
|
56561
|
+
schema: {
|
|
56562
|
+
type: "object",
|
|
56563
|
+
additionalProperties: false,
|
|
56564
|
+
required: ["capability"],
|
|
56565
|
+
properties: {
|
|
56566
|
+
capability: {
|
|
56567
|
+
$ref: "#/components/schemas/ProjectRegistrationCapability"
|
|
56568
|
+
}
|
|
56569
|
+
}
|
|
56570
|
+
}
|
|
56571
|
+
}
|
|
56572
|
+
}
|
|
56573
|
+
}
|
|
56574
|
+
}
|
|
56575
|
+
}
|
|
56576
|
+
},
|
|
56577
|
+
"/v1/project-registration/resources": {
|
|
56578
|
+
get: {
|
|
56579
|
+
operationId: "listProjectRegistrationResources",
|
|
56580
|
+
summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
|
|
56581
|
+
parameters: [
|
|
56582
|
+
{
|
|
56583
|
+
name: "source_project_id",
|
|
56584
|
+
in: "query",
|
|
56585
|
+
required: true,
|
|
56586
|
+
schema: { type: "string" }
|
|
56587
|
+
},
|
|
56588
|
+
{
|
|
56589
|
+
name: "include_anchors",
|
|
56590
|
+
in: "query",
|
|
56591
|
+
schema: { type: "boolean", default: false }
|
|
56592
|
+
},
|
|
56593
|
+
{
|
|
56594
|
+
name: "limit",
|
|
56595
|
+
in: "query",
|
|
56596
|
+
schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
|
|
56597
|
+
},
|
|
56598
|
+
{
|
|
56599
|
+
name: "cursor",
|
|
56600
|
+
in: "query",
|
|
56601
|
+
schema: { type: "string" }
|
|
56602
|
+
}
|
|
56603
|
+
],
|
|
56604
|
+
responses: {
|
|
56605
|
+
"200": {
|
|
56606
|
+
content: {
|
|
56607
|
+
"application/json": {
|
|
56608
|
+
schema: {
|
|
56609
|
+
type: "object",
|
|
56610
|
+
additionalProperties: false,
|
|
56611
|
+
required: ["page"],
|
|
56612
|
+
properties: {
|
|
56613
|
+
page: { $ref: "#/components/schemas/ProjectResourcePage" }
|
|
56614
|
+
}
|
|
56615
|
+
}
|
|
56616
|
+
}
|
|
56617
|
+
}
|
|
56618
|
+
},
|
|
56619
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56620
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56621
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56622
|
+
}
|
|
56623
|
+
}
|
|
56624
|
+
},
|
|
56625
|
+
"/v1/project-registration/create": {
|
|
56626
|
+
post: {
|
|
56627
|
+
operationId: "createProjectRegistrationResource",
|
|
56628
|
+
summary: "Create or deterministically bind one Projects to Todos resource",
|
|
56629
|
+
requestBody: {
|
|
56630
|
+
required: true,
|
|
56631
|
+
content: {
|
|
56632
|
+
"application/json": {
|
|
56633
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
56634
|
+
}
|
|
56635
|
+
}
|
|
56636
|
+
},
|
|
56637
|
+
responses: {
|
|
56638
|
+
"201": {
|
|
56639
|
+
content: {
|
|
56640
|
+
"application/json": {
|
|
56641
|
+
schema: {
|
|
56642
|
+
type: "object",
|
|
56643
|
+
additionalProperties: false,
|
|
56644
|
+
required: ["receipt"],
|
|
56645
|
+
properties: {
|
|
56646
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
|
|
56647
|
+
}
|
|
56648
|
+
}
|
|
56649
|
+
}
|
|
56650
|
+
}
|
|
56651
|
+
},
|
|
56652
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56653
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56654
|
+
}
|
|
56655
|
+
}
|
|
56656
|
+
},
|
|
56657
|
+
"/v1/project-registration/read-exact": {
|
|
56658
|
+
post: {
|
|
56659
|
+
operationId: "readExactProjectRegistrationResource",
|
|
56660
|
+
summary: "Read one registered project or task list by exact full UUID",
|
|
56661
|
+
requestBody: {
|
|
56662
|
+
required: true,
|
|
56663
|
+
content: {
|
|
56664
|
+
"application/json": {
|
|
56665
|
+
schema: {
|
|
56666
|
+
type: "object",
|
|
56667
|
+
additionalProperties: false,
|
|
56668
|
+
required: [
|
|
56669
|
+
"resource_kind",
|
|
56670
|
+
"target_id",
|
|
56671
|
+
"response_byte_limit",
|
|
56672
|
+
"time_budget_ms"
|
|
56673
|
+
],
|
|
56674
|
+
properties: {
|
|
56675
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
56676
|
+
target_id: { type: "string", format: "uuid" },
|
|
56677
|
+
...projectRegistrationBoundsProperties
|
|
56678
|
+
}
|
|
56679
|
+
}
|
|
56680
|
+
}
|
|
56681
|
+
}
|
|
56682
|
+
},
|
|
56683
|
+
responses: {
|
|
56684
|
+
"200": {
|
|
56685
|
+
content: {
|
|
56686
|
+
"application/json": {
|
|
56687
|
+
schema: {
|
|
56688
|
+
type: "object",
|
|
56689
|
+
additionalProperties: false,
|
|
56690
|
+
required: ["record"],
|
|
56691
|
+
properties: {
|
|
56692
|
+
record: {
|
|
56693
|
+
type: "object",
|
|
56694
|
+
additionalProperties: false,
|
|
56695
|
+
required: ["target_id", "revision", "digest"],
|
|
56696
|
+
properties: {
|
|
56697
|
+
target_id: { type: "string", format: "uuid" },
|
|
56698
|
+
revision: { type: "string" },
|
|
56699
|
+
digest: { type: "string" }
|
|
56700
|
+
}
|
|
56701
|
+
}
|
|
56702
|
+
}
|
|
56703
|
+
}
|
|
56704
|
+
}
|
|
56705
|
+
}
|
|
56706
|
+
},
|
|
56707
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56708
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56709
|
+
}
|
|
56710
|
+
}
|
|
56711
|
+
},
|
|
56712
|
+
"/v1/project-registration/receipts/lookup": {
|
|
56713
|
+
post: {
|
|
56714
|
+
operationId: "lookupProjectRegistrationReceipt",
|
|
56715
|
+
summary: "Recover one exact immutable terminal registration receipt",
|
|
56716
|
+
requestBody: {
|
|
56717
|
+
required: true,
|
|
56718
|
+
content: {
|
|
56719
|
+
"application/json": {
|
|
56720
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
|
|
56721
|
+
}
|
|
56722
|
+
}
|
|
56723
|
+
},
|
|
56724
|
+
responses: {
|
|
56725
|
+
"200": {
|
|
56726
|
+
content: {
|
|
56727
|
+
"application/json": {
|
|
56728
|
+
schema: {
|
|
56729
|
+
type: "object",
|
|
56730
|
+
additionalProperties: false,
|
|
56731
|
+
required: ["receipt", "response_control"],
|
|
56732
|
+
properties: {
|
|
56733
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
56734
|
+
response_control: {
|
|
56735
|
+
type: "object",
|
|
56736
|
+
additionalProperties: false,
|
|
56737
|
+
required: [
|
|
56738
|
+
"response_byte_limit",
|
|
56739
|
+
"time_budget_ms",
|
|
56740
|
+
"response_bytes",
|
|
56741
|
+
"elapsed_ms",
|
|
56742
|
+
"complete",
|
|
56743
|
+
"truncated"
|
|
56744
|
+
],
|
|
56745
|
+
properties: {
|
|
56746
|
+
...projectRegistrationBoundsProperties,
|
|
56747
|
+
response_bytes: { type: "integer", minimum: 0 },
|
|
56748
|
+
elapsed_ms: { type: "integer", minimum: 0 },
|
|
56749
|
+
complete: { type: "boolean", enum: [true] },
|
|
56750
|
+
truncated: { type: "boolean", enum: [false] }
|
|
56751
|
+
}
|
|
56752
|
+
}
|
|
56753
|
+
}
|
|
56754
|
+
}
|
|
56755
|
+
}
|
|
56756
|
+
}
|
|
56757
|
+
},
|
|
56758
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56759
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56760
|
+
}
|
|
56761
|
+
}
|
|
56762
|
+
},
|
|
56763
|
+
"/v1/project-registration/validate-prior-adoption": {
|
|
56764
|
+
post: {
|
|
56765
|
+
operationId: "validatePriorRegistrationAdoption",
|
|
56766
|
+
summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
|
|
56767
|
+
requestBody: {
|
|
56768
|
+
required: true,
|
|
56769
|
+
content: {
|
|
56770
|
+
"application/json": {
|
|
56771
|
+
schema: {
|
|
56772
|
+
type: "object",
|
|
56773
|
+
additionalProperties: false,
|
|
56774
|
+
required: ["source_request", "source_receipt", "current_record"],
|
|
56775
|
+
properties: {
|
|
56776
|
+
source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
|
|
56777
|
+
source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
56778
|
+
current_record: {
|
|
56779
|
+
oneOf: [
|
|
56780
|
+
{ $ref: "#/components/schemas/Project" },
|
|
56781
|
+
{ $ref: "#/components/schemas/TaskList" }
|
|
56782
|
+
]
|
|
56783
|
+
}
|
|
56784
|
+
}
|
|
56785
|
+
}
|
|
56786
|
+
}
|
|
56787
|
+
}
|
|
56788
|
+
},
|
|
56789
|
+
responses: {
|
|
56790
|
+
"200": {
|
|
56791
|
+
content: {
|
|
56792
|
+
"application/json": {
|
|
56793
|
+
schema: {
|
|
56794
|
+
type: "object",
|
|
56795
|
+
additionalProperties: false,
|
|
56796
|
+
required: ["validation"],
|
|
56797
|
+
properties: {
|
|
56798
|
+
validation: {
|
|
56799
|
+
$ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
|
|
56800
|
+
}
|
|
56801
|
+
}
|
|
56802
|
+
}
|
|
56803
|
+
}
|
|
56804
|
+
}
|
|
56805
|
+
},
|
|
56806
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56807
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56808
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56809
|
+
}
|
|
56810
|
+
}
|
|
56811
|
+
},
|
|
56812
|
+
"/v1/project-registration/compensate": {
|
|
56813
|
+
post: {
|
|
56814
|
+
operationId: "compensateProjectRegistrationResource",
|
|
56815
|
+
summary: "Conditionally remove an unchanged receipt-owned registration resource",
|
|
56816
|
+
requestBody: {
|
|
56817
|
+
required: true,
|
|
56818
|
+
content: {
|
|
56819
|
+
"application/json": {
|
|
56820
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
56821
|
+
}
|
|
56822
|
+
}
|
|
56823
|
+
},
|
|
56824
|
+
responses: {
|
|
56825
|
+
"201": {
|
|
56826
|
+
content: {
|
|
56827
|
+
"application/json": {
|
|
56828
|
+
schema: {
|
|
56829
|
+
type: "object",
|
|
56830
|
+
additionalProperties: false,
|
|
56831
|
+
required: ["receipt"],
|
|
56832
|
+
properties: {
|
|
56833
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
|
|
56834
|
+
}
|
|
56835
|
+
}
|
|
56836
|
+
}
|
|
56837
|
+
}
|
|
56838
|
+
},
|
|
56839
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56840
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56841
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56842
|
+
}
|
|
56843
|
+
}
|
|
56844
|
+
},
|
|
56845
|
+
"/v1/project-registration/verify-inverse": {
|
|
56846
|
+
post: {
|
|
56847
|
+
operationId: "verifyInverseProjectRegistrationResource",
|
|
56848
|
+
summary: "Verify exact absence after conditional registration compensation",
|
|
56849
|
+
requestBody: {
|
|
56850
|
+
required: true,
|
|
56851
|
+
content: {
|
|
56852
|
+
"application/json": {
|
|
56853
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
56854
|
+
}
|
|
56855
|
+
}
|
|
56856
|
+
},
|
|
56857
|
+
responses: {
|
|
56858
|
+
"200": {
|
|
56859
|
+
content: {
|
|
56860
|
+
"application/json": {
|
|
56861
|
+
schema: {
|
|
56862
|
+
type: "object",
|
|
56863
|
+
additionalProperties: false,
|
|
56864
|
+
required: ["verification"],
|
|
56865
|
+
properties: {
|
|
56866
|
+
verification: {
|
|
56867
|
+
type: "object",
|
|
56868
|
+
additionalProperties: false,
|
|
56869
|
+
required: ["target_id", "accepted_receipt_id", "absent", "digest"],
|
|
56870
|
+
properties: {
|
|
56871
|
+
target_id: { type: "string", format: "uuid" },
|
|
56872
|
+
accepted_receipt_id: { type: "string" },
|
|
56873
|
+
absent: { type: "boolean", enum: [true] },
|
|
56874
|
+
digest: { type: "string" }
|
|
56875
|
+
}
|
|
56876
|
+
}
|
|
56877
|
+
}
|
|
56878
|
+
}
|
|
56879
|
+
}
|
|
56880
|
+
}
|
|
56881
|
+
},
|
|
56882
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56883
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56884
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56885
|
+
}
|
|
56886
|
+
}
|
|
56887
|
+
},
|
|
55518
56888
|
"/v1/task-manifest/capability": {
|
|
55519
56889
|
get: {
|
|
55520
56890
|
operationId: "getTaskManifestCapability",
|
|
@@ -55532,6 +56902,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55532
56902
|
}
|
|
55533
56903
|
}
|
|
55534
56904
|
},
|
|
56905
|
+
"/v1/task-manifest/apply": {
|
|
56906
|
+
post: {
|
|
56907
|
+
operationId: "applyTaskManifest",
|
|
56908
|
+
summary: "Apply one exact task-manifest graph through the Todos authority",
|
|
56909
|
+
requestBody: {
|
|
56910
|
+
required: true,
|
|
56911
|
+
content: {
|
|
56912
|
+
"application/json": {
|
|
56913
|
+
schema: { $ref: "#/components/schemas/TaskManifest" }
|
|
56914
|
+
}
|
|
56915
|
+
}
|
|
56916
|
+
},
|
|
56917
|
+
responses: {
|
|
56918
|
+
"201": {
|
|
56919
|
+
content: {
|
|
56920
|
+
"application/json": {
|
|
56921
|
+
schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
|
|
56922
|
+
}
|
|
56923
|
+
}
|
|
56924
|
+
},
|
|
56925
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56926
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56927
|
+
"503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56928
|
+
}
|
|
56929
|
+
}
|
|
56930
|
+
},
|
|
56931
|
+
"/v1/task-manifest/read-exact": {
|
|
56932
|
+
post: {
|
|
56933
|
+
operationId: "readExactTaskManifest",
|
|
56934
|
+
summary: "Read one exact immutable task-manifest apply receipt",
|
|
56935
|
+
requestBody: {
|
|
56936
|
+
required: true,
|
|
56937
|
+
content: {
|
|
56938
|
+
"application/json": {
|
|
56939
|
+
schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
|
|
56940
|
+
}
|
|
56941
|
+
}
|
|
56942
|
+
},
|
|
56943
|
+
responses: {
|
|
56944
|
+
"200": {
|
|
56945
|
+
content: {
|
|
56946
|
+
"application/json": {
|
|
56947
|
+
schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
|
|
56948
|
+
}
|
|
56949
|
+
}
|
|
56950
|
+
},
|
|
56951
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56952
|
+
}
|
|
56953
|
+
}
|
|
56954
|
+
},
|
|
56955
|
+
"/v1/task-manifest/compensate": {
|
|
56956
|
+
post: {
|
|
56957
|
+
operationId: "compensateTaskManifest",
|
|
56958
|
+
summary: "Compensate one exact untouched task-manifest graph with CAS protection",
|
|
56959
|
+
requestBody: {
|
|
56960
|
+
required: true,
|
|
56961
|
+
content: {
|
|
56962
|
+
"application/json": {
|
|
56963
|
+
schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
|
|
56964
|
+
}
|
|
56965
|
+
}
|
|
56966
|
+
},
|
|
56967
|
+
responses: {
|
|
56968
|
+
"201": {
|
|
56969
|
+
content: {
|
|
56970
|
+
"application/json": {
|
|
56971
|
+
schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
|
|
56972
|
+
}
|
|
56973
|
+
}
|
|
56974
|
+
},
|
|
56975
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56976
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56977
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56978
|
+
}
|
|
56979
|
+
}
|
|
56980
|
+
},
|
|
55535
56981
|
"/v1/task-manifest/bindings/lookup": {
|
|
55536
56982
|
post: {
|
|
55537
56983
|
operationId: "lookupTaskManifestBinding",
|
|
@@ -56392,7 +57838,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
56392
57838
|
}
|
|
56393
57839
|
});
|
|
56394
57840
|
}
|
|
56395
|
-
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;
|
|
57841
|
+
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;
|
|
56396
57842
|
var init_openapi = __esm(() => {
|
|
56397
57843
|
init_package_version();
|
|
56398
57844
|
init_types();
|
|
@@ -56454,6 +57900,10 @@ var init_openapi = __esm(() => {
|
|
|
56454
57900
|
"tenant_id",
|
|
56455
57901
|
"backend",
|
|
56456
57902
|
"deterministic_ids",
|
|
57903
|
+
"operation_step_identity",
|
|
57904
|
+
"deterministic_idempotency_keys",
|
|
57905
|
+
"terminal_nonacceptance_receipts",
|
|
57906
|
+
"plan_slug_provenance",
|
|
56457
57907
|
"immutable_receipts",
|
|
56458
57908
|
"transactional_outbox",
|
|
56459
57909
|
"idempotent_outbox_delivery",
|
|
@@ -56469,6 +57919,10 @@ var init_openapi = __esm(() => {
|
|
|
56469
57919
|
tenant_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
56470
57920
|
backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
|
|
56471
57921
|
deterministic_ids: { type: "boolean", enum: [true] },
|
|
57922
|
+
operation_step_identity: { type: "boolean", enum: [true] },
|
|
57923
|
+
deterministic_idempotency_keys: { type: "boolean", enum: [true] },
|
|
57924
|
+
terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
|
|
57925
|
+
plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
|
|
56472
57926
|
immutable_receipts: { type: "boolean", enum: [true] },
|
|
56473
57927
|
transactional_outbox: { type: "boolean", enum: [true] },
|
|
56474
57928
|
idempotent_outbox_delivery: { type: "boolean", enum: [true] },
|
|
@@ -56522,6 +57976,8 @@ var init_openapi = __esm(() => {
|
|
|
56522
57976
|
"schema_version",
|
|
56523
57977
|
"tenant_id",
|
|
56524
57978
|
"plan_id",
|
|
57979
|
+
"operation_id",
|
|
57980
|
+
"step_id",
|
|
56525
57981
|
"apply_receipt_id",
|
|
56526
57982
|
"binding_version",
|
|
56527
57983
|
"state"
|
|
@@ -56532,11 +57988,169 @@ var init_openapi = __esm(() => {
|
|
|
56532
57988
|
schema_version: { type: "integer", enum: [1] },
|
|
56533
57989
|
tenant_id: { type: "string" },
|
|
56534
57990
|
plan_id: { type: "string", format: "uuid" },
|
|
57991
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
57992
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
56535
57993
|
apply_receipt_id: { type: "string", format: "uuid" },
|
|
56536
57994
|
binding_version: { type: "integer", minimum: 1 },
|
|
56537
57995
|
state: { type: "string", enum: ["applied", "compensated"] }
|
|
56538
57996
|
}
|
|
56539
57997
|
};
|
|
57998
|
+
taskManifestSchema = {
|
|
57999
|
+
type: "object",
|
|
58000
|
+
additionalProperties: false,
|
|
58001
|
+
required: [
|
|
58002
|
+
"version",
|
|
58003
|
+
"operation_id",
|
|
58004
|
+
"step_id",
|
|
58005
|
+
"idempotency_key",
|
|
58006
|
+
"precondition_digest",
|
|
58007
|
+
"project_id",
|
|
58008
|
+
"plan",
|
|
58009
|
+
"tasks"
|
|
58010
|
+
],
|
|
58011
|
+
properties: {
|
|
58012
|
+
version: { type: "integer", enum: [1] },
|
|
58013
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
58014
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
58015
|
+
idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
|
|
58016
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58017
|
+
project_id: { type: "string", format: "uuid" },
|
|
58018
|
+
task_list_id: { type: "string", format: "uuid" },
|
|
58019
|
+
if_binding_version: { type: "integer", minimum: 0 },
|
|
58020
|
+
plan: {
|
|
58021
|
+
type: "object",
|
|
58022
|
+
additionalProperties: false,
|
|
58023
|
+
required: ["key", "name"],
|
|
58024
|
+
properties: {
|
|
58025
|
+
key: { type: "string", minLength: 1, maxLength: 200 },
|
|
58026
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
58027
|
+
description: { type: "string" },
|
|
58028
|
+
status: { type: "string", enum: ["active", "completed", "archived"] }
|
|
58029
|
+
}
|
|
58030
|
+
},
|
|
58031
|
+
tasks: {
|
|
58032
|
+
type: "array",
|
|
58033
|
+
minItems: 1,
|
|
58034
|
+
items: {
|
|
58035
|
+
type: "object",
|
|
58036
|
+
additionalProperties: false,
|
|
58037
|
+
required: ["key", "title"],
|
|
58038
|
+
properties: {
|
|
58039
|
+
key: { type: "string", minLength: 1, maxLength: 200 },
|
|
58040
|
+
title: { type: "string", minLength: 1, maxLength: 200 },
|
|
58041
|
+
description: { type: "string" },
|
|
58042
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
|
|
58043
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
58044
|
+
assigned_to: { type: "string" },
|
|
58045
|
+
created_by: { type: "string" },
|
|
58046
|
+
tags: { type: "array", items: { type: "string" } },
|
|
58047
|
+
metadata: { type: "object", additionalProperties: true },
|
|
58048
|
+
comments: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
58049
|
+
verifications: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
58050
|
+
}
|
|
58051
|
+
}
|
|
58052
|
+
},
|
|
58053
|
+
dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
58054
|
+
effects: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
58055
|
+
}
|
|
58056
|
+
};
|
|
58057
|
+
taskManifestReceiptSchema = {
|
|
58058
|
+
type: "object",
|
|
58059
|
+
additionalProperties: false,
|
|
58060
|
+
required: [
|
|
58061
|
+
"receipt_id",
|
|
58062
|
+
"authority",
|
|
58063
|
+
"route",
|
|
58064
|
+
"schema_version",
|
|
58065
|
+
"kind",
|
|
58066
|
+
"operation_id",
|
|
58067
|
+
"step_id",
|
|
58068
|
+
"idempotency_key",
|
|
58069
|
+
"request_digest",
|
|
58070
|
+
"precondition_digest",
|
|
58071
|
+
"result_digest",
|
|
58072
|
+
"outcome",
|
|
58073
|
+
"reason",
|
|
58074
|
+
"duplicate_of_receipt_id",
|
|
58075
|
+
"binding_version",
|
|
58076
|
+
"apply_receipt_id",
|
|
58077
|
+
"created_at"
|
|
58078
|
+
],
|
|
58079
|
+
properties: {
|
|
58080
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
58081
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58082
|
+
route: { type: "string", enum: ["todos.task-manifest.v1"] },
|
|
58083
|
+
schema_version: { type: "integer", enum: [1] },
|
|
58084
|
+
kind: { type: "string", enum: ["apply", "compensate"] },
|
|
58085
|
+
operation_id: { type: "string" },
|
|
58086
|
+
step_id: { type: "string" },
|
|
58087
|
+
idempotency_key: { type: "string" },
|
|
58088
|
+
request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58089
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58090
|
+
result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58091
|
+
outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
|
|
58092
|
+
reason: { type: "string", nullable: true },
|
|
58093
|
+
duplicate_of_receipt_id: { type: "string", nullable: true },
|
|
58094
|
+
binding_version: { type: "integer", minimum: 0 },
|
|
58095
|
+
apply_receipt_id: { type: "string", nullable: true },
|
|
58096
|
+
created_at: { type: "string", format: "date-time" }
|
|
58097
|
+
}
|
|
58098
|
+
};
|
|
58099
|
+
taskManifestApplyResultSchema = {
|
|
58100
|
+
type: "object",
|
|
58101
|
+
additionalProperties: false,
|
|
58102
|
+
required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
|
|
58103
|
+
properties: {
|
|
58104
|
+
duplicate: { type: "boolean" },
|
|
58105
|
+
receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
|
|
58106
|
+
graph: { type: "object", additionalProperties: true },
|
|
58107
|
+
readback: { type: "object", additionalProperties: true },
|
|
58108
|
+
outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
|
|
58109
|
+
result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
|
|
58110
|
+
}
|
|
58111
|
+
};
|
|
58112
|
+
taskManifestApplyResponseSchema = {
|
|
58113
|
+
type: "object",
|
|
58114
|
+
additionalProperties: false,
|
|
58115
|
+
required: ["result"],
|
|
58116
|
+
properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
|
|
58117
|
+
};
|
|
58118
|
+
taskManifestCompensateRequestSchema = {
|
|
58119
|
+
type: "object",
|
|
58120
|
+
additionalProperties: false,
|
|
58121
|
+
required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
|
|
58122
|
+
properties: {
|
|
58123
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
58124
|
+
operation_id: { type: "string" },
|
|
58125
|
+
step_id: { type: "string" },
|
|
58126
|
+
idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
|
|
58127
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58128
|
+
if_binding_version: { type: "integer", minimum: 1 }
|
|
58129
|
+
}
|
|
58130
|
+
};
|
|
58131
|
+
taskManifestCompensationResultSchema = {
|
|
58132
|
+
type: "object",
|
|
58133
|
+
additionalProperties: false,
|
|
58134
|
+
required: ["duplicate", "receipt", "absent", "readback"],
|
|
58135
|
+
properties: {
|
|
58136
|
+
duplicate: { type: "boolean" },
|
|
58137
|
+
receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
|
|
58138
|
+
absent: { type: "boolean", enum: [true] },
|
|
58139
|
+
readback: { type: "object", additionalProperties: true }
|
|
58140
|
+
}
|
|
58141
|
+
};
|
|
58142
|
+
taskManifestCompensateResponseSchema = {
|
|
58143
|
+
type: "object",
|
|
58144
|
+
additionalProperties: false,
|
|
58145
|
+
required: ["result"],
|
|
58146
|
+
properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
|
|
58147
|
+
};
|
|
58148
|
+
taskManifestReadExactRequestSchema = {
|
|
58149
|
+
type: "object",
|
|
58150
|
+
additionalProperties: false,
|
|
58151
|
+
required: ["receipt_id"],
|
|
58152
|
+
properties: { receipt_id: { type: "string", format: "uuid" } }
|
|
58153
|
+
};
|
|
56540
58154
|
taskManifestBindingLookupResponseSchema = {
|
|
56541
58155
|
type: "object",
|
|
56542
58156
|
additionalProperties: false,
|
|
@@ -56886,6 +58500,292 @@ var init_openapi = __esm(() => {
|
|
|
56886
58500
|
metadata: { type: "object", additionalProperties: true }
|
|
56887
58501
|
}
|
|
56888
58502
|
};
|
|
58503
|
+
projectRegistrationBoundsProperties = {
|
|
58504
|
+
response_byte_limit: { type: "integer", minimum: 1 },
|
|
58505
|
+
time_budget_ms: { type: "integer", minimum: 1 }
|
|
58506
|
+
};
|
|
58507
|
+
projectRegistrationReceiptSchema = {
|
|
58508
|
+
type: "object",
|
|
58509
|
+
additionalProperties: false,
|
|
58510
|
+
required: [
|
|
58511
|
+
"receipt_id",
|
|
58512
|
+
"authority",
|
|
58513
|
+
"route",
|
|
58514
|
+
"package_version",
|
|
58515
|
+
"authority_id",
|
|
58516
|
+
"tenant_id",
|
|
58517
|
+
"corpus_id",
|
|
58518
|
+
"operation_id",
|
|
58519
|
+
"step_id",
|
|
58520
|
+
"resource_kind",
|
|
58521
|
+
"direction",
|
|
58522
|
+
"idempotency_key",
|
|
58523
|
+
"request_digest",
|
|
58524
|
+
"precondition_digest",
|
|
58525
|
+
"outcome",
|
|
58526
|
+
"reason",
|
|
58527
|
+
"target_id",
|
|
58528
|
+
"result_revision",
|
|
58529
|
+
"result_digest",
|
|
58530
|
+
"duplicate_of_receipt_id",
|
|
58531
|
+
"accepted_receipt_id",
|
|
58532
|
+
"created_by_operation",
|
|
58533
|
+
"created_at"
|
|
58534
|
+
],
|
|
58535
|
+
properties: {
|
|
58536
|
+
receipt_id: { type: "string" },
|
|
58537
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58538
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
58539
|
+
package_version: { type: "string" },
|
|
58540
|
+
authority_id: { type: "string" },
|
|
58541
|
+
tenant_id: { type: "string" },
|
|
58542
|
+
corpus_id: { type: "string" },
|
|
58543
|
+
operation_id: { type: "string" },
|
|
58544
|
+
step_id: { type: "string" },
|
|
58545
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58546
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
58547
|
+
idempotency_key: { type: "string" },
|
|
58548
|
+
request_digest: { type: "string" },
|
|
58549
|
+
precondition_digest: { type: "string" },
|
|
58550
|
+
outcome: {
|
|
58551
|
+
type: "string",
|
|
58552
|
+
enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
|
|
58553
|
+
},
|
|
58554
|
+
reason: { type: "string", nullable: true },
|
|
58555
|
+
target_id: { type: "string", format: "uuid", nullable: true },
|
|
58556
|
+
result_revision: { type: "string", nullable: true },
|
|
58557
|
+
result_digest: { type: "string", nullable: true },
|
|
58558
|
+
duplicate_of_receipt_id: { type: "string", nullable: true },
|
|
58559
|
+
accepted_receipt_id: { type: "string", nullable: true },
|
|
58560
|
+
created_by_operation: { type: "boolean" },
|
|
58561
|
+
created_at: { type: "string", format: "date-time" }
|
|
58562
|
+
}
|
|
58563
|
+
};
|
|
58564
|
+
projectRegistrationCapabilitySchema = {
|
|
58565
|
+
type: "object",
|
|
58566
|
+
additionalProperties: false,
|
|
58567
|
+
required: [
|
|
58568
|
+
"authority",
|
|
58569
|
+
"route",
|
|
58570
|
+
"package_version",
|
|
58571
|
+
"authority_id",
|
|
58572
|
+
"tenant_id",
|
|
58573
|
+
"corpus_id",
|
|
58574
|
+
"supported_resources",
|
|
58575
|
+
"conditional_create",
|
|
58576
|
+
"immutable_receipts",
|
|
58577
|
+
"exact_terminal_lookup",
|
|
58578
|
+
"exact_readback",
|
|
58579
|
+
"bind_existing_adoption",
|
|
58580
|
+
"prior_registration_adoption_validation",
|
|
58581
|
+
"project_resource_enumeration",
|
|
58582
|
+
"project_resource_page_limit",
|
|
58583
|
+
"conditional_inverse",
|
|
58584
|
+
"ambiguous_outcome_reconciliation"
|
|
58585
|
+
],
|
|
58586
|
+
properties: {
|
|
58587
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58588
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
58589
|
+
package_version: { type: "string" },
|
|
58590
|
+
authority_id: { type: "string" },
|
|
58591
|
+
tenant_id: { type: "string" },
|
|
58592
|
+
corpus_id: { type: "string" },
|
|
58593
|
+
supported_resources: {
|
|
58594
|
+
type: "array",
|
|
58595
|
+
items: { type: "string", enum: ["project", "task_list"] }
|
|
58596
|
+
},
|
|
58597
|
+
conditional_create: { type: "boolean", enum: [true] },
|
|
58598
|
+
immutable_receipts: { type: "boolean", enum: [true] },
|
|
58599
|
+
exact_terminal_lookup: { type: "boolean", enum: [true] },
|
|
58600
|
+
exact_readback: { type: "boolean", enum: [true] },
|
|
58601
|
+
bind_existing_adoption: { type: "boolean", enum: [true] },
|
|
58602
|
+
prior_registration_adoption_validation: { type: "boolean", enum: [true] },
|
|
58603
|
+
project_resource_enumeration: { type: "boolean", enum: [true] },
|
|
58604
|
+
project_resource_page_limit: { type: "integer", minimum: 1 },
|
|
58605
|
+
conditional_inverse: { type: "boolean", enum: [true] },
|
|
58606
|
+
ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
|
|
58607
|
+
}
|
|
58608
|
+
};
|
|
58609
|
+
projectRegistrationRequestSchema = {
|
|
58610
|
+
type: "object",
|
|
58611
|
+
additionalProperties: false,
|
|
58612
|
+
required: [
|
|
58613
|
+
"operation_id",
|
|
58614
|
+
"step_id",
|
|
58615
|
+
"resource_kind",
|
|
58616
|
+
"direction",
|
|
58617
|
+
"authority_route",
|
|
58618
|
+
"package_version",
|
|
58619
|
+
"authority_id",
|
|
58620
|
+
"tenant_id",
|
|
58621
|
+
"corpus_id",
|
|
58622
|
+
"target_selector",
|
|
58623
|
+
"idempotency_key",
|
|
58624
|
+
"request_digest",
|
|
58625
|
+
"precondition_digest",
|
|
58626
|
+
"project_id",
|
|
58627
|
+
"project_slug",
|
|
58628
|
+
"project_name",
|
|
58629
|
+
"desired",
|
|
58630
|
+
"response_byte_limit",
|
|
58631
|
+
"time_budget_ms"
|
|
58632
|
+
],
|
|
58633
|
+
properties: {
|
|
58634
|
+
operation_id: { type: "string" },
|
|
58635
|
+
step_id: { type: "string" },
|
|
58636
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58637
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
58638
|
+
authority_route: { type: "string" },
|
|
58639
|
+
package_version: { type: "string" },
|
|
58640
|
+
authority_id: { type: "string" },
|
|
58641
|
+
tenant_id: { type: "string" },
|
|
58642
|
+
corpus_id: { type: "string" },
|
|
58643
|
+
target_selector: { type: "string" },
|
|
58644
|
+
idempotency_key: { type: "string" },
|
|
58645
|
+
request_digest: { type: "string" },
|
|
58646
|
+
precondition_digest: { type: "string" },
|
|
58647
|
+
project_id: { type: "string" },
|
|
58648
|
+
project_slug: { type: "string" },
|
|
58649
|
+
project_name: { type: "string" },
|
|
58650
|
+
desired: { type: "object", additionalProperties: true },
|
|
58651
|
+
bind_existing: { type: "boolean" },
|
|
58652
|
+
accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
58653
|
+
...projectRegistrationBoundsProperties
|
|
58654
|
+
}
|
|
58655
|
+
};
|
|
58656
|
+
projectRegistrationLookupRequestSchema = {
|
|
58657
|
+
type: "object",
|
|
58658
|
+
additionalProperties: false,
|
|
58659
|
+
required: [
|
|
58660
|
+
"operation_id",
|
|
58661
|
+
"step_id",
|
|
58662
|
+
"resource_kind",
|
|
58663
|
+
"direction",
|
|
58664
|
+
"authority",
|
|
58665
|
+
"authority_route",
|
|
58666
|
+
"package_version",
|
|
58667
|
+
"authority_id",
|
|
58668
|
+
"tenant_id",
|
|
58669
|
+
"corpus_id",
|
|
58670
|
+
"target_selector",
|
|
58671
|
+
"idempotency_key",
|
|
58672
|
+
"max_items",
|
|
58673
|
+
"response_byte_limit",
|
|
58674
|
+
"time_budget_ms"
|
|
58675
|
+
],
|
|
58676
|
+
properties: {
|
|
58677
|
+
operation_id: { type: "string" },
|
|
58678
|
+
step_id: { type: "string" },
|
|
58679
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58680
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
58681
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58682
|
+
authority_route: { type: "string" },
|
|
58683
|
+
package_version: { type: "string" },
|
|
58684
|
+
authority_id: { type: "string" },
|
|
58685
|
+
tenant_id: { type: "string" },
|
|
58686
|
+
corpus_id: { type: "string" },
|
|
58687
|
+
target_selector: { type: "string" },
|
|
58688
|
+
idempotency_key: { type: "string" },
|
|
58689
|
+
target_id: { type: "string", format: "uuid" },
|
|
58690
|
+
max_items: { type: "integer", enum: [1] },
|
|
58691
|
+
...projectRegistrationBoundsProperties
|
|
58692
|
+
}
|
|
58693
|
+
};
|
|
58694
|
+
priorRegistrationAdoptionValidationSchema = {
|
|
58695
|
+
type: "object",
|
|
58696
|
+
additionalProperties: false,
|
|
58697
|
+
required: [
|
|
58698
|
+
"valid",
|
|
58699
|
+
"resource_kind",
|
|
58700
|
+
"target_id",
|
|
58701
|
+
"source_receipt_id",
|
|
58702
|
+
"accepted_receipt_id",
|
|
58703
|
+
"source_outcome",
|
|
58704
|
+
"created_at",
|
|
58705
|
+
"current_revision",
|
|
58706
|
+
"accepted_result_digest"
|
|
58707
|
+
],
|
|
58708
|
+
properties: {
|
|
58709
|
+
valid: { type: "boolean", enum: [true] },
|
|
58710
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58711
|
+
target_id: { type: "string", format: "uuid" },
|
|
58712
|
+
source_receipt_id: { type: "string" },
|
|
58713
|
+
accepted_receipt_id: { type: "string" },
|
|
58714
|
+
source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
|
|
58715
|
+
created_at: { type: "string", format: "date-time" },
|
|
58716
|
+
current_revision: { type: "string", format: "date-time" },
|
|
58717
|
+
accepted_result_digest: { type: "string" }
|
|
58718
|
+
}
|
|
58719
|
+
};
|
|
58720
|
+
projectResourceSchema = {
|
|
58721
|
+
type: "object",
|
|
58722
|
+
additionalProperties: false,
|
|
58723
|
+
required: [
|
|
58724
|
+
"source_project_id",
|
|
58725
|
+
"kind",
|
|
58726
|
+
"scope",
|
|
58727
|
+
"target_id",
|
|
58728
|
+
"parent_id",
|
|
58729
|
+
"revision",
|
|
58730
|
+
"digest"
|
|
58731
|
+
],
|
|
58732
|
+
properties: {
|
|
58733
|
+
source_project_id: { type: "string" },
|
|
58734
|
+
kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
|
|
58735
|
+
scope: { type: "string", enum: ["collection", "resource"] },
|
|
58736
|
+
target_id: { type: "string", format: "uuid" },
|
|
58737
|
+
parent_id: { type: "string", format: "uuid", nullable: true },
|
|
58738
|
+
revision: { type: "string" },
|
|
58739
|
+
digest: { type: "string" }
|
|
58740
|
+
}
|
|
58741
|
+
};
|
|
58742
|
+
projectResourcePageSchema = {
|
|
58743
|
+
type: "object",
|
|
58744
|
+
additionalProperties: false,
|
|
58745
|
+
required: [
|
|
58746
|
+
"authority",
|
|
58747
|
+
"route",
|
|
58748
|
+
"package_version",
|
|
58749
|
+
"authority_id",
|
|
58750
|
+
"tenant_id",
|
|
58751
|
+
"corpus_id",
|
|
58752
|
+
"source_project_id",
|
|
58753
|
+
"todos_project_id",
|
|
58754
|
+
"task_list_id",
|
|
58755
|
+
"include_anchors",
|
|
58756
|
+
"collection_revision",
|
|
58757
|
+
"limit",
|
|
58758
|
+
"count",
|
|
58759
|
+
"resources",
|
|
58760
|
+
"has_more",
|
|
58761
|
+
"next_cursor",
|
|
58762
|
+
"complete",
|
|
58763
|
+
"truncated"
|
|
58764
|
+
],
|
|
58765
|
+
properties: {
|
|
58766
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58767
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
58768
|
+
package_version: { type: "string" },
|
|
58769
|
+
authority_id: { type: "string" },
|
|
58770
|
+
tenant_id: { type: "string" },
|
|
58771
|
+
corpus_id: { type: "string" },
|
|
58772
|
+
source_project_id: { type: "string" },
|
|
58773
|
+
todos_project_id: { type: "string", format: "uuid" },
|
|
58774
|
+
task_list_id: { type: "string", format: "uuid" },
|
|
58775
|
+
include_anchors: { type: "boolean" },
|
|
58776
|
+
collection_revision: { type: "string" },
|
|
58777
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
58778
|
+
count: { type: "integer", minimum: 0 },
|
|
58779
|
+
resources: {
|
|
58780
|
+
type: "array",
|
|
58781
|
+
items: { $ref: "#/components/schemas/ProjectResource" }
|
|
58782
|
+
},
|
|
58783
|
+
has_more: { type: "boolean" },
|
|
58784
|
+
next_cursor: { type: "string", nullable: true },
|
|
58785
|
+
complete: { type: "boolean" },
|
|
58786
|
+
truncated: { type: "boolean", enum: [false] }
|
|
58787
|
+
}
|
|
58788
|
+
};
|
|
56889
58789
|
});
|
|
56890
58790
|
|
|
56891
58791
|
// src/server/pr-groups.ts
|
|
@@ -57032,11 +58932,11 @@ function canonicalJson2(value) {
|
|
|
57032
58932
|
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
57033
58933
|
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
|
|
57034
58934
|
}
|
|
57035
|
-
function
|
|
58935
|
+
function digest2(value) {
|
|
57036
58936
|
return createHash17("sha256").update(canonicalJson2(value)).digest("hex");
|
|
57037
58937
|
}
|
|
57038
58938
|
function deriveIdempotencyKey(projectId, slug) {
|
|
57039
|
-
return `ptlk_${
|
|
58939
|
+
return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
57040
58940
|
}
|
|
57041
58941
|
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
57042
58942
|
const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
@@ -57045,13 +58945,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
|
|
|
57045
58945
|
}
|
|
57046
58946
|
return key2;
|
|
57047
58947
|
}
|
|
57048
|
-
function receiptId2(projectId, slug,
|
|
57049
|
-
return `ptlr_${
|
|
58948
|
+
function receiptId2(projectId, slug, idempotencyKey2) {
|
|
58949
|
+
return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
|
|
57050
58950
|
}
|
|
57051
58951
|
function semanticListDigest(list) {
|
|
57052
58952
|
const metadata = { ...list.metadata ?? {} };
|
|
57053
58953
|
delete metadata[RECEIPT_METADATA_KEY];
|
|
57054
|
-
return
|
|
58954
|
+
return digest2({
|
|
57055
58955
|
project_id: list.project_id,
|
|
57056
58956
|
slug: list.slug,
|
|
57057
58957
|
name: list.name,
|
|
@@ -57068,10 +58968,10 @@ function storedMarker(list) {
|
|
|
57068
58968
|
return null;
|
|
57069
58969
|
return marker;
|
|
57070
58970
|
}
|
|
57071
|
-
function receiptFor(store, project, list,
|
|
58971
|
+
function receiptFor(store, project, list, idempotencyKey2) {
|
|
57072
58972
|
const marker = storedMarker(list);
|
|
57073
58973
|
const owned = marker?.project_id === project.id && marker.slug === list.slug;
|
|
57074
|
-
if (owned && marker.idempotency_key !==
|
|
58974
|
+
if (owned && marker.idempotency_key !== idempotencyKey2) {
|
|
57075
58975
|
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
|
|
57076
58976
|
project_id: project.id,
|
|
57077
58977
|
task_list_id: list.id,
|
|
@@ -57080,8 +58980,8 @@ function receiptFor(store, project, list, idempotencyKey) {
|
|
|
57080
58980
|
}
|
|
57081
58981
|
return {
|
|
57082
58982
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
57083
|
-
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${
|
|
57084
|
-
idempotency_key: owned ? marker.idempotency_key :
|
|
58983
|
+
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
|
|
58984
|
+
idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
|
|
57085
58985
|
project_id: project.id,
|
|
57086
58986
|
task_list_id: list.id,
|
|
57087
58987
|
slug: list.slug,
|
|
@@ -57133,20 +59033,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
57133
59033
|
});
|
|
57134
59034
|
}
|
|
57135
59035
|
const slug = project.task_list_id;
|
|
57136
|
-
const
|
|
59036
|
+
const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
|
|
57137
59037
|
if (state.scoped) {
|
|
57138
59038
|
return {
|
|
57139
59039
|
mode: "apply",
|
|
57140
59040
|
action: "already_present",
|
|
57141
59041
|
project,
|
|
57142
59042
|
task_list: state.scoped,
|
|
57143
|
-
receipt: receiptFor(store, project, state.scoped,
|
|
59043
|
+
receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
|
|
57144
59044
|
};
|
|
57145
59045
|
}
|
|
57146
59046
|
const marker = {
|
|
57147
59047
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
57148
|
-
receipt_id: receiptId2(project.id, slug,
|
|
57149
|
-
idempotency_key:
|
|
59048
|
+
receipt_id: receiptId2(project.id, slug, idempotencyKey2),
|
|
59049
|
+
idempotency_key: idempotencyKey2,
|
|
57150
59050
|
project_id: project.id,
|
|
57151
59051
|
slug,
|
|
57152
59052
|
result_digest: semanticListDigest({
|
|
@@ -57184,7 +59084,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
57184
59084
|
action: "already_present",
|
|
57185
59085
|
project: raced.project,
|
|
57186
59086
|
task_list: raced.scoped,
|
|
57187
|
-
receipt: receiptFor(store, raced.project, raced.scoped,
|
|
59087
|
+
receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
|
|
57188
59088
|
};
|
|
57189
59089
|
}
|
|
57190
59090
|
const projectReadback = await store.projects.get(project.id);
|
|
@@ -57214,7 +59114,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
57214
59114
|
action: "created",
|
|
57215
59115
|
project: projectReadback,
|
|
57216
59116
|
task_list: readback,
|
|
57217
|
-
receipt: receiptFor(store, projectReadback, readback,
|
|
59117
|
+
receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
|
|
57218
59118
|
};
|
|
57219
59119
|
}
|
|
57220
59120
|
async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
@@ -57259,7 +59159,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
|
57259
59159
|
project_id: project.id,
|
|
57260
59160
|
task_list_id: list.id,
|
|
57261
59161
|
accepted_receipt_id: options.receipt_id,
|
|
57262
|
-
rollback_receipt_id: `ptlr_inverse_${
|
|
59162
|
+
rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
57263
59163
|
removed_at: new Date().toISOString()
|
|
57264
59164
|
};
|
|
57265
59165
|
}
|
|
@@ -57458,6 +59358,9 @@ function validateTaskPatchVocabulary(value) {
|
|
|
57458
59358
|
if (!parsed.ok)
|
|
57459
59359
|
return { ok: false, message: parsed.message };
|
|
57460
59360
|
}
|
|
59361
|
+
if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
|
|
59362
|
+
return { ok: false, message: "parent_id must be a non-empty task id or null" };
|
|
59363
|
+
}
|
|
57461
59364
|
return { ok: true, patch: body2 };
|
|
57462
59365
|
}
|
|
57463
59366
|
function validateProjectPatch(value) {
|
|
@@ -57942,8 +59845,8 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57942
59845
|
}
|
|
57943
59846
|
const created = await store.tasks.create(body2, storageContext);
|
|
57944
59847
|
const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
|
|
57945
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
|
|
57946
|
-
return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id and
|
|
59848
|
+
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null) || body2.created_by !== undefined && persisted.created_by !== body2.created_by) {
|
|
59849
|
+
return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, plan_id, and explicit created_by", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
|
|
57947
59850
|
}
|
|
57948
59851
|
return json5({ task: persisted }, 201);
|
|
57949
59852
|
}
|
|
@@ -58817,6 +60720,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
58817
60720
|
if (e instanceof TaskNotFoundError) {
|
|
58818
60721
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
58819
60722
|
}
|
|
60723
|
+
if (e instanceof VersionConflictError) {
|
|
60724
|
+
return error(409, e.message, {
|
|
60725
|
+
code: VersionConflictError.code,
|
|
60726
|
+
conflict: true,
|
|
60727
|
+
task_id: e.taskId,
|
|
60728
|
+
expected_version: e.expectedVersion,
|
|
60729
|
+
current_version: e.actualVersion
|
|
60730
|
+
});
|
|
60731
|
+
}
|
|
58820
60732
|
if (e instanceof StaleLockHandoffError) {
|
|
58821
60733
|
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;
|
|
58822
60734
|
return error(status2, e.message, {
|