@hasna/todos 0.15.28 → 0.15.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +15 -0
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +5 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -0
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
- package/dist/cli/index.js +13040 -10368
- package/dist/contracts.js +64 -2
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1489 -149
- package/dist/lib/task-parent-integrity.d.ts +14 -0
- package/dist/lib/task-parent-integrity.d.ts.map +1 -0
- package/dist/mcp/index.js +2073 -140
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/project-registration/adoption-validation.d.ts +3 -0
- package/dist/project-registration/adoption-validation.d.ts.map +1 -0
- package/dist/project-registration/authority.d.ts +3 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/backend.d.ts +26 -1
- package/dist/project-registration/backend.d.ts.map +1 -1
- package/dist/project-registration/http.d.ts +3 -1
- package/dist/project-registration/http.d.ts.map +1 -1
- package/dist/project-registration/index.d.ts +2 -1
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/page-validation.d.ts +5 -0
- package/dist/project-registration/page-validation.d.ts.map +1 -0
- package/dist/project-registration/postgres.d.ts +13 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration/schema.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts +13 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration/types.d.ts +68 -1
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +873 -63
- package/dist/registry.js +64 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +151 -0
- package/dist/sdk/v1.generated.d.ts +282 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +2064 -131
- package/dist/server/openapi.d.ts +2856 -1357
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +8 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +244 -19
- package/dist/task-manifest/authority.d.ts +13 -1
- package/dist/task-manifest/authority.d.ts.map +1 -1
- package/dist/task-manifest/backend.d.ts +5 -0
- package/dist/task-manifest/backend.d.ts.map +1 -1
- package/dist/task-manifest/index.d.ts +2 -2
- package/dist/task-manifest/index.d.ts.map +1 -1
- package/dist/task-manifest/plan-slug.d.ts +20 -0
- package/dist/task-manifest/plan-slug.d.ts.map +1 -1
- package/dist/task-manifest/postgres.d.ts +1 -0
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/schema-sql.d.ts.map +1 -1
- package/dist/task-manifest/schema.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -0
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest/types.d.ts +21 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +592 -61
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -289,6 +289,12 @@ function sqliteTodosProjectRegistrationSchemaSql() {
|
|
|
289
289
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
290
290
|
resource_kind, direction, idempotency_key
|
|
291
291
|
);
|
|
292
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_source_identity
|
|
293
|
+
ON todos_project_registration_receipts (
|
|
294
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
295
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
296
|
+
target_selector
|
|
297
|
+
);
|
|
292
298
|
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
293
299
|
ON todos_project_registration_receipts (
|
|
294
300
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -388,6 +394,12 @@ function postgresTodosProjectRegistrationSchemaSql() {
|
|
|
388
394
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
389
395
|
resource_kind, direction, idempotency_key
|
|
390
396
|
)`,
|
|
397
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_source_identity_idx
|
|
398
|
+
ON todos_project_registration_receipts (
|
|
399
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
400
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
401
|
+
target_selector
|
|
402
|
+
)`,
|
|
391
403
|
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
392
404
|
ON todos_project_registration_receipts (
|
|
393
405
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -12480,6 +12492,46 @@ function guardPlanRowsSqlite(planIds, db) {
|
|
|
12480
12492
|
}
|
|
12481
12493
|
}
|
|
12482
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
|
+
|
|
12483
12535
|
// src/lib/creator-identity.ts
|
|
12484
12536
|
import { existsSync as existsSync6, rmSync } from "fs";
|
|
12485
12537
|
import { join as join5 } from "path";
|
|
@@ -14059,6 +14111,7 @@ function createTaskStored(input, d) {
|
|
|
14059
14111
|
let id = uuid();
|
|
14060
14112
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
14061
14113
|
try {
|
|
14114
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
14062
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)
|
|
14063
14116
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
14064
14117
|
id,
|
|
@@ -14437,6 +14490,7 @@ function updateTaskStored(id, input, db) {
|
|
|
14437
14490
|
throw new VersionConflictError(id, input.version, task.version);
|
|
14438
14491
|
}
|
|
14439
14492
|
input = sanitizeUpdateTaskInput(input);
|
|
14493
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
14440
14494
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
|
|
14441
14495
|
const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
|
|
14442
14496
|
if (linkedProjectId) {
|
|
@@ -14490,6 +14544,10 @@ function updateTaskStored(id, input, db) {
|
|
|
14490
14544
|
sets.push("project_id = ?");
|
|
14491
14545
|
params.push(input.project_id);
|
|
14492
14546
|
}
|
|
14547
|
+
if (input.parent_id !== undefined) {
|
|
14548
|
+
sets.push("parent_id = ?");
|
|
14549
|
+
params.push(input.parent_id);
|
|
14550
|
+
}
|
|
14493
14551
|
if (input.assigned_to !== undefined) {
|
|
14494
14552
|
sets.push("assigned_to = ?");
|
|
14495
14553
|
params.push(input.assigned_to);
|
|
@@ -14605,6 +14663,8 @@ function updateTaskStored(id, input, db) {
|
|
|
14605
14663
|
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
14606
14664
|
if (input.title !== undefined && input.title !== task.title)
|
|
14607
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);
|
|
14608
14668
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
14609
14669
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
14610
14670
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
@@ -14662,7 +14722,8 @@ function updateTask(id, input, db) {
|
|
|
14662
14722
|
if (!before)
|
|
14663
14723
|
throw new TaskNotFoundError(id);
|
|
14664
14724
|
const guardedPlanIds = [before.plan_id, input.plan_id];
|
|
14665
|
-
|
|
14725
|
+
const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
|
|
14726
|
+
if (!needsSerializedWrite)
|
|
14666
14727
|
return updateTaskStored(id, input, d);
|
|
14667
14728
|
return d.transaction(() => {
|
|
14668
14729
|
guardPlanRowsSqlite(guardedPlanIds, d);
|
|
@@ -14698,6 +14759,7 @@ var init_task_crud = __esm(() => {
|
|
|
14698
14759
|
init_checklists();
|
|
14699
14760
|
init_storage_tombstones();
|
|
14700
14761
|
init_prewrite_secrets();
|
|
14762
|
+
init_task_parent_integrity();
|
|
14701
14763
|
});
|
|
14702
14764
|
|
|
14703
14765
|
// src/db/task-status.ts
|
|
@@ -21004,6 +21066,31 @@ var init_http_client = __esm(() => {
|
|
|
21004
21066
|
]);
|
|
21005
21067
|
});
|
|
21006
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
|
+
|
|
21007
21094
|
// src/cli/cloud-router.ts
|
|
21008
21095
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
21009
21096
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
@@ -21404,12 +21491,39 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
21404
21491
|
union.sort(compareCloudTaskOrder);
|
|
21405
21492
|
return union.slice(start, windowEnd);
|
|
21406
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
|
+
}
|
|
21407
21520
|
async function cloudGetTask(client, id) {
|
|
21408
21521
|
const raw = await client.get("tasks", id);
|
|
21409
21522
|
return raw == null ? null : unwrapTask(raw);
|
|
21410
21523
|
}
|
|
21411
21524
|
async function cloudCreateTask(client, input, verification = {}) {
|
|
21412
21525
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
21526
|
+
const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
|
|
21413
21527
|
const expectedCreatedBy = typeof verification.expectedCreatedBy === "string" && verification.expectedCreatedBy.trim() ? verification.expectedCreatedBy : null;
|
|
21414
21528
|
if (expectedCreatedBy !== null) {
|
|
21415
21529
|
await requireTaskCreatorCapability(client);
|
|
@@ -21422,9 +21536,9 @@ async function cloudCreateTask(client, input, verification = {}) {
|
|
|
21422
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");
|
|
21423
21537
|
}
|
|
21424
21538
|
const persisted = await cloudGetTask(client, created.id);
|
|
21425
|
-
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) {
|
|
21426
21540
|
const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
|
|
21427
|
-
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`);
|
|
21428
21542
|
}
|
|
21429
21543
|
return persisted;
|
|
21430
21544
|
}
|
|
@@ -21691,6 +21805,8 @@ var init_cloud_router = __esm(() => {
|
|
|
21691
21805
|
init_redaction();
|
|
21692
21806
|
init_plan_project_link_contract();
|
|
21693
21807
|
init_http_client();
|
|
21808
|
+
init_adoption_validation();
|
|
21809
|
+
init_page_validation();
|
|
21694
21810
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21695
21811
|
TRANSPORT_TOKENS = {
|
|
21696
21812
|
sqlite: "sqlite",
|
|
@@ -21727,6 +21843,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
21727
21843
|
compact["version"] = task.version;
|
|
21728
21844
|
compact["created_at"] = task.created_at;
|
|
21729
21845
|
compact["task_list_id"] = task.task_list_id;
|
|
21846
|
+
compact["parent_id"] = task.parent_id;
|
|
21730
21847
|
return compactJson(compact);
|
|
21731
21848
|
}
|
|
21732
21849
|
function versionFor(taskId, version) {
|
|
@@ -21996,6 +22113,7 @@ ${task.description}` : null
|
|
|
21996
22113
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
21997
22114
|
assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
|
|
21998
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"),
|
|
21999
22117
|
task_list_id: exports_external.string().nullable().optional(),
|
|
22000
22118
|
depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
|
|
22001
22119
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
@@ -22027,6 +22145,9 @@ ${task.description}` : null
|
|
|
22027
22145
|
if (typeof patch.project_id === "string" && patch.project_id) {
|
|
22028
22146
|
patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
|
|
22029
22147
|
}
|
|
22148
|
+
if (typeof patch.parent_id === "string" && patch.parent_id) {
|
|
22149
|
+
patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
|
|
22150
|
+
}
|
|
22030
22151
|
if (typeof patch.task_list_id === "string" && patch.task_list_id) {
|
|
22031
22152
|
let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
|
|
22032
22153
|
if (!scope) {
|
|
@@ -22035,9 +22156,22 @@ ${task.description}` : null
|
|
|
22035
22156
|
}
|
|
22036
22157
|
patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
|
|
22037
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
|
+
}
|
|
22038
22165
|
if (version2 !== undefined)
|
|
22039
22166
|
patch.version = version2;
|
|
22040
|
-
|
|
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
|
+
}
|
|
22041
22175
|
return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
|
|
22042
22176
|
}
|
|
22043
22177
|
const resolvedId = resolveId(params.task_id);
|
|
@@ -22049,6 +22183,8 @@ ${task.description}` : null
|
|
|
22049
22183
|
resolved.assigned_to = resolveAssignee(resolved.assigned_to);
|
|
22050
22184
|
if (resolved.project_id && typeof resolved.project_id === "string")
|
|
22051
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);
|
|
22052
22188
|
if (resolved.task_list_id && typeof resolved.task_list_id === "string")
|
|
22053
22189
|
resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
|
|
22054
22190
|
if (resolved.depends_on && Array.isArray(resolved.depends_on))
|
|
@@ -32748,7 +32884,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
32748
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",
|
|
32749
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",
|
|
32750
32886
|
get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
|
|
32751
|
-
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",
|
|
32752
32888
|
delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
|
|
32753
32889
|
start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
|
|
32754
32890
|
complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
|
|
@@ -35803,7 +35939,7 @@ var package_default;
|
|
|
35803
35939
|
var init_package = __esm(() => {
|
|
35804
35940
|
package_default = {
|
|
35805
35941
|
name: "@hasna/todos",
|
|
35806
|
-
version: "0.15.
|
|
35942
|
+
version: "0.15.32",
|
|
35807
35943
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35808
35944
|
type: "module",
|
|
35809
35945
|
main: "dist/index.js",
|
|
@@ -46935,8 +47071,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
46935
47071
|
resolveRef: (ref) => store.resolveTaskRef(ref),
|
|
46936
47072
|
list: (filter = {}) => store.listTasks(filter),
|
|
46937
47073
|
count: (filter = {}) => store.countTasks(filter),
|
|
46938
|
-
update: (id, input) => updateTask2(id, input, store),
|
|
46939
|
-
delete: (id, context) => store.
|
|
47074
|
+
update: (id, input, context) => updateTask2(id, input, store, context),
|
|
47075
|
+
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
46940
47076
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
46941
47077
|
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
46942
47078
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
@@ -47469,22 +47605,70 @@ class PostgresJsonRecordStore {
|
|
|
47469
47605
|
return "identical";
|
|
47470
47606
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
47471
47607
|
}
|
|
47472
|
-
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) {
|
|
47473
47618
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
47474
|
-
if (planIds.length === 0)
|
|
47619
|
+
if (planIds.length === 0 && !parentGuard)
|
|
47475
47620
|
return this.upsert("tasks", value, context);
|
|
47476
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;
|
|
47477
47626
|
const updatedAt = value.updated_at;
|
|
47478
47627
|
const targetPlanId = value.plan_id;
|
|
47479
|
-
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
|
+
),
|
|
47480
47634
|
locked_plans AS MATERIALIZED (
|
|
47481
47635
|
SELECT object_id, payload FROM ${this.tableName}
|
|
47482
47636
|
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
|
|
47483
47637
|
AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
|
|
47484
47638
|
ORDER BY object_id
|
|
47485
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
|
|
47486
47661
|
), validation AS (
|
|
47487
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,
|
|
47488
47672
|
(SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
|
|
47489
47673
|
($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
47490
47674
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
@@ -47505,7 +47689,13 @@ class PostgresJsonRecordStore {
|
|
|
47505
47689
|
)
|
|
47506
47690
|
SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
|
|
47507
47691
|
FROM guarded
|
|
47508
|
-
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
|
|
47509
47699
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
47510
47700
|
payload = EXCLUDED.payload,
|
|
47511
47701
|
updated_at = EXCLUDED.updated_at,
|
|
@@ -47518,8 +47708,10 @@ class PostgresJsonRecordStore {
|
|
|
47518
47708
|
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
47519
47709
|
RETURNING payload
|
|
47520
47710
|
)
|
|
47521
|
-
SELECT guarded.
|
|
47522
|
-
|
|
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
|
|
47523
47715
|
FROM guarded`, [
|
|
47524
47716
|
this.service,
|
|
47525
47717
|
value.id,
|
|
@@ -47529,9 +47721,26 @@ class PostgresJsonRecordStore {
|
|
|
47529
47721
|
numberValue3(value.version),
|
|
47530
47722
|
jsonbParam(planIds),
|
|
47531
47723
|
targetPlanId,
|
|
47532
|
-
explicitProject
|
|
47724
|
+
explicitProject,
|
|
47725
|
+
Boolean(parentGuard),
|
|
47726
|
+
parentGuard?.parentId ?? null,
|
|
47727
|
+
parentGuard?.expectedVersion ?? null,
|
|
47728
|
+
parentGuard?.operation === "update"
|
|
47533
47729
|
]);
|
|
47534
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
|
+
}
|
|
47535
47744
|
if (!row?.all_plans_found || !row.target_plan_found) {
|
|
47536
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 });
|
|
47537
47746
|
}
|
|
@@ -47539,6 +47748,8 @@ class PostgresJsonRecordStore {
|
|
|
47539
47748
|
throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
|
|
47540
47749
|
}
|
|
47541
47750
|
if (!row.payload) {
|
|
47751
|
+
if (row.current_payload)
|
|
47752
|
+
return payloadRecord2(row.current_payload);
|
|
47542
47753
|
return await requireRecord("tasks", value.id, this);
|
|
47543
47754
|
}
|
|
47544
47755
|
return payloadRecord2(row.payload);
|
|
@@ -47864,6 +48075,80 @@ class PostgresJsonRecordStore {
|
|
|
47864
48075
|
version: numberValue3(existing["version"])
|
|
47865
48076
|
}, context);
|
|
47866
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
|
+
}
|
|
47867
48152
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
47868
48153
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
47869
48154
|
return value ? assertPlanProjectLinkReceipt(value) : null;
|
|
@@ -48284,9 +48569,8 @@ class PostgresJsonRecordStore {
|
|
|
48284
48569
|
}
|
|
48285
48570
|
async function createTask3(input, store, context) {
|
|
48286
48571
|
const timestamp4 = new Date().toISOString();
|
|
48287
|
-
|
|
48288
|
-
|
|
48289
|
-
}
|
|
48572
|
+
const taskId = randomUUID4();
|
|
48573
|
+
await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
|
|
48290
48574
|
const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
|
|
48291
48575
|
const requestedProjectId = input.project_id ?? context?.projectId ?? null;
|
|
48292
48576
|
if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
|
|
@@ -48295,7 +48579,7 @@ async function createTask3(input, store, context) {
|
|
|
48295
48579
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
48296
48580
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
48297
48581
|
const task2 = {
|
|
48298
|
-
id:
|
|
48582
|
+
id: taskId,
|
|
48299
48583
|
short_id: shortId,
|
|
48300
48584
|
project_id: effectiveProjectId,
|
|
48301
48585
|
parent_id: input.parent_id ?? null,
|
|
@@ -48351,15 +48635,16 @@ async function createTask3(input, store, context) {
|
|
|
48351
48635
|
synced_at: null,
|
|
48352
48636
|
archived_at: null
|
|
48353
48637
|
};
|
|
48354
|
-
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);
|
|
48355
48639
|
await logTaskChange2(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
|
|
48356
48640
|
return storedTask;
|
|
48357
48641
|
}
|
|
48358
|
-
async function updateTask2(id, input, store) {
|
|
48642
|
+
async function updateTask2(id, input, store, context) {
|
|
48359
48643
|
const existing = await requireRecord("tasks", id, store);
|
|
48360
48644
|
if (existing.version !== input.version) {
|
|
48361
|
-
throw new
|
|
48645
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
48362
48646
|
}
|
|
48647
|
+
await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
|
|
48363
48648
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
|
|
48364
48649
|
const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
|
|
48365
48650
|
if (linkedPlan?.project_id) {
|
|
@@ -48384,10 +48669,19 @@ async function updateTask2(id, input, store) {
|
|
|
48384
48669
|
metadata: input.metadata ?? existing.metadata,
|
|
48385
48670
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
48386
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,
|
|
48387
48673
|
created_by: existing.created_by,
|
|
48388
48674
|
completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
|
|
48389
48675
|
};
|
|
48390
|
-
|
|
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;
|
|
48391
48685
|
}
|
|
48392
48686
|
async function startTask2(id, agentId, store) {
|
|
48393
48687
|
const task2 = await requireRecord("tasks", id, store);
|
|
@@ -48451,7 +48745,11 @@ async function patchTask(task2, patch, store) {
|
|
|
48451
48745
|
version: task2.version + 1,
|
|
48452
48746
|
updated_at: new Date().toISOString()
|
|
48453
48747
|
};
|
|
48454
|
-
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
|
+
});
|
|
48455
48753
|
}
|
|
48456
48754
|
function sameCloudLockHolder(stored, incoming) {
|
|
48457
48755
|
if (!stored || !incoming)
|
|
@@ -49154,6 +49452,7 @@ var init_postgres_adapter = __esm(() => {
|
|
|
49154
49452
|
init_postgres_sync();
|
|
49155
49453
|
init_integrity();
|
|
49156
49454
|
init_redaction();
|
|
49455
|
+
init_task_parent_integrity();
|
|
49157
49456
|
init_audit_history_import();
|
|
49158
49457
|
init_canonical();
|
|
49159
49458
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
@@ -49639,21 +49938,6 @@ var init_postgres = __esm(() => {
|
|
|
49639
49938
|
init_types3();
|
|
49640
49939
|
});
|
|
49641
49940
|
|
|
49642
|
-
// src/project-registration/types.ts
|
|
49643
|
-
var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1", TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1", TodosProjectRegistrationError;
|
|
49644
|
-
var init_types4 = __esm(() => {
|
|
49645
|
-
TodosProjectRegistrationError = class TodosProjectRegistrationError extends Error {
|
|
49646
|
-
code;
|
|
49647
|
-
details;
|
|
49648
|
-
constructor(code, message, details = {}) {
|
|
49649
|
-
super(message);
|
|
49650
|
-
this.code = code;
|
|
49651
|
-
this.details = details;
|
|
49652
|
-
this.name = "TodosProjectRegistrationError";
|
|
49653
|
-
}
|
|
49654
|
-
};
|
|
49655
|
-
});
|
|
49656
|
-
|
|
49657
49941
|
// src/project-registration/postgres.ts
|
|
49658
49942
|
function safeIdentifier(value, field) {
|
|
49659
49943
|
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
|
|
@@ -49717,8 +50001,9 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49717
50001
|
const result = await this.client.query(`
|
|
49718
50002
|
SELECT * FROM todos_project_registration_receipts
|
|
49719
50003
|
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
49720
|
-
AND
|
|
49721
|
-
AND
|
|
50004
|
+
AND route = $4 AND package_version = $5
|
|
50005
|
+
AND operation_id = $6 AND step_id = $7 AND resource_kind = $8
|
|
50006
|
+
AND direction = $9 AND idempotency_key = $10 AND target_selector = $11
|
|
49722
50007
|
ORDER BY CASE outcome
|
|
49723
50008
|
WHEN 'terminal_nonacceptance' THEN 0
|
|
49724
50009
|
WHEN 'duplicate_of_accepted' THEN 1
|
|
@@ -49729,6 +50014,8 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49729
50014
|
identity.authority_id,
|
|
49730
50015
|
identity.tenant_id,
|
|
49731
50016
|
identity.corpus_id,
|
|
50017
|
+
identity.route,
|
|
50018
|
+
identity.package_version,
|
|
49732
50019
|
identity.operation_id,
|
|
49733
50020
|
identity.step_id,
|
|
49734
50021
|
identity.resource_kind,
|
|
@@ -49928,6 +50215,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49928
50215
|
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
49929
50216
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
49930
50217
|
LIMIT 1
|
|
50218
|
+
FOR UPDATE
|
|
49931
50219
|
`, [this.service, path, taskListSlug]);
|
|
49932
50220
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
49933
50221
|
}
|
|
@@ -49938,6 +50226,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49938
50226
|
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
49939
50227
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
49940
50228
|
LIMIT 1
|
|
50229
|
+
FOR UPDATE
|
|
49941
50230
|
`, [this.service, projectId, slug]);
|
|
49942
50231
|
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
49943
50232
|
}
|
|
@@ -49948,10 +50237,24 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
49948
50237
|
return await this.storage.taskLists.create(input);
|
|
49949
50238
|
}
|
|
49950
50239
|
async getProject(id) {
|
|
49951
|
-
|
|
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;
|
|
49952
50248
|
}
|
|
49953
50249
|
async getTaskList(id) {
|
|
49954
|
-
|
|
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;
|
|
49955
50258
|
}
|
|
49956
50259
|
async lockCompensationWrites() {
|
|
49957
50260
|
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
@@ -50031,6 +50334,96 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
50031
50334
|
async getTaskList(id) {
|
|
50032
50335
|
return (await this.direct()).getTaskList(id);
|
|
50033
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
|
+
}
|
|
50034
50427
|
}
|
|
50035
50428
|
var init_postgres2 = __esm(() => {
|
|
50036
50429
|
init_postgres_adapter();
|
|
@@ -50123,6 +50516,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
50123
50516
|
const row = this.db.query(`
|
|
50124
50517
|
SELECT * FROM todos_project_registration_receipts
|
|
50125
50518
|
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
50519
|
+
AND route = ? AND package_version = ?
|
|
50126
50520
|
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
50127
50521
|
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
50128
50522
|
ORDER BY CASE outcome
|
|
@@ -50131,7 +50525,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
50131
50525
|
ELSE 2
|
|
50132
50526
|
END, created_at DESC, receipt_id DESC
|
|
50133
50527
|
LIMIT 1
|
|
50134
|
-
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
50528
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.route, identity.package_version, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
50135
50529
|
return row ? receiptFromRow2(row) : null;
|
|
50136
50530
|
}
|
|
50137
50531
|
async getReceiptById(receiptId) {
|
|
@@ -50285,7 +50679,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
50285
50679
|
}
|
|
50286
50680
|
async lockStep(_identity) {}
|
|
50287
50681
|
async getReceiptForLookup(identity) {
|
|
50288
|
-
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
50682
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.route === identity.route && receipt.package_version === identity.package_version && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
50289
50683
|
const stored = await this.direct.getReceiptForLookup(identity);
|
|
50290
50684
|
if (stored)
|
|
50291
50685
|
staged.push(stored);
|
|
@@ -50773,35 +51167,68 @@ function projectRecord(project) {
|
|
|
50773
51167
|
return {
|
|
50774
51168
|
target_id: project.id,
|
|
50775
51169
|
revision: project.updated_at,
|
|
50776
|
-
digest:
|
|
50777
|
-
id: project.id,
|
|
50778
|
-
name: project.name,
|
|
50779
|
-
path: project.path,
|
|
50780
|
-
description: project.description,
|
|
50781
|
-
task_list_id: project.task_list_id,
|
|
50782
|
-
task_prefix: project.task_prefix,
|
|
50783
|
-
task_counter: project.task_counter,
|
|
50784
|
-
created_at: project.created_at,
|
|
50785
|
-
updated_at: project.updated_at
|
|
50786
|
-
})
|
|
51170
|
+
digest: projectRegistrationDigest(project)
|
|
50787
51171
|
};
|
|
50788
51172
|
}
|
|
50789
51173
|
function taskListRecord(taskList) {
|
|
50790
51174
|
return {
|
|
50791
51175
|
target_id: taskList.id,
|
|
50792
51176
|
revision: taskList.updated_at,
|
|
50793
|
-
digest:
|
|
50794
|
-
|
|
50795
|
-
|
|
50796
|
-
|
|
50797
|
-
|
|
50798
|
-
|
|
50799
|
-
|
|
50800
|
-
|
|
50801
|
-
|
|
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
|
|
50802
51197
|
})
|
|
50803
51198
|
};
|
|
50804
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
|
+
}
|
|
50805
51232
|
function receiptId(input) {
|
|
50806
51233
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
50807
51234
|
}
|
|
@@ -50821,6 +51248,29 @@ function assertCapabilityRequest(request, capability) {
|
|
|
50821
51248
|
}
|
|
50822
51249
|
}
|
|
50823
51250
|
function normalizedCallDigest(request) {
|
|
51251
|
+
return digestProjectRegistrationValue({
|
|
51252
|
+
authority_route: request.authority_route,
|
|
51253
|
+
package_version: request.package_version,
|
|
51254
|
+
authority_id: request.authority_id,
|
|
51255
|
+
tenant_id: request.tenant_id,
|
|
51256
|
+
corpus_id: request.corpus_id,
|
|
51257
|
+
operation_id: request.operation_id,
|
|
51258
|
+
step_id: request.step_id,
|
|
51259
|
+
resource_kind: request.resource_kind,
|
|
51260
|
+
direction: request.direction,
|
|
51261
|
+
target_selector: request.target_selector,
|
|
51262
|
+
idempotency_key: request.idempotency_key,
|
|
51263
|
+
request_digest: request.request_digest,
|
|
51264
|
+
precondition_digest: request.precondition_digest,
|
|
51265
|
+
project_id: request.project_id,
|
|
51266
|
+
project_slug: request.project_slug,
|
|
51267
|
+
project_name: request.project_name,
|
|
51268
|
+
desired: request.desired,
|
|
51269
|
+
bind_existing: request.bind_existing === true,
|
|
51270
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
51271
|
+
});
|
|
51272
|
+
}
|
|
51273
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
50824
51274
|
return digestProjectRegistrationValue({
|
|
50825
51275
|
authority_route: request.authority_route,
|
|
50826
51276
|
package_version: request.package_version,
|
|
@@ -50842,6 +51292,11 @@ function normalizedCallDigest(request) {
|
|
|
50842
51292
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
50843
51293
|
});
|
|
50844
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
|
+
}
|
|
50845
51300
|
function assertCommonRequest(request, capability) {
|
|
50846
51301
|
assertBounds(request);
|
|
50847
51302
|
assertResourceKind(request.resource_kind);
|
|
@@ -50883,6 +51338,9 @@ function assertCommonRequest(request, capability) {
|
|
|
50883
51338
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
50884
51339
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
50885
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
|
+
}
|
|
50886
51344
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
50887
51345
|
operation_id: request.operation_id,
|
|
50888
51346
|
step_id: request.step_id,
|
|
@@ -50904,7 +51362,7 @@ function assertForwardRequest(request, capability) {
|
|
|
50904
51362
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
50905
51363
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
50906
51364
|
target_selector: request.target_selector,
|
|
50907
|
-
expected: "absent"
|
|
51365
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
50908
51366
|
});
|
|
50909
51367
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
50910
51368
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -50983,7 +51441,7 @@ function receiptBase(request, callDigest, capability) {
|
|
|
50983
51441
|
normalized_call_digest: callDigest
|
|
50984
51442
|
};
|
|
50985
51443
|
}
|
|
50986
|
-
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2) {
|
|
51444
|
+
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2, createdByOperation = true) {
|
|
50987
51445
|
return makeReceipt({
|
|
50988
51446
|
...receiptBase(request, callDigest, capability),
|
|
50989
51447
|
outcome: "accepted",
|
|
@@ -50993,7 +51451,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2
|
|
|
50993
51451
|
result_digest: record.digest,
|
|
50994
51452
|
duplicate_of_receipt_id: null,
|
|
50995
51453
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
50996
|
-
created_by_operation:
|
|
51454
|
+
created_by_operation: createdByOperation
|
|
50997
51455
|
}, createdAt2);
|
|
50998
51456
|
}
|
|
50999
51457
|
function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt2) {
|
|
@@ -51055,6 +51513,53 @@ function bindingFor(request, callDigest, timestamp4, capability) {
|
|
|
51055
51513
|
updated_at: timestamp4
|
|
51056
51514
|
};
|
|
51057
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
|
+
}
|
|
51058
51563
|
|
|
51059
51564
|
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
51060
51565
|
backend;
|
|
@@ -51076,6 +51581,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51076
51581
|
immutable_receipts: true,
|
|
51077
51582
|
exact_terminal_lookup: true,
|
|
51078
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,
|
|
51079
51588
|
conditional_inverse: true,
|
|
51080
51589
|
ambiguous_outcome_reconciliation: true
|
|
51081
51590
|
};
|
|
@@ -51120,6 +51629,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51120
51629
|
async existingForwardResolution(transaction, request, callDigest) {
|
|
51121
51630
|
const exact = await transaction.getReceiptForLookup({
|
|
51122
51631
|
...authorityScope(this.capabilityValue),
|
|
51632
|
+
route: this.capabilityValue.route,
|
|
51633
|
+
package_version: this.capabilityValue.package_version,
|
|
51123
51634
|
operation_id: request.operation_id,
|
|
51124
51635
|
step_id: request.step_id,
|
|
51125
51636
|
resource_kind: request.resource_kind,
|
|
@@ -51134,7 +51645,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51134
51645
|
if (!accepted2) {
|
|
51135
51646
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
51136
51647
|
}
|
|
51137
|
-
if (accepted2
|
|
51648
|
+
if (!acceptedCallMatches(request, accepted2, callDigest)) {
|
|
51138
51649
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
51139
51650
|
}
|
|
51140
51651
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
@@ -51148,7 +51659,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51148
51659
|
});
|
|
51149
51660
|
if (!accepted)
|
|
51150
51661
|
return null;
|
|
51151
|
-
if (accepted
|
|
51662
|
+
if (acceptedCallMatches(request, accepted, callDigest)) {
|
|
51152
51663
|
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
51153
51664
|
}
|
|
51154
51665
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
@@ -51159,6 +51670,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51159
51670
|
const slug2 = taskListSlug(request.project_slug);
|
|
51160
51671
|
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
51161
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
|
+
}
|
|
51162
51679
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
51163
51680
|
}
|
|
51164
51681
|
await this.fault("before_object_write", request);
|
|
@@ -51170,7 +51687,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51170
51687
|
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
51171
51688
|
});
|
|
51172
51689
|
await this.fault("after_object_write", request);
|
|
51173
|
-
return
|
|
51690
|
+
return {
|
|
51691
|
+
record: projectRecord(project),
|
|
51692
|
+
created_by_operation: true
|
|
51693
|
+
};
|
|
51174
51694
|
}
|
|
51175
51695
|
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
51176
51696
|
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
@@ -51184,6 +51704,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51184
51704
|
const slug = taskListSlug(request.project_slug);
|
|
51185
51705
|
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
51186
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
|
+
}
|
|
51187
51713
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
51188
51714
|
}
|
|
51189
51715
|
await this.fault("before_object_write", request);
|
|
@@ -51200,7 +51726,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51200
51726
|
if (taskList.project_id !== todosProjectId) {
|
|
51201
51727
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
51202
51728
|
}
|
|
51203
|
-
return
|
|
51729
|
+
return {
|
|
51730
|
+
record: taskListRecord(taskList),
|
|
51731
|
+
created_by_operation: true
|
|
51732
|
+
};
|
|
51204
51733
|
}
|
|
51205
51734
|
async create(request) {
|
|
51206
51735
|
const startedAt = Date.now();
|
|
@@ -51222,9 +51751,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51222
51751
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
|
|
51223
51752
|
if (!claimed) {
|
|
51224
51753
|
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
51225
|
-
if (binding?.state === "accepted" && binding.
|
|
51754
|
+
if (binding?.state === "accepted" && binding.accepted_receipt_id) {
|
|
51226
51755
|
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
51227
|
-
if (accepted2) {
|
|
51756
|
+
if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
|
|
51228
51757
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
51229
51758
|
}
|
|
51230
51759
|
}
|
|
@@ -51235,15 +51764,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51235
51764
|
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
51236
51765
|
return recordOrTerminal;
|
|
51237
51766
|
}
|
|
51238
|
-
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);
|
|
51239
51768
|
await this.fault("before_receipt_write", request);
|
|
51240
51769
|
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
51241
51770
|
await this.fault("after_receipt_write", request);
|
|
51242
51771
|
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
51243
|
-
target_id: recordOrTerminal.target_id,
|
|
51772
|
+
target_id: recordOrTerminal.record.target_id,
|
|
51244
51773
|
accepted_receipt_id: stored.receipt_id,
|
|
51245
|
-
result_revision: recordOrTerminal.revision,
|
|
51246
|
-
result_digest: recordOrTerminal.digest,
|
|
51774
|
+
result_revision: recordOrTerminal.record.revision,
|
|
51775
|
+
result_digest: recordOrTerminal.record.digest,
|
|
51247
51776
|
updated_at: this.now()
|
|
51248
51777
|
});
|
|
51249
51778
|
return stored;
|
|
@@ -51272,6 +51801,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51272
51801
|
});
|
|
51273
51802
|
const exact = await transaction.getReceiptForLookup({
|
|
51274
51803
|
...authorityScope(this.capabilityValue),
|
|
51804
|
+
route: this.capabilityValue.route,
|
|
51805
|
+
package_version: this.capabilityValue.package_version,
|
|
51275
51806
|
operation_id: request.operation_id,
|
|
51276
51807
|
step_id: request.step_id,
|
|
51277
51808
|
resource_kind: request.resource_kind,
|
|
@@ -51289,7 +51820,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51289
51820
|
direction: request.direction
|
|
51290
51821
|
});
|
|
51291
51822
|
if (accepted) {
|
|
51292
|
-
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 });
|
|
51293
51824
|
}
|
|
51294
51825
|
const timestamp4 = this.now();
|
|
51295
51826
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
|
|
@@ -51322,9 +51853,18 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51322
51853
|
if (request.max_items !== 1) {
|
|
51323
51854
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
51324
51855
|
}
|
|
51325
|
-
if (request.authority !== "todos" || request.
|
|
51856
|
+
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
|
|
51326
51857
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
51327
51858
|
}
|
|
51859
|
+
requireString(request.authority_route, "authority_route", {
|
|
51860
|
+
min: 3,
|
|
51861
|
+
max: 128,
|
|
51862
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
51863
|
+
});
|
|
51864
|
+
requireString(request.package_version, "package_version", {
|
|
51865
|
+
max: 128,
|
|
51866
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
51867
|
+
});
|
|
51328
51868
|
requireString(request.operation_id, "operation_id", {
|
|
51329
51869
|
min: 8,
|
|
51330
51870
|
max: 128,
|
|
@@ -51346,6 +51886,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51346
51886
|
}
|
|
51347
51887
|
const receipt = await this.backend.getReceiptForLookup({
|
|
51348
51888
|
...authorityScope(this.capabilityValue),
|
|
51889
|
+
route: request.authority_route,
|
|
51890
|
+
package_version: request.package_version,
|
|
51349
51891
|
operation_id: request.operation_id,
|
|
51350
51892
|
step_id: request.step_id,
|
|
51351
51893
|
resource_kind: request.resource_kind,
|
|
@@ -51358,6 +51900,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51358
51900
|
}
|
|
51359
51901
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
51360
51902
|
}
|
|
51903
|
+
async listProjectResources(request) {
|
|
51904
|
+
const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
|
|
51905
|
+
if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
|
|
51906
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
|
|
51907
|
+
}
|
|
51908
|
+
if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
|
|
51909
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
|
|
51910
|
+
}
|
|
51911
|
+
const includeAnchors = request.include_anchors === true;
|
|
51912
|
+
const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
|
|
51913
|
+
if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
|
|
51914
|
+
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 });
|
|
51915
|
+
}
|
|
51916
|
+
const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
|
|
51917
|
+
if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
|
|
51918
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
|
|
51919
|
+
source_project_id: sourceProjectId,
|
|
51920
|
+
todos_project_id: projectBinding.target_id
|
|
51921
|
+
});
|
|
51922
|
+
}
|
|
51923
|
+
const cursor = decodeProjectResourceCursor(request.cursor, {
|
|
51924
|
+
source_project_id: sourceProjectId,
|
|
51925
|
+
include_anchors: includeAnchors
|
|
51926
|
+
});
|
|
51927
|
+
const collectionInput = {
|
|
51928
|
+
todos_project_id: projectBinding.target_id,
|
|
51929
|
+
task_list_id: taskListBinding.target_id,
|
|
51930
|
+
include_anchors: includeAnchors
|
|
51931
|
+
};
|
|
51932
|
+
const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
51933
|
+
if (cursor && cursor.collection_revision !== collectionRevision) {
|
|
51934
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
|
|
51935
|
+
source_project_id: sourceProjectId,
|
|
51936
|
+
expected_collection_revision: cursor.collection_revision,
|
|
51937
|
+
current_collection_revision: collectionRevision
|
|
51938
|
+
});
|
|
51939
|
+
}
|
|
51940
|
+
const candidates = await this.backend.listProjectResourceCandidates({
|
|
51941
|
+
...collectionInput,
|
|
51942
|
+
after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
|
|
51943
|
+
limit: request.limit + 1
|
|
51944
|
+
});
|
|
51945
|
+
const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
51946
|
+
if (verifiedCollectionRevision !== collectionRevision) {
|
|
51947
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
|
|
51948
|
+
source_project_id: sourceProjectId,
|
|
51949
|
+
expected_collection_revision: collectionRevision,
|
|
51950
|
+
current_collection_revision: verifiedCollectionRevision
|
|
51951
|
+
});
|
|
51952
|
+
}
|
|
51953
|
+
const hasMore = candidates.length > request.limit;
|
|
51954
|
+
const pageCandidates = candidates.slice(0, request.limit);
|
|
51955
|
+
const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
|
|
51956
|
+
const last = pageCandidates.at(-1);
|
|
51957
|
+
return {
|
|
51958
|
+
authority: "todos",
|
|
51959
|
+
route: this.capabilityValue.route,
|
|
51960
|
+
package_version: this.capabilityValue.package_version,
|
|
51961
|
+
authority_id: this.capabilityValue.authority_id,
|
|
51962
|
+
tenant_id: this.capabilityValue.tenant_id,
|
|
51963
|
+
corpus_id: this.capabilityValue.corpus_id,
|
|
51964
|
+
source_project_id: sourceProjectId,
|
|
51965
|
+
todos_project_id: projectBinding.target_id,
|
|
51966
|
+
task_list_id: taskListBinding.target_id,
|
|
51967
|
+
include_anchors: includeAnchors,
|
|
51968
|
+
collection_revision: collectionRevision,
|
|
51969
|
+
limit: request.limit,
|
|
51970
|
+
count: resources.length,
|
|
51971
|
+
resources,
|
|
51972
|
+
has_more: hasMore,
|
|
51973
|
+
next_cursor: hasMore && last ? encodeProjectResourceCursor({
|
|
51974
|
+
source_project_id: sourceProjectId,
|
|
51975
|
+
include_anchors: includeAnchors,
|
|
51976
|
+
collection_revision: collectionRevision,
|
|
51977
|
+
kind_rank: last.kind_rank,
|
|
51978
|
+
target_id: last.target_id
|
|
51979
|
+
}) : null,
|
|
51980
|
+
complete: !hasMore,
|
|
51981
|
+
truncated: false
|
|
51982
|
+
};
|
|
51983
|
+
}
|
|
51984
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
51985
|
+
const startedAt = Date.now();
|
|
51986
|
+
if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
|
|
51987
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
|
|
51988
|
+
}
|
|
51989
|
+
requireString(sourceRequest.package_version, "package_version", {
|
|
51990
|
+
max: 128,
|
|
51991
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
51992
|
+
});
|
|
51993
|
+
assertForwardRequest(sourceRequest, {
|
|
51994
|
+
...this.capabilityValue,
|
|
51995
|
+
package_version: sourceRequest.package_version
|
|
51996
|
+
});
|
|
51997
|
+
const validation = await this.backend.transaction(async (transaction) => {
|
|
51998
|
+
const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
|
|
51999
|
+
if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
|
|
52000
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
52001
|
+
}
|
|
52002
|
+
const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
|
|
52003
|
+
if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
|
|
52004
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
52005
|
+
}
|
|
52006
|
+
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);
|
|
52007
|
+
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)) {
|
|
52008
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
|
|
52009
|
+
}
|
|
52010
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
|
|
52011
|
+
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) {
|
|
52012
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
|
|
52013
|
+
}
|
|
52014
|
+
const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
52015
|
+
if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
|
|
52016
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
|
|
52017
|
+
}
|
|
52018
|
+
let stableMatch = false;
|
|
52019
|
+
if (sourceRequest.resource_kind === "task_list") {
|
|
52020
|
+
stableMatch = taskListRegistrationDigest({
|
|
52021
|
+
...current,
|
|
52022
|
+
updated_at: accepted.result_revision
|
|
52023
|
+
}) === accepted.result_digest;
|
|
52024
|
+
} else {
|
|
52025
|
+
const project = current;
|
|
52026
|
+
if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
|
|
52027
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
|
|
52028
|
+
}
|
|
52029
|
+
for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
|
|
52030
|
+
if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
|
|
52031
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
|
|
52032
|
+
}
|
|
52033
|
+
if (projectRegistrationDigest({
|
|
52034
|
+
...project,
|
|
52035
|
+
task_counter: priorTaskCounter,
|
|
52036
|
+
updated_at: accepted.result_revision
|
|
52037
|
+
}) === accepted.result_digest) {
|
|
52038
|
+
stableMatch = true;
|
|
52039
|
+
break;
|
|
52040
|
+
}
|
|
52041
|
+
}
|
|
52042
|
+
}
|
|
52043
|
+
if (!stableMatch) {
|
|
52044
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
|
|
52045
|
+
}
|
|
52046
|
+
return {
|
|
52047
|
+
valid: true,
|
|
52048
|
+
resource_kind: sourceRequest.resource_kind,
|
|
52049
|
+
target_id: accepted.target_id,
|
|
52050
|
+
source_receipt_id: storedSource.receipt_id,
|
|
52051
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
52052
|
+
source_outcome: storedSource.outcome,
|
|
52053
|
+
created_at: current.created_at,
|
|
52054
|
+
current_revision: current.updated_at,
|
|
52055
|
+
accepted_result_digest: accepted.result_digest
|
|
52056
|
+
};
|
|
52057
|
+
});
|
|
52058
|
+
assertWithinBounds(validation, sourceRequest, startedAt);
|
|
52059
|
+
return validation;
|
|
52060
|
+
}
|
|
51361
52061
|
async storedAcceptedReceipt(request, supplied) {
|
|
51362
52062
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
51363
52063
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -51384,6 +52084,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51384
52084
|
});
|
|
51385
52085
|
const exact = await transaction.getReceiptForLookup({
|
|
51386
52086
|
...authorityScope(this.capabilityValue),
|
|
52087
|
+
route: this.capabilityValue.route,
|
|
52088
|
+
package_version: this.capabilityValue.package_version,
|
|
51387
52089
|
operation_id: request.operation_id,
|
|
51388
52090
|
step_id: request.step_id,
|
|
51389
52091
|
resource_kind: request.resource_kind,
|
|
@@ -51478,6 +52180,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51478
52180
|
await this.storedAcceptedReceipt(request, accepted);
|
|
51479
52181
|
const receipt = await this.backend.getReceiptForLookup({
|
|
51480
52182
|
...authorityScope(this.capabilityValue),
|
|
52183
|
+
route: this.capabilityValue.route,
|
|
52184
|
+
package_version: this.capabilityValue.package_version,
|
|
51481
52185
|
operation_id: request.operation_id,
|
|
51482
52186
|
step_id: request.step_id,
|
|
51483
52187
|
resource_kind: request.resource_kind,
|
|
@@ -51522,7 +52226,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
51522
52226
|
cursorTableName
|
|
51523
52227
|
}), authorityOptions);
|
|
51524
52228
|
}
|
|
51525
|
-
var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
|
|
52229
|
+
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;
|
|
51526
52230
|
var init_authority = __esm(() => {
|
|
51527
52231
|
init_package_version();
|
|
51528
52232
|
init_postgres2();
|
|
@@ -51532,6 +52236,8 @@ var init_authority = __esm(() => {
|
|
|
51532
52236
|
WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
|
|
51533
52237
|
OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
51534
52238
|
STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
52239
|
+
AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
52240
|
+
PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
51535
52241
|
SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
51536
52242
|
IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
51537
52243
|
WriteBoundaryError = class WriteBoundaryError extends Error {
|
|
@@ -51590,6 +52296,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
51590
52296
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
51591
52297
|
return json({ capability: await authority.capability() });
|
|
51592
52298
|
}
|
|
52299
|
+
if (action === "resources" && method === "GET") {
|
|
52300
|
+
const sourceProjectId = url.searchParams.get("source_project_id");
|
|
52301
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
52302
|
+
const includeAnchorsRaw = url.searchParams.get("include_anchors");
|
|
52303
|
+
const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
|
|
52304
|
+
return json({
|
|
52305
|
+
page: await authority.listProjectResources({
|
|
52306
|
+
source_project_id: sourceProjectId,
|
|
52307
|
+
limit,
|
|
52308
|
+
include_anchors: includeAnchors,
|
|
52309
|
+
cursor: url.searchParams.get("cursor") ?? undefined
|
|
52310
|
+
})
|
|
52311
|
+
});
|
|
52312
|
+
}
|
|
51593
52313
|
if (method !== "POST")
|
|
51594
52314
|
return json({ error: "method not allowed" }, 405);
|
|
51595
52315
|
const body = await readJson(req);
|
|
@@ -51612,6 +52332,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
51612
52332
|
record: await authority.readExact(body)
|
|
51613
52333
|
});
|
|
51614
52334
|
}
|
|
52335
|
+
if (action === "validate-prior-adoption") {
|
|
52336
|
+
const input = body;
|
|
52337
|
+
return json({
|
|
52338
|
+
validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
|
|
52339
|
+
});
|
|
52340
|
+
}
|
|
51615
52341
|
if (action === "compensate") {
|
|
51616
52342
|
return json({
|
|
51617
52343
|
receipt: await authority.compensate(body)
|
|
@@ -51644,6 +52370,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
51644
52370
|
var JSON_HEADERS;
|
|
51645
52371
|
var init_http2 = __esm(() => {
|
|
51646
52372
|
init_types4();
|
|
52373
|
+
init_adoption_validation();
|
|
51647
52374
|
JSON_HEADERS = { "Content-Type": "application/json" };
|
|
51648
52375
|
});
|
|
51649
52376
|
|
|
@@ -51651,13 +52378,14 @@ var init_http2 = __esm(() => {
|
|
|
51651
52378
|
var init_project_registration = __esm(() => {
|
|
51652
52379
|
init_authority();
|
|
51653
52380
|
init_http2();
|
|
52381
|
+
init_adoption_validation();
|
|
51654
52382
|
init_postgres2();
|
|
51655
52383
|
init_sqlite();
|
|
51656
52384
|
init_types4();
|
|
51657
52385
|
});
|
|
51658
52386
|
|
|
51659
52387
|
// src/task-manifest/types.ts
|
|
51660
|
-
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
|
|
52388
|
+
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;
|
|
51661
52389
|
var init_types5 = __esm(() => {
|
|
51662
52390
|
TodosTaskManifestError = class TodosTaskManifestError extends Error {
|
|
51663
52391
|
code;
|
|
@@ -51744,7 +52472,7 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
51744
52472
|
}
|
|
51745
52473
|
return parsed.data;
|
|
51746
52474
|
}
|
|
51747
|
-
var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
52475
|
+
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) => {
|
|
51748
52476
|
if (Object.keys(value).length > limit) {
|
|
51749
52477
|
context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
|
|
51750
52478
|
}
|
|
@@ -51766,6 +52494,8 @@ var init_schema2 = __esm(() => {
|
|
|
51766
52494
|
};
|
|
51767
52495
|
key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
51768
52496
|
identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
52497
|
+
digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
|
|
52498
|
+
idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
|
|
51769
52499
|
uuid2 = exports_external.string().uuid();
|
|
51770
52500
|
scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
51771
52501
|
comment = exports_external.object({
|
|
@@ -51803,7 +52533,9 @@ var init_schema2 = __esm(() => {
|
|
|
51803
52533
|
schema = exports_external.object({
|
|
51804
52534
|
version: exports_external.literal(1),
|
|
51805
52535
|
operation_id: identifier,
|
|
51806
|
-
|
|
52536
|
+
step_id: identifier,
|
|
52537
|
+
idempotency_key: idempotencyKey,
|
|
52538
|
+
precondition_digest: digest,
|
|
51807
52539
|
project_id: uuid2,
|
|
51808
52540
|
task_list_id: uuid2.optional(),
|
|
51809
52541
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -51819,7 +52551,10 @@ var init_schema2 = __esm(() => {
|
|
|
51819
52551
|
}).strict();
|
|
51820
52552
|
compensationSchema = exports_external.object({
|
|
51821
52553
|
receipt_id: uuid2,
|
|
51822
|
-
|
|
52554
|
+
operation_id: identifier,
|
|
52555
|
+
step_id: identifier,
|
|
52556
|
+
idempotency_key: idempotencyKey,
|
|
52557
|
+
precondition_digest: digest,
|
|
51823
52558
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
51824
52559
|
}).strict();
|
|
51825
52560
|
bindingLookupSchema = exports_external.object({
|
|
@@ -51837,6 +52572,7 @@ function taskManifestPlanSlug(manifest, planId) {
|
|
|
51837
52572
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
51838
52573
|
return `${base}-${planId}`;
|
|
51839
52574
|
}
|
|
52575
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
51840
52576
|
var init_plan_slug = () => {};
|
|
51841
52577
|
|
|
51842
52578
|
// src/task-manifest/backend.ts
|
|
@@ -51850,11 +52586,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
51850
52586
|
const row = rows[0];
|
|
51851
52587
|
const bindingVersion = Number(row.binding_version);
|
|
51852
52588
|
const state = row.state;
|
|
51853
|
-
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") {
|
|
52589
|
+
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") {
|
|
51854
52590
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
51855
52591
|
}
|
|
51856
52592
|
return {
|
|
51857
52593
|
plan_id: planId,
|
|
52594
|
+
operation_id: row.binding_operation_id,
|
|
52595
|
+
step_id: row.binding_step_id,
|
|
51858
52596
|
apply_receipt_id: row.apply_receipt_id,
|
|
51859
52597
|
binding_version: bindingVersion,
|
|
51860
52598
|
state
|
|
@@ -51907,9 +52645,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51907
52645
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
51908
52646
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
51909
52647
|
operation_id text NOT NULL,
|
|
52648
|
+
step_id text NOT NULL,
|
|
51910
52649
|
idempotency_key text NOT NULL,
|
|
51911
52650
|
request_digest text NOT NULL,
|
|
52651
|
+
precondition_digest text NOT NULL,
|
|
51912
52652
|
result_digest text NOT NULL,
|
|
52653
|
+
slug_provenance text,
|
|
52654
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
52655
|
+
reason text,
|
|
52656
|
+
duplicate_of_receipt_id text,
|
|
51913
52657
|
binding_version integer NOT NULL,
|
|
51914
52658
|
apply_receipt_id text,
|
|
51915
52659
|
manifest_json jsonb,
|
|
@@ -51921,12 +52665,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51921
52665
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
51922
52666
|
`ALTER TABLE todos_task_manifest_receipts
|
|
51923
52667
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
52668
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52669
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
52670
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52671
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
52672
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52673
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
52674
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52675
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
52676
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52677
|
+
ADD COLUMN IF NOT EXISTS reason text`,
|
|
52678
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
52679
|
+
ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
|
|
51924
52680
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
51925
52681
|
operation_id text PRIMARY KEY,
|
|
51926
52682
|
tenant_id text NOT NULL,
|
|
52683
|
+
step_id text NOT NULL,
|
|
51927
52684
|
idempotency_key text NOT NULL UNIQUE,
|
|
51928
52685
|
request_digest text NOT NULL,
|
|
52686
|
+
precondition_digest text NOT NULL,
|
|
51929
52687
|
result_digest text NOT NULL,
|
|
52688
|
+
slug_provenance text,
|
|
52689
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
51930
52690
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
51931
52691
|
manifest_json jsonb NOT NULL,
|
|
51932
52692
|
result_json jsonb NOT NULL,
|
|
@@ -51940,6 +52700,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51940
52700
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
51941
52701
|
`ALTER TABLE todos_task_manifest_bindings
|
|
51942
52702
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
52703
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52704
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
52705
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52706
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
52707
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52708
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
52709
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
52710
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
51943
52711
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
51944
52712
|
id text PRIMARY KEY,
|
|
51945
52713
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -51951,10 +52719,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51951
52719
|
created_at timestamptz NOT NULL,
|
|
51952
52720
|
delivered_at timestamptz
|
|
51953
52721
|
)`,
|
|
52722
|
+
`CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
52723
|
+
receipt_id text PRIMARY KEY,
|
|
52724
|
+
tenant_id text NOT NULL,
|
|
52725
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
52726
|
+
route text NOT NULL,
|
|
52727
|
+
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
52728
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
52729
|
+
operation_id text NOT NULL,
|
|
52730
|
+
step_id text NOT NULL,
|
|
52731
|
+
idempotency_key text NOT NULL,
|
|
52732
|
+
request_digest text NOT NULL,
|
|
52733
|
+
precondition_digest text NOT NULL,
|
|
52734
|
+
result_digest text NOT NULL,
|
|
52735
|
+
outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
52736
|
+
reason text NOT NULL,
|
|
52737
|
+
binding_version integer NOT NULL,
|
|
52738
|
+
apply_receipt_id text,
|
|
52739
|
+
manifest_json jsonb,
|
|
52740
|
+
result_json jsonb NOT NULL,
|
|
52741
|
+
created_at timestamptz NOT NULL
|
|
52742
|
+
)`,
|
|
51954
52743
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
51955
52744
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
51956
52745
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
51957
52746
|
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
52747
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
|
|
52748
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
|
|
52749
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
|
|
52750
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
52751
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
|
|
52752
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
51958
52753
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
51959
52754
|
ON todos_task_manifest_bindings(
|
|
51960
52755
|
tenant_id,
|
|
@@ -51967,6 +52762,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
51967
52762
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
51968
52763
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
51969
52764
|
BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
|
|
52765
|
+
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
|
|
52766
|
+
`DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
|
|
52767
|
+
`CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
|
|
52768
|
+
BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
|
|
51970
52769
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
51971
52770
|
];
|
|
51972
52771
|
}
|
|
@@ -51991,6 +52790,39 @@ function safeIdentifier2(value, field) {
|
|
|
51991
52790
|
function parseJson2(value) {
|
|
51992
52791
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
51993
52792
|
}
|
|
52793
|
+
function parseApplyResult(value, duplicate) {
|
|
52794
|
+
const parsed = parseJson2(value);
|
|
52795
|
+
return {
|
|
52796
|
+
...parsed,
|
|
52797
|
+
duplicate,
|
|
52798
|
+
receipt: {
|
|
52799
|
+
...parsed.receipt,
|
|
52800
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
52801
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
52802
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
52803
|
+
reason: parsed.receipt.reason ?? null,
|
|
52804
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
52805
|
+
}
|
|
52806
|
+
};
|
|
52807
|
+
}
|
|
52808
|
+
function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
|
|
52809
|
+
if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
52810
|
+
const expected = taskManifestPlanSlug(manifest, planId);
|
|
52811
|
+
if (slug !== expected) {
|
|
52812
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
|
|
52813
|
+
}
|
|
52814
|
+
return expected;
|
|
52815
|
+
}
|
|
52816
|
+
if (provenance !== null && provenance !== undefined) {
|
|
52817
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
52818
|
+
}
|
|
52819
|
+
if (slug === null || slug === undefined)
|
|
52820
|
+
return null;
|
|
52821
|
+
if (slug !== null && slug !== undefined) {
|
|
52822
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
|
|
52823
|
+
}
|
|
52824
|
+
return null;
|
|
52825
|
+
}
|
|
51994
52826
|
function timestamp4(value) {
|
|
51995
52827
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
51996
52828
|
}
|
|
@@ -51998,6 +52830,41 @@ function fault(faults, point) {
|
|
|
51998
52830
|
if (faults.points.has(point))
|
|
51999
52831
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
52000
52832
|
}
|
|
52833
|
+
function terminalApplyResult(input, reason) {
|
|
52834
|
+
const receipt = {
|
|
52835
|
+
receipt_id: input.terminal_receipt_id,
|
|
52836
|
+
authority: "todos",
|
|
52837
|
+
route: "todos.task-manifest.v1",
|
|
52838
|
+
schema_version: 1,
|
|
52839
|
+
kind: "apply",
|
|
52840
|
+
operation_id: input.manifest.operation_id,
|
|
52841
|
+
step_id: input.manifest.step_id,
|
|
52842
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
52843
|
+
request_digest: input.request_digest,
|
|
52844
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
52845
|
+
result_digest: canonicalDigest({
|
|
52846
|
+
outcome: "terminal_nonacceptance",
|
|
52847
|
+
reason,
|
|
52848
|
+
operation_id: input.manifest.operation_id,
|
|
52849
|
+
step_id: input.manifest.step_id,
|
|
52850
|
+
request_digest: input.request_digest
|
|
52851
|
+
}),
|
|
52852
|
+
outcome: "terminal_nonacceptance",
|
|
52853
|
+
reason,
|
|
52854
|
+
duplicate_of_receipt_id: null,
|
|
52855
|
+
binding_version: 0,
|
|
52856
|
+
apply_receipt_id: null,
|
|
52857
|
+
created_at: input.now
|
|
52858
|
+
};
|
|
52859
|
+
return {
|
|
52860
|
+
duplicate: false,
|
|
52861
|
+
receipt,
|
|
52862
|
+
graph: input.graph,
|
|
52863
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
52864
|
+
outbox_ids: [],
|
|
52865
|
+
result_digest: receipt.result_digest
|
|
52866
|
+
};
|
|
52867
|
+
}
|
|
52001
52868
|
function receiptFromRow3(row) {
|
|
52002
52869
|
return {
|
|
52003
52870
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -52006,9 +52873,14 @@ function receiptFromRow3(row) {
|
|
|
52006
52873
|
schema_version: 1,
|
|
52007
52874
|
kind: row["kind"],
|
|
52008
52875
|
operation_id: String(row["operation_id"]),
|
|
52876
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
52009
52877
|
idempotency_key: String(row["idempotency_key"]),
|
|
52010
52878
|
request_digest: String(row["request_digest"]),
|
|
52879
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
52011
52880
|
result_digest: String(row["result_digest"]),
|
|
52881
|
+
outcome: row["outcome"] ?? "accepted",
|
|
52882
|
+
reason: row["reason"] == null ? null : row["reason"],
|
|
52883
|
+
duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
|
|
52012
52884
|
binding_version: Number(row["binding_version"]),
|
|
52013
52885
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
52014
52886
|
created_at: timestamp4(row["created_at"])
|
|
@@ -52126,46 +52998,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52126
52998
|
now4
|
|
52127
52999
|
]);
|
|
52128
53000
|
}
|
|
53001
|
+
async persistTerminal(tx, input, reason) {
|
|
53002
|
+
const result = terminalApplyResult(input, reason);
|
|
53003
|
+
const resultJson = canonicalJson(result);
|
|
53004
|
+
await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
|
|
53005
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
53006
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
53007
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
53008
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
|
|
53009
|
+
'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
|
|
53010
|
+
ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
|
|
53011
|
+
result.receipt.receipt_id,
|
|
53012
|
+
this.tenantId,
|
|
53013
|
+
input.manifest.operation_id,
|
|
53014
|
+
input.manifest.step_id,
|
|
53015
|
+
input.manifest.idempotency_key,
|
|
53016
|
+
input.request_digest,
|
|
53017
|
+
input.manifest.precondition_digest,
|
|
53018
|
+
result.receipt.result_digest,
|
|
53019
|
+
reason,
|
|
53020
|
+
canonicalJson(input.manifest),
|
|
53021
|
+
resultJson,
|
|
53022
|
+
input.now
|
|
53023
|
+
]);
|
|
53024
|
+
const stored = await tx.query(`SELECT receipt_id, result_json
|
|
53025
|
+
FROM todos_task_manifest_terminal_receipts
|
|
53026
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
53027
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
53028
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
53029
|
+
LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
|
|
53030
|
+
return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
|
|
53031
|
+
}
|
|
52129
53032
|
async apply(input, faults) {
|
|
52130
53033
|
await this.ensureSchema();
|
|
52131
53034
|
return this.client.transaction(async (tx) => {
|
|
52132
53035
|
const { manifest } = input;
|
|
52133
53036
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
52134
53037
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
53038
|
+
const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
|
|
53039
|
+
WHERE tenant_id = $1
|
|
53040
|
+
AND kind = 'apply'
|
|
53041
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
53042
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
53043
|
+
LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
|
|
53044
|
+
if (terminal.rows[0]) {
|
|
53045
|
+
return parseApplyResult(terminal.rows[0]["result_json"], true);
|
|
53046
|
+
}
|
|
52135
53047
|
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]);
|
|
52136
53048
|
if (existing.rows[0]) {
|
|
52137
53049
|
const binding = existing.rows[0];
|
|
52138
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
52139
|
-
|
|
53050
|
+
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) {
|
|
53051
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
52140
53052
|
}
|
|
52141
53053
|
if (binding["state"] !== "applied") {
|
|
52142
|
-
|
|
53054
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
52143
53055
|
}
|
|
52144
|
-
return
|
|
53056
|
+
return parseApplyResult(binding["result_json"], true);
|
|
52145
53057
|
}
|
|
52146
53058
|
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]);
|
|
52147
53059
|
if (reused.rows[0])
|
|
52148
|
-
|
|
53060
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
53061
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
53062
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
53063
|
+
}
|
|
52149
53064
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
52150
|
-
|
|
53065
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
52151
53066
|
}
|
|
52152
53067
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
52153
53068
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
52154
53069
|
if (!project.rows[0])
|
|
52155
|
-
|
|
53070
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
52156
53071
|
if (manifest.task_list_id) {
|
|
52157
53072
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
52158
53073
|
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]);
|
|
52159
53074
|
const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
|
|
52160
53075
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
52161
|
-
|
|
53076
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
52162
53077
|
}
|
|
52163
53078
|
}
|
|
52164
53079
|
const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
|
|
52165
53080
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
52166
53081
|
WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
52167
53082
|
if (conflict.rows[0])
|
|
52168
|
-
|
|
53083
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
52169
53084
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
52170
53085
|
fault(faults, "after_plan_write");
|
|
52171
53086
|
for (const task3 of manifest.tasks) {
|
|
@@ -52235,9 +53150,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52235
53150
|
schema_version: 1,
|
|
52236
53151
|
kind: "apply",
|
|
52237
53152
|
operation_id: manifest.operation_id,
|
|
53153
|
+
step_id: manifest.step_id,
|
|
52238
53154
|
idempotency_key: manifest.idempotency_key,
|
|
52239
53155
|
request_digest: input.request_digest,
|
|
53156
|
+
precondition_digest: manifest.precondition_digest,
|
|
52240
53157
|
result_digest: input.result_digest,
|
|
53158
|
+
outcome: "accepted",
|
|
53159
|
+
reason: null,
|
|
53160
|
+
duplicate_of_receipt_id: null,
|
|
52241
53161
|
binding_version: 1,
|
|
52242
53162
|
apply_receipt_id: null,
|
|
52243
53163
|
created_at: input.now
|
|
@@ -52254,14 +53174,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52254
53174
|
const resultJson = canonicalJson(result);
|
|
52255
53175
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
52256
53176
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
52257
|
-
|
|
52258
|
-
|
|
53177
|
+
step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
|
|
53178
|
+
reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
53179
|
+
) 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)`, [
|
|
52259
53180
|
input.receipt_id,
|
|
52260
53181
|
this.tenantId,
|
|
52261
53182
|
manifest.operation_id,
|
|
52262
53183
|
manifest.idempotency_key,
|
|
53184
|
+
manifest.step_id,
|
|
52263
53185
|
input.request_digest,
|
|
53186
|
+
manifest.precondition_digest,
|
|
52264
53187
|
input.result_digest,
|
|
53188
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
52265
53189
|
manifestJson,
|
|
52266
53190
|
resultJson,
|
|
52267
53191
|
input.now
|
|
@@ -52280,14 +53204,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52280
53204
|
}
|
|
52281
53205
|
fault(faults, "after_outbox_write");
|
|
52282
53206
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
52283
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
52284
|
-
|
|
52285
|
-
|
|
53207
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
|
|
53208
|
+
result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
|
|
53209
|
+
state, version, created_at, updated_at
|
|
53210
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
|
|
52286
53211
|
manifest.operation_id,
|
|
52287
53212
|
this.tenantId,
|
|
53213
|
+
manifest.step_id,
|
|
52288
53214
|
manifest.idempotency_key,
|
|
52289
53215
|
input.request_digest,
|
|
53216
|
+
manifest.precondition_digest,
|
|
52290
53217
|
input.result_digest,
|
|
53218
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
52291
53219
|
input.receipt_id,
|
|
52292
53220
|
manifestJson,
|
|
52293
53221
|
resultJson,
|
|
@@ -52300,9 +53228,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52300
53228
|
async readExact(receiptId2) {
|
|
52301
53229
|
await this.ensureSchema();
|
|
52302
53230
|
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]);
|
|
52303
|
-
if (
|
|
53231
|
+
if (result.rows[0])
|
|
53232
|
+
return parseApplyResult(result.rows[0]["result_json"], false);
|
|
53233
|
+
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]);
|
|
53234
|
+
if (!terminal.rows[0])
|
|
52304
53235
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
52305
|
-
return
|
|
53236
|
+
return parseApplyResult(terminal.rows[0]["result_json"], false);
|
|
52306
53237
|
}
|
|
52307
53238
|
async lookupBindingByPlanId(planId) {
|
|
52308
53239
|
await this.ensureSchema();
|
|
@@ -52313,6 +53244,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52313
53244
|
b.version AS binding_version,
|
|
52314
53245
|
b.tenant_id AS binding_tenant_id,
|
|
52315
53246
|
b.operation_id AS binding_operation_id,
|
|
53247
|
+
b.step_id AS binding_step_id,
|
|
52316
53248
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
52317
53249
|
r.tenant_id AS receipt_tenant_id,
|
|
52318
53250
|
r.authority AS receipt_authority,
|
|
@@ -52320,6 +53252,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52320
53252
|
r.schema_version AS receipt_schema_version,
|
|
52321
53253
|
r.kind AS receipt_kind,
|
|
52322
53254
|
r.operation_id AS receipt_operation_id,
|
|
53255
|
+
r.step_id AS receipt_step_id,
|
|
52323
53256
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
52324
53257
|
FROM todos_task_manifest_bindings b
|
|
52325
53258
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -52406,6 +53339,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52406
53339
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
52407
53340
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
52408
53341
|
}
|
|
53342
|
+
const appliedReceipt = receiptFromRow3(applyRow);
|
|
53343
|
+
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"]) {
|
|
53344
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
53345
|
+
}
|
|
52409
53346
|
if (binding["state"] !== "applied")
|
|
52410
53347
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
52411
53348
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -52420,12 +53357,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52420
53357
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
52421
53358
|
if (delivered.rows[0])
|
|
52422
53359
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
52423
|
-
const applyResult =
|
|
53360
|
+
const applyResult = parseApplyResult(applyRow["result_json"], false);
|
|
52424
53361
|
const manifest = parseJson2(applyRow["manifest_json"]);
|
|
53362
|
+
const manifestRecord2 = manifest;
|
|
53363
|
+
const applyStepId = typeof manifestRecord2["step_id"] === "string" ? String(manifestRecord2["step_id"]) : null;
|
|
52425
53364
|
const expectedEffects = [
|
|
52426
53365
|
{
|
|
52427
53366
|
topic: "todos.task-manifest.applied",
|
|
52428
|
-
payload: {
|
|
53367
|
+
payload: {
|
|
53368
|
+
operation_id: manifest.operation_id,
|
|
53369
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
53370
|
+
project_id: manifest.project_id
|
|
53371
|
+
}
|
|
52429
53372
|
},
|
|
52430
53373
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
52431
53374
|
];
|
|
@@ -52473,9 +53416,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52473
53416
|
}
|
|
52474
53417
|
const appliedAt = receiptFromRow3(applyRow).created_at;
|
|
52475
53418
|
const expectedPayloads = new Map;
|
|
53419
|
+
const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
53420
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
53421
|
+
LIMIT 1`, [this.service, applyResult.graph.plan_id]);
|
|
53422
|
+
const actualPlan = planRow.rows[0] ? parseJson2(planRow.rows[0]["payload"]) : null;
|
|
53423
|
+
const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
|
|
53424
|
+
planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
|
|
52476
53425
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
52477
53426
|
type: "plans",
|
|
52478
|
-
payload: canonicalJson(
|
|
53427
|
+
payload: canonicalJson(planExpected)
|
|
52479
53428
|
});
|
|
52480
53429
|
for (const task3 of manifest.tasks)
|
|
52481
53430
|
expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
|
|
@@ -52575,14 +53524,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
52575
53524
|
const readback = await this.readback(tx, applyResult.graph);
|
|
52576
53525
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
52577
53526
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
52578
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
52579
|
-
request_digest,
|
|
52580
|
-
|
|
53527
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
53528
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
53529
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
53530
|
+
) 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)`, [
|
|
52581
53531
|
compensationReceiptId,
|
|
52582
53532
|
this.tenantId,
|
|
52583
53533
|
receipt.operation_id,
|
|
53534
|
+
receipt.step_id,
|
|
52584
53535
|
input.idempotency_key,
|
|
52585
53536
|
requestDigest,
|
|
53537
|
+
input.precondition_digest,
|
|
52586
53538
|
receipt.result_digest,
|
|
52587
53539
|
receipt.binding_version,
|
|
52588
53540
|
input.receipt_id,
|
|
@@ -52639,36 +53591,83 @@ function resolveTenantId(value) {
|
|
|
52639
53591
|
}
|
|
52640
53592
|
return tenantId;
|
|
52641
53593
|
}
|
|
53594
|
+
function taskManifestRequestDigest(manifest) {
|
|
53595
|
+
const { idempotency_key: _idempotencyKey, ...request } = manifest;
|
|
53596
|
+
return canonicalDigest(request);
|
|
53597
|
+
}
|
|
53598
|
+
function taskManifestCompensationRequestDigest(request) {
|
|
53599
|
+
return canonicalDigest(request);
|
|
53600
|
+
}
|
|
53601
|
+
function deriveTodosTaskManifestApplyPreconditionDigest(input) {
|
|
53602
|
+
return canonicalDigest({
|
|
53603
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
53604
|
+
direction: "apply",
|
|
53605
|
+
operation_id: input.operation_id,
|
|
53606
|
+
step_id: input.step_id,
|
|
53607
|
+
project_id: input.project_id,
|
|
53608
|
+
task_list_id: input.task_list_id ?? null,
|
|
53609
|
+
expected_binding_version: input.if_binding_version ?? 0
|
|
53610
|
+
});
|
|
53611
|
+
}
|
|
53612
|
+
function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
|
|
53613
|
+
return canonicalDigest({
|
|
53614
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
53615
|
+
direction: "compensate",
|
|
53616
|
+
operation_id: input.operation_id,
|
|
53617
|
+
step_id: input.step_id,
|
|
53618
|
+
apply_receipt_id: input.receipt_id,
|
|
53619
|
+
expected_binding_version: input.if_binding_version
|
|
53620
|
+
});
|
|
53621
|
+
}
|
|
53622
|
+
function deriveTodosTaskManifestIdempotencyKey(input) {
|
|
53623
|
+
return `tmk_${canonicalDigest({
|
|
53624
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
53625
|
+
...input
|
|
53626
|
+
}).slice(0, 48)}`;
|
|
53627
|
+
}
|
|
52642
53628
|
function normalize(input, now4) {
|
|
52643
53629
|
const parsed = parseTodosTaskManifest(input);
|
|
52644
53630
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
52645
53631
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
52646
53632
|
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 });
|
|
52647
53633
|
}
|
|
53634
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
53635
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
52648
53636
|
const manifest = sanitizeManifest(parsed);
|
|
53637
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
|
|
53638
|
+
if (manifest.precondition_digest !== expectedPreconditionDigest) {
|
|
53639
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
53640
|
+
}
|
|
53641
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
53642
|
+
operation_id: manifest.operation_id,
|
|
53643
|
+
step_id: manifest.step_id,
|
|
53644
|
+
direction: "apply",
|
|
53645
|
+
target_selector: manifest.project_id,
|
|
53646
|
+
request_digest,
|
|
53647
|
+
precondition_digest: manifest.precondition_digest
|
|
53648
|
+
});
|
|
52649
53649
|
const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
|
|
52650
53650
|
task3.key,
|
|
52651
|
-
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task3.key)
|
|
53651
|
+
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task3.key)
|
|
52652
53652
|
]));
|
|
52653
53653
|
const graph = {
|
|
52654
|
-
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
|
|
53654
|
+
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
|
|
52655
53655
|
task_ids,
|
|
52656
|
-
comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task3.key, String(index)))),
|
|
52657
|
-
verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task3.key, String(index)))),
|
|
53656
|
+
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)))),
|
|
53657
|
+
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)))),
|
|
52658
53658
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
52659
53659
|
};
|
|
52660
|
-
const request_digest = canonicalDigest(parsed);
|
|
52661
53660
|
const effectInputs = [
|
|
52662
53661
|
{
|
|
52663
53662
|
topic: "todos.task-manifest.applied",
|
|
52664
|
-
payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
|
|
53663
|
+
payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
|
|
52665
53664
|
},
|
|
52666
53665
|
...manifest.effects ?? []
|
|
52667
53666
|
];
|
|
52668
53667
|
const outbox = effectInputs.map((effect2, index) => {
|
|
52669
53668
|
const payload = { ...effect2.payload };
|
|
52670
53669
|
return {
|
|
52671
|
-
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
|
|
53670
|
+
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
|
|
52672
53671
|
topic: effect2.topic,
|
|
52673
53672
|
payload,
|
|
52674
53673
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -52678,11 +53677,14 @@ function normalize(input, now4) {
|
|
|
52678
53677
|
return {
|
|
52679
53678
|
manifest,
|
|
52680
53679
|
request_digest,
|
|
53680
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
52681
53681
|
result_digest,
|
|
52682
|
-
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
|
|
53682
|
+
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
53683
|
+
terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
52683
53684
|
graph,
|
|
52684
53685
|
outbox,
|
|
52685
|
-
now: now4
|
|
53686
|
+
now: now4,
|
|
53687
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
52686
53688
|
};
|
|
52687
53689
|
}
|
|
52688
53690
|
function sanitizeManifest(manifest) {
|
|
@@ -52740,6 +53742,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
52740
53742
|
tenant_id: this.tenantId,
|
|
52741
53743
|
backend: this.backend.kind,
|
|
52742
53744
|
deterministic_ids: true,
|
|
53745
|
+
operation_step_identity: true,
|
|
53746
|
+
deterministic_idempotency_keys: true,
|
|
53747
|
+
terminal_nonacceptance_receipts: true,
|
|
53748
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
52743
53749
|
immutable_receipts: true,
|
|
52744
53750
|
transactional_outbox: true,
|
|
52745
53751
|
idempotent_outbox_delivery: true,
|
|
@@ -52769,7 +53775,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
52769
53775
|
async apply(input) {
|
|
52770
53776
|
const normalized = normalize(input, this.now());
|
|
52771
53777
|
const faults = await this.prepareFaults();
|
|
52772
|
-
|
|
53778
|
+
const result = this.bounded(await this.backend.apply(normalized, faults));
|
|
53779
|
+
if (result.receipt.outcome === "terminal_nonacceptance") {
|
|
53780
|
+
throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
|
|
53781
|
+
}
|
|
53782
|
+
return result;
|
|
52773
53783
|
}
|
|
52774
53784
|
readExact(receiptId2) {
|
|
52775
53785
|
if (!receiptId2 || receiptId2.length > 200) {
|
|
@@ -52802,18 +53812,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
52802
53812
|
async compensate(input) {
|
|
52803
53813
|
const request = parseTodosTaskManifestCompensation(input);
|
|
52804
53814
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
52805
|
-
|
|
52806
|
-
|
|
53815
|
+
if (applied.receipt.outcome !== "accepted") {
|
|
53816
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
|
|
53817
|
+
}
|
|
53818
|
+
if (request.operation_id !== applied.receipt.operation_id) {
|
|
53819
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
|
|
53820
|
+
}
|
|
53821
|
+
if (request.step_id === applied.receipt.step_id) {
|
|
53822
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
|
|
53823
|
+
}
|
|
53824
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
|
|
53825
|
+
if (request.precondition_digest !== expectedPreconditionDigest) {
|
|
53826
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
53827
|
+
}
|
|
53828
|
+
const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
|
|
53829
|
+
const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
|
|
53830
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
53831
|
+
operation_id: request.operation_id,
|
|
53832
|
+
step_id: request.step_id,
|
|
53833
|
+
direction: "compensate",
|
|
53834
|
+
target_selector: request.receipt_id,
|
|
53835
|
+
request_digest: requestDigest,
|
|
53836
|
+
precondition_digest: request.precondition_digest
|
|
53837
|
+
});
|
|
53838
|
+
if (request.idempotency_key !== expectedIdempotencyKey) {
|
|
53839
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
|
|
53840
|
+
}
|
|
53841
|
+
const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
|
|
52807
53842
|
const receipt = {
|
|
52808
53843
|
receipt_id: compensationReceiptId,
|
|
52809
53844
|
authority: "todos",
|
|
52810
53845
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
52811
53846
|
schema_version: 1,
|
|
52812
53847
|
kind: "compensate",
|
|
52813
|
-
operation_id:
|
|
53848
|
+
operation_id: request.operation_id,
|
|
53849
|
+
step_id: request.step_id,
|
|
52814
53850
|
idempotency_key: request.idempotency_key,
|
|
52815
53851
|
request_digest: requestDigest,
|
|
53852
|
+
precondition_digest: request.precondition_digest,
|
|
52816
53853
|
result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
|
|
53854
|
+
outcome: "accepted",
|
|
53855
|
+
reason: null,
|
|
53856
|
+
duplicate_of_receipt_id: null,
|
|
52817
53857
|
binding_version: request.if_binding_version + 1,
|
|
52818
53858
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
52819
53859
|
created_at: this.now()
|
|
@@ -54479,9 +55519,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
54479
55519
|
TaskManifestBounds: taskManifestBoundsSchema,
|
|
54480
55520
|
TaskManifestCapability: taskManifestCapabilitySchema,
|
|
54481
55521
|
TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
|
|
55522
|
+
TaskManifest: taskManifestSchema,
|
|
55523
|
+
TaskManifestReceipt: taskManifestReceiptSchema,
|
|
55524
|
+
TaskManifestApplyResult: taskManifestApplyResultSchema,
|
|
55525
|
+
TaskManifestApplyResponse: taskManifestApplyResponseSchema,
|
|
55526
|
+
TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
|
|
55527
|
+
TaskManifestCompensationResult: taskManifestCompensationResultSchema,
|
|
55528
|
+
TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
|
|
55529
|
+
TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
|
|
54482
55530
|
TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
|
|
54483
55531
|
TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
|
|
54484
55532
|
TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
|
|
55533
|
+
ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
|
|
55534
|
+
ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
|
|
55535
|
+
ProjectRegistrationRequest: projectRegistrationRequestSchema,
|
|
55536
|
+
ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
|
|
55537
|
+
PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
|
|
55538
|
+
ProjectResource: projectResourceSchema,
|
|
55539
|
+
ProjectResourcePage: projectResourcePageSchema,
|
|
54485
55540
|
TaskList: taskListSchema,
|
|
54486
55541
|
ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
|
|
54487
55542
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
@@ -54524,6 +55579,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
54524
55579
|
priority: { type: "string", enum: [...TASK_PRIORITIES] },
|
|
54525
55580
|
assigned_to: { type: "string" },
|
|
54526
55581
|
project_id: { type: "string", nullable: true },
|
|
55582
|
+
parent_id: { type: "string", nullable: true },
|
|
54527
55583
|
plan_id: { type: "string", nullable: true },
|
|
54528
55584
|
task_list_id: { type: "string", nullable: true },
|
|
54529
55585
|
version: { type: "number" }
|
|
@@ -55478,6 +56534,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55478
56534
|
},
|
|
55479
56535
|
security: [{ apiKey: [] }],
|
|
55480
56536
|
paths: {
|
|
56537
|
+
"/v1/project-registration/capability": {
|
|
56538
|
+
get: {
|
|
56539
|
+
operationId: "getProjectRegistrationCapability",
|
|
56540
|
+
summary: "Read the live package-owned Projects to Todos registration capability",
|
|
56541
|
+
responses: {
|
|
56542
|
+
"200": {
|
|
56543
|
+
content: {
|
|
56544
|
+
"application/json": {
|
|
56545
|
+
schema: {
|
|
56546
|
+
type: "object",
|
|
56547
|
+
additionalProperties: false,
|
|
56548
|
+
required: ["capability"],
|
|
56549
|
+
properties: {
|
|
56550
|
+
capability: {
|
|
56551
|
+
$ref: "#/components/schemas/ProjectRegistrationCapability"
|
|
56552
|
+
}
|
|
56553
|
+
}
|
|
56554
|
+
}
|
|
56555
|
+
}
|
|
56556
|
+
}
|
|
56557
|
+
}
|
|
56558
|
+
}
|
|
56559
|
+
}
|
|
56560
|
+
},
|
|
56561
|
+
"/v1/project-registration/resources": {
|
|
56562
|
+
get: {
|
|
56563
|
+
operationId: "listProjectRegistrationResources",
|
|
56564
|
+
summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
|
|
56565
|
+
parameters: [
|
|
56566
|
+
{
|
|
56567
|
+
name: "source_project_id",
|
|
56568
|
+
in: "query",
|
|
56569
|
+
required: true,
|
|
56570
|
+
schema: { type: "string" }
|
|
56571
|
+
},
|
|
56572
|
+
{
|
|
56573
|
+
name: "include_anchors",
|
|
56574
|
+
in: "query",
|
|
56575
|
+
schema: { type: "boolean", default: false }
|
|
56576
|
+
},
|
|
56577
|
+
{
|
|
56578
|
+
name: "limit",
|
|
56579
|
+
in: "query",
|
|
56580
|
+
schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
|
|
56581
|
+
},
|
|
56582
|
+
{
|
|
56583
|
+
name: "cursor",
|
|
56584
|
+
in: "query",
|
|
56585
|
+
schema: { type: "string" }
|
|
56586
|
+
}
|
|
56587
|
+
],
|
|
56588
|
+
responses: {
|
|
56589
|
+
"200": {
|
|
56590
|
+
content: {
|
|
56591
|
+
"application/json": {
|
|
56592
|
+
schema: {
|
|
56593
|
+
type: "object",
|
|
56594
|
+
additionalProperties: false,
|
|
56595
|
+
required: ["page"],
|
|
56596
|
+
properties: {
|
|
56597
|
+
page: { $ref: "#/components/schemas/ProjectResourcePage" }
|
|
56598
|
+
}
|
|
56599
|
+
}
|
|
56600
|
+
}
|
|
56601
|
+
}
|
|
56602
|
+
},
|
|
56603
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56604
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56605
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56606
|
+
}
|
|
56607
|
+
}
|
|
56608
|
+
},
|
|
56609
|
+
"/v1/project-registration/create": {
|
|
56610
|
+
post: {
|
|
56611
|
+
operationId: "createProjectRegistrationResource",
|
|
56612
|
+
summary: "Create or deterministically bind one Projects to Todos resource",
|
|
56613
|
+
requestBody: {
|
|
56614
|
+
required: true,
|
|
56615
|
+
content: {
|
|
56616
|
+
"application/json": {
|
|
56617
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
56618
|
+
}
|
|
56619
|
+
}
|
|
56620
|
+
},
|
|
56621
|
+
responses: {
|
|
56622
|
+
"201": {
|
|
56623
|
+
content: {
|
|
56624
|
+
"application/json": {
|
|
56625
|
+
schema: {
|
|
56626
|
+
type: "object",
|
|
56627
|
+
additionalProperties: false,
|
|
56628
|
+
required: ["receipt"],
|
|
56629
|
+
properties: {
|
|
56630
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
|
|
56631
|
+
}
|
|
56632
|
+
}
|
|
56633
|
+
}
|
|
56634
|
+
}
|
|
56635
|
+
},
|
|
56636
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56637
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56638
|
+
}
|
|
56639
|
+
}
|
|
56640
|
+
},
|
|
56641
|
+
"/v1/project-registration/read-exact": {
|
|
56642
|
+
post: {
|
|
56643
|
+
operationId: "readExactProjectRegistrationResource",
|
|
56644
|
+
summary: "Read one registered project or task list by exact full UUID",
|
|
56645
|
+
requestBody: {
|
|
56646
|
+
required: true,
|
|
56647
|
+
content: {
|
|
56648
|
+
"application/json": {
|
|
56649
|
+
schema: {
|
|
56650
|
+
type: "object",
|
|
56651
|
+
additionalProperties: false,
|
|
56652
|
+
required: [
|
|
56653
|
+
"resource_kind",
|
|
56654
|
+
"target_id",
|
|
56655
|
+
"response_byte_limit",
|
|
56656
|
+
"time_budget_ms"
|
|
56657
|
+
],
|
|
56658
|
+
properties: {
|
|
56659
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
56660
|
+
target_id: { type: "string", format: "uuid" },
|
|
56661
|
+
...projectRegistrationBoundsProperties
|
|
56662
|
+
}
|
|
56663
|
+
}
|
|
56664
|
+
}
|
|
56665
|
+
}
|
|
56666
|
+
},
|
|
56667
|
+
responses: {
|
|
56668
|
+
"200": {
|
|
56669
|
+
content: {
|
|
56670
|
+
"application/json": {
|
|
56671
|
+
schema: {
|
|
56672
|
+
type: "object",
|
|
56673
|
+
additionalProperties: false,
|
|
56674
|
+
required: ["record"],
|
|
56675
|
+
properties: {
|
|
56676
|
+
record: {
|
|
56677
|
+
type: "object",
|
|
56678
|
+
additionalProperties: false,
|
|
56679
|
+
required: ["target_id", "revision", "digest"],
|
|
56680
|
+
properties: {
|
|
56681
|
+
target_id: { type: "string", format: "uuid" },
|
|
56682
|
+
revision: { type: "string" },
|
|
56683
|
+
digest: { type: "string" }
|
|
56684
|
+
}
|
|
56685
|
+
}
|
|
56686
|
+
}
|
|
56687
|
+
}
|
|
56688
|
+
}
|
|
56689
|
+
}
|
|
56690
|
+
},
|
|
56691
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56692
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56693
|
+
}
|
|
56694
|
+
}
|
|
56695
|
+
},
|
|
56696
|
+
"/v1/project-registration/receipts/lookup": {
|
|
56697
|
+
post: {
|
|
56698
|
+
operationId: "lookupProjectRegistrationReceipt",
|
|
56699
|
+
summary: "Recover one exact immutable terminal registration receipt",
|
|
56700
|
+
requestBody: {
|
|
56701
|
+
required: true,
|
|
56702
|
+
content: {
|
|
56703
|
+
"application/json": {
|
|
56704
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
|
|
56705
|
+
}
|
|
56706
|
+
}
|
|
56707
|
+
},
|
|
56708
|
+
responses: {
|
|
56709
|
+
"200": {
|
|
56710
|
+
content: {
|
|
56711
|
+
"application/json": {
|
|
56712
|
+
schema: {
|
|
56713
|
+
type: "object",
|
|
56714
|
+
additionalProperties: false,
|
|
56715
|
+
required: ["receipt", "response_control"],
|
|
56716
|
+
properties: {
|
|
56717
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
56718
|
+
response_control: {
|
|
56719
|
+
type: "object",
|
|
56720
|
+
additionalProperties: false,
|
|
56721
|
+
required: [
|
|
56722
|
+
"response_byte_limit",
|
|
56723
|
+
"time_budget_ms",
|
|
56724
|
+
"response_bytes",
|
|
56725
|
+
"elapsed_ms",
|
|
56726
|
+
"complete",
|
|
56727
|
+
"truncated"
|
|
56728
|
+
],
|
|
56729
|
+
properties: {
|
|
56730
|
+
...projectRegistrationBoundsProperties,
|
|
56731
|
+
response_bytes: { type: "integer", minimum: 0 },
|
|
56732
|
+
elapsed_ms: { type: "integer", minimum: 0 },
|
|
56733
|
+
complete: { type: "boolean", enum: [true] },
|
|
56734
|
+
truncated: { type: "boolean", enum: [false] }
|
|
56735
|
+
}
|
|
56736
|
+
}
|
|
56737
|
+
}
|
|
56738
|
+
}
|
|
56739
|
+
}
|
|
56740
|
+
}
|
|
56741
|
+
},
|
|
56742
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56743
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56744
|
+
}
|
|
56745
|
+
}
|
|
56746
|
+
},
|
|
56747
|
+
"/v1/project-registration/validate-prior-adoption": {
|
|
56748
|
+
post: {
|
|
56749
|
+
operationId: "validatePriorRegistrationAdoption",
|
|
56750
|
+
summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
|
|
56751
|
+
requestBody: {
|
|
56752
|
+
required: true,
|
|
56753
|
+
content: {
|
|
56754
|
+
"application/json": {
|
|
56755
|
+
schema: {
|
|
56756
|
+
type: "object",
|
|
56757
|
+
additionalProperties: false,
|
|
56758
|
+
required: ["source_request", "source_receipt", "current_record"],
|
|
56759
|
+
properties: {
|
|
56760
|
+
source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
|
|
56761
|
+
source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
56762
|
+
current_record: {
|
|
56763
|
+
oneOf: [
|
|
56764
|
+
{ $ref: "#/components/schemas/Project" },
|
|
56765
|
+
{ $ref: "#/components/schemas/TaskList" }
|
|
56766
|
+
]
|
|
56767
|
+
}
|
|
56768
|
+
}
|
|
56769
|
+
}
|
|
56770
|
+
}
|
|
56771
|
+
}
|
|
56772
|
+
},
|
|
56773
|
+
responses: {
|
|
56774
|
+
"200": {
|
|
56775
|
+
content: {
|
|
56776
|
+
"application/json": {
|
|
56777
|
+
schema: {
|
|
56778
|
+
type: "object",
|
|
56779
|
+
additionalProperties: false,
|
|
56780
|
+
required: ["validation"],
|
|
56781
|
+
properties: {
|
|
56782
|
+
validation: {
|
|
56783
|
+
$ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
|
|
56784
|
+
}
|
|
56785
|
+
}
|
|
56786
|
+
}
|
|
56787
|
+
}
|
|
56788
|
+
}
|
|
56789
|
+
},
|
|
56790
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56791
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56792
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56793
|
+
}
|
|
56794
|
+
}
|
|
56795
|
+
},
|
|
56796
|
+
"/v1/project-registration/compensate": {
|
|
56797
|
+
post: {
|
|
56798
|
+
operationId: "compensateProjectRegistrationResource",
|
|
56799
|
+
summary: "Conditionally remove an unchanged receipt-owned registration resource",
|
|
56800
|
+
requestBody: {
|
|
56801
|
+
required: true,
|
|
56802
|
+
content: {
|
|
56803
|
+
"application/json": {
|
|
56804
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
56805
|
+
}
|
|
56806
|
+
}
|
|
56807
|
+
},
|
|
56808
|
+
responses: {
|
|
56809
|
+
"201": {
|
|
56810
|
+
content: {
|
|
56811
|
+
"application/json": {
|
|
56812
|
+
schema: {
|
|
56813
|
+
type: "object",
|
|
56814
|
+
additionalProperties: false,
|
|
56815
|
+
required: ["receipt"],
|
|
56816
|
+
properties: {
|
|
56817
|
+
receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
|
|
56818
|
+
}
|
|
56819
|
+
}
|
|
56820
|
+
}
|
|
56821
|
+
}
|
|
56822
|
+
},
|
|
56823
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56824
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56825
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56826
|
+
}
|
|
56827
|
+
}
|
|
56828
|
+
},
|
|
56829
|
+
"/v1/project-registration/verify-inverse": {
|
|
56830
|
+
post: {
|
|
56831
|
+
operationId: "verifyInverseProjectRegistrationResource",
|
|
56832
|
+
summary: "Verify exact absence after conditional registration compensation",
|
|
56833
|
+
requestBody: {
|
|
56834
|
+
required: true,
|
|
56835
|
+
content: {
|
|
56836
|
+
"application/json": {
|
|
56837
|
+
schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
|
|
56838
|
+
}
|
|
56839
|
+
}
|
|
56840
|
+
},
|
|
56841
|
+
responses: {
|
|
56842
|
+
"200": {
|
|
56843
|
+
content: {
|
|
56844
|
+
"application/json": {
|
|
56845
|
+
schema: {
|
|
56846
|
+
type: "object",
|
|
56847
|
+
additionalProperties: false,
|
|
56848
|
+
required: ["verification"],
|
|
56849
|
+
properties: {
|
|
56850
|
+
verification: {
|
|
56851
|
+
type: "object",
|
|
56852
|
+
additionalProperties: false,
|
|
56853
|
+
required: ["target_id", "accepted_receipt_id", "absent", "digest"],
|
|
56854
|
+
properties: {
|
|
56855
|
+
target_id: { type: "string", format: "uuid" },
|
|
56856
|
+
accepted_receipt_id: { type: "string" },
|
|
56857
|
+
absent: { type: "boolean", enum: [true] },
|
|
56858
|
+
digest: { type: "string" }
|
|
56859
|
+
}
|
|
56860
|
+
}
|
|
56861
|
+
}
|
|
56862
|
+
}
|
|
56863
|
+
}
|
|
56864
|
+
}
|
|
56865
|
+
},
|
|
56866
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56867
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56868
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56869
|
+
}
|
|
56870
|
+
}
|
|
56871
|
+
},
|
|
55481
56872
|
"/v1/task-manifest/capability": {
|
|
55482
56873
|
get: {
|
|
55483
56874
|
operationId: "getTaskManifestCapability",
|
|
@@ -55495,6 +56886,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55495
56886
|
}
|
|
55496
56887
|
}
|
|
55497
56888
|
},
|
|
56889
|
+
"/v1/task-manifest/apply": {
|
|
56890
|
+
post: {
|
|
56891
|
+
operationId: "applyTaskManifest",
|
|
56892
|
+
summary: "Apply one exact task-manifest graph through the Todos authority",
|
|
56893
|
+
requestBody: {
|
|
56894
|
+
required: true,
|
|
56895
|
+
content: {
|
|
56896
|
+
"application/json": {
|
|
56897
|
+
schema: { $ref: "#/components/schemas/TaskManifest" }
|
|
56898
|
+
}
|
|
56899
|
+
}
|
|
56900
|
+
},
|
|
56901
|
+
responses: {
|
|
56902
|
+
"201": {
|
|
56903
|
+
content: {
|
|
56904
|
+
"application/json": {
|
|
56905
|
+
schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
|
|
56906
|
+
}
|
|
56907
|
+
}
|
|
56908
|
+
},
|
|
56909
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56910
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56911
|
+
"503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56912
|
+
}
|
|
56913
|
+
}
|
|
56914
|
+
},
|
|
56915
|
+
"/v1/task-manifest/read-exact": {
|
|
56916
|
+
post: {
|
|
56917
|
+
operationId: "readExactTaskManifest",
|
|
56918
|
+
summary: "Read one exact immutable task-manifest apply receipt",
|
|
56919
|
+
requestBody: {
|
|
56920
|
+
required: true,
|
|
56921
|
+
content: {
|
|
56922
|
+
"application/json": {
|
|
56923
|
+
schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
|
|
56924
|
+
}
|
|
56925
|
+
}
|
|
56926
|
+
},
|
|
56927
|
+
responses: {
|
|
56928
|
+
"200": {
|
|
56929
|
+
content: {
|
|
56930
|
+
"application/json": {
|
|
56931
|
+
schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
|
|
56932
|
+
}
|
|
56933
|
+
}
|
|
56934
|
+
},
|
|
56935
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56936
|
+
}
|
|
56937
|
+
}
|
|
56938
|
+
},
|
|
56939
|
+
"/v1/task-manifest/compensate": {
|
|
56940
|
+
post: {
|
|
56941
|
+
operationId: "compensateTaskManifest",
|
|
56942
|
+
summary: "Compensate one exact untouched task-manifest graph with CAS protection",
|
|
56943
|
+
requestBody: {
|
|
56944
|
+
required: true,
|
|
56945
|
+
content: {
|
|
56946
|
+
"application/json": {
|
|
56947
|
+
schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
|
|
56948
|
+
}
|
|
56949
|
+
}
|
|
56950
|
+
},
|
|
56951
|
+
responses: {
|
|
56952
|
+
"201": {
|
|
56953
|
+
content: {
|
|
56954
|
+
"application/json": {
|
|
56955
|
+
schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
|
|
56956
|
+
}
|
|
56957
|
+
}
|
|
56958
|
+
},
|
|
56959
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56960
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
56961
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
56962
|
+
}
|
|
56963
|
+
}
|
|
56964
|
+
},
|
|
55498
56965
|
"/v1/task-manifest/bindings/lookup": {
|
|
55499
56966
|
post: {
|
|
55500
56967
|
operationId: "lookupTaskManifestBinding",
|
|
@@ -56355,7 +57822,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
56355
57822
|
}
|
|
56356
57823
|
});
|
|
56357
57824
|
}
|
|
56358
|
-
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;
|
|
57825
|
+
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;
|
|
56359
57826
|
var init_openapi = __esm(() => {
|
|
56360
57827
|
init_package_version();
|
|
56361
57828
|
init_types();
|
|
@@ -56417,6 +57884,10 @@ var init_openapi = __esm(() => {
|
|
|
56417
57884
|
"tenant_id",
|
|
56418
57885
|
"backend",
|
|
56419
57886
|
"deterministic_ids",
|
|
57887
|
+
"operation_step_identity",
|
|
57888
|
+
"deterministic_idempotency_keys",
|
|
57889
|
+
"terminal_nonacceptance_receipts",
|
|
57890
|
+
"plan_slug_provenance",
|
|
56420
57891
|
"immutable_receipts",
|
|
56421
57892
|
"transactional_outbox",
|
|
56422
57893
|
"idempotent_outbox_delivery",
|
|
@@ -56432,6 +57903,10 @@ var init_openapi = __esm(() => {
|
|
|
56432
57903
|
tenant_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
56433
57904
|
backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
|
|
56434
57905
|
deterministic_ids: { type: "boolean", enum: [true] },
|
|
57906
|
+
operation_step_identity: { type: "boolean", enum: [true] },
|
|
57907
|
+
deterministic_idempotency_keys: { type: "boolean", enum: [true] },
|
|
57908
|
+
terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
|
|
57909
|
+
plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
|
|
56435
57910
|
immutable_receipts: { type: "boolean", enum: [true] },
|
|
56436
57911
|
transactional_outbox: { type: "boolean", enum: [true] },
|
|
56437
57912
|
idempotent_outbox_delivery: { type: "boolean", enum: [true] },
|
|
@@ -56485,6 +57960,8 @@ var init_openapi = __esm(() => {
|
|
|
56485
57960
|
"schema_version",
|
|
56486
57961
|
"tenant_id",
|
|
56487
57962
|
"plan_id",
|
|
57963
|
+
"operation_id",
|
|
57964
|
+
"step_id",
|
|
56488
57965
|
"apply_receipt_id",
|
|
56489
57966
|
"binding_version",
|
|
56490
57967
|
"state"
|
|
@@ -56495,11 +57972,169 @@ var init_openapi = __esm(() => {
|
|
|
56495
57972
|
schema_version: { type: "integer", enum: [1] },
|
|
56496
57973
|
tenant_id: { type: "string" },
|
|
56497
57974
|
plan_id: { type: "string", format: "uuid" },
|
|
57975
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
57976
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
56498
57977
|
apply_receipt_id: { type: "string", format: "uuid" },
|
|
56499
57978
|
binding_version: { type: "integer", minimum: 1 },
|
|
56500
57979
|
state: { type: "string", enum: ["applied", "compensated"] }
|
|
56501
57980
|
}
|
|
56502
57981
|
};
|
|
57982
|
+
taskManifestSchema = {
|
|
57983
|
+
type: "object",
|
|
57984
|
+
additionalProperties: false,
|
|
57985
|
+
required: [
|
|
57986
|
+
"version",
|
|
57987
|
+
"operation_id",
|
|
57988
|
+
"step_id",
|
|
57989
|
+
"idempotency_key",
|
|
57990
|
+
"precondition_digest",
|
|
57991
|
+
"project_id",
|
|
57992
|
+
"plan",
|
|
57993
|
+
"tasks"
|
|
57994
|
+
],
|
|
57995
|
+
properties: {
|
|
57996
|
+
version: { type: "integer", enum: [1] },
|
|
57997
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
57998
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
57999
|
+
idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
|
|
58000
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58001
|
+
project_id: { type: "string", format: "uuid" },
|
|
58002
|
+
task_list_id: { type: "string", format: "uuid" },
|
|
58003
|
+
if_binding_version: { type: "integer", minimum: 0 },
|
|
58004
|
+
plan: {
|
|
58005
|
+
type: "object",
|
|
58006
|
+
additionalProperties: false,
|
|
58007
|
+
required: ["key", "name"],
|
|
58008
|
+
properties: {
|
|
58009
|
+
key: { type: "string", minLength: 1, maxLength: 200 },
|
|
58010
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
58011
|
+
description: { type: "string" },
|
|
58012
|
+
status: { type: "string", enum: ["active", "completed", "archived"] }
|
|
58013
|
+
}
|
|
58014
|
+
},
|
|
58015
|
+
tasks: {
|
|
58016
|
+
type: "array",
|
|
58017
|
+
minItems: 1,
|
|
58018
|
+
items: {
|
|
58019
|
+
type: "object",
|
|
58020
|
+
additionalProperties: false,
|
|
58021
|
+
required: ["key", "title"],
|
|
58022
|
+
properties: {
|
|
58023
|
+
key: { type: "string", minLength: 1, maxLength: 200 },
|
|
58024
|
+
title: { type: "string", minLength: 1, maxLength: 200 },
|
|
58025
|
+
description: { type: "string" },
|
|
58026
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
|
|
58027
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
58028
|
+
assigned_to: { type: "string" },
|
|
58029
|
+
created_by: { type: "string" },
|
|
58030
|
+
tags: { type: "array", items: { type: "string" } },
|
|
58031
|
+
metadata: { type: "object", additionalProperties: true },
|
|
58032
|
+
comments: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
58033
|
+
verifications: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
58034
|
+
}
|
|
58035
|
+
}
|
|
58036
|
+
},
|
|
58037
|
+
dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
58038
|
+
effects: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
58039
|
+
}
|
|
58040
|
+
};
|
|
58041
|
+
taskManifestReceiptSchema = {
|
|
58042
|
+
type: "object",
|
|
58043
|
+
additionalProperties: false,
|
|
58044
|
+
required: [
|
|
58045
|
+
"receipt_id",
|
|
58046
|
+
"authority",
|
|
58047
|
+
"route",
|
|
58048
|
+
"schema_version",
|
|
58049
|
+
"kind",
|
|
58050
|
+
"operation_id",
|
|
58051
|
+
"step_id",
|
|
58052
|
+
"idempotency_key",
|
|
58053
|
+
"request_digest",
|
|
58054
|
+
"precondition_digest",
|
|
58055
|
+
"result_digest",
|
|
58056
|
+
"outcome",
|
|
58057
|
+
"reason",
|
|
58058
|
+
"duplicate_of_receipt_id",
|
|
58059
|
+
"binding_version",
|
|
58060
|
+
"apply_receipt_id",
|
|
58061
|
+
"created_at"
|
|
58062
|
+
],
|
|
58063
|
+
properties: {
|
|
58064
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
58065
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58066
|
+
route: { type: "string", enum: ["todos.task-manifest.v1"] },
|
|
58067
|
+
schema_version: { type: "integer", enum: [1] },
|
|
58068
|
+
kind: { type: "string", enum: ["apply", "compensate"] },
|
|
58069
|
+
operation_id: { type: "string" },
|
|
58070
|
+
step_id: { type: "string" },
|
|
58071
|
+
idempotency_key: { type: "string" },
|
|
58072
|
+
request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58073
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58074
|
+
result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58075
|
+
outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
|
|
58076
|
+
reason: { type: "string", nullable: true },
|
|
58077
|
+
duplicate_of_receipt_id: { type: "string", nullable: true },
|
|
58078
|
+
binding_version: { type: "integer", minimum: 0 },
|
|
58079
|
+
apply_receipt_id: { type: "string", nullable: true },
|
|
58080
|
+
created_at: { type: "string", format: "date-time" }
|
|
58081
|
+
}
|
|
58082
|
+
};
|
|
58083
|
+
taskManifestApplyResultSchema = {
|
|
58084
|
+
type: "object",
|
|
58085
|
+
additionalProperties: false,
|
|
58086
|
+
required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
|
|
58087
|
+
properties: {
|
|
58088
|
+
duplicate: { type: "boolean" },
|
|
58089
|
+
receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
|
|
58090
|
+
graph: { type: "object", additionalProperties: true },
|
|
58091
|
+
readback: { type: "object", additionalProperties: true },
|
|
58092
|
+
outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
|
|
58093
|
+
result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
|
|
58094
|
+
}
|
|
58095
|
+
};
|
|
58096
|
+
taskManifestApplyResponseSchema = {
|
|
58097
|
+
type: "object",
|
|
58098
|
+
additionalProperties: false,
|
|
58099
|
+
required: ["result"],
|
|
58100
|
+
properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
|
|
58101
|
+
};
|
|
58102
|
+
taskManifestCompensateRequestSchema = {
|
|
58103
|
+
type: "object",
|
|
58104
|
+
additionalProperties: false,
|
|
58105
|
+
required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
|
|
58106
|
+
properties: {
|
|
58107
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
58108
|
+
operation_id: { type: "string" },
|
|
58109
|
+
step_id: { type: "string" },
|
|
58110
|
+
idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
|
|
58111
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
58112
|
+
if_binding_version: { type: "integer", minimum: 1 }
|
|
58113
|
+
}
|
|
58114
|
+
};
|
|
58115
|
+
taskManifestCompensationResultSchema = {
|
|
58116
|
+
type: "object",
|
|
58117
|
+
additionalProperties: false,
|
|
58118
|
+
required: ["duplicate", "receipt", "absent", "readback"],
|
|
58119
|
+
properties: {
|
|
58120
|
+
duplicate: { type: "boolean" },
|
|
58121
|
+
receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
|
|
58122
|
+
absent: { type: "boolean", enum: [true] },
|
|
58123
|
+
readback: { type: "object", additionalProperties: true }
|
|
58124
|
+
}
|
|
58125
|
+
};
|
|
58126
|
+
taskManifestCompensateResponseSchema = {
|
|
58127
|
+
type: "object",
|
|
58128
|
+
additionalProperties: false,
|
|
58129
|
+
required: ["result"],
|
|
58130
|
+
properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
|
|
58131
|
+
};
|
|
58132
|
+
taskManifestReadExactRequestSchema = {
|
|
58133
|
+
type: "object",
|
|
58134
|
+
additionalProperties: false,
|
|
58135
|
+
required: ["receipt_id"],
|
|
58136
|
+
properties: { receipt_id: { type: "string", format: "uuid" } }
|
|
58137
|
+
};
|
|
56503
58138
|
taskManifestBindingLookupResponseSchema = {
|
|
56504
58139
|
type: "object",
|
|
56505
58140
|
additionalProperties: false,
|
|
@@ -56849,6 +58484,292 @@ var init_openapi = __esm(() => {
|
|
|
56849
58484
|
metadata: { type: "object", additionalProperties: true }
|
|
56850
58485
|
}
|
|
56851
58486
|
};
|
|
58487
|
+
projectRegistrationBoundsProperties = {
|
|
58488
|
+
response_byte_limit: { type: "integer", minimum: 1 },
|
|
58489
|
+
time_budget_ms: { type: "integer", minimum: 1 }
|
|
58490
|
+
};
|
|
58491
|
+
projectRegistrationReceiptSchema = {
|
|
58492
|
+
type: "object",
|
|
58493
|
+
additionalProperties: false,
|
|
58494
|
+
required: [
|
|
58495
|
+
"receipt_id",
|
|
58496
|
+
"authority",
|
|
58497
|
+
"route",
|
|
58498
|
+
"package_version",
|
|
58499
|
+
"authority_id",
|
|
58500
|
+
"tenant_id",
|
|
58501
|
+
"corpus_id",
|
|
58502
|
+
"operation_id",
|
|
58503
|
+
"step_id",
|
|
58504
|
+
"resource_kind",
|
|
58505
|
+
"direction",
|
|
58506
|
+
"idempotency_key",
|
|
58507
|
+
"request_digest",
|
|
58508
|
+
"precondition_digest",
|
|
58509
|
+
"outcome",
|
|
58510
|
+
"reason",
|
|
58511
|
+
"target_id",
|
|
58512
|
+
"result_revision",
|
|
58513
|
+
"result_digest",
|
|
58514
|
+
"duplicate_of_receipt_id",
|
|
58515
|
+
"accepted_receipt_id",
|
|
58516
|
+
"created_by_operation",
|
|
58517
|
+
"created_at"
|
|
58518
|
+
],
|
|
58519
|
+
properties: {
|
|
58520
|
+
receipt_id: { type: "string" },
|
|
58521
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58522
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
58523
|
+
package_version: { type: "string" },
|
|
58524
|
+
authority_id: { type: "string" },
|
|
58525
|
+
tenant_id: { type: "string" },
|
|
58526
|
+
corpus_id: { type: "string" },
|
|
58527
|
+
operation_id: { type: "string" },
|
|
58528
|
+
step_id: { type: "string" },
|
|
58529
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58530
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
58531
|
+
idempotency_key: { type: "string" },
|
|
58532
|
+
request_digest: { type: "string" },
|
|
58533
|
+
precondition_digest: { type: "string" },
|
|
58534
|
+
outcome: {
|
|
58535
|
+
type: "string",
|
|
58536
|
+
enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
|
|
58537
|
+
},
|
|
58538
|
+
reason: { type: "string", nullable: true },
|
|
58539
|
+
target_id: { type: "string", format: "uuid", nullable: true },
|
|
58540
|
+
result_revision: { type: "string", nullable: true },
|
|
58541
|
+
result_digest: { type: "string", nullable: true },
|
|
58542
|
+
duplicate_of_receipt_id: { type: "string", nullable: true },
|
|
58543
|
+
accepted_receipt_id: { type: "string", nullable: true },
|
|
58544
|
+
created_by_operation: { type: "boolean" },
|
|
58545
|
+
created_at: { type: "string", format: "date-time" }
|
|
58546
|
+
}
|
|
58547
|
+
};
|
|
58548
|
+
projectRegistrationCapabilitySchema = {
|
|
58549
|
+
type: "object",
|
|
58550
|
+
additionalProperties: false,
|
|
58551
|
+
required: [
|
|
58552
|
+
"authority",
|
|
58553
|
+
"route",
|
|
58554
|
+
"package_version",
|
|
58555
|
+
"authority_id",
|
|
58556
|
+
"tenant_id",
|
|
58557
|
+
"corpus_id",
|
|
58558
|
+
"supported_resources",
|
|
58559
|
+
"conditional_create",
|
|
58560
|
+
"immutable_receipts",
|
|
58561
|
+
"exact_terminal_lookup",
|
|
58562
|
+
"exact_readback",
|
|
58563
|
+
"bind_existing_adoption",
|
|
58564
|
+
"prior_registration_adoption_validation",
|
|
58565
|
+
"project_resource_enumeration",
|
|
58566
|
+
"project_resource_page_limit",
|
|
58567
|
+
"conditional_inverse",
|
|
58568
|
+
"ambiguous_outcome_reconciliation"
|
|
58569
|
+
],
|
|
58570
|
+
properties: {
|
|
58571
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58572
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
58573
|
+
package_version: { type: "string" },
|
|
58574
|
+
authority_id: { type: "string" },
|
|
58575
|
+
tenant_id: { type: "string" },
|
|
58576
|
+
corpus_id: { type: "string" },
|
|
58577
|
+
supported_resources: {
|
|
58578
|
+
type: "array",
|
|
58579
|
+
items: { type: "string", enum: ["project", "task_list"] }
|
|
58580
|
+
},
|
|
58581
|
+
conditional_create: { type: "boolean", enum: [true] },
|
|
58582
|
+
immutable_receipts: { type: "boolean", enum: [true] },
|
|
58583
|
+
exact_terminal_lookup: { type: "boolean", enum: [true] },
|
|
58584
|
+
exact_readback: { type: "boolean", enum: [true] },
|
|
58585
|
+
bind_existing_adoption: { type: "boolean", enum: [true] },
|
|
58586
|
+
prior_registration_adoption_validation: { type: "boolean", enum: [true] },
|
|
58587
|
+
project_resource_enumeration: { type: "boolean", enum: [true] },
|
|
58588
|
+
project_resource_page_limit: { type: "integer", minimum: 1 },
|
|
58589
|
+
conditional_inverse: { type: "boolean", enum: [true] },
|
|
58590
|
+
ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
|
|
58591
|
+
}
|
|
58592
|
+
};
|
|
58593
|
+
projectRegistrationRequestSchema = {
|
|
58594
|
+
type: "object",
|
|
58595
|
+
additionalProperties: false,
|
|
58596
|
+
required: [
|
|
58597
|
+
"operation_id",
|
|
58598
|
+
"step_id",
|
|
58599
|
+
"resource_kind",
|
|
58600
|
+
"direction",
|
|
58601
|
+
"authority_route",
|
|
58602
|
+
"package_version",
|
|
58603
|
+
"authority_id",
|
|
58604
|
+
"tenant_id",
|
|
58605
|
+
"corpus_id",
|
|
58606
|
+
"target_selector",
|
|
58607
|
+
"idempotency_key",
|
|
58608
|
+
"request_digest",
|
|
58609
|
+
"precondition_digest",
|
|
58610
|
+
"project_id",
|
|
58611
|
+
"project_slug",
|
|
58612
|
+
"project_name",
|
|
58613
|
+
"desired",
|
|
58614
|
+
"response_byte_limit",
|
|
58615
|
+
"time_budget_ms"
|
|
58616
|
+
],
|
|
58617
|
+
properties: {
|
|
58618
|
+
operation_id: { type: "string" },
|
|
58619
|
+
step_id: { type: "string" },
|
|
58620
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58621
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
58622
|
+
authority_route: { type: "string" },
|
|
58623
|
+
package_version: { type: "string" },
|
|
58624
|
+
authority_id: { type: "string" },
|
|
58625
|
+
tenant_id: { type: "string" },
|
|
58626
|
+
corpus_id: { type: "string" },
|
|
58627
|
+
target_selector: { type: "string" },
|
|
58628
|
+
idempotency_key: { type: "string" },
|
|
58629
|
+
request_digest: { type: "string" },
|
|
58630
|
+
precondition_digest: { type: "string" },
|
|
58631
|
+
project_id: { type: "string" },
|
|
58632
|
+
project_slug: { type: "string" },
|
|
58633
|
+
project_name: { type: "string" },
|
|
58634
|
+
desired: { type: "object", additionalProperties: true },
|
|
58635
|
+
bind_existing: { type: "boolean" },
|
|
58636
|
+
accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
|
|
58637
|
+
...projectRegistrationBoundsProperties
|
|
58638
|
+
}
|
|
58639
|
+
};
|
|
58640
|
+
projectRegistrationLookupRequestSchema = {
|
|
58641
|
+
type: "object",
|
|
58642
|
+
additionalProperties: false,
|
|
58643
|
+
required: [
|
|
58644
|
+
"operation_id",
|
|
58645
|
+
"step_id",
|
|
58646
|
+
"resource_kind",
|
|
58647
|
+
"direction",
|
|
58648
|
+
"authority",
|
|
58649
|
+
"authority_route",
|
|
58650
|
+
"package_version",
|
|
58651
|
+
"authority_id",
|
|
58652
|
+
"tenant_id",
|
|
58653
|
+
"corpus_id",
|
|
58654
|
+
"target_selector",
|
|
58655
|
+
"idempotency_key",
|
|
58656
|
+
"max_items",
|
|
58657
|
+
"response_byte_limit",
|
|
58658
|
+
"time_budget_ms"
|
|
58659
|
+
],
|
|
58660
|
+
properties: {
|
|
58661
|
+
operation_id: { type: "string" },
|
|
58662
|
+
step_id: { type: "string" },
|
|
58663
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58664
|
+
direction: { type: "string", enum: ["forward", "inverse"] },
|
|
58665
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58666
|
+
authority_route: { type: "string" },
|
|
58667
|
+
package_version: { type: "string" },
|
|
58668
|
+
authority_id: { type: "string" },
|
|
58669
|
+
tenant_id: { type: "string" },
|
|
58670
|
+
corpus_id: { type: "string" },
|
|
58671
|
+
target_selector: { type: "string" },
|
|
58672
|
+
idempotency_key: { type: "string" },
|
|
58673
|
+
target_id: { type: "string", format: "uuid" },
|
|
58674
|
+
max_items: { type: "integer", enum: [1] },
|
|
58675
|
+
...projectRegistrationBoundsProperties
|
|
58676
|
+
}
|
|
58677
|
+
};
|
|
58678
|
+
priorRegistrationAdoptionValidationSchema = {
|
|
58679
|
+
type: "object",
|
|
58680
|
+
additionalProperties: false,
|
|
58681
|
+
required: [
|
|
58682
|
+
"valid",
|
|
58683
|
+
"resource_kind",
|
|
58684
|
+
"target_id",
|
|
58685
|
+
"source_receipt_id",
|
|
58686
|
+
"accepted_receipt_id",
|
|
58687
|
+
"source_outcome",
|
|
58688
|
+
"created_at",
|
|
58689
|
+
"current_revision",
|
|
58690
|
+
"accepted_result_digest"
|
|
58691
|
+
],
|
|
58692
|
+
properties: {
|
|
58693
|
+
valid: { type: "boolean", enum: [true] },
|
|
58694
|
+
resource_kind: { type: "string", enum: ["project", "task_list"] },
|
|
58695
|
+
target_id: { type: "string", format: "uuid" },
|
|
58696
|
+
source_receipt_id: { type: "string" },
|
|
58697
|
+
accepted_receipt_id: { type: "string" },
|
|
58698
|
+
source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
|
|
58699
|
+
created_at: { type: "string", format: "date-time" },
|
|
58700
|
+
current_revision: { type: "string", format: "date-time" },
|
|
58701
|
+
accepted_result_digest: { type: "string" }
|
|
58702
|
+
}
|
|
58703
|
+
};
|
|
58704
|
+
projectResourceSchema = {
|
|
58705
|
+
type: "object",
|
|
58706
|
+
additionalProperties: false,
|
|
58707
|
+
required: [
|
|
58708
|
+
"source_project_id",
|
|
58709
|
+
"kind",
|
|
58710
|
+
"scope",
|
|
58711
|
+
"target_id",
|
|
58712
|
+
"parent_id",
|
|
58713
|
+
"revision",
|
|
58714
|
+
"digest"
|
|
58715
|
+
],
|
|
58716
|
+
properties: {
|
|
58717
|
+
source_project_id: { type: "string" },
|
|
58718
|
+
kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
|
|
58719
|
+
scope: { type: "string", enum: ["collection", "resource"] },
|
|
58720
|
+
target_id: { type: "string", format: "uuid" },
|
|
58721
|
+
parent_id: { type: "string", format: "uuid", nullable: true },
|
|
58722
|
+
revision: { type: "string" },
|
|
58723
|
+
digest: { type: "string" }
|
|
58724
|
+
}
|
|
58725
|
+
};
|
|
58726
|
+
projectResourcePageSchema = {
|
|
58727
|
+
type: "object",
|
|
58728
|
+
additionalProperties: false,
|
|
58729
|
+
required: [
|
|
58730
|
+
"authority",
|
|
58731
|
+
"route",
|
|
58732
|
+
"package_version",
|
|
58733
|
+
"authority_id",
|
|
58734
|
+
"tenant_id",
|
|
58735
|
+
"corpus_id",
|
|
58736
|
+
"source_project_id",
|
|
58737
|
+
"todos_project_id",
|
|
58738
|
+
"task_list_id",
|
|
58739
|
+
"include_anchors",
|
|
58740
|
+
"collection_revision",
|
|
58741
|
+
"limit",
|
|
58742
|
+
"count",
|
|
58743
|
+
"resources",
|
|
58744
|
+
"has_more",
|
|
58745
|
+
"next_cursor",
|
|
58746
|
+
"complete",
|
|
58747
|
+
"truncated"
|
|
58748
|
+
],
|
|
58749
|
+
properties: {
|
|
58750
|
+
authority: { type: "string", enum: ["todos"] },
|
|
58751
|
+
route: { type: "string", enum: ["todos.project-registration.v1"] },
|
|
58752
|
+
package_version: { type: "string" },
|
|
58753
|
+
authority_id: { type: "string" },
|
|
58754
|
+
tenant_id: { type: "string" },
|
|
58755
|
+
corpus_id: { type: "string" },
|
|
58756
|
+
source_project_id: { type: "string" },
|
|
58757
|
+
todos_project_id: { type: "string", format: "uuid" },
|
|
58758
|
+
task_list_id: { type: "string", format: "uuid" },
|
|
58759
|
+
include_anchors: { type: "boolean" },
|
|
58760
|
+
collection_revision: { type: "string" },
|
|
58761
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
58762
|
+
count: { type: "integer", minimum: 0 },
|
|
58763
|
+
resources: {
|
|
58764
|
+
type: "array",
|
|
58765
|
+
items: { $ref: "#/components/schemas/ProjectResource" }
|
|
58766
|
+
},
|
|
58767
|
+
has_more: { type: "boolean" },
|
|
58768
|
+
next_cursor: { type: "string", nullable: true },
|
|
58769
|
+
complete: { type: "boolean" },
|
|
58770
|
+
truncated: { type: "boolean", enum: [false] }
|
|
58771
|
+
}
|
|
58772
|
+
};
|
|
56852
58773
|
});
|
|
56853
58774
|
|
|
56854
58775
|
// src/server/pr-groups.ts
|
|
@@ -56995,11 +58916,11 @@ function canonicalJson2(value) {
|
|
|
56995
58916
|
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
56996
58917
|
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
|
|
56997
58918
|
}
|
|
56998
|
-
function
|
|
58919
|
+
function digest2(value) {
|
|
56999
58920
|
return createHash17("sha256").update(canonicalJson2(value)).digest("hex");
|
|
57000
58921
|
}
|
|
57001
58922
|
function deriveIdempotencyKey(projectId, slug) {
|
|
57002
|
-
return `ptlk_${
|
|
58923
|
+
return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
57003
58924
|
}
|
|
57004
58925
|
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
57005
58926
|
const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
@@ -57008,13 +58929,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
|
|
|
57008
58929
|
}
|
|
57009
58930
|
return key2;
|
|
57010
58931
|
}
|
|
57011
|
-
function receiptId2(projectId, slug,
|
|
57012
|
-
return `ptlr_${
|
|
58932
|
+
function receiptId2(projectId, slug, idempotencyKey2) {
|
|
58933
|
+
return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
|
|
57013
58934
|
}
|
|
57014
58935
|
function semanticListDigest(list) {
|
|
57015
58936
|
const metadata = { ...list.metadata ?? {} };
|
|
57016
58937
|
delete metadata[RECEIPT_METADATA_KEY];
|
|
57017
|
-
return
|
|
58938
|
+
return digest2({
|
|
57018
58939
|
project_id: list.project_id,
|
|
57019
58940
|
slug: list.slug,
|
|
57020
58941
|
name: list.name,
|
|
@@ -57031,10 +58952,10 @@ function storedMarker(list) {
|
|
|
57031
58952
|
return null;
|
|
57032
58953
|
return marker;
|
|
57033
58954
|
}
|
|
57034
|
-
function receiptFor(store, project, list,
|
|
58955
|
+
function receiptFor(store, project, list, idempotencyKey2) {
|
|
57035
58956
|
const marker = storedMarker(list);
|
|
57036
58957
|
const owned = marker?.project_id === project.id && marker.slug === list.slug;
|
|
57037
|
-
if (owned && marker.idempotency_key !==
|
|
58958
|
+
if (owned && marker.idempotency_key !== idempotencyKey2) {
|
|
57038
58959
|
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
|
|
57039
58960
|
project_id: project.id,
|
|
57040
58961
|
task_list_id: list.id,
|
|
@@ -57043,8 +58964,8 @@ function receiptFor(store, project, list, idempotencyKey) {
|
|
|
57043
58964
|
}
|
|
57044
58965
|
return {
|
|
57045
58966
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
57046
|
-
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${
|
|
57047
|
-
idempotency_key: owned ? marker.idempotency_key :
|
|
58967
|
+
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
|
|
58968
|
+
idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
|
|
57048
58969
|
project_id: project.id,
|
|
57049
58970
|
task_list_id: list.id,
|
|
57050
58971
|
slug: list.slug,
|
|
@@ -57096,20 +59017,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
57096
59017
|
});
|
|
57097
59018
|
}
|
|
57098
59019
|
const slug = project.task_list_id;
|
|
57099
|
-
const
|
|
59020
|
+
const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
|
|
57100
59021
|
if (state.scoped) {
|
|
57101
59022
|
return {
|
|
57102
59023
|
mode: "apply",
|
|
57103
59024
|
action: "already_present",
|
|
57104
59025
|
project,
|
|
57105
59026
|
task_list: state.scoped,
|
|
57106
|
-
receipt: receiptFor(store, project, state.scoped,
|
|
59027
|
+
receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
|
|
57107
59028
|
};
|
|
57108
59029
|
}
|
|
57109
59030
|
const marker = {
|
|
57110
59031
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
57111
|
-
receipt_id: receiptId2(project.id, slug,
|
|
57112
|
-
idempotency_key:
|
|
59032
|
+
receipt_id: receiptId2(project.id, slug, idempotencyKey2),
|
|
59033
|
+
idempotency_key: idempotencyKey2,
|
|
57113
59034
|
project_id: project.id,
|
|
57114
59035
|
slug,
|
|
57115
59036
|
result_digest: semanticListDigest({
|
|
@@ -57147,7 +59068,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
57147
59068
|
action: "already_present",
|
|
57148
59069
|
project: raced.project,
|
|
57149
59070
|
task_list: raced.scoped,
|
|
57150
|
-
receipt: receiptFor(store, raced.project, raced.scoped,
|
|
59071
|
+
receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
|
|
57151
59072
|
};
|
|
57152
59073
|
}
|
|
57153
59074
|
const projectReadback = await store.projects.get(project.id);
|
|
@@ -57177,7 +59098,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
|
57177
59098
|
action: "created",
|
|
57178
59099
|
project: projectReadback,
|
|
57179
59100
|
task_list: readback,
|
|
57180
|
-
receipt: receiptFor(store, projectReadback, readback,
|
|
59101
|
+
receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
|
|
57181
59102
|
};
|
|
57182
59103
|
}
|
|
57183
59104
|
async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
@@ -57222,7 +59143,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
|
57222
59143
|
project_id: project.id,
|
|
57223
59144
|
task_list_id: list.id,
|
|
57224
59145
|
accepted_receipt_id: options.receipt_id,
|
|
57225
|
-
rollback_receipt_id: `ptlr_inverse_${
|
|
59146
|
+
rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
57226
59147
|
removed_at: new Date().toISOString()
|
|
57227
59148
|
};
|
|
57228
59149
|
}
|
|
@@ -57421,6 +59342,9 @@ function validateTaskPatchVocabulary(value) {
|
|
|
57421
59342
|
if (!parsed.ok)
|
|
57422
59343
|
return { ok: false, message: parsed.message };
|
|
57423
59344
|
}
|
|
59345
|
+
if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
|
|
59346
|
+
return { ok: false, message: "parent_id must be a non-empty task id or null" };
|
|
59347
|
+
}
|
|
57424
59348
|
return { ok: true, patch: body2 };
|
|
57425
59349
|
}
|
|
57426
59350
|
function validateProjectPatch(value) {
|
|
@@ -57905,8 +59829,8 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57905
59829
|
}
|
|
57906
59830
|
const created = await store.tasks.create(body2, storageContext);
|
|
57907
59831
|
const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
|
|
57908
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
|
|
57909
|
-
return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id and
|
|
59832
|
+
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null)) {
|
|
59833
|
+
return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, and plan_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
|
|
57910
59834
|
}
|
|
57911
59835
|
return json5({ task: persisted }, 201);
|
|
57912
59836
|
}
|
|
@@ -58780,6 +60704,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
58780
60704
|
if (e instanceof TaskNotFoundError) {
|
|
58781
60705
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
58782
60706
|
}
|
|
60707
|
+
if (e instanceof VersionConflictError) {
|
|
60708
|
+
return error(409, e.message, {
|
|
60709
|
+
code: VersionConflictError.code,
|
|
60710
|
+
conflict: true,
|
|
60711
|
+
task_id: e.taskId,
|
|
60712
|
+
expected_version: e.expectedVersion,
|
|
60713
|
+
current_version: e.actualVersion
|
|
60714
|
+
});
|
|
60715
|
+
}
|
|
58783
60716
|
if (e instanceof StaleLockHandoffError) {
|
|
58784
60717
|
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;
|
|
58785
60718
|
return error(status2, e.message, {
|