@hasna/todos 0.15.25 → 0.15.27
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 +7 -0
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/task-manifest-commands.d.ts +3 -0
- package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -0
- package/dist/cli/index.js +870 -29
- package/dist/contracts.js +7 -3
- package/dist/index.js +16 -6
- package/dist/mcp/index.js +108 -7
- package/dist/mcp.js +7 -3
- package/dist/project-registration.js +7 -3
- package/dist/registry.js +7 -3
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +31 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +172 -71
- package/dist/server/openapi.d.ts +147 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/task-manifest/plan-slug.d.ts +11 -0
- package/dist/task-manifest/plan-slug.d.ts.map +1 -0
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest.js +9 -3
- package/package.json +7 -3
package/dist/cli/index.js
CHANGED
|
@@ -2123,7 +2123,7 @@ var package_default;
|
|
|
2123
2123
|
var init_package = __esm(() => {
|
|
2124
2124
|
package_default = {
|
|
2125
2125
|
name: "@hasna/todos",
|
|
2126
|
-
version: "0.15.
|
|
2126
|
+
version: "0.15.27",
|
|
2127
2127
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
2128
2128
|
type: "module",
|
|
2129
2129
|
main: "dist/index.js",
|
|
@@ -2172,7 +2172,8 @@ var init_package = __esm(() => {
|
|
|
2172
2172
|
}
|
|
2173
2173
|
},
|
|
2174
2174
|
workspaces: [
|
|
2175
|
-
"dashboard"
|
|
2175
|
+
"dashboard",
|
|
2176
|
+
"ai"
|
|
2176
2177
|
],
|
|
2177
2178
|
files: [
|
|
2178
2179
|
"dist",
|
|
@@ -2196,7 +2197,9 @@ var init_package = __esm(() => {
|
|
|
2196
2197
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
2197
2198
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
2198
2199
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
2199
|
-
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
2200
|
+
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts scripts/attested-container-compatibility-vector.test.ts",
|
|
2201
|
+
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
2202
|
+
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
2200
2203
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
2201
2204
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
2202
2205
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
@@ -2243,6 +2246,7 @@ var init_package = __esm(() => {
|
|
|
2243
2246
|
zod: "^3.24.2"
|
|
2244
2247
|
},
|
|
2245
2248
|
overrides: {
|
|
2249
|
+
ajv: "8.20.0",
|
|
2246
2250
|
"fast-uri": "3.1.2"
|
|
2247
2251
|
},
|
|
2248
2252
|
devDependencies: {
|
|
@@ -5788,6 +5792,39 @@ async function cloudPrGroupEvents(client, groupId, options = {}) {
|
|
|
5788
5792
|
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} did not return a complete closed authoritative remote event page; ` + "local SQLite fallback is disabled", { cause: error });
|
|
5789
5793
|
}
|
|
5790
5794
|
}
|
|
5795
|
+
function unwrapTaskManifestEnvelope(raw, key, route) {
|
|
5796
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !(key in raw)) {
|
|
5797
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a non-authoritative task-manifest response envelope; ` + "local SQLite fallback is disabled");
|
|
5798
|
+
}
|
|
5799
|
+
return raw[key];
|
|
5800
|
+
}
|
|
5801
|
+
async function cloudTaskManifestCapability(client) {
|
|
5802
|
+
const route = "/v1/task-manifest/capability";
|
|
5803
|
+
return unwrapTaskManifestEnvelope(await requiredRemoteRoute(client, route, () => client.transport.get("/task-manifest/capability")), "capability", route);
|
|
5804
|
+
}
|
|
5805
|
+
async function cloudApplyTaskManifest(client, input) {
|
|
5806
|
+
const route = "/v1/task-manifest/apply";
|
|
5807
|
+
return unwrapTaskManifestEnvelope(await requiredRemoteRoute(client, route, () => client.transport.post("/task-manifest/apply", input)), "result", route);
|
|
5808
|
+
}
|
|
5809
|
+
async function cloudReadExactTaskManifest(client, receiptId) {
|
|
5810
|
+
const route = "/v1/task-manifest/read-exact";
|
|
5811
|
+
return unwrapTaskManifestEnvelope(await requiredRemoteRoute(client, route, () => client.transport.post("/task-manifest/read-exact", { receipt_id: receiptId })), "result", route);
|
|
5812
|
+
}
|
|
5813
|
+
async function cloudLookupTaskManifestBinding(client, input) {
|
|
5814
|
+
const route = "/v1/task-manifest/bindings/lookup";
|
|
5815
|
+
return unwrapTaskManifestEnvelope(await requiredRemoteRoute(client, route, () => client.transport.post("/task-manifest/bindings/lookup", input)), "result", route);
|
|
5816
|
+
}
|
|
5817
|
+
async function cloudCompensateTaskManifest(client, input) {
|
|
5818
|
+
const route = "/v1/task-manifest/compensate";
|
|
5819
|
+
return unwrapTaskManifestEnvelope(await requiredRemoteRoute(client, route, () => client.transport.post("/task-manifest/compensate", input)), "result", route);
|
|
5820
|
+
}
|
|
5821
|
+
async function cloudMarkTaskManifestOutboxDelivered(client, outboxId) {
|
|
5822
|
+
const route = "/v1/task-manifest/outbox/delivered";
|
|
5823
|
+
const delivered = unwrapTaskManifestEnvelope(await requiredRemoteRoute(client, route, () => client.transport.post("/task-manifest/outbox/delivered", { outbox_id: outboxId })), "delivered", route);
|
|
5824
|
+
if (delivered !== true) {
|
|
5825
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} did not confirm task-manifest outbox delivery; ` + "local SQLite fallback is disabled");
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5791
5828
|
function unwrapTask(raw) {
|
|
5792
5829
|
if (raw && typeof raw === "object" && "task" in raw) {
|
|
5793
5830
|
return raw.task;
|
|
@@ -7759,6 +7796,7 @@ var init_stage_a = __esm(() => {
|
|
|
7759
7796
|
"sync",
|
|
7760
7797
|
"tag",
|
|
7761
7798
|
"task",
|
|
7799
|
+
"task-manifest",
|
|
7762
7800
|
"template-export",
|
|
7763
7801
|
"template-history",
|
|
7764
7802
|
"template-import",
|
|
@@ -7884,6 +7922,7 @@ var init_stage_a = __esm(() => {
|
|
|
7884
7922
|
"task",
|
|
7885
7923
|
"task-lists",
|
|
7886
7924
|
"stale-lock-handoff",
|
|
7925
|
+
"task-manifest",
|
|
7887
7926
|
"template-export",
|
|
7888
7927
|
"template-import",
|
|
7889
7928
|
"template-preview",
|
|
@@ -46091,6 +46130,13 @@ var init_schema2 = __esm(() => {
|
|
|
46091
46130
|
}).strict();
|
|
46092
46131
|
});
|
|
46093
46132
|
|
|
46133
|
+
// src/task-manifest/plan-slug.ts
|
|
46134
|
+
function taskManifestPlanSlug(manifest, planId) {
|
|
46135
|
+
const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
|
|
46136
|
+
return `${base}-${planId}`;
|
|
46137
|
+
}
|
|
46138
|
+
var init_plan_slug = () => {};
|
|
46139
|
+
|
|
46094
46140
|
// src/task-manifest/backend.ts
|
|
46095
46141
|
function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
46096
46142
|
if (rows.length === 0) {
|
|
@@ -46119,6 +46165,67 @@ var init_backend = __esm(() => {
|
|
|
46119
46165
|
});
|
|
46120
46166
|
|
|
46121
46167
|
// src/task-manifest/reference-guard.ts
|
|
46168
|
+
function quoteSqliteIdentifier2(value) {
|
|
46169
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
46170
|
+
}
|
|
46171
|
+
function placeholders3(count) {
|
|
46172
|
+
return Array.from({ length: count }, () => "?").join(",");
|
|
46173
|
+
}
|
|
46174
|
+
function sqliteManagedReferencePredicate(table, field, target, managed) {
|
|
46175
|
+
const taskPlaceholders = placeholders3(managed.task_ids.length);
|
|
46176
|
+
if (table === "tasks" && field === "plan_id" && target === "plans") {
|
|
46177
|
+
return {
|
|
46178
|
+
sql: `id IN (${taskPlaceholders})`,
|
|
46179
|
+
values: managed.task_ids
|
|
46180
|
+
};
|
|
46181
|
+
}
|
|
46182
|
+
if (table === "task_dependencies" && target === "tasks" && (field === "task_id" || field === "depends_on")) {
|
|
46183
|
+
return {
|
|
46184
|
+
sql: `task_id IN (${taskPlaceholders}) AND depends_on IN (${taskPlaceholders})`,
|
|
46185
|
+
values: [...managed.task_ids, ...managed.task_ids]
|
|
46186
|
+
};
|
|
46187
|
+
}
|
|
46188
|
+
if (table === "task_comments" && field === "task_id" && target === "tasks") {
|
|
46189
|
+
return managed.comment_ids.length > 0 ? { sql: `id IN (${placeholders3(managed.comment_ids.length)})`, values: managed.comment_ids } : { sql: "0", values: [] };
|
|
46190
|
+
}
|
|
46191
|
+
if (table === "task_verifications" && field === "task_id" && target === "tasks") {
|
|
46192
|
+
return managed.verification_ids.length > 0 ? { sql: `id IN (${placeholders3(managed.verification_ids.length)})`, values: managed.verification_ids } : { sql: "0", values: [] };
|
|
46193
|
+
}
|
|
46194
|
+
if (table === "task_tags" && field === "task_id" && target === "tasks") {
|
|
46195
|
+
return {
|
|
46196
|
+
sql: `task_id IN (${taskPlaceholders})`,
|
|
46197
|
+
values: managed.task_ids
|
|
46198
|
+
};
|
|
46199
|
+
}
|
|
46200
|
+
return null;
|
|
46201
|
+
}
|
|
46202
|
+
function findSqliteTaskManifestForeignReference(db, managed) {
|
|
46203
|
+
const tables = db.query(`SELECT name FROM sqlite_master
|
|
46204
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`).all();
|
|
46205
|
+
for (const { name: table } of tables) {
|
|
46206
|
+
const foreignKeys = db.query(`PRAGMA foreign_key_list(${quoteSqliteIdentifier2(table)})`).all();
|
|
46207
|
+
for (const foreignKey of foreignKeys) {
|
|
46208
|
+
if (foreignKey.table !== "tasks" && foreignKey.table !== "plans")
|
|
46209
|
+
continue;
|
|
46210
|
+
const target = foreignKey.table;
|
|
46211
|
+
const targetIds = target === "tasks" ? managed.task_ids : [managed.plan_id];
|
|
46212
|
+
const owned = sqliteManagedReferencePredicate(table, foreignKey.from, target, managed);
|
|
46213
|
+
const sql = `SELECT 1 AS found FROM ${quoteSqliteIdentifier2(table)}
|
|
46214
|
+
WHERE ${quoteSqliteIdentifier2(foreignKey.from)} IN (${placeholders3(targetIds.length)})
|
|
46215
|
+
${owned ? `AND NOT (${owned.sql})` : ""}
|
|
46216
|
+
LIMIT 1`;
|
|
46217
|
+
if (db.query(sql).get(...targetIds, ...owned?.values ?? [])) {
|
|
46218
|
+
return {
|
|
46219
|
+
surface: table,
|
|
46220
|
+
field: foreignKey.from,
|
|
46221
|
+
target,
|
|
46222
|
+
on_delete: foreignKey.on_delete.toUpperCase()
|
|
46223
|
+
};
|
|
46224
|
+
}
|
|
46225
|
+
}
|
|
46226
|
+
}
|
|
46227
|
+
return null;
|
|
46228
|
+
}
|
|
46122
46229
|
function postgresTaskManifestForeignReferenceSql(tableName) {
|
|
46123
46230
|
return `SELECT object_type, object_id
|
|
46124
46231
|
FROM ${tableName}
|
|
@@ -46148,6 +46255,89 @@ function postgresTaskManifestForeignReferenceSql(tableName) {
|
|
|
46148
46255
|
function sqlString(value) {
|
|
46149
46256
|
return `'${value.replaceAll("'", "''")}'`;
|
|
46150
46257
|
}
|
|
46258
|
+
function sqliteTodosTaskManifestSchemaSql() {
|
|
46259
|
+
return `
|
|
46260
|
+
CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
|
|
46261
|
+
receipt_id TEXT PRIMARY KEY,
|
|
46262
|
+
tenant_id TEXT NOT NULL,
|
|
46263
|
+
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
46264
|
+
route TEXT NOT NULL,
|
|
46265
|
+
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
46266
|
+
kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
|
|
46267
|
+
operation_id TEXT NOT NULL,
|
|
46268
|
+
idempotency_key TEXT NOT NULL,
|
|
46269
|
+
request_digest TEXT NOT NULL,
|
|
46270
|
+
result_digest TEXT NOT NULL,
|
|
46271
|
+
binding_version INTEGER NOT NULL,
|
|
46272
|
+
apply_receipt_id TEXT,
|
|
46273
|
+
manifest_json TEXT,
|
|
46274
|
+
result_json TEXT NOT NULL,
|
|
46275
|
+
created_at TEXT NOT NULL,
|
|
46276
|
+
UNIQUE(kind, idempotency_key)
|
|
46277
|
+
);
|
|
46278
|
+
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
46279
|
+
operation_id TEXT PRIMARY KEY,
|
|
46280
|
+
tenant_id TEXT NOT NULL,
|
|
46281
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
46282
|
+
request_digest TEXT NOT NULL,
|
|
46283
|
+
result_digest TEXT NOT NULL,
|
|
46284
|
+
apply_receipt_id TEXT NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
46285
|
+
manifest_json TEXT NOT NULL,
|
|
46286
|
+
result_json TEXT NOT NULL,
|
|
46287
|
+
state TEXT NOT NULL CHECK(state IN ('applied', 'compensated')),
|
|
46288
|
+
version INTEGER NOT NULL,
|
|
46289
|
+
compensation_receipt_id TEXT,
|
|
46290
|
+
created_at TEXT NOT NULL,
|
|
46291
|
+
updated_at TEXT NOT NULL
|
|
46292
|
+
);
|
|
46293
|
+
CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
46294
|
+
id TEXT PRIMARY KEY,
|
|
46295
|
+
apply_receipt_id TEXT NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
46296
|
+
topic TEXT NOT NULL,
|
|
46297
|
+
payload TEXT NOT NULL,
|
|
46298
|
+
payload_digest TEXT NOT NULL,
|
|
46299
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'delivered', 'cancelled')),
|
|
46300
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
46301
|
+
created_at TEXT NOT NULL,
|
|
46302
|
+
delivered_at TEXT
|
|
46303
|
+
);
|
|
46304
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_outbox_receipt
|
|
46305
|
+
ON todos_task_manifest_outbox(apply_receipt_id, status);
|
|
46306
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_receipts_immutable_update
|
|
46307
|
+
BEFORE UPDATE ON todos_task_manifest_receipts BEGIN
|
|
46308
|
+
SELECT RAISE(ABORT, 'todos task manifest receipts are immutable');
|
|
46309
|
+
END;
|
|
46310
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_manifest_receipts_immutable_delete
|
|
46311
|
+
BEFORE DELETE ON todos_task_manifest_receipts BEGIN
|
|
46312
|
+
SELECT RAISE(ABORT, 'todos task manifest receipts are immutable');
|
|
46313
|
+
END;
|
|
46314
|
+
`;
|
|
46315
|
+
}
|
|
46316
|
+
function sqliteTableHasColumn(db, tableName, columnName) {
|
|
46317
|
+
const columns = db.query(`PRAGMA table_info("${tableName}")`).all();
|
|
46318
|
+
return columns.some((column) => column.name === columnName);
|
|
46319
|
+
}
|
|
46320
|
+
function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
46321
|
+
db.exec(sqliteTodosTaskManifestSchemaSql());
|
|
46322
|
+
const tenantDefault = sqlString(tenantId);
|
|
46323
|
+
for (const tableName of [
|
|
46324
|
+
"todos_task_manifest_receipts",
|
|
46325
|
+
"todos_task_manifest_bindings"
|
|
46326
|
+
]) {
|
|
46327
|
+
if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
|
|
46328
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
|
|
46329
|
+
}
|
|
46330
|
+
}
|
|
46331
|
+
db.exec(`
|
|
46332
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
|
|
46333
|
+
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind);
|
|
46334
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_bindings_tenant_plan
|
|
46335
|
+
ON todos_task_manifest_bindings(
|
|
46336
|
+
tenant_id,
|
|
46337
|
+
json_extract(result_json, '$.graph.plan_id')
|
|
46338
|
+
);
|
|
46339
|
+
`);
|
|
46340
|
+
}
|
|
46151
46341
|
function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
46152
46342
|
const tenantDefault = sqlString(tenantId);
|
|
46153
46343
|
return [
|
|
@@ -46224,9 +46414,411 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
|
46224
46414
|
}
|
|
46225
46415
|
|
|
46226
46416
|
// src/task-manifest/sqlite.ts
|
|
46417
|
+
function fault(faults, point) {
|
|
46418
|
+
if (faults.points.has(point))
|
|
46419
|
+
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
46420
|
+
}
|
|
46421
|
+
function parseApplyResult(value, duplicate) {
|
|
46422
|
+
return { ...JSON.parse(value), duplicate };
|
|
46423
|
+
}
|
|
46424
|
+
|
|
46425
|
+
class SqliteTodosTaskManifestBackend {
|
|
46426
|
+
db;
|
|
46427
|
+
tenantId;
|
|
46428
|
+
kind = "sqlite";
|
|
46429
|
+
constructor(db, tenantId = "default") {
|
|
46430
|
+
this.db = db;
|
|
46431
|
+
this.tenantId = tenantId;
|
|
46432
|
+
ensureSqliteTodosTaskManifestSchema(db, tenantId);
|
|
46433
|
+
}
|
|
46434
|
+
async serialized(run) {
|
|
46435
|
+
const previous = sqliteTails.get(this.db) ?? Promise.resolve();
|
|
46436
|
+
let release;
|
|
46437
|
+
const current = new Promise((resolve17) => {
|
|
46438
|
+
release = resolve17;
|
|
46439
|
+
});
|
|
46440
|
+
const tail = previous.then(() => current);
|
|
46441
|
+
sqliteTails.set(this.db, tail);
|
|
46442
|
+
await previous;
|
|
46443
|
+
try {
|
|
46444
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
46445
|
+
try {
|
|
46446
|
+
const result = run();
|
|
46447
|
+
this.db.exec("COMMIT");
|
|
46448
|
+
return result;
|
|
46449
|
+
} catch (error) {
|
|
46450
|
+
this.db.exec("ROLLBACK");
|
|
46451
|
+
throw error;
|
|
46452
|
+
}
|
|
46453
|
+
} finally {
|
|
46454
|
+
release();
|
|
46455
|
+
if (sqliteTails.get(this.db) === tail)
|
|
46456
|
+
sqliteTails.delete(this.db);
|
|
46457
|
+
}
|
|
46458
|
+
}
|
|
46459
|
+
async apply(input, faults) {
|
|
46460
|
+
return this.serialized(() => {
|
|
46461
|
+
const { manifest } = input;
|
|
46462
|
+
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);
|
|
46463
|
+
if (binding) {
|
|
46464
|
+
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
46465
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
|
|
46466
|
+
}
|
|
46467
|
+
if (binding["state"] !== "applied") {
|
|
46468
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
|
|
46469
|
+
}
|
|
46470
|
+
return parseApplyResult(String(binding["result_json"]), true);
|
|
46471
|
+
}
|
|
46472
|
+
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);
|
|
46473
|
+
if (idempotency)
|
|
46474
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
|
|
46475
|
+
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
46476
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "New manifest binding version must be 0");
|
|
46477
|
+
}
|
|
46478
|
+
if (!this.db.query("SELECT 1 AS found FROM projects WHERE id = ? LIMIT 1").get(manifest.project_id)) {
|
|
46479
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
|
|
46480
|
+
}
|
|
46481
|
+
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)) {
|
|
46482
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Task list does not belong to the project");
|
|
46483
|
+
}
|
|
46484
|
+
const allIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids];
|
|
46485
|
+
for (const id of allIds) {
|
|
46486
|
+
for (const table of ["plans", "tasks", "task_comments", "task_verifications"]) {
|
|
46487
|
+
if (this.db.query(`SELECT 1 AS found FROM ${table} WHERE id = ? LIMIT 1`).get(id)) {
|
|
46488
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Deterministic id already exists: ${id}`);
|
|
46489
|
+
}
|
|
46490
|
+
}
|
|
46491
|
+
}
|
|
46492
|
+
this.db.query(`INSERT INTO plans (id, project_id, name, description, status, task_list_id, slug, created_at, updated_at)
|
|
46493
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.graph.plan_id, manifest.project_id, manifest.plan.name, manifest.plan.description ?? null, manifest.plan.status ?? "active", manifest.task_list_id ?? null, taskManifestPlanSlug(manifest, input.graph.plan_id), input.now, input.now);
|
|
46494
|
+
fault(faults, "after_plan_write");
|
|
46495
|
+
for (const task2 of manifest.tasks) {
|
|
46496
|
+
const taskId = input.graph.task_ids[task2.key];
|
|
46497
|
+
this.db.query(`INSERT INTO tasks (
|
|
46498
|
+
id, project_id, title, description, status, priority, assigned_to, tags, metadata,
|
|
46499
|
+
version, plan_id, task_list_id, created_at, updated_at, created_by
|
|
46500
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)`).run(taskId, manifest.project_id, task2.title, task2.description ?? null, task2.status ?? "pending", task2.priority ?? "medium", task2.assigned_to ?? null, JSON.stringify(task2.tags ?? []), canonicalJson2(task2.metadata ?? {}), input.graph.plan_id, manifest.task_list_id ?? null, input.now, input.now, task2.created_by ?? null);
|
|
46501
|
+
for (const tag of [...new Set(task2.tags ?? [])].sort()) {
|
|
46502
|
+
this.db.query("INSERT INTO task_tags (task_id, tag) VALUES (?, ?)").run(taskId, tag);
|
|
46503
|
+
}
|
|
46504
|
+
}
|
|
46505
|
+
fault(faults, "after_task_write");
|
|
46506
|
+
for (const edge of manifest.dependencies ?? []) {
|
|
46507
|
+
this.db.query("INSERT INTO task_dependencies (task_id, depends_on) VALUES (?, ?)").run(input.graph.task_ids[edge.task], input.graph.task_ids[edge.depends_on]);
|
|
46508
|
+
}
|
|
46509
|
+
fault(faults, "after_dependency_write");
|
|
46510
|
+
let commentIndex = 0;
|
|
46511
|
+
for (const task2 of manifest.tasks)
|
|
46512
|
+
for (const comment2 of task2.comments ?? []) {
|
|
46513
|
+
this.db.query(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, created_at, type, progress_pct)
|
|
46514
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.graph.comment_ids[commentIndex++], input.graph.task_ids[task2.key], comment2.agent_id ?? null, comment2.session_id ?? null, comment2.content, input.now, comment2.type ?? "comment", comment2.progress_pct ?? null);
|
|
46515
|
+
}
|
|
46516
|
+
fault(faults, "after_comment_write");
|
|
46517
|
+
let verificationIndex = 0;
|
|
46518
|
+
for (const task2 of manifest.tasks)
|
|
46519
|
+
for (const verification2 of task2.verifications ?? []) {
|
|
46520
|
+
this.db.query(`INSERT INTO task_verifications (
|
|
46521
|
+
id, task_id, command, status, output_summary, artifact_path, agent_id, run_at, created_at
|
|
46522
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.graph.verification_ids[verificationIndex++], input.graph.task_ids[task2.key], verification2.command, verification2.status ?? "unknown", verification2.output_summary ?? null, verification2.artifact_path ?? null, verification2.agent_id ?? null, input.now, input.now);
|
|
46523
|
+
}
|
|
46524
|
+
fault(faults, "after_verification_write");
|
|
46525
|
+
const readback = this.readback(input.graph);
|
|
46526
|
+
const expected = {
|
|
46527
|
+
plans: 1,
|
|
46528
|
+
tasks: manifest.tasks.length,
|
|
46529
|
+
dependencies: manifest.dependencies?.length ?? 0,
|
|
46530
|
+
comments: input.graph.comment_ids.length,
|
|
46531
|
+
verifications: input.graph.verification_ids.length,
|
|
46532
|
+
complete: true
|
|
46533
|
+
};
|
|
46534
|
+
if (canonicalJson2(readback) !== canonicalJson2(expected)) {
|
|
46535
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_READBACK_MISMATCH", "Exact graph readback did not match", { expected, readback });
|
|
46536
|
+
}
|
|
46537
|
+
const receipt = {
|
|
46538
|
+
receipt_id: input.receipt_id,
|
|
46539
|
+
authority: "todos",
|
|
46540
|
+
route: "todos.task-manifest.v1",
|
|
46541
|
+
schema_version: 1,
|
|
46542
|
+
kind: "apply",
|
|
46543
|
+
operation_id: manifest.operation_id,
|
|
46544
|
+
idempotency_key: manifest.idempotency_key,
|
|
46545
|
+
request_digest: input.request_digest,
|
|
46546
|
+
result_digest: input.result_digest,
|
|
46547
|
+
binding_version: 1,
|
|
46548
|
+
apply_receipt_id: null,
|
|
46549
|
+
created_at: input.now
|
|
46550
|
+
};
|
|
46551
|
+
const result = {
|
|
46552
|
+
duplicate: false,
|
|
46553
|
+
receipt,
|
|
46554
|
+
graph: input.graph,
|
|
46555
|
+
readback,
|
|
46556
|
+
outbox_ids: input.outbox.map((entry2) => entry2.id),
|
|
46557
|
+
result_digest: input.result_digest
|
|
46558
|
+
};
|
|
46559
|
+
const resultJson2 = canonicalJson2(result);
|
|
46560
|
+
const manifestJson = canonicalJson2(manifest);
|
|
46561
|
+
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
46562
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
46563
|
+
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
46564
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, 1, NULL, ?, ?, ?)`).run(input.receipt_id, this.tenantId, manifest.operation_id, manifest.idempotency_key, input.request_digest, input.result_digest, manifestJson, resultJson2, input.now);
|
|
46565
|
+
for (const entry2 of input.outbox) {
|
|
46566
|
+
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
46567
|
+
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
46568
|
+
) VALUES (?, ?, ?, ?, ?, 'pending', ?)`).run(entry2.id, input.receipt_id, entry2.topic, canonicalJson2(entry2.payload), entry2.digest, input.now);
|
|
46569
|
+
}
|
|
46570
|
+
fault(faults, "after_outbox_write");
|
|
46571
|
+
this.db.query(`INSERT INTO todos_task_manifest_bindings (
|
|
46572
|
+
operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
46573
|
+
manifest_json, result_json, state, version, created_at, updated_at
|
|
46574
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.idempotency_key, input.request_digest, input.result_digest, input.receipt_id, manifestJson, resultJson2, input.now, input.now);
|
|
46575
|
+
fault(faults, "after_receipt_write");
|
|
46576
|
+
return result;
|
|
46577
|
+
});
|
|
46578
|
+
}
|
|
46579
|
+
async readExact(receiptId3) {
|
|
46580
|
+
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, receiptId3);
|
|
46581
|
+
if (!row)
|
|
46582
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId3}`);
|
|
46583
|
+
return parseApplyResult(row.result_json, false);
|
|
46584
|
+
}
|
|
46585
|
+
async lookupBindingByPlanId(planId) {
|
|
46586
|
+
const rows = this.db.query(`
|
|
46587
|
+
SELECT
|
|
46588
|
+
b.apply_receipt_id AS apply_receipt_id,
|
|
46589
|
+
b.state AS state,
|
|
46590
|
+
b.version AS binding_version,
|
|
46591
|
+
b.tenant_id AS binding_tenant_id,
|
|
46592
|
+
b.operation_id AS binding_operation_id,
|
|
46593
|
+
json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
|
|
46594
|
+
r.tenant_id AS receipt_tenant_id,
|
|
46595
|
+
r.authority AS receipt_authority,
|
|
46596
|
+
r.route AS receipt_route,
|
|
46597
|
+
r.schema_version AS receipt_schema_version,
|
|
46598
|
+
r.kind AS receipt_kind,
|
|
46599
|
+
r.operation_id AS receipt_operation_id,
|
|
46600
|
+
json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
|
|
46601
|
+
FROM todos_task_manifest_bindings b
|
|
46602
|
+
LEFT JOIN todos_task_manifest_receipts r
|
|
46603
|
+
ON r.receipt_id = b.apply_receipt_id
|
|
46604
|
+
AND r.tenant_id = b.tenant_id
|
|
46605
|
+
WHERE b.tenant_id = ?
|
|
46606
|
+
AND json_extract(b.result_json, '$.graph.plan_id') = ?
|
|
46607
|
+
LIMIT 2
|
|
46608
|
+
`).all(this.tenantId, planId);
|
|
46609
|
+
return validateTaskManifestBindingLookupRows(rows, this.tenantId, planId);
|
|
46610
|
+
}
|
|
46611
|
+
async markOutboxDelivered(outboxId, deliveredAt) {
|
|
46612
|
+
await this.serialized(() => {
|
|
46613
|
+
const result = this.db.query(`UPDATE todos_task_manifest_outbox
|
|
46614
|
+
SET status = 'delivered', delivered_at = ?, attempts = attempts + 1
|
|
46615
|
+
WHERE id = ? AND status = 'pending'
|
|
46616
|
+
AND EXISTS (
|
|
46617
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
46618
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
46619
|
+
AND r.tenant_id = ?
|
|
46620
|
+
AND r.authority = 'todos'
|
|
46621
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
46622
|
+
AND r.schema_version = 1
|
|
46623
|
+
AND r.kind = 'apply'
|
|
46624
|
+
)`).run(deliveredAt, outboxId, this.tenantId);
|
|
46625
|
+
if (result.changes === 1)
|
|
46626
|
+
return;
|
|
46627
|
+
const existing = this.db.query(`SELECT o.status
|
|
46628
|
+
FROM todos_task_manifest_outbox o
|
|
46629
|
+
JOIN todos_task_manifest_receipts r
|
|
46630
|
+
ON r.receipt_id = o.apply_receipt_id
|
|
46631
|
+
WHERE r.tenant_id = ?
|
|
46632
|
+
AND r.authority = 'todos'
|
|
46633
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
46634
|
+
AND r.schema_version = 1
|
|
46635
|
+
AND r.kind = 'apply'
|
|
46636
|
+
AND o.id = ?
|
|
46637
|
+
LIMIT 1`).get(this.tenantId, outboxId);
|
|
46638
|
+
if (existing?.status === "delivered")
|
|
46639
|
+
return;
|
|
46640
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
46641
|
+
});
|
|
46642
|
+
}
|
|
46643
|
+
async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
|
|
46644
|
+
return this.serialized(() => {
|
|
46645
|
+
const existing = this.db.query(`SELECT apply_receipt_id, request_digest, result_json
|
|
46646
|
+
FROM todos_task_manifest_receipts
|
|
46647
|
+
WHERE tenant_id = ? AND kind = 'compensate' AND idempotency_key = ?
|
|
46648
|
+
LIMIT 1`).get(this.tenantId, input.idempotency_key);
|
|
46649
|
+
if (existing) {
|
|
46650
|
+
if (existing.apply_receipt_id !== input.receipt_id || existing.request_digest !== requestDigest) {
|
|
46651
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation idempotency key is already used");
|
|
46652
|
+
}
|
|
46653
|
+
return { ...JSON.parse(existing.result_json), duplicate: true };
|
|
46654
|
+
}
|
|
46655
|
+
const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, input.receipt_id);
|
|
46656
|
+
if (!row)
|
|
46657
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
|
|
46658
|
+
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = ? AND operation_id = ? LIMIT 1").get(this.tenantId, String(row["operation_id"]));
|
|
46659
|
+
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
46660
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
46661
|
+
}
|
|
46662
|
+
if (binding["state"] !== "applied")
|
|
46663
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
46664
|
+
const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
46665
|
+
JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
|
|
46666
|
+
WHERE r.tenant_id = ? AND o.apply_receipt_id = ? AND o.status = 'delivered'
|
|
46667
|
+
LIMIT 1`).get(this.tenantId, input.receipt_id);
|
|
46668
|
+
if (delivered)
|
|
46669
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
46670
|
+
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
46671
|
+
const manifest = JSON.parse(String(row["manifest_json"]));
|
|
46672
|
+
const expectedEffects = [
|
|
46673
|
+
{
|
|
46674
|
+
topic: "todos.task-manifest.applied",
|
|
46675
|
+
payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
|
|
46676
|
+
},
|
|
46677
|
+
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
46678
|
+
];
|
|
46679
|
+
const storedOutbox = this.db.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
|
|
46680
|
+
FROM todos_task_manifest_outbox
|
|
46681
|
+
WHERE apply_receipt_id = ?
|
|
46682
|
+
AND EXISTS (
|
|
46683
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
46684
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
46685
|
+
AND r.tenant_id = ?
|
|
46686
|
+
)
|
|
46687
|
+
ORDER BY id`).all(input.receipt_id, this.tenantId);
|
|
46688
|
+
if (storedOutbox.length !== expectedEffects.length) {
|
|
46689
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
46690
|
+
}
|
|
46691
|
+
const outboxById = new Map(storedOutbox.map((entry2) => [String(entry2["id"]), entry2]));
|
|
46692
|
+
for (const [index, expectedEffect] of expectedEffects.entries()) {
|
|
46693
|
+
const stored = outboxById.get(applyResult.outbox_ids[index]);
|
|
46694
|
+
const expectedPayload = canonicalJson2(expectedEffect.payload);
|
|
46695
|
+
if (!stored || stored["topic"] !== expectedEffect.topic || stored["payload"] !== expectedPayload || stored["payload_digest"] !== canonicalDigest(expectedEffect) || stored["status"] !== "pending" || Number(stored["attempts"]) !== 0 || stored["delivered_at"] !== null) {
|
|
46696
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
46697
|
+
}
|
|
46698
|
+
}
|
|
46699
|
+
const taskIds = Object.values(applyResult.graph.task_ids);
|
|
46700
|
+
const placeholders4 = taskIds.map(() => "?").join(",");
|
|
46701
|
+
const foreignReference = findSqliteTaskManifestForeignReference(this.db, {
|
|
46702
|
+
plan_id: applyResult.graph.plan_id,
|
|
46703
|
+
task_ids: taskIds,
|
|
46704
|
+
dependency_ids: applyResult.graph.dependency_ids,
|
|
46705
|
+
comment_ids: applyResult.graph.comment_ids,
|
|
46706
|
+
verification_ids: applyResult.graph.verification_ids
|
|
46707
|
+
});
|
|
46708
|
+
if (foreignReference) {
|
|
46709
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", `Compensation refused: foreign reference at ${foreignReference.surface}.${foreignReference.field} would be changed by ${foreignReference.on_delete}`, { ...foreignReference });
|
|
46710
|
+
}
|
|
46711
|
+
const actualReadback = this.readback(applyResult.graph);
|
|
46712
|
+
if (canonicalJson2(actualReadback) !== canonicalJson2(applyResult.readback)) {
|
|
46713
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: graph changed since apply", { actualReadback });
|
|
46714
|
+
}
|
|
46715
|
+
const plan = this.db.query("SELECT project_id, name, description, status, task_list_id, slug FROM plans WHERE id = ? LIMIT 1").get(applyResult.graph.plan_id);
|
|
46716
|
+
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) || plan["slug"] !== taskManifestPlanSlug(manifest, applyResult.graph.plan_id)) {
|
|
46717
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
|
|
46718
|
+
}
|
|
46719
|
+
for (const task2 of manifest.tasks) {
|
|
46720
|
+
const stored = this.db.query("SELECT title, description, status, priority, assigned_to, tags, metadata, created_by FROM tasks WHERE id = ? LIMIT 1").get(applyResult.graph.task_ids[task2.key]);
|
|
46721
|
+
if (!stored || stored["title"] !== task2.title || stored["description"] !== (task2.description ?? null) || stored["status"] !== (task2.status ?? "pending") || stored["priority"] !== (task2.priority ?? "medium") || stored["assigned_to"] !== (task2.assigned_to ?? null) || stored["tags"] !== JSON.stringify(task2.tags ?? []) || stored["metadata"] !== canonicalJson2(task2.metadata ?? {}) || stored["created_by"] !== (task2.created_by ?? null)) {
|
|
46722
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", `Compensation refused: task ${task2.key} changed since apply`);
|
|
46723
|
+
}
|
|
46724
|
+
const storedTags = this.db.query("SELECT tag FROM task_tags WHERE task_id = ? ORDER BY tag").all(applyResult.graph.task_ids[task2.key]).map((row2) => row2.tag);
|
|
46725
|
+
const expectedTags = [...new Set(task2.tags ?? [])].sort();
|
|
46726
|
+
if (canonicalJson2(storedTags) !== canonicalJson2(expectedTags)) {
|
|
46727
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", `Compensation refused: task ${task2.key} tags changed since apply`);
|
|
46728
|
+
}
|
|
46729
|
+
}
|
|
46730
|
+
const storedDependencies = this.db.query(`SELECT task_id, depends_on FROM task_dependencies
|
|
46731
|
+
WHERE task_id IN (${placeholders4}) AND depends_on IN (${placeholders4}) ORDER BY task_id, depends_on`).all(...taskIds, ...taskIds);
|
|
46732
|
+
const expectedDependencies = (manifest.dependencies ?? []).map((edge) => ({
|
|
46733
|
+
task_id: applyResult.graph.task_ids[edge.task],
|
|
46734
|
+
depends_on: applyResult.graph.task_ids[edge.depends_on]
|
|
46735
|
+
})).sort((left, right) => left.task_id.localeCompare(right.task_id) || left.depends_on.localeCompare(right.depends_on));
|
|
46736
|
+
if (canonicalJson2(storedDependencies) !== canonicalJson2(expectedDependencies)) {
|
|
46737
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: dependencies changed since apply");
|
|
46738
|
+
}
|
|
46739
|
+
let expectedCommentIndex = 0;
|
|
46740
|
+
for (const task2 of manifest.tasks)
|
|
46741
|
+
for (const comment2 of task2.comments ?? []) {
|
|
46742
|
+
const stored = this.db.query(`SELECT task_id, agent_id, session_id, content, type, progress_pct
|
|
46743
|
+
FROM task_comments WHERE id = ? LIMIT 1`).get(applyResult.graph.comment_ids[expectedCommentIndex]);
|
|
46744
|
+
const expected = {
|
|
46745
|
+
task_id: applyResult.graph.task_ids[task2.key],
|
|
46746
|
+
agent_id: comment2.agent_id ?? null,
|
|
46747
|
+
session_id: comment2.session_id ?? null,
|
|
46748
|
+
content: comment2.content,
|
|
46749
|
+
type: comment2.type ?? "comment",
|
|
46750
|
+
progress_pct: comment2.progress_pct ?? null
|
|
46751
|
+
};
|
|
46752
|
+
if (!stored || canonicalJson2(stored) !== canonicalJson2(expected)) {
|
|
46753
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: comment changed since apply");
|
|
46754
|
+
}
|
|
46755
|
+
expectedCommentIndex += 1;
|
|
46756
|
+
}
|
|
46757
|
+
let expectedVerificationIndex = 0;
|
|
46758
|
+
for (const task2 of manifest.tasks)
|
|
46759
|
+
for (const verification2 of task2.verifications ?? []) {
|
|
46760
|
+
const stored = this.db.query(`SELECT task_id, command, status, output_summary, artifact_path, agent_id
|
|
46761
|
+
FROM task_verifications WHERE id = ? LIMIT 1`).get(applyResult.graph.verification_ids[expectedVerificationIndex]);
|
|
46762
|
+
const expected = {
|
|
46763
|
+
task_id: applyResult.graph.task_ids[task2.key],
|
|
46764
|
+
command: verification2.command,
|
|
46765
|
+
status: verification2.status ?? "unknown",
|
|
46766
|
+
output_summary: verification2.output_summary ?? null,
|
|
46767
|
+
artifact_path: verification2.artifact_path ?? null,
|
|
46768
|
+
agent_id: verification2.agent_id ?? null
|
|
46769
|
+
};
|
|
46770
|
+
if (!stored || canonicalJson2(stored) !== canonicalJson2(expected)) {
|
|
46771
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: verification changed since apply");
|
|
46772
|
+
}
|
|
46773
|
+
expectedVerificationIndex += 1;
|
|
46774
|
+
}
|
|
46775
|
+
this.db.query(`UPDATE todos_task_manifest_outbox
|
|
46776
|
+
SET status = 'cancelled'
|
|
46777
|
+
WHERE apply_receipt_id = ? AND status = 'pending'
|
|
46778
|
+
AND EXISTS (
|
|
46779
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
46780
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
46781
|
+
AND r.tenant_id = ?
|
|
46782
|
+
)`).run(input.receipt_id, this.tenantId);
|
|
46783
|
+
for (const table of ["task_tags", "task_dependencies", "task_comments", "task_verifications"]) {
|
|
46784
|
+
const column = table === "task_dependencies" ? "task_id" : "task_id";
|
|
46785
|
+
this.db.query(`DELETE FROM ${table} WHERE ${column} IN (${placeholders4})`).run(...taskIds);
|
|
46786
|
+
}
|
|
46787
|
+
this.db.query(`DELETE FROM tasks WHERE id IN (${placeholders4})`).run(...taskIds);
|
|
46788
|
+
this.db.query("DELETE FROM plans WHERE id = ?").run(applyResult.graph.plan_id);
|
|
46789
|
+
const readback = this.readback(applyResult.graph);
|
|
46790
|
+
const result = { duplicate: false, receipt, absent: true, readback };
|
|
46791
|
+
const resultJson2 = canonicalJson2(result);
|
|
46792
|
+
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
46793
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
46794
|
+
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
46795
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, this.tenantId, receipt.operation_id, input.idempotency_key, requestDigest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson2, now4);
|
|
46796
|
+
const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
|
|
46797
|
+
WHERE tenant_id = ? AND operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now4, this.tenantId, receipt.operation_id, input.if_binding_version);
|
|
46798
|
+
if (updated.changes !== 1) {
|
|
46799
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding changed during compensation");
|
|
46800
|
+
}
|
|
46801
|
+
return result;
|
|
46802
|
+
});
|
|
46803
|
+
}
|
|
46804
|
+
readback(graph) {
|
|
46805
|
+
const taskIds = Object.values(graph.task_ids);
|
|
46806
|
+
const placeholders4 = taskIds.map(() => "?").join(",");
|
|
46807
|
+
const count = (sql, ...values) => Number(this.db.query(sql).get(...values).count);
|
|
46808
|
+
return {
|
|
46809
|
+
plans: count("SELECT count(*) AS count FROM plans WHERE id = ?", graph.plan_id),
|
|
46810
|
+
tasks: count(`SELECT count(*) AS count FROM tasks WHERE id IN (${placeholders4})`, ...taskIds),
|
|
46811
|
+
dependencies: count(`SELECT count(*) AS count FROM task_dependencies WHERE task_id IN (${placeholders4}) AND depends_on IN (${placeholders4})`, ...taskIds, ...taskIds),
|
|
46812
|
+
comments: count(`SELECT count(*) AS count FROM task_comments WHERE id IN (${graph.comment_ids.map(() => "?").join(",") || "NULL"})`, ...graph.comment_ids),
|
|
46813
|
+
verifications: count(`SELECT count(*) AS count FROM task_verifications WHERE id IN (${graph.verification_ids.map(() => "?").join(",") || "NULL"})`, ...graph.verification_ids),
|
|
46814
|
+
complete: true
|
|
46815
|
+
};
|
|
46816
|
+
}
|
|
46817
|
+
}
|
|
46227
46818
|
var sqliteTails;
|
|
46228
46819
|
var init_sqlite2 = __esm(() => {
|
|
46229
46820
|
init_canonical();
|
|
46821
|
+
init_plan_slug();
|
|
46230
46822
|
init_backend();
|
|
46231
46823
|
init_types5();
|
|
46232
46824
|
sqliteTails = new WeakMap;
|
|
@@ -46245,7 +46837,7 @@ function parseJson2(value) {
|
|
|
46245
46837
|
function timestamp2(value) {
|
|
46246
46838
|
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
46247
46839
|
}
|
|
46248
|
-
function
|
|
46840
|
+
function fault2(faults, point) {
|
|
46249
46841
|
if (faults.points.has(point))
|
|
46250
46842
|
throw new Error(`Injected task-manifest fault at ${point}`);
|
|
46251
46843
|
}
|
|
@@ -46327,7 +46919,7 @@ function taskPayload(manifest, task2, taskId, planId, now4) {
|
|
|
46327
46919
|
function planPayload(input) {
|
|
46328
46920
|
return {
|
|
46329
46921
|
id: input.graph.plan_id,
|
|
46330
|
-
slug:
|
|
46922
|
+
slug: taskManifestPlanSlug(input.manifest, input.graph.plan_id),
|
|
46331
46923
|
project_id: input.manifest.project_id,
|
|
46332
46924
|
task_list_id: input.manifest.task_list_id ?? null,
|
|
46333
46925
|
agent_id: null,
|
|
@@ -46340,7 +46932,7 @@ function planPayload(input) {
|
|
|
46340
46932
|
synced_at: null
|
|
46341
46933
|
};
|
|
46342
46934
|
}
|
|
46343
|
-
function
|
|
46935
|
+
function placeholders4(start, count) {
|
|
46344
46936
|
return Array.from({ length: count }, (_, index) => `$${start + index}`).join(",");
|
|
46345
46937
|
}
|
|
46346
46938
|
|
|
@@ -46414,15 +47006,15 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46414
47006
|
}
|
|
46415
47007
|
const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
|
|
46416
47008
|
const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
|
|
46417
|
-
WHERE service = $1 AND object_id IN (${
|
|
47009
|
+
WHERE service = $1 AND object_id IN (${placeholders4(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
|
|
46418
47010
|
if (conflict.rows[0])
|
|
46419
47011
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "A deterministic graph id already exists");
|
|
46420
47012
|
await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
|
|
46421
|
-
|
|
47013
|
+
fault2(faults, "after_plan_write");
|
|
46422
47014
|
for (const task2 of manifest.tasks) {
|
|
46423
47015
|
await this.insertSync(tx, "tasks", input.graph.task_ids[task2.key], taskPayload(manifest, task2, input.graph.task_ids[task2.key], input.graph.plan_id, input.now), input.now);
|
|
46424
47016
|
}
|
|
46425
|
-
|
|
47017
|
+
fault2(faults, "after_task_write");
|
|
46426
47018
|
for (const [index, edge] of (manifest.dependencies ?? []).entries()) {
|
|
46427
47019
|
await this.insertSync(tx, "dependencies", input.graph.dependency_ids[index], {
|
|
46428
47020
|
id: input.graph.dependency_ids[index],
|
|
@@ -46432,7 +47024,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46432
47024
|
updated_at: input.now
|
|
46433
47025
|
}, input.now);
|
|
46434
47026
|
}
|
|
46435
|
-
|
|
47027
|
+
fault2(faults, "after_dependency_write");
|
|
46436
47028
|
let commentIndex = 0;
|
|
46437
47029
|
for (const task2 of manifest.tasks)
|
|
46438
47030
|
for (const comment2 of task2.comments ?? []) {
|
|
@@ -46448,7 +47040,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46448
47040
|
created_at: input.now
|
|
46449
47041
|
}, input.now);
|
|
46450
47042
|
}
|
|
46451
|
-
|
|
47043
|
+
fault2(faults, "after_comment_write");
|
|
46452
47044
|
let verificationIndex = 0;
|
|
46453
47045
|
for (const task2 of manifest.tasks)
|
|
46454
47046
|
for (const verification2 of task2.verifications ?? []) {
|
|
@@ -46466,7 +47058,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46466
47058
|
updated_at: input.now
|
|
46467
47059
|
}, input.now);
|
|
46468
47060
|
}
|
|
46469
|
-
|
|
47061
|
+
fault2(faults, "after_verification_write");
|
|
46470
47062
|
const readback = await this.readback(tx, input.graph);
|
|
46471
47063
|
const expected = {
|
|
46472
47064
|
plans: 1,
|
|
@@ -46529,7 +47121,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46529
47121
|
input.now
|
|
46530
47122
|
]);
|
|
46531
47123
|
}
|
|
46532
|
-
|
|
47124
|
+
fault2(faults, "after_outbox_write");
|
|
46533
47125
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
46534
47126
|
operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
46535
47127
|
manifest_json, result_json, state, version, created_at, updated_at
|
|
@@ -46544,7 +47136,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46544
47136
|
resultJson2,
|
|
46545
47137
|
input.now
|
|
46546
47138
|
]);
|
|
46547
|
-
|
|
47139
|
+
fault2(faults, "after_receipt_write");
|
|
46548
47140
|
return result;
|
|
46549
47141
|
});
|
|
46550
47142
|
}
|
|
@@ -46784,7 +47376,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46784
47376
|
}
|
|
46785
47377
|
const managedIds = [...expectedPayloads.keys()];
|
|
46786
47378
|
const stored = await tx.query(`SELECT object_type, object_id, payload FROM ${this.tableName}
|
|
46787
|
-
WHERE service = $1 AND object_id IN (${
|
|
47379
|
+
WHERE service = $1 AND object_id IN (${placeholders4(2, managedIds.length)})`, [this.service, ...managedIds]);
|
|
46788
47380
|
for (const row of stored.rows) {
|
|
46789
47381
|
const expected = expectedPayloads.get(String(row["object_id"]));
|
|
46790
47382
|
if (!expected || expected.type !== row["object_type"] || expected.payload !== canonicalJson2(parseJson2(row["payload"]))) {
|
|
@@ -46821,7 +47413,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46821
47413
|
if (!ids.length)
|
|
46822
47414
|
continue;
|
|
46823
47415
|
await tx.query(`DELETE FROM ${this.tableName} WHERE service = $1 AND object_type = $2
|
|
46824
|
-
AND object_id IN (${
|
|
47416
|
+
AND object_id IN (${placeholders4(3, ids.length)})`, [this.service, objectType2, ...ids]);
|
|
46825
47417
|
}
|
|
46826
47418
|
const readback = await this.readback(tx, applyResult.graph);
|
|
46827
47419
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
@@ -46861,7 +47453,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46861
47453
|
return 0;
|
|
46862
47454
|
const result = await tx.query(`SELECT count(*) AS count FROM ${this.tableName}
|
|
46863
47455
|
WHERE service = $1 AND object_type = $2 AND deleted_at IS NULL
|
|
46864
|
-
AND object_id IN (${
|
|
47456
|
+
AND object_id IN (${placeholders4(3, ids.length)})`, [this.service, objectType2, ...ids]);
|
|
46865
47457
|
return Number(result.rows[0]?.count ?? 0);
|
|
46866
47458
|
};
|
|
46867
47459
|
return {
|
|
@@ -46876,6 +47468,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
46876
47468
|
}
|
|
46877
47469
|
var init_postgres3 = __esm(() => {
|
|
46878
47470
|
init_canonical();
|
|
47471
|
+
init_plan_slug();
|
|
46879
47472
|
init_backend();
|
|
46880
47473
|
init_postgres_sync();
|
|
46881
47474
|
init_types5();
|
|
@@ -47078,6 +47671,13 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
47078
47671
|
return result;
|
|
47079
47672
|
}
|
|
47080
47673
|
}
|
|
47674
|
+
function createSqliteTodosTaskManifestAuthority(options) {
|
|
47675
|
+
if (!options?.database) {
|
|
47676
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An explicit SQLite Database is required");
|
|
47677
|
+
}
|
|
47678
|
+
const tenantId = resolveTenantId(options.tenantId);
|
|
47679
|
+
return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database, tenantId), { ...options, tenantId });
|
|
47680
|
+
}
|
|
47081
47681
|
function createPostgresTodosTaskManifestAuthority(client, options = {}) {
|
|
47082
47682
|
if (!client || typeof client.transaction !== "function") {
|
|
47083
47683
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An authoritative PostgreSQL transaction(callback) client is required");
|
|
@@ -49404,7 +50004,7 @@ __export(exports_openapi, {
|
|
|
49404
50004
|
buildV1OpenApiDocument: () => buildV1OpenApiDocument
|
|
49405
50005
|
});
|
|
49406
50006
|
function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
49407
|
-
return {
|
|
50007
|
+
return structuredClone({
|
|
49408
50008
|
openapi: "3.1.0",
|
|
49409
50009
|
info: {
|
|
49410
50010
|
title: "Todos V1 API",
|
|
@@ -49419,6 +50019,9 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49419
50019
|
schemas: {
|
|
49420
50020
|
Task: taskSchema,
|
|
49421
50021
|
Project: projectSchema,
|
|
50022
|
+
TaskManifestBounds: taskManifestBoundsSchema,
|
|
50023
|
+
TaskManifestCapability: taskManifestCapabilitySchema,
|
|
50024
|
+
TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
|
|
49422
50025
|
TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
|
|
49423
50026
|
TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
|
|
49424
50027
|
TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
|
|
@@ -50418,6 +51021,23 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
50418
51021
|
},
|
|
50419
51022
|
security: [{ apiKey: [] }],
|
|
50420
51023
|
paths: {
|
|
51024
|
+
"/v1/task-manifest/capability": {
|
|
51025
|
+
get: {
|
|
51026
|
+
operationId: "getTaskManifestCapability",
|
|
51027
|
+
summary: "Read the current task-manifest authority capability and tenant",
|
|
51028
|
+
responses: {
|
|
51029
|
+
"200": {
|
|
51030
|
+
content: {
|
|
51031
|
+
"application/json": {
|
|
51032
|
+
schema: { $ref: "#/components/schemas/TaskManifestCapabilityResponse" }
|
|
51033
|
+
}
|
|
51034
|
+
}
|
|
51035
|
+
},
|
|
51036
|
+
"401": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
51037
|
+
"503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
51038
|
+
}
|
|
51039
|
+
}
|
|
51040
|
+
},
|
|
50421
51041
|
"/v1/task-manifest/bindings/lookup": {
|
|
50422
51042
|
post: {
|
|
50423
51043
|
operationId: "lookupTaskManifestBinding",
|
|
@@ -51276,9 +51896,9 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
51276
51896
|
}
|
|
51277
51897
|
}
|
|
51278
51898
|
}
|
|
51279
|
-
};
|
|
51899
|
+
});
|
|
51280
51900
|
}
|
|
51281
|
-
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
51901
|
+
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;
|
|
51282
51902
|
var init_openapi = __esm(() => {
|
|
51283
51903
|
init_package_version();
|
|
51284
51904
|
init_types();
|
|
@@ -51304,6 +51924,74 @@ var init_openapi = __esm(() => {
|
|
|
51304
51924
|
updated_at: { type: "string" }
|
|
51305
51925
|
}
|
|
51306
51926
|
};
|
|
51927
|
+
taskManifestBoundsSchema = {
|
|
51928
|
+
type: "object",
|
|
51929
|
+
additionalProperties: false,
|
|
51930
|
+
required: [
|
|
51931
|
+
"tasks",
|
|
51932
|
+
"dependencies",
|
|
51933
|
+
"comments",
|
|
51934
|
+
"verifications",
|
|
51935
|
+
"effects",
|
|
51936
|
+
"metadata_fields",
|
|
51937
|
+
"effect_payload_fields",
|
|
51938
|
+
"request_bytes",
|
|
51939
|
+
"response_bytes"
|
|
51940
|
+
],
|
|
51941
|
+
properties: {
|
|
51942
|
+
tasks: { type: "integer", minimum: 1 },
|
|
51943
|
+
dependencies: { type: "integer", minimum: 1 },
|
|
51944
|
+
comments: { type: "integer", minimum: 1 },
|
|
51945
|
+
verifications: { type: "integer", minimum: 1 },
|
|
51946
|
+
effects: { type: "integer", minimum: 1 },
|
|
51947
|
+
metadata_fields: { type: "integer", minimum: 1 },
|
|
51948
|
+
effect_payload_fields: { type: "integer", minimum: 1 },
|
|
51949
|
+
request_bytes: { type: "integer", minimum: 1 },
|
|
51950
|
+
response_bytes: { type: "integer", minimum: 1 }
|
|
51951
|
+
}
|
|
51952
|
+
};
|
|
51953
|
+
taskManifestCapabilitySchema = {
|
|
51954
|
+
type: "object",
|
|
51955
|
+
additionalProperties: false,
|
|
51956
|
+
required: [
|
|
51957
|
+
"authority",
|
|
51958
|
+
"route",
|
|
51959
|
+
"schema_version",
|
|
51960
|
+
"tenant_id",
|
|
51961
|
+
"backend",
|
|
51962
|
+
"deterministic_ids",
|
|
51963
|
+
"immutable_receipts",
|
|
51964
|
+
"transactional_outbox",
|
|
51965
|
+
"idempotent_outbox_delivery",
|
|
51966
|
+
"exact_bounded_readback",
|
|
51967
|
+
"conditional_compensation",
|
|
51968
|
+
"transcript_safe",
|
|
51969
|
+
"bounds"
|
|
51970
|
+
],
|
|
51971
|
+
properties: {
|
|
51972
|
+
authority: { type: "string", enum: ["todos"] },
|
|
51973
|
+
route: { type: "string", enum: ["todos.task-manifest.v1"] },
|
|
51974
|
+
schema_version: { type: "integer", enum: [1] },
|
|
51975
|
+
tenant_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
51976
|
+
backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
|
|
51977
|
+
deterministic_ids: { type: "boolean", enum: [true] },
|
|
51978
|
+
immutable_receipts: { type: "boolean", enum: [true] },
|
|
51979
|
+
transactional_outbox: { type: "boolean", enum: [true] },
|
|
51980
|
+
idempotent_outbox_delivery: { type: "boolean", enum: [true] },
|
|
51981
|
+
exact_bounded_readback: { type: "boolean", enum: [true] },
|
|
51982
|
+
conditional_compensation: { type: "boolean", enum: [true] },
|
|
51983
|
+
transcript_safe: { type: "boolean", enum: [false] },
|
|
51984
|
+
bounds: { $ref: "#/components/schemas/TaskManifestBounds" }
|
|
51985
|
+
}
|
|
51986
|
+
};
|
|
51987
|
+
taskManifestCapabilityResponseSchema = {
|
|
51988
|
+
type: "object",
|
|
51989
|
+
additionalProperties: false,
|
|
51990
|
+
required: ["capability"],
|
|
51991
|
+
properties: {
|
|
51992
|
+
capability: { $ref: "#/components/schemas/TaskManifestCapability" }
|
|
51993
|
+
}
|
|
51994
|
+
};
|
|
51307
51995
|
projectSchema = {
|
|
51308
51996
|
type: "object",
|
|
51309
51997
|
properties: {
|
|
@@ -60189,18 +60877,18 @@ function createSessionRecoveryHandoff(input, db) {
|
|
|
60189
60877
|
LIMIT ?
|
|
60190
60878
|
`).all(...params, limit);
|
|
60191
60879
|
const taskIds = tasks.map((task2) => task2.id);
|
|
60192
|
-
const
|
|
60880
|
+
const placeholders5 = taskIds.map(() => "?").join(",");
|
|
60193
60881
|
const files = taskIds.length ? d.query(`
|
|
60194
60882
|
SELECT DISTINCT path
|
|
60195
60883
|
FROM task_files
|
|
60196
|
-
WHERE task_id IN (${
|
|
60884
|
+
WHERE task_id IN (${placeholders5}) AND status != 'removed'
|
|
60197
60885
|
ORDER BY updated_at DESC, path
|
|
60198
60886
|
LIMIT ?
|
|
60199
60887
|
`).all(...taskIds, limit).map((row) => row.path) : [];
|
|
60200
60888
|
const runs = taskIds.length ? d.query(`
|
|
60201
60889
|
SELECT id
|
|
60202
60890
|
FROM task_runs
|
|
60203
|
-
WHERE task_id IN (${
|
|
60891
|
+
WHERE task_id IN (${placeholders5})
|
|
60204
60892
|
ORDER BY started_at DESC, created_at DESC
|
|
60205
60893
|
LIMIT ?
|
|
60206
60894
|
`).all(...taskIds, limit).map((row) => row.id) : [];
|
|
@@ -69771,8 +70459,8 @@ function findPath(sourceId, targetId, opts, db) {
|
|
|
69771
70459
|
let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
|
|
69772
70460
|
const params = [current.id];
|
|
69773
70461
|
if (opts?.relation_types && opts.relation_types.length > 0) {
|
|
69774
|
-
const
|
|
69775
|
-
sql += ` AND relation_type IN (${
|
|
70462
|
+
const placeholders5 = opts.relation_types.map(() => "?").join(",");
|
|
70463
|
+
sql += ` AND relation_type IN (${placeholders5})`;
|
|
69776
70464
|
params.push(...opts.relation_types);
|
|
69777
70465
|
}
|
|
69778
70466
|
const edges = d.query(sql).all(...params).map(rowToEdge);
|
|
@@ -69807,8 +70495,8 @@ function getImpactAnalysis(entityId, opts, db) {
|
|
|
69807
70495
|
let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
|
|
69808
70496
|
const params = [current.id];
|
|
69809
70497
|
if (opts?.relation_types && opts.relation_types.length > 0) {
|
|
69810
|
-
const
|
|
69811
|
-
sql += ` AND relation_type IN (${
|
|
70498
|
+
const placeholders5 = opts.relation_types.map(() => "?").join(",");
|
|
70499
|
+
sql += ` AND relation_type IN (${placeholders5})`;
|
|
69812
70500
|
params.push(...opts.relation_types);
|
|
69813
70501
|
}
|
|
69814
70502
|
const edges = d.query(sql).all(...params).map(rowToEdge);
|
|
@@ -90970,6 +91658,156 @@ var init_pr_group_commands = __esm(() => {
|
|
|
90970
91658
|
init_helpers();
|
|
90971
91659
|
});
|
|
90972
91660
|
|
|
91661
|
+
// src/cli/commands/task-manifest-commands.ts
|
|
91662
|
+
var exports_task_manifest_commands = {};
|
|
91663
|
+
__export(exports_task_manifest_commands, {
|
|
91664
|
+
registerTaskManifestCommands: () => registerTaskManifestCommands
|
|
91665
|
+
});
|
|
91666
|
+
import chalk32 from "chalk";
|
|
91667
|
+
import { readFileSync as readFileSync26 } from "fs";
|
|
91668
|
+
function globalOptions11(program2) {
|
|
91669
|
+
const command = program2;
|
|
91670
|
+
return command.optsWithGlobals?.() ?? program2.opts();
|
|
91671
|
+
}
|
|
91672
|
+
function jsonRequested(program2, opts) {
|
|
91673
|
+
return opts.json === true || globalOptions11(program2)["json"] === true;
|
|
91674
|
+
}
|
|
91675
|
+
function parseJsonFile(path) {
|
|
91676
|
+
try {
|
|
91677
|
+
return JSON.parse(readFileSync26(path, "utf8"));
|
|
91678
|
+
} catch (error2) {
|
|
91679
|
+
throw new Error(`task-manifest input must be a readable JSON file: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
91680
|
+
}
|
|
91681
|
+
}
|
|
91682
|
+
function parseInteger2(value, label, min) {
|
|
91683
|
+
const parsed = Number(value);
|
|
91684
|
+
if (!Number.isSafeInteger(parsed) || parsed < min) {
|
|
91685
|
+
throw new Error(`${label} must be an integer >= ${min}`);
|
|
91686
|
+
}
|
|
91687
|
+
return parsed;
|
|
91688
|
+
}
|
|
91689
|
+
async function localAuthority(tenantId) {
|
|
91690
|
+
return createSqliteTodosTaskManifestAuthority({
|
|
91691
|
+
database: getDatabase(),
|
|
91692
|
+
...tenantId ? { tenantId } : {}
|
|
91693
|
+
});
|
|
91694
|
+
}
|
|
91695
|
+
async function currentTenantId(remote, authority, explicit) {
|
|
91696
|
+
if (explicit)
|
|
91697
|
+
return explicit;
|
|
91698
|
+
const capability = remote ? await cloudTaskManifestCapability(remote) : await authority.capability();
|
|
91699
|
+
return capability.tenant_id;
|
|
91700
|
+
}
|
|
91701
|
+
function registerTaskManifestCommands(program2) {
|
|
91702
|
+
const taskManifest = program2.command("task-manifest").description("Apply and inspect package-owned task-manifest safe mutations");
|
|
91703
|
+
taskManifest.command("capability").description("Show the current task-manifest authority capability").option("-j, --json", "Output as JSON").option("--tenant-id <id>", "Tenant id for local SQLite authority").action(async (opts) => {
|
|
91704
|
+
try {
|
|
91705
|
+
const remote = getTodosCloudClient();
|
|
91706
|
+
const result = remote ? await cloudTaskManifestCapability(remote) : await (await localAuthority(opts.tenantId)).capability();
|
|
91707
|
+
if (jsonRequested(program2, opts)) {
|
|
91708
|
+
output({ capability: result }, true);
|
|
91709
|
+
return;
|
|
91710
|
+
}
|
|
91711
|
+
console.log(`${chalk32.bold(result.route)} tenant=${result.tenant_id} backend=${result.backend}`);
|
|
91712
|
+
} catch (error2) {
|
|
91713
|
+
handleError(error2);
|
|
91714
|
+
}
|
|
91715
|
+
});
|
|
91716
|
+
taskManifest.command("apply").description("Apply a task-manifest JSON file exactly once by operation/idempotency key").requiredOption("--file <path>", "Task manifest JSON file").option("-j, --json", "Output as JSON").option("--tenant-id <id>", "Tenant id for local SQLite authority").action(async (opts) => {
|
|
91717
|
+
try {
|
|
91718
|
+
const manifest = parseJsonFile(opts.file);
|
|
91719
|
+
const remote = getTodosCloudClient();
|
|
91720
|
+
const result = remote ? await cloudApplyTaskManifest(remote, manifest) : await (await localAuthority(opts.tenantId)).apply(manifest);
|
|
91721
|
+
if (jsonRequested(program2, opts)) {
|
|
91722
|
+
output({ result }, true);
|
|
91723
|
+
return;
|
|
91724
|
+
}
|
|
91725
|
+
console.log(`${result.duplicate ? "duplicate" : "applied"} receipt=${result.receipt.receipt_id}`);
|
|
91726
|
+
console.log(`plan=${result.graph.plan_id} tasks=${Object.keys(result.graph.task_ids).length}`);
|
|
91727
|
+
} catch (error2) {
|
|
91728
|
+
handleError(error2);
|
|
91729
|
+
}
|
|
91730
|
+
});
|
|
91731
|
+
taskManifest.command("read-exact <receipt-id>").description("Read one immutable task-manifest apply receipt by full receipt id").option("-j, --json", "Output as JSON").option("--tenant-id <id>", "Tenant id for local SQLite authority").action(async (receiptId3, opts) => {
|
|
91732
|
+
try {
|
|
91733
|
+
const remote = getTodosCloudClient();
|
|
91734
|
+
const result = remote ? await cloudReadExactTaskManifest(remote, receiptId3) : await (await localAuthority(opts.tenantId)).readExact(receiptId3);
|
|
91735
|
+
if (jsonRequested(program2, opts)) {
|
|
91736
|
+
output({ result }, true);
|
|
91737
|
+
return;
|
|
91738
|
+
}
|
|
91739
|
+
console.log(`receipt=${result.receipt.receipt_id} duplicate=${String(result.duplicate)}`);
|
|
91740
|
+
console.log(`readback=${JSON.stringify(result.readback)}`);
|
|
91741
|
+
} catch (error2) {
|
|
91742
|
+
handleError(error2);
|
|
91743
|
+
}
|
|
91744
|
+
});
|
|
91745
|
+
taskManifest.command("lookup").description("Recover one exact managed apply receipt from a full plan id").requiredOption("--plan-id <uuid>", "Full managed plan UUID").option("--tenant-id <id>", "Authority tenant id; defaults to capability tenant").option("-j, --json", "Output as JSON").action(async (opts) => {
|
|
91746
|
+
try {
|
|
91747
|
+
const remote = getTodosCloudClient();
|
|
91748
|
+
const authority = remote ? null : await localAuthority(opts.tenantId);
|
|
91749
|
+
const tenantId = await currentTenantId(remote, authority, opts.tenantId);
|
|
91750
|
+
const request = {
|
|
91751
|
+
authority: "todos",
|
|
91752
|
+
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
91753
|
+
schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
91754
|
+
tenant_id: tenantId,
|
|
91755
|
+
plan_id: opts.planId,
|
|
91756
|
+
max_items: 1
|
|
91757
|
+
};
|
|
91758
|
+
const result = remote ? await cloudLookupTaskManifestBinding(remote, request) : await authority.lookupBinding(request);
|
|
91759
|
+
if (jsonRequested(program2, opts)) {
|
|
91760
|
+
output({ result }, true);
|
|
91761
|
+
return;
|
|
91762
|
+
}
|
|
91763
|
+
console.log(`plan=${result.plan_id} receipt=${result.apply_receipt_id} state=${result.state}`);
|
|
91764
|
+
} catch (error2) {
|
|
91765
|
+
handleError(error2);
|
|
91766
|
+
}
|
|
91767
|
+
});
|
|
91768
|
+
taskManifest.command("compensate").description("Compensate an untouched task-manifest graph with CAS protection").requiredOption("--receipt-id <uuid>", "Full apply receipt UUID").requiredOption("--idempotency-key <key>", "Stable compensation idempotency key").requiredOption("--if-binding-version <n>", "Expected current binding version").option("-j, --json", "Output as JSON").option("--tenant-id <id>", "Tenant id for local SQLite authority").action(async (opts) => {
|
|
91769
|
+
try {
|
|
91770
|
+
const request = {
|
|
91771
|
+
receipt_id: opts.receiptId,
|
|
91772
|
+
idempotency_key: opts.idempotencyKey,
|
|
91773
|
+
if_binding_version: parseInteger2(opts.ifBindingVersion, "--if-binding-version", 1)
|
|
91774
|
+
};
|
|
91775
|
+
const remote = getTodosCloudClient();
|
|
91776
|
+
const result = remote ? await cloudCompensateTaskManifest(remote, request) : await (await localAuthority(opts.tenantId)).compensate(request);
|
|
91777
|
+
if (jsonRequested(program2, opts)) {
|
|
91778
|
+
output({ result }, true);
|
|
91779
|
+
return;
|
|
91780
|
+
}
|
|
91781
|
+
console.log(`${result.duplicate ? "duplicate" : "compensated"} receipt=${result.receipt.receipt_id}`);
|
|
91782
|
+
console.log(`absent=${String(result.absent)} readback=${JSON.stringify(result.readback)}`);
|
|
91783
|
+
} catch (error2) {
|
|
91784
|
+
handleError(error2);
|
|
91785
|
+
}
|
|
91786
|
+
});
|
|
91787
|
+
taskManifest.command("outbox-delivered <outbox-id>").description("Mark one task-manifest outbox effect delivered").option("-j, --json", "Output as JSON").option("--tenant-id <id>", "Tenant id for local SQLite authority").action(async (outboxId, opts) => {
|
|
91788
|
+
try {
|
|
91789
|
+
const remote = getTodosCloudClient();
|
|
91790
|
+
if (remote)
|
|
91791
|
+
await cloudMarkTaskManifestOutboxDelivered(remote, outboxId);
|
|
91792
|
+
else
|
|
91793
|
+
await (await localAuthority(opts.tenantId)).markOutboxDelivered(outboxId);
|
|
91794
|
+
if (jsonRequested(program2, opts)) {
|
|
91795
|
+
output({ delivered: true }, true);
|
|
91796
|
+
return;
|
|
91797
|
+
}
|
|
91798
|
+
console.log(`delivered=${outboxId}`);
|
|
91799
|
+
} catch (error2) {
|
|
91800
|
+
handleError(error2);
|
|
91801
|
+
}
|
|
91802
|
+
});
|
|
91803
|
+
}
|
|
91804
|
+
var init_task_manifest_commands = __esm(() => {
|
|
91805
|
+
init_database();
|
|
91806
|
+
init_task_manifest();
|
|
91807
|
+
init_cloud_router();
|
|
91808
|
+
init_helpers();
|
|
91809
|
+
});
|
|
91810
|
+
|
|
90973
91811
|
// src/lib/cli-help.ts
|
|
90974
91812
|
function optionEntry(option) {
|
|
90975
91813
|
return {
|
|
@@ -91233,7 +92071,7 @@ var exports_help_commands = {};
|
|
|
91233
92071
|
__export(exports_help_commands, {
|
|
91234
92072
|
registerHelpCommands: () => registerHelpCommands
|
|
91235
92073
|
});
|
|
91236
|
-
function
|
|
92074
|
+
function globalOptions12(program2) {
|
|
91237
92075
|
const command = program2;
|
|
91238
92076
|
return command.optsWithGlobals?.() ?? program2.opts();
|
|
91239
92077
|
}
|
|
@@ -91252,7 +92090,7 @@ function registerHelpCommands(program2, route = "local", remoteCapabilities = ne
|
|
|
91252
92090
|
});
|
|
91253
92091
|
program2.command("manual").description("Print the complete local CLI manual").option("--format <format>", "markdown or json", "markdown").option("-j, --json", "Output as JSON").action((opts) => {
|
|
91254
92092
|
try {
|
|
91255
|
-
const globalOpts =
|
|
92093
|
+
const globalOpts = globalOptions12(program2);
|
|
91256
92094
|
const manual = createCliManual(program2, {
|
|
91257
92095
|
isCommandVisible: (command) => isTodosCliCommandVisibleForRoute(command, route, remoteCapabilities),
|
|
91258
92096
|
localOnly: route === "local"
|
|
@@ -91387,6 +92225,7 @@ var [
|
|
|
91387
92225
|
{ registerStorageCommands: registerStorageCommands2 },
|
|
91388
92226
|
{ registerScaleHardeningCommands: registerScaleHardeningCommands2 },
|
|
91389
92227
|
{ registerPrGroupCommands: registerPrGroupCommands2 },
|
|
92228
|
+
{ registerTaskManifestCommands: registerTaskManifestCommands2 },
|
|
91390
92229
|
{ registerHelpCommands: registerHelpCommands2 }
|
|
91391
92230
|
] = await Promise.all([
|
|
91392
92231
|
Promise.resolve().then(() => (init_helpers(), exports_helpers)),
|
|
@@ -91420,6 +92259,7 @@ var [
|
|
|
91420
92259
|
Promise.resolve().then(() => (init_storage_commands(), exports_storage_commands)),
|
|
91421
92260
|
Promise.resolve().then(() => (init_scale_hardening_commands(), exports_scale_hardening_commands)),
|
|
91422
92261
|
Promise.resolve().then(() => (init_pr_group_commands(), exports_pr_group_commands)),
|
|
92262
|
+
Promise.resolve().then(() => (init_task_manifest_commands(), exports_task_manifest_commands)),
|
|
91423
92263
|
Promise.resolve().then(() => (init_help_commands(), exports_help_commands))
|
|
91424
92264
|
]);
|
|
91425
92265
|
registerTaskCommands2(program2);
|
|
@@ -91452,6 +92292,7 @@ registerLocalBackupCommands2(program2);
|
|
|
91452
92292
|
registerStorageCommands2(program2);
|
|
91453
92293
|
registerScaleHardeningCommands2(program2);
|
|
91454
92294
|
registerPrGroupCommands2(program2);
|
|
92295
|
+
registerTaskManifestCommands2(program2);
|
|
91455
92296
|
await registerOptionalEventsCommands(program2);
|
|
91456
92297
|
registerHelpCommands2(program2, authority.route, remoteCommandCapabilities);
|
|
91457
92298
|
applyTodosCliHelpVisibility(program2, authority.route, remoteCommandCapabilities);
|