@hasna/todos 0.15.29 → 0.15.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +13 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +2 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2835 -334
- package/dist/contracts.js +52 -2
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1447 -144
- package/dist/lib/task-parent-integrity.d.ts +14 -0
- package/dist/lib/task-parent-integrity.d.ts.map +1 -0
- package/dist/mcp/index.js +2031 -135
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/project-registration/adoption-validation.d.ts +3 -0
- package/dist/project-registration/adoption-validation.d.ts.map +1 -0
- package/dist/project-registration/authority.d.ts +3 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/backend.d.ts +24 -1
- package/dist/project-registration/backend.d.ts.map +1 -1
- package/dist/project-registration/http.d.ts +3 -1
- package/dist/project-registration/http.d.ts.map +1 -1
- package/dist/project-registration/index.d.ts +2 -1
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/page-validation.d.ts +5 -0
- package/dist/project-registration/page-validation.d.ts.map +1 -0
- package/dist/project-registration/postgres.d.ts +13 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts +13 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration/types.d.ts +68 -1
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +831 -58
- package/dist/registry.js +52 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +151 -0
- package/dist/sdk/v1.generated.d.ts +282 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +2022 -126
- package/dist/server/openapi.d.ts +2856 -1357
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +8 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +232 -19
- package/dist/task-manifest/authority.d.ts +13 -1
- package/dist/task-manifest/authority.d.ts.map +1 -1
- package/dist/task-manifest/backend.d.ts +5 -0
- package/dist/task-manifest/backend.d.ts.map +1 -1
- package/dist/task-manifest/index.d.ts +2 -2
- package/dist/task-manifest/index.d.ts.map +1 -1
- package/dist/task-manifest/plan-slug.d.ts +20 -0
- package/dist/task-manifest/plan-slug.d.ts.map +1 -1
- package/dist/task-manifest/postgres.d.ts +1 -0
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/schema-sql.d.ts.map +1 -1
- package/dist/task-manifest/schema.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -0
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest/types.d.ts +21 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +592 -61
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/task-manifest.js
CHANGED
|
@@ -4873,7 +4873,9 @@ init_types();
|
|
|
4873
4873
|
|
|
4874
4874
|
// src/task-manifest/types.ts
|
|
4875
4875
|
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
|
|
4876
|
+
var TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1";
|
|
4876
4877
|
var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
|
|
4878
|
+
var TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1";
|
|
4877
4879
|
function supportsIdempotentOutboxDelivery(capability) {
|
|
4878
4880
|
return capability !== null && typeof capability === "object" && capability["idempotent_outbox_delivery"] === true;
|
|
4879
4881
|
}
|
|
@@ -4903,6 +4905,8 @@ var TODOS_TASK_MANIFEST_BOUNDS = {
|
|
|
4903
4905
|
};
|
|
4904
4906
|
var key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
|
|
4905
4907
|
var identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
4908
|
+
var digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
|
|
4909
|
+
var idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
|
|
4906
4910
|
var uuid = exports_external.string().uuid();
|
|
4907
4911
|
var scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
|
|
4908
4912
|
var boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
|
|
@@ -4945,7 +4949,9 @@ var effect = exports_external.object({
|
|
|
4945
4949
|
var schema = exports_external.object({
|
|
4946
4950
|
version: exports_external.literal(1),
|
|
4947
4951
|
operation_id: identifier,
|
|
4948
|
-
|
|
4952
|
+
step_id: identifier,
|
|
4953
|
+
idempotency_key: idempotencyKey,
|
|
4954
|
+
precondition_digest: digest,
|
|
4949
4955
|
project_id: uuid,
|
|
4950
4956
|
task_list_id: uuid.optional(),
|
|
4951
4957
|
if_binding_version: exports_external.number().int().min(0).optional(),
|
|
@@ -4961,7 +4967,10 @@ var schema = exports_external.object({
|
|
|
4961
4967
|
}).strict();
|
|
4962
4968
|
var compensationSchema = exports_external.object({
|
|
4963
4969
|
receipt_id: uuid,
|
|
4964
|
-
|
|
4970
|
+
operation_id: identifier,
|
|
4971
|
+
step_id: identifier,
|
|
4972
|
+
idempotency_key: idempotencyKey,
|
|
4973
|
+
precondition_digest: digest,
|
|
4965
4974
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
4966
4975
|
}).strict();
|
|
4967
4976
|
var bindingLookupSchema = exports_external.object({
|
|
@@ -5046,10 +5055,31 @@ function parseTodosTaskManifestBindingLookup(input) {
|
|
|
5046
5055
|
}
|
|
5047
5056
|
|
|
5048
5057
|
// src/task-manifest/plan-slug.ts
|
|
5058
|
+
var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
|
|
5049
5059
|
function taskManifestPlanSlug(manifest, planId) {
|
|
5050
5060
|
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
5051
5061
|
return `${base}-${planId}`;
|
|
5052
5062
|
}
|
|
5063
|
+
function sqliteLegacyTaskManifestPlanSlug(rows, planId, targetBase) {
|
|
5064
|
+
const target = rows.find((row) => row.id === planId);
|
|
5065
|
+
if (!target)
|
|
5066
|
+
return null;
|
|
5067
|
+
const used = new Set;
|
|
5068
|
+
const ordered = [...rows].filter((row) => row.project_id === target.project_id).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
|
|
5069
|
+
for (const row of ordered) {
|
|
5070
|
+
const base = row.id === planId ? normalizeSlug(targetBase ?? row.name) || "plan" : normalizeSlug(row.slug || row.name) || "plan";
|
|
5071
|
+
let candidate = base;
|
|
5072
|
+
let suffix = 2;
|
|
5073
|
+
while (used.has(candidate)) {
|
|
5074
|
+
candidate = `${base}-${suffix}`;
|
|
5075
|
+
suffix += 1;
|
|
5076
|
+
}
|
|
5077
|
+
if (row.id === planId)
|
|
5078
|
+
return candidate;
|
|
5079
|
+
used.add(candidate);
|
|
5080
|
+
}
|
|
5081
|
+
return null;
|
|
5082
|
+
}
|
|
5053
5083
|
|
|
5054
5084
|
// src/task-manifest/backend.ts
|
|
5055
5085
|
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -5063,11 +5093,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
|
5063
5093
|
const row = rows[0];
|
|
5064
5094
|
const bindingVersion = Number(row.binding_version);
|
|
5065
5095
|
const state = row.state;
|
|
5066
|
-
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_PATTERN.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
|
|
5096
|
+
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_PATTERN.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
|
|
5067
5097
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
5068
5098
|
}
|
|
5069
5099
|
return {
|
|
5070
5100
|
plan_id: planId,
|
|
5101
|
+
operation_id: row.binding_operation_id,
|
|
5102
|
+
step_id: row.binding_step_id,
|
|
5071
5103
|
apply_receipt_id: row.apply_receipt_id,
|
|
5072
5104
|
binding_version: bindingVersion,
|
|
5073
5105
|
state
|
|
@@ -5175,9 +5207,15 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
5175
5207
|
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
5176
5208
|
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
5177
5209
|
operation_id TEXT NOT NULL,
|
|
5210
|
+
step_id TEXT NOT NULL,
|
|
5178
5211
|
idempotency_key TEXT NOT NULL,
|
|
5179
5212
|
request_digest TEXT NOT NULL,
|
|
5213
|
+
precondition_digest TEXT NOT NULL,
|
|
5180
5214
|
result_digest TEXT NOT NULL,
|
|
5215
|
+
slug_provenance TEXT,
|
|
5216
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
5217
|
+
reason TEXT,
|
|
5218
|
+
duplicate_of_receipt_id TEXT,
|
|
5181
5219
|
binding_version INTEGER NOT NULL,
|
|
5182
5220
|
apply_receipt_id TEXT,
|
|
5183
5221
|
manifest_json TEXT,
|
|
@@ -5188,9 +5226,13 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
5188
5226
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
5189
5227
|
operation_id TEXT PRIMARY KEY,
|
|
5190
5228
|
tenant_id TEXT NOT NULL,
|
|
5229
|
+
step_id TEXT NOT NULL,
|
|
5191
5230
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
5192
5231
|
request_digest TEXT NOT NULL,
|
|
5232
|
+
precondition_digest TEXT NOT NULL,
|
|
5193
5233
|
result_digest TEXT NOT NULL,
|
|
5234
|
+
slug_provenance TEXT,
|
|
5235
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
5194
5236
|
apply_receipt_id TEXT NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
5195
5237
|
manifest_json TEXT NOT NULL,
|
|
5196
5238
|
result_json TEXT NOT NULL,
|
|
@@ -5211,6 +5253,27 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
5211
5253
|
created_at TEXT NOT NULL,
|
|
5212
5254
|
delivered_at TEXT
|
|
5213
5255
|
);
|
|
5256
|
+
CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
5257
|
+
receipt_id TEXT PRIMARY KEY,
|
|
5258
|
+
tenant_id TEXT NOT NULL,
|
|
5259
|
+
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
5260
|
+
route TEXT NOT NULL,
|
|
5261
|
+
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
5262
|
+
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
5263
|
+
operation_id TEXT NOT NULL,
|
|
5264
|
+
step_id TEXT NOT NULL,
|
|
5265
|
+
idempotency_key TEXT NOT NULL,
|
|
5266
|
+
request_digest TEXT NOT NULL,
|
|
5267
|
+
precondition_digest TEXT NOT NULL,
|
|
5268
|
+
result_digest TEXT NOT NULL,
|
|
5269
|
+
outcome TEXT NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
5270
|
+
reason TEXT NOT NULL,
|
|
5271
|
+
binding_version INTEGER NOT NULL,
|
|
5272
|
+
apply_receipt_id TEXT,
|
|
5273
|
+
manifest_json TEXT,
|
|
5274
|
+
result_json TEXT NOT NULL,
|
|
5275
|
+
created_at TEXT NOT NULL
|
|
5276
|
+
);
|
|
5214
5277
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_outbox_receipt
|
|
5215
5278
|
ON todos_task_manifest_outbox(apply_receipt_id, status);
|
|
5216
5279
|
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_receipts_immutable_update
|
|
@@ -5221,6 +5284,14 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
5221
5284
|
BEFORE DELETE ON todos_task_manifest_receipts BEGIN
|
|
5222
5285
|
SELECT RAISE(ABORT, 'todos task manifest receipts are immutable');
|
|
5223
5286
|
END;
|
|
5287
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_update
|
|
5288
|
+
BEFORE UPDATE ON todos_task_manifest_terminal_receipts BEGIN
|
|
5289
|
+
SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
|
|
5290
|
+
END;
|
|
5291
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_delete
|
|
5292
|
+
BEFORE DELETE ON todos_task_manifest_terminal_receipts BEGIN
|
|
5293
|
+
SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
|
|
5294
|
+
END;
|
|
5224
5295
|
`;
|
|
5225
5296
|
}
|
|
5226
5297
|
function sqliteTableHasColumn(db, tableName, columnName) {
|
|
@@ -5237,6 +5308,24 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
5237
5308
|
if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
|
|
5238
5309
|
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
|
|
5239
5310
|
}
|
|
5311
|
+
if (!sqliteTableHasColumn(db, tableName, "slug_provenance")) {
|
|
5312
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN slug_provenance TEXT`);
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
const defaults = [
|
|
5316
|
+
["todos_task_manifest_receipts", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
|
|
5317
|
+
["todos_task_manifest_receipts", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
|
|
5318
|
+
["todos_task_manifest_receipts", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"],
|
|
5319
|
+
["todos_task_manifest_receipts", "reason", "TEXT"],
|
|
5320
|
+
["todos_task_manifest_receipts", "duplicate_of_receipt_id", "TEXT"],
|
|
5321
|
+
["todos_task_manifest_bindings", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
|
|
5322
|
+
["todos_task_manifest_bindings", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
|
|
5323
|
+
["todos_task_manifest_bindings", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"]
|
|
5324
|
+
];
|
|
5325
|
+
for (const [tableName, columnName, definition] of defaults) {
|
|
5326
|
+
if (!sqliteTableHasColumn(db, tableName, columnName)) {
|
|
5327
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${definition}`);
|
|
5328
|
+
}
|
|
5240
5329
|
}
|
|
5241
5330
|
db.exec(`
|
|
5242
5331
|
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
|
|
@@ -5246,6 +5335,12 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
|
5246
5335
|
tenant_id,
|
|
5247
5336
|
json_extract(result_json, '$.graph.plan_id')
|
|
5248
5337
|
);
|
|
5338
|
+
DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_lookup;
|
|
5339
|
+
DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_identity;
|
|
5340
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_lookup
|
|
5341
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
|
|
5342
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_identity
|
|
5343
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
|
|
5249
5344
|
`);
|
|
5250
5345
|
}
|
|
5251
5346
|
function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
@@ -5259,9 +5354,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
5259
5354
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
5260
5355
|
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
5261
5356
|
operation_id text NOT NULL,
|
|
5357
|
+
step_id text NOT NULL,
|
|
5262
5358
|
idempotency_key text NOT NULL,
|
|
5263
5359
|
request_digest text NOT NULL,
|
|
5360
|
+
precondition_digest text NOT NULL,
|
|
5264
5361
|
result_digest text NOT NULL,
|
|
5362
|
+
slug_provenance text,
|
|
5363
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
5364
|
+
reason text,
|
|
5365
|
+
duplicate_of_receipt_id text,
|
|
5265
5366
|
binding_version integer NOT NULL,
|
|
5266
5367
|
apply_receipt_id text,
|
|
5267
5368
|
manifest_json jsonb,
|
|
@@ -5273,12 +5374,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
5273
5374
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
5274
5375
|
`ALTER TABLE todos_task_manifest_receipts
|
|
5275
5376
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
5377
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
5378
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
5379
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
5380
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
5381
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
5382
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
5383
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
5384
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
5385
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
5386
|
+
ADD COLUMN IF NOT EXISTS reason text`,
|
|
5387
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
5388
|
+
ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
|
|
5276
5389
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
5277
5390
|
operation_id text PRIMARY KEY,
|
|
5278
5391
|
tenant_id text NOT NULL,
|
|
5392
|
+
step_id text NOT NULL,
|
|
5279
5393
|
idempotency_key text NOT NULL UNIQUE,
|
|
5280
5394
|
request_digest text NOT NULL,
|
|
5395
|
+
precondition_digest text NOT NULL,
|
|
5281
5396
|
result_digest text NOT NULL,
|
|
5397
|
+
slug_provenance text,
|
|
5398
|
+
outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
|
|
5282
5399
|
apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
5283
5400
|
manifest_json jsonb NOT NULL,
|
|
5284
5401
|
result_json jsonb NOT NULL,
|
|
@@ -5292,6 +5409,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
5292
5409
|
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
5293
5410
|
`ALTER TABLE todos_task_manifest_bindings
|
|
5294
5411
|
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
5412
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
5413
|
+
ADD COLUMN IF NOT EXISTS slug_provenance text`,
|
|
5414
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
5415
|
+
ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
|
|
5416
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
5417
|
+
ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
|
|
5418
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
5419
|
+
ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
|
|
5295
5420
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
5296
5421
|
id text PRIMARY KEY,
|
|
5297
5422
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -5303,10 +5428,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
5303
5428
|
created_at timestamptz NOT NULL,
|
|
5304
5429
|
delivered_at timestamptz
|
|
5305
5430
|
)`,
|
|
5431
|
+
`CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
|
|
5432
|
+
receipt_id text PRIMARY KEY,
|
|
5433
|
+
tenant_id text NOT NULL,
|
|
5434
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
5435
|
+
route text NOT NULL,
|
|
5436
|
+
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
5437
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
5438
|
+
operation_id text NOT NULL,
|
|
5439
|
+
step_id text NOT NULL,
|
|
5440
|
+
idempotency_key text NOT NULL,
|
|
5441
|
+
request_digest text NOT NULL,
|
|
5442
|
+
precondition_digest text NOT NULL,
|
|
5443
|
+
result_digest text NOT NULL,
|
|
5444
|
+
outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
|
|
5445
|
+
reason text NOT NULL,
|
|
5446
|
+
binding_version integer NOT NULL,
|
|
5447
|
+
apply_receipt_id text,
|
|
5448
|
+
manifest_json jsonb,
|
|
5449
|
+
result_json jsonb NOT NULL,
|
|
5450
|
+
created_at timestamptz NOT NULL
|
|
5451
|
+
)`,
|
|
5306
5452
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
5307
5453
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
5308
5454
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
5309
5455
|
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
5456
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
|
|
5457
|
+
`DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
|
|
5458
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
|
|
5459
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
5460
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
|
|
5461
|
+
ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
|
|
5310
5462
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
5311
5463
|
ON todos_task_manifest_bindings(
|
|
5312
5464
|
tenant_id,
|
|
@@ -5319,6 +5471,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
5319
5471
|
`DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
|
|
5320
5472
|
`CREATE TRIGGER todos_task_manifest_receipts_immutable
|
|
5321
5473
|
BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
|
|
5474
|
+
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
|
|
5475
|
+
`DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
|
|
5476
|
+
`CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
|
|
5477
|
+
BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
|
|
5322
5478
|
FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
|
|
5323
5479
|
];
|
|
5324
5480
|
}
|
|
@@ -5330,7 +5486,76 @@ function fault(faults, point) {
|
|
|
5330
5486
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
5331
5487
|
}
|
|
5332
5488
|
function parseApplyResult(value, duplicate) {
|
|
5333
|
-
|
|
5489
|
+
const parsed = JSON.parse(value);
|
|
5490
|
+
return {
|
|
5491
|
+
...parsed,
|
|
5492
|
+
duplicate,
|
|
5493
|
+
receipt: {
|
|
5494
|
+
...parsed.receipt,
|
|
5495
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
5496
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
5497
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
5498
|
+
reason: parsed.receipt.reason ?? null,
|
|
5499
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
5500
|
+
}
|
|
5501
|
+
};
|
|
5502
|
+
}
|
|
5503
|
+
function terminalApplyResult(input, reason) {
|
|
5504
|
+
const receipt = {
|
|
5505
|
+
receipt_id: input.terminal_receipt_id,
|
|
5506
|
+
authority: "todos",
|
|
5507
|
+
route: "todos.task-manifest.v1",
|
|
5508
|
+
schema_version: 1,
|
|
5509
|
+
kind: "apply",
|
|
5510
|
+
operation_id: input.manifest.operation_id,
|
|
5511
|
+
step_id: input.manifest.step_id,
|
|
5512
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
5513
|
+
request_digest: input.request_digest,
|
|
5514
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
5515
|
+
result_digest: canonicalDigest({
|
|
5516
|
+
outcome: "terminal_nonacceptance",
|
|
5517
|
+
reason,
|
|
5518
|
+
operation_id: input.manifest.operation_id,
|
|
5519
|
+
step_id: input.manifest.step_id,
|
|
5520
|
+
request_digest: input.request_digest
|
|
5521
|
+
}),
|
|
5522
|
+
outcome: "terminal_nonacceptance",
|
|
5523
|
+
reason,
|
|
5524
|
+
duplicate_of_receipt_id: null,
|
|
5525
|
+
binding_version: 0,
|
|
5526
|
+
apply_receipt_id: null,
|
|
5527
|
+
created_at: input.now
|
|
5528
|
+
};
|
|
5529
|
+
return {
|
|
5530
|
+
duplicate: false,
|
|
5531
|
+
receipt,
|
|
5532
|
+
graph: input.graph,
|
|
5533
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
5534
|
+
outbox_ids: [],
|
|
5535
|
+
result_digest: receipt.result_digest
|
|
5536
|
+
};
|
|
5537
|
+
}
|
|
5538
|
+
function validateCompensationPlanSlug(db, manifest, planId, slugProvenance) {
|
|
5539
|
+
const plan = db.query("SELECT id, project_id, name, slug, created_at FROM plans WHERE id = ? LIMIT 1").get(planId);
|
|
5540
|
+
if (!plan) {
|
|
5541
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
5542
|
+
}
|
|
5543
|
+
if (slugProvenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
5544
|
+
if (plan.slug !== taskManifestPlanSlug(manifest, planId)) {
|
|
5545
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
5546
|
+
}
|
|
5547
|
+
return;
|
|
5548
|
+
}
|
|
5549
|
+
if (slugProvenance !== null && slugProvenance !== undefined) {
|
|
5550
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
5551
|
+
}
|
|
5552
|
+
if (plan.slug === null)
|
|
5553
|
+
return;
|
|
5554
|
+
const rows = db.query("SELECT id, project_id, name, slug, created_at FROM plans WHERE project_id IS ? ORDER BY created_at ASC, id ASC").all(plan.project_id);
|
|
5555
|
+
const expected = sqliteLegacyTaskManifestPlanSlug(rows, planId, manifest.plan.key || manifest.plan.name);
|
|
5556
|
+
if (expected === null || plan.slug !== expected) {
|
|
5557
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy plan slug was not produced by SQLite allocation");
|
|
5558
|
+
}
|
|
5334
5559
|
}
|
|
5335
5560
|
|
|
5336
5561
|
class SqliteTodosTaskManifestBackend {
|
|
@@ -5370,33 +5595,49 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5370
5595
|
async apply(input, faults) {
|
|
5371
5596
|
return this.serialized(() => {
|
|
5372
5597
|
const { manifest } = input;
|
|
5598
|
+
const terminal = this.db.query(`SELECT result_json
|
|
5599
|
+
FROM todos_task_manifest_terminal_receipts
|
|
5600
|
+
WHERE tenant_id = ?
|
|
5601
|
+
AND kind = 'apply'
|
|
5602
|
+
AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
|
|
5603
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
5604
|
+
LIMIT 1`).get(this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id);
|
|
5605
|
+
if (terminal)
|
|
5606
|
+
return parseApplyResult(terminal.result_json, true);
|
|
5373
5607
|
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = ? AND operation_id = ? LIMIT 1").get(this.tenantId, manifest.operation_id);
|
|
5374
5608
|
if (binding) {
|
|
5375
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
5376
|
-
|
|
5609
|
+
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) {
|
|
5610
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
5611
|
+
}
|
|
5612
|
+
if (binding["outcome"] === "terminal_nonacceptance") {
|
|
5613
|
+
const terminalResult = this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
5614
|
+
return { ...terminalResult, duplicate: true };
|
|
5377
5615
|
}
|
|
5378
5616
|
if (binding["state"] !== "applied") {
|
|
5379
|
-
|
|
5617
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
5380
5618
|
}
|
|
5381
5619
|
return parseApplyResult(String(binding["result_json"]), true);
|
|
5382
5620
|
}
|
|
5383
5621
|
const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE tenant_id = ? AND idempotency_key = ? LIMIT 1").get(this.tenantId, manifest.idempotency_key);
|
|
5384
5622
|
if (idempotency)
|
|
5385
|
-
|
|
5623
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
5624
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
5625
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
5626
|
+
}
|
|
5386
5627
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
5387
|
-
|
|
5628
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
5388
5629
|
}
|
|
5389
5630
|
if (!this.db.query("SELECT 1 AS found FROM projects WHERE id = ? LIMIT 1").get(manifest.project_id)) {
|
|
5390
|
-
|
|
5631
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
5391
5632
|
}
|
|
5392
5633
|
if (manifest.task_list_id && !this.db.query("SELECT 1 AS found FROM task_lists WHERE id = ? AND project_id = ? LIMIT 1").get(manifest.task_list_id, manifest.project_id)) {
|
|
5393
|
-
|
|
5634
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
5394
5635
|
}
|
|
5395
5636
|
const allIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids];
|
|
5396
5637
|
for (const id of allIds) {
|
|
5397
5638
|
for (const table of ["plans", "tasks", "task_comments", "task_verifications"]) {
|
|
5398
5639
|
if (this.db.query(`SELECT 1 AS found FROM ${table} WHERE id = ? LIMIT 1`).get(id)) {
|
|
5399
|
-
|
|
5640
|
+
return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
5400
5641
|
}
|
|
5401
5642
|
}
|
|
5402
5643
|
}
|
|
@@ -5452,9 +5693,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5452
5693
|
schema_version: 1,
|
|
5453
5694
|
kind: "apply",
|
|
5454
5695
|
operation_id: manifest.operation_id,
|
|
5696
|
+
step_id: manifest.step_id,
|
|
5455
5697
|
idempotency_key: manifest.idempotency_key,
|
|
5456
5698
|
request_digest: input.request_digest,
|
|
5699
|
+
precondition_digest: manifest.precondition_digest,
|
|
5457
5700
|
result_digest: input.result_digest,
|
|
5701
|
+
outcome: "accepted",
|
|
5702
|
+
reason: null,
|
|
5703
|
+
duplicate_of_receipt_id: null,
|
|
5458
5704
|
binding_version: 1,
|
|
5459
5705
|
apply_receipt_id: null,
|
|
5460
5706
|
created_at: input.now
|
|
@@ -5470,9 +5716,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5470
5716
|
const resultJson = canonicalJson(result);
|
|
5471
5717
|
const manifestJson = canonicalJson(manifest);
|
|
5472
5718
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
5473
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
5474
|
-
request_digest,
|
|
5475
|
-
|
|
5719
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
5720
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
5721
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
5722
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, ?, ?, ?, 'accepted', NULL, NULL, 1, NULL, ?, ?, ?)`).run(input.receipt_id, this.tenantId, manifest.operation_id, manifest.step_id, manifest.idempotency_key, input.request_digest, manifest.precondition_digest, input.result_digest, TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE, manifestJson, resultJson, input.now);
|
|
5476
5723
|
for (const entry of input.outbox) {
|
|
5477
5724
|
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
5478
5725
|
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
@@ -5480,18 +5727,37 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5480
5727
|
}
|
|
5481
5728
|
fault(faults, "after_outbox_write");
|
|
5482
5729
|
this.db.query(`INSERT INTO todos_task_manifest_bindings (
|
|
5483
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
5484
|
-
manifest_json, result_json, state, version, created_at, updated_at
|
|
5485
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.idempotency_key, input.request_digest, input.result_digest, input.receipt_id, manifestJson, resultJson, input.now, input.now);
|
|
5730
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest, result_digest,
|
|
5731
|
+
slug_provenance, outcome, apply_receipt_id, manifest_json, result_json, state, version, created_at, updated_at
|
|
5732
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.step_id, manifest.idempotency_key, input.request_digest, manifest.precondition_digest, input.result_digest, TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE, input.receipt_id, manifestJson, resultJson, input.now, input.now);
|
|
5486
5733
|
fault(faults, "after_receipt_write");
|
|
5487
5734
|
return result;
|
|
5488
5735
|
});
|
|
5489
5736
|
}
|
|
5737
|
+
persistTerminal(input, reason) {
|
|
5738
|
+
const result = terminalApplyResult(input, reason);
|
|
5739
|
+
const resultJson = canonicalJson(result);
|
|
5740
|
+
this.db.query(`INSERT OR IGNORE INTO todos_task_manifest_terminal_receipts (
|
|
5741
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
5742
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
5743
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
5744
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, ?, ?, 'terminal_nonacceptance', ?, 0, NULL, ?, ?, ?)`).run(result.receipt.receipt_id, this.tenantId, input.manifest.operation_id, input.manifest.step_id, input.manifest.idempotency_key, input.request_digest, input.manifest.precondition_digest, result.receipt.result_digest, reason, canonicalJson(input.manifest), resultJson, input.now);
|
|
5745
|
+
const stored = this.db.query(`SELECT receipt_id, result_json
|
|
5746
|
+
FROM todos_task_manifest_terminal_receipts
|
|
5747
|
+
WHERE tenant_id = ? AND kind = 'apply'
|
|
5748
|
+
AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
|
|
5749
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
5750
|
+
LIMIT 1`).get(this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id);
|
|
5751
|
+
return stored ? parseApplyResult(stored.result_json, stored.receipt_id !== result.receipt.receipt_id) : result;
|
|
5752
|
+
}
|
|
5490
5753
|
async readExact(receiptId) {
|
|
5491
5754
|
const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, receiptId);
|
|
5492
|
-
if (
|
|
5755
|
+
if (row)
|
|
5756
|
+
return parseApplyResult(row.result_json, false);
|
|
5757
|
+
const terminal = this.db.query("SELECT result_json FROM todos_task_manifest_terminal_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, receiptId);
|
|
5758
|
+
if (!terminal)
|
|
5493
5759
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId}`);
|
|
5494
|
-
return parseApplyResult(
|
|
5760
|
+
return parseApplyResult(terminal.result_json, false);
|
|
5495
5761
|
}
|
|
5496
5762
|
async lookupBindingByPlanId(planId) {
|
|
5497
5763
|
const rows = this.db.query(`
|
|
@@ -5501,6 +5767,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5501
5767
|
b.version AS binding_version,
|
|
5502
5768
|
b.tenant_id AS binding_tenant_id,
|
|
5503
5769
|
b.operation_id AS binding_operation_id,
|
|
5770
|
+
b.step_id AS binding_step_id,
|
|
5504
5771
|
json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
|
|
5505
5772
|
r.tenant_id AS receipt_tenant_id,
|
|
5506
5773
|
r.authority AS receipt_authority,
|
|
@@ -5508,6 +5775,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5508
5775
|
r.schema_version AS receipt_schema_version,
|
|
5509
5776
|
r.kind AS receipt_kind,
|
|
5510
5777
|
r.operation_id AS receipt_operation_id,
|
|
5778
|
+
r.step_id AS receipt_step_id,
|
|
5511
5779
|
json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
|
|
5512
5780
|
FROM todos_task_manifest_bindings b
|
|
5513
5781
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -5570,6 +5838,12 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5570
5838
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
5571
5839
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
5572
5840
|
}
|
|
5841
|
+
const storedStepId = String(row["step_id"] ?? "legacy-apply");
|
|
5842
|
+
const storedRequestDigest = String(row["request_digest"]);
|
|
5843
|
+
const storedPreconditionDigest = String(row["precondition_digest"] ?? "0".repeat(64));
|
|
5844
|
+
if (String(row["receipt_id"]) !== input.receipt_id || String(row["operation_id"]) !== input.operation_id || String(binding["operation_id"]) !== String(row["operation_id"]) || String(binding["step_id"] ?? "legacy-apply") !== storedStepId || String(binding["idempotency_key"]) !== String(row["idempotency_key"]) || String(binding["request_digest"]) !== storedRequestDigest || String(binding["precondition_digest"] ?? "0".repeat(64)) !== storedPreconditionDigest || String(binding["apply_receipt_id"]) !== input.receipt_id || binding["slug_provenance"] !== row["slug_provenance"]) {
|
|
5845
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
5846
|
+
}
|
|
5573
5847
|
if (binding["state"] !== "applied")
|
|
5574
5848
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
5575
5849
|
const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -5580,10 +5854,16 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5580
5854
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
5581
5855
|
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
5582
5856
|
const manifest = JSON.parse(String(row["manifest_json"]));
|
|
5857
|
+
const manifestRecord = manifest;
|
|
5858
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
5583
5859
|
const expectedEffects = [
|
|
5584
5860
|
{
|
|
5585
5861
|
topic: "todos.task-manifest.applied",
|
|
5586
|
-
payload: {
|
|
5862
|
+
payload: {
|
|
5863
|
+
operation_id: manifest.operation_id,
|
|
5864
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
5865
|
+
project_id: manifest.project_id
|
|
5866
|
+
}
|
|
5587
5867
|
},
|
|
5588
5868
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
5589
5869
|
];
|
|
@@ -5623,8 +5903,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5623
5903
|
if (canonicalJson(actualReadback) !== canonicalJson(applyResult.readback)) {
|
|
5624
5904
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: graph changed since apply", { actualReadback });
|
|
5625
5905
|
}
|
|
5626
|
-
const
|
|
5627
|
-
|
|
5906
|
+
const slugProvenance = row["slug_provenance"];
|
|
5907
|
+
validateCompensationPlanSlug(this.db, manifest, applyResult.graph.plan_id, slugProvenance);
|
|
5908
|
+
const plan = this.db.query("SELECT project_id, name, description, status, task_list_id FROM plans WHERE id = ? LIMIT 1").get(applyResult.graph.plan_id);
|
|
5909
|
+
if (!plan || plan["project_id"] !== manifest.project_id || plan["name"] !== manifest.plan.name || plan["description"] !== (manifest.plan.description ?? null) || plan["status"] !== (manifest.plan.status ?? "active") || plan["task_list_id"] !== (manifest.task_list_id ?? null)) {
|
|
5628
5910
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
5629
5911
|
}
|
|
5630
5912
|
for (const task2 of manifest.tasks) {
|
|
@@ -5701,9 +5983,10 @@ class SqliteTodosTaskManifestBackend {
|
|
|
5701
5983
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
5702
5984
|
const resultJson = canonicalJson(result);
|
|
5703
5985
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
5704
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
5705
|
-
request_digest,
|
|
5706
|
-
|
|
5986
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
5987
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
5988
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
5989
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, 'accepted', NULL, NULL, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, this.tenantId, receipt.operation_id, receipt.step_id, input.idempotency_key, requestDigest, input.precondition_digest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson, now);
|
|
5707
5990
|
const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
|
|
5708
5991
|
WHERE tenant_id = ? AND operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now, this.tenantId, receipt.operation_id, input.if_binding_version);
|
|
5709
5992
|
if (updated.changes !== 1) {
|
|
@@ -6141,6 +6424,39 @@ function safeIdentifier(value, field) {
|
|
|
6141
6424
|
function parseJson(value) {
|
|
6142
6425
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
6143
6426
|
}
|
|
6427
|
+
function parseApplyResult2(value, duplicate) {
|
|
6428
|
+
const parsed = parseJson(value);
|
|
6429
|
+
return {
|
|
6430
|
+
...parsed,
|
|
6431
|
+
duplicate,
|
|
6432
|
+
receipt: {
|
|
6433
|
+
...parsed.receipt,
|
|
6434
|
+
step_id: parsed.receipt.step_id ?? "legacy-apply",
|
|
6435
|
+
precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
|
|
6436
|
+
outcome: parsed.receipt.outcome ?? "accepted",
|
|
6437
|
+
reason: parsed.receipt.reason ?? null,
|
|
6438
|
+
duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
|
|
6439
|
+
}
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
6442
|
+
function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
|
|
6443
|
+
if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
|
|
6444
|
+
const expected = taskManifestPlanSlug(manifest, planId);
|
|
6445
|
+
if (slug !== expected) {
|
|
6446
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
|
|
6447
|
+
}
|
|
6448
|
+
return expected;
|
|
6449
|
+
}
|
|
6450
|
+
if (provenance !== null && provenance !== undefined) {
|
|
6451
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
|
|
6452
|
+
}
|
|
6453
|
+
if (slug === null || slug === undefined)
|
|
6454
|
+
return null;
|
|
6455
|
+
if (slug !== null && slug !== undefined) {
|
|
6456
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
|
|
6457
|
+
}
|
|
6458
|
+
return null;
|
|
6459
|
+
}
|
|
6144
6460
|
function timestamp(value) {
|
|
6145
6461
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
6146
6462
|
}
|
|
@@ -6148,6 +6464,41 @@ function fault2(faults, point) {
|
|
|
6148
6464
|
if (faults.points.has(point))
|
|
6149
6465
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
6150
6466
|
}
|
|
6467
|
+
function terminalApplyResult2(input, reason) {
|
|
6468
|
+
const receipt = {
|
|
6469
|
+
receipt_id: input.terminal_receipt_id,
|
|
6470
|
+
authority: "todos",
|
|
6471
|
+
route: "todos.task-manifest.v1",
|
|
6472
|
+
schema_version: 1,
|
|
6473
|
+
kind: "apply",
|
|
6474
|
+
operation_id: input.manifest.operation_id,
|
|
6475
|
+
step_id: input.manifest.step_id,
|
|
6476
|
+
idempotency_key: input.manifest.idempotency_key,
|
|
6477
|
+
request_digest: input.request_digest,
|
|
6478
|
+
precondition_digest: input.manifest.precondition_digest,
|
|
6479
|
+
result_digest: canonicalDigest({
|
|
6480
|
+
outcome: "terminal_nonacceptance",
|
|
6481
|
+
reason,
|
|
6482
|
+
operation_id: input.manifest.operation_id,
|
|
6483
|
+
step_id: input.manifest.step_id,
|
|
6484
|
+
request_digest: input.request_digest
|
|
6485
|
+
}),
|
|
6486
|
+
outcome: "terminal_nonacceptance",
|
|
6487
|
+
reason,
|
|
6488
|
+
duplicate_of_receipt_id: null,
|
|
6489
|
+
binding_version: 0,
|
|
6490
|
+
apply_receipt_id: null,
|
|
6491
|
+
created_at: input.now
|
|
6492
|
+
};
|
|
6493
|
+
return {
|
|
6494
|
+
duplicate: false,
|
|
6495
|
+
receipt,
|
|
6496
|
+
graph: input.graph,
|
|
6497
|
+
readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
|
|
6498
|
+
outbox_ids: [],
|
|
6499
|
+
result_digest: receipt.result_digest
|
|
6500
|
+
};
|
|
6501
|
+
}
|
|
6151
6502
|
function receiptFromRow(row) {
|
|
6152
6503
|
return {
|
|
6153
6504
|
receipt_id: String(row["receipt_id"]),
|
|
@@ -6156,9 +6507,14 @@ function receiptFromRow(row) {
|
|
|
6156
6507
|
schema_version: 1,
|
|
6157
6508
|
kind: row["kind"],
|
|
6158
6509
|
operation_id: String(row["operation_id"]),
|
|
6510
|
+
step_id: String(row["step_id"] ?? "legacy-apply"),
|
|
6159
6511
|
idempotency_key: String(row["idempotency_key"]),
|
|
6160
6512
|
request_digest: String(row["request_digest"]),
|
|
6513
|
+
precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
|
|
6161
6514
|
result_digest: String(row["result_digest"]),
|
|
6515
|
+
outcome: row["outcome"] ?? "accepted",
|
|
6516
|
+
reason: row["reason"] == null ? null : row["reason"],
|
|
6517
|
+
duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
|
|
6162
6518
|
binding_version: Number(row["binding_version"]),
|
|
6163
6519
|
apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
|
|
6164
6520
|
created_at: timestamp(row["created_at"])
|
|
@@ -6276,46 +6632,89 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6276
6632
|
now
|
|
6277
6633
|
]);
|
|
6278
6634
|
}
|
|
6635
|
+
async persistTerminal(tx, input, reason) {
|
|
6636
|
+
const result = terminalApplyResult2(input, reason);
|
|
6637
|
+
const resultJson = canonicalJson(result);
|
|
6638
|
+
await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
|
|
6639
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
|
|
6640
|
+
idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
|
|
6641
|
+
binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
6642
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
|
|
6643
|
+
'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
|
|
6644
|
+
ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
|
|
6645
|
+
result.receipt.receipt_id,
|
|
6646
|
+
this.tenantId,
|
|
6647
|
+
input.manifest.operation_id,
|
|
6648
|
+
input.manifest.step_id,
|
|
6649
|
+
input.manifest.idempotency_key,
|
|
6650
|
+
input.request_digest,
|
|
6651
|
+
input.manifest.precondition_digest,
|
|
6652
|
+
result.receipt.result_digest,
|
|
6653
|
+
reason,
|
|
6654
|
+
canonicalJson(input.manifest),
|
|
6655
|
+
resultJson,
|
|
6656
|
+
input.now
|
|
6657
|
+
]);
|
|
6658
|
+
const stored = await tx.query(`SELECT receipt_id, result_json
|
|
6659
|
+
FROM todos_task_manifest_terminal_receipts
|
|
6660
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
6661
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
6662
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
6663
|
+
LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
|
|
6664
|
+
return stored.rows[0] ? parseApplyResult2(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
|
|
6665
|
+
}
|
|
6279
6666
|
async apply(input, faults) {
|
|
6280
6667
|
await this.ensureSchema();
|
|
6281
6668
|
return this.client.transaction(async (tx) => {
|
|
6282
6669
|
const { manifest } = input;
|
|
6283
6670
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
6284
6671
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
6672
|
+
const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
|
|
6673
|
+
WHERE tenant_id = $1
|
|
6674
|
+
AND kind = 'apply'
|
|
6675
|
+
AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
|
|
6676
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
6677
|
+
LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
|
|
6678
|
+
if (terminal.rows[0]) {
|
|
6679
|
+
return parseApplyResult2(terminal.rows[0]["result_json"], true);
|
|
6680
|
+
}
|
|
6285
6681
|
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]);
|
|
6286
6682
|
if (existing.rows[0]) {
|
|
6287
6683
|
const binding = existing.rows[0];
|
|
6288
|
-
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
6289
|
-
|
|
6684
|
+
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) {
|
|
6685
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
6290
6686
|
}
|
|
6291
6687
|
if (binding["state"] !== "applied") {
|
|
6292
|
-
|
|
6688
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
6293
6689
|
}
|
|
6294
|
-
return
|
|
6690
|
+
return parseApplyResult2(binding["result_json"], true);
|
|
6295
6691
|
}
|
|
6296
6692
|
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]);
|
|
6297
6693
|
if (reused.rows[0])
|
|
6298
|
-
|
|
6694
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
|
|
6695
|
+
if (manifest.idempotency_key !== input.expected_idempotency_key) {
|
|
6696
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
|
|
6697
|
+
}
|
|
6299
6698
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
6300
|
-
|
|
6699
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
|
|
6301
6700
|
}
|
|
6302
6701
|
const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
|
|
6303
6702
|
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
|
|
6304
6703
|
if (!project.rows[0])
|
|
6305
|
-
|
|
6704
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
6306
6705
|
if (manifest.task_list_id) {
|
|
6307
6706
|
const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
6308
6707
|
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]);
|
|
6309
6708
|
const payload = taskList.rows[0] ? parseJson(taskList.rows[0]["payload"]) : null;
|
|
6310
6709
|
if (!payload || payload["project_id"] !== manifest.project_id) {
|
|
6311
|
-
|
|
6710
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
|
|
6312
6711
|
}
|
|
6313
6712
|
}
|
|
6314
6713
|
const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
|
|
6315
6714
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
6316
6715
|
WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
6317
6716
|
if (conflict.rows[0])
|
|
6318
|
-
|
|
6717
|
+
return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
|
|
6319
6718
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
6320
6719
|
fault2(faults, "after_plan_write");
|
|
6321
6720
|
for (const task2 of manifest.tasks) {
|
|
@@ -6385,9 +6784,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6385
6784
|
schema_version: 1,
|
|
6386
6785
|
kind: "apply",
|
|
6387
6786
|
operation_id: manifest.operation_id,
|
|
6787
|
+
step_id: manifest.step_id,
|
|
6388
6788
|
idempotency_key: manifest.idempotency_key,
|
|
6389
6789
|
request_digest: input.request_digest,
|
|
6790
|
+
precondition_digest: manifest.precondition_digest,
|
|
6390
6791
|
result_digest: input.result_digest,
|
|
6792
|
+
outcome: "accepted",
|
|
6793
|
+
reason: null,
|
|
6794
|
+
duplicate_of_receipt_id: null,
|
|
6391
6795
|
binding_version: 1,
|
|
6392
6796
|
apply_receipt_id: null,
|
|
6393
6797
|
created_at: input.now
|
|
@@ -6404,14 +6808,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6404
6808
|
const resultJson = canonicalJson(result);
|
|
6405
6809
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
6406
6810
|
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
6407
|
-
|
|
6408
|
-
|
|
6811
|
+
step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
|
|
6812
|
+
reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
6813
|
+
) 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)`, [
|
|
6409
6814
|
input.receipt_id,
|
|
6410
6815
|
this.tenantId,
|
|
6411
6816
|
manifest.operation_id,
|
|
6412
6817
|
manifest.idempotency_key,
|
|
6818
|
+
manifest.step_id,
|
|
6413
6819
|
input.request_digest,
|
|
6820
|
+
manifest.precondition_digest,
|
|
6414
6821
|
input.result_digest,
|
|
6822
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
6415
6823
|
manifestJson,
|
|
6416
6824
|
resultJson,
|
|
6417
6825
|
input.now
|
|
@@ -6430,14 +6838,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6430
6838
|
}
|
|
6431
6839
|
fault2(faults, "after_outbox_write");
|
|
6432
6840
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
6433
|
-
operation_id, tenant_id, idempotency_key, request_digest,
|
|
6434
|
-
|
|
6435
|
-
|
|
6841
|
+
operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
|
|
6842
|
+
result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
|
|
6843
|
+
state, version, created_at, updated_at
|
|
6844
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
|
|
6436
6845
|
manifest.operation_id,
|
|
6437
6846
|
this.tenantId,
|
|
6847
|
+
manifest.step_id,
|
|
6438
6848
|
manifest.idempotency_key,
|
|
6439
6849
|
input.request_digest,
|
|
6850
|
+
manifest.precondition_digest,
|
|
6440
6851
|
input.result_digest,
|
|
6852
|
+
TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
|
|
6441
6853
|
input.receipt_id,
|
|
6442
6854
|
manifestJson,
|
|
6443
6855
|
resultJson,
|
|
@@ -6450,9 +6862,12 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6450
6862
|
async readExact(receiptId) {
|
|
6451
6863
|
await this.ensureSchema();
|
|
6452
6864
|
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, receiptId]);
|
|
6453
|
-
if (
|
|
6865
|
+
if (result.rows[0])
|
|
6866
|
+
return parseApplyResult2(result.rows[0]["result_json"], false);
|
|
6867
|
+
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, receiptId]);
|
|
6868
|
+
if (!terminal.rows[0])
|
|
6454
6869
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId}`);
|
|
6455
|
-
return
|
|
6870
|
+
return parseApplyResult2(terminal.rows[0]["result_json"], false);
|
|
6456
6871
|
}
|
|
6457
6872
|
async lookupBindingByPlanId(planId) {
|
|
6458
6873
|
await this.ensureSchema();
|
|
@@ -6463,6 +6878,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6463
6878
|
b.version AS binding_version,
|
|
6464
6879
|
b.tenant_id AS binding_tenant_id,
|
|
6465
6880
|
b.operation_id AS binding_operation_id,
|
|
6881
|
+
b.step_id AS binding_step_id,
|
|
6466
6882
|
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
6467
6883
|
r.tenant_id AS receipt_tenant_id,
|
|
6468
6884
|
r.authority AS receipt_authority,
|
|
@@ -6470,6 +6886,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6470
6886
|
r.schema_version AS receipt_schema_version,
|
|
6471
6887
|
r.kind AS receipt_kind,
|
|
6472
6888
|
r.operation_id AS receipt_operation_id,
|
|
6889
|
+
r.step_id AS receipt_step_id,
|
|
6473
6890
|
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
6474
6891
|
FROM todos_task_manifest_bindings b
|
|
6475
6892
|
LEFT JOIN todos_task_manifest_receipts r
|
|
@@ -6556,6 +6973,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6556
6973
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
6557
6974
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
6558
6975
|
}
|
|
6976
|
+
const appliedReceipt = receiptFromRow(applyRow);
|
|
6977
|
+
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"]) {
|
|
6978
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
|
|
6979
|
+
}
|
|
6559
6980
|
if (binding["state"] !== "applied")
|
|
6560
6981
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
6561
6982
|
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
@@ -6570,12 +6991,18 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6570
6991
|
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
6571
6992
|
if (delivered.rows[0])
|
|
6572
6993
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
6573
|
-
const applyResult =
|
|
6994
|
+
const applyResult = parseApplyResult2(applyRow["result_json"], false);
|
|
6574
6995
|
const manifest = parseJson(applyRow["manifest_json"]);
|
|
6996
|
+
const manifestRecord = manifest;
|
|
6997
|
+
const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
|
|
6575
6998
|
const expectedEffects = [
|
|
6576
6999
|
{
|
|
6577
7000
|
topic: "todos.task-manifest.applied",
|
|
6578
|
-
payload: {
|
|
7001
|
+
payload: {
|
|
7002
|
+
operation_id: manifest.operation_id,
|
|
7003
|
+
...applyStepId ? { step_id: applyStepId } : {},
|
|
7004
|
+
project_id: manifest.project_id
|
|
7005
|
+
}
|
|
6579
7006
|
},
|
|
6580
7007
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
6581
7008
|
];
|
|
@@ -6623,9 +7050,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6623
7050
|
}
|
|
6624
7051
|
const appliedAt = receiptFromRow(applyRow).created_at;
|
|
6625
7052
|
const expectedPayloads = new Map;
|
|
7053
|
+
const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
7054
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
7055
|
+
LIMIT 1`, [this.service, applyResult.graph.plan_id]);
|
|
7056
|
+
const actualPlan = planRow.rows[0] ? parseJson(planRow.rows[0]["payload"]) : null;
|
|
7057
|
+
const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
|
|
7058
|
+
planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
|
|
6626
7059
|
expectedPayloads.set(applyResult.graph.plan_id, {
|
|
6627
7060
|
type: "plans",
|
|
6628
|
-
payload: canonicalJson(
|
|
7061
|
+
payload: canonicalJson(planExpected)
|
|
6629
7062
|
});
|
|
6630
7063
|
for (const task2 of manifest.tasks)
|
|
6631
7064
|
expectedPayloads.set(applyResult.graph.task_ids[task2.key], {
|
|
@@ -6725,14 +7158,17 @@ class PostgresTodosTaskManifestBackend {
|
|
|
6725
7158
|
const readback = await this.readback(tx, applyResult.graph);
|
|
6726
7159
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
6727
7160
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
6728
|
-
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
6729
|
-
request_digest,
|
|
6730
|
-
|
|
7161
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
|
|
7162
|
+
request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
|
|
7163
|
+
duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
7164
|
+
) 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)`, [
|
|
6731
7165
|
compensationReceiptId,
|
|
6732
7166
|
this.tenantId,
|
|
6733
7167
|
receipt.operation_id,
|
|
7168
|
+
receipt.step_id,
|
|
6734
7169
|
input.idempotency_key,
|
|
6735
7170
|
requestDigest,
|
|
7171
|
+
input.precondition_digest,
|
|
6736
7172
|
receipt.result_digest,
|
|
6737
7173
|
receipt.binding_version,
|
|
6738
7174
|
input.receipt_id,
|
|
@@ -6792,36 +7228,83 @@ function resolveTenantId(value) {
|
|
|
6792
7228
|
}
|
|
6793
7229
|
return tenantId;
|
|
6794
7230
|
}
|
|
7231
|
+
function taskManifestRequestDigest(manifest) {
|
|
7232
|
+
const { idempotency_key: _idempotencyKey, ...request } = manifest;
|
|
7233
|
+
return canonicalDigest(request);
|
|
7234
|
+
}
|
|
7235
|
+
function taskManifestCompensationRequestDigest(request) {
|
|
7236
|
+
return canonicalDigest(request);
|
|
7237
|
+
}
|
|
7238
|
+
function deriveTodosTaskManifestApplyPreconditionDigest(input) {
|
|
7239
|
+
return canonicalDigest({
|
|
7240
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
7241
|
+
direction: "apply",
|
|
7242
|
+
operation_id: input.operation_id,
|
|
7243
|
+
step_id: input.step_id,
|
|
7244
|
+
project_id: input.project_id,
|
|
7245
|
+
task_list_id: input.task_list_id ?? null,
|
|
7246
|
+
expected_binding_version: input.if_binding_version ?? 0
|
|
7247
|
+
});
|
|
7248
|
+
}
|
|
7249
|
+
function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
|
|
7250
|
+
return canonicalDigest({
|
|
7251
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
7252
|
+
direction: "compensate",
|
|
7253
|
+
operation_id: input.operation_id,
|
|
7254
|
+
step_id: input.step_id,
|
|
7255
|
+
apply_receipt_id: input.receipt_id,
|
|
7256
|
+
expected_binding_version: input.if_binding_version
|
|
7257
|
+
});
|
|
7258
|
+
}
|
|
7259
|
+
function deriveTodosTaskManifestIdempotencyKey(input) {
|
|
7260
|
+
return `tmk_${canonicalDigest({
|
|
7261
|
+
route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
7262
|
+
...input
|
|
7263
|
+
}).slice(0, 48)}`;
|
|
7264
|
+
}
|
|
6795
7265
|
function normalize(input, now) {
|
|
6796
7266
|
const parsed = parseTodosTaskManifest(input);
|
|
6797
7267
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
6798
7268
|
if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
|
|
6799
7269
|
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 });
|
|
6800
7270
|
}
|
|
7271
|
+
const { idempotency_key: _idempotencyKey, ...request } = parsed;
|
|
7272
|
+
const request_digest = taskManifestRequestDigest(request);
|
|
6801
7273
|
const manifest = sanitizeManifest(parsed);
|
|
7274
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
|
|
7275
|
+
if (manifest.precondition_digest !== expectedPreconditionDigest) {
|
|
7276
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
7277
|
+
}
|
|
7278
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
7279
|
+
operation_id: manifest.operation_id,
|
|
7280
|
+
step_id: manifest.step_id,
|
|
7281
|
+
direction: "apply",
|
|
7282
|
+
target_selector: manifest.project_id,
|
|
7283
|
+
request_digest,
|
|
7284
|
+
precondition_digest: manifest.precondition_digest
|
|
7285
|
+
});
|
|
6802
7286
|
const task_ids = Object.fromEntries(manifest.tasks.map((task2) => [
|
|
6803
7287
|
task2.key,
|
|
6804
|
-
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task2.key)
|
|
7288
|
+
deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task2.key)
|
|
6805
7289
|
]));
|
|
6806
7290
|
const graph = {
|
|
6807
|
-
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
|
|
7291
|
+
plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
|
|
6808
7292
|
task_ids,
|
|
6809
|
-
comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task2.key, String(index)))),
|
|
6810
|
-
verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task2.key, String(index)))),
|
|
7293
|
+
comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "comment", task2.key, String(index)))),
|
|
7294
|
+
verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "verification", task2.key, String(index)))),
|
|
6811
7295
|
dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
|
|
6812
7296
|
};
|
|
6813
|
-
const request_digest = canonicalDigest(parsed);
|
|
6814
7297
|
const effectInputs = [
|
|
6815
7298
|
{
|
|
6816
7299
|
topic: "todos.task-manifest.applied",
|
|
6817
|
-
payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
|
|
7300
|
+
payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
|
|
6818
7301
|
},
|
|
6819
7302
|
...manifest.effects ?? []
|
|
6820
7303
|
];
|
|
6821
7304
|
const outbox = effectInputs.map((effect2, index) => {
|
|
6822
7305
|
const payload = { ...effect2.payload };
|
|
6823
7306
|
return {
|
|
6824
|
-
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
|
|
7307
|
+
id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
|
|
6825
7308
|
topic: effect2.topic,
|
|
6826
7309
|
payload,
|
|
6827
7310
|
digest: canonicalDigest({ topic: effect2.topic, payload })
|
|
@@ -6831,11 +7314,14 @@ function normalize(input, now) {
|
|
|
6831
7314
|
return {
|
|
6832
7315
|
manifest,
|
|
6833
7316
|
request_digest,
|
|
7317
|
+
expected_idempotency_key: expectedIdempotencyKey,
|
|
6834
7318
|
result_digest,
|
|
6835
|
-
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
|
|
7319
|
+
receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
7320
|
+
terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
|
|
6836
7321
|
graph,
|
|
6837
7322
|
outbox,
|
|
6838
|
-
now
|
|
7323
|
+
now,
|
|
7324
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
|
|
6839
7325
|
};
|
|
6840
7326
|
}
|
|
6841
7327
|
function sanitizeManifest(manifest) {
|
|
@@ -6893,6 +7379,10 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
6893
7379
|
tenant_id: this.tenantId,
|
|
6894
7380
|
backend: this.backend.kind,
|
|
6895
7381
|
deterministic_ids: true,
|
|
7382
|
+
operation_step_identity: true,
|
|
7383
|
+
deterministic_idempotency_keys: true,
|
|
7384
|
+
terminal_nonacceptance_receipts: true,
|
|
7385
|
+
plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
6896
7386
|
immutable_receipts: true,
|
|
6897
7387
|
transactional_outbox: true,
|
|
6898
7388
|
idempotent_outbox_delivery: true,
|
|
@@ -6922,7 +7412,11 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
6922
7412
|
async apply(input) {
|
|
6923
7413
|
const normalized = normalize(input, this.now());
|
|
6924
7414
|
const faults = await this.prepareFaults();
|
|
6925
|
-
|
|
7415
|
+
const result = this.bounded(await this.backend.apply(normalized, faults));
|
|
7416
|
+
if (result.receipt.outcome === "terminal_nonacceptance") {
|
|
7417
|
+
throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
|
|
7418
|
+
}
|
|
7419
|
+
return result;
|
|
6926
7420
|
}
|
|
6927
7421
|
readExact(receiptId) {
|
|
6928
7422
|
if (!receiptId || receiptId.length > 200) {
|
|
@@ -6955,18 +7449,48 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
6955
7449
|
async compensate(input) {
|
|
6956
7450
|
const request = parseTodosTaskManifestCompensation(input);
|
|
6957
7451
|
const applied = await this.backend.readExact(request.receipt_id);
|
|
6958
|
-
|
|
6959
|
-
|
|
7452
|
+
if (applied.receipt.outcome !== "accepted") {
|
|
7453
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
|
|
7454
|
+
}
|
|
7455
|
+
if (request.operation_id !== applied.receipt.operation_id) {
|
|
7456
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
|
|
7457
|
+
}
|
|
7458
|
+
if (request.step_id === applied.receipt.step_id) {
|
|
7459
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
|
|
7460
|
+
}
|
|
7461
|
+
const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
|
|
7462
|
+
if (request.precondition_digest !== expectedPreconditionDigest) {
|
|
7463
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
|
|
7464
|
+
}
|
|
7465
|
+
const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
|
|
7466
|
+
const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
|
|
7467
|
+
const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
|
|
7468
|
+
operation_id: request.operation_id,
|
|
7469
|
+
step_id: request.step_id,
|
|
7470
|
+
direction: "compensate",
|
|
7471
|
+
target_selector: request.receipt_id,
|
|
7472
|
+
request_digest: requestDigest,
|
|
7473
|
+
precondition_digest: request.precondition_digest
|
|
7474
|
+
});
|
|
7475
|
+
if (request.idempotency_key !== expectedIdempotencyKey) {
|
|
7476
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
|
|
7477
|
+
}
|
|
7478
|
+
const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
|
|
6960
7479
|
const receipt = {
|
|
6961
7480
|
receipt_id: compensationReceiptId,
|
|
6962
7481
|
authority: "todos",
|
|
6963
7482
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
6964
7483
|
schema_version: 1,
|
|
6965
7484
|
kind: "compensate",
|
|
6966
|
-
operation_id:
|
|
7485
|
+
operation_id: request.operation_id,
|
|
7486
|
+
step_id: request.step_id,
|
|
6967
7487
|
idempotency_key: request.idempotency_key,
|
|
6968
7488
|
request_digest: requestDigest,
|
|
7489
|
+
precondition_digest: request.precondition_digest,
|
|
6969
7490
|
result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
|
|
7491
|
+
outcome: "accepted",
|
|
7492
|
+
reason: null,
|
|
7493
|
+
duplicate_of_receipt_id: null,
|
|
6970
7494
|
binding_version: request.if_binding_version + 1,
|
|
6971
7495
|
apply_receipt_id: applied.receipt.receipt_id,
|
|
6972
7496
|
created_at: this.now()
|
|
@@ -7148,6 +7672,8 @@ function createTodosTaskManifestHttpClient(options) {
|
|
|
7148
7672
|
return new TodosTaskManifestHttpClient(options);
|
|
7149
7673
|
}
|
|
7150
7674
|
export {
|
|
7675
|
+
taskManifestRequestDigest,
|
|
7676
|
+
taskManifestCompensationRequestDigest,
|
|
7151
7677
|
supportsIdempotentOutboxDelivery,
|
|
7152
7678
|
sqliteTodosTaskManifestSchemaSql,
|
|
7153
7679
|
postgresTodosTaskManifestSchemaSql,
|
|
@@ -7157,6 +7683,9 @@ export {
|
|
|
7157
7683
|
handleTodosTaskManifestHttpRequest,
|
|
7158
7684
|
canonicalDigest as digestTodosTaskManifest,
|
|
7159
7685
|
deterministicUuid as deterministicTodosTaskManifestId,
|
|
7686
|
+
deriveTodosTaskManifestIdempotencyKey,
|
|
7687
|
+
deriveTodosTaskManifestCompensationPreconditionDigest,
|
|
7688
|
+
deriveTodosTaskManifestApplyPreconditionDigest,
|
|
7160
7689
|
createTodosTaskManifestHttpClient,
|
|
7161
7690
|
createSqliteTodosTaskManifestAuthority,
|
|
7162
7691
|
createPostgresTodosTaskManifestAuthority,
|
|
@@ -7165,6 +7694,8 @@ export {
|
|
|
7165
7694
|
TodosTaskManifestError,
|
|
7166
7695
|
TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
7167
7696
|
TODOS_TASK_MANIFEST_ROUTE,
|
|
7697
|
+
TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
|
|
7698
|
+
TODOS_TASK_MANIFEST_CALLER_ROUTE,
|
|
7168
7699
|
TODOS_TASK_MANIFEST_BOUNDS,
|
|
7169
7700
|
SqliteTodosTaskManifestBackend,
|
|
7170
7701
|
PostgresTodosTaskManifestBackend,
|