@hasna/todos 0.15.7 → 0.15.9
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-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2300 -1123
- package/dist/contracts.js +35 -1
- package/dist/db/task-lists.d.ts +5 -0
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/index.js +489 -35
- package/dist/lib/assignee-validation.d.ts +44 -0
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/project-task-list-ensure.d.ts +19 -0
- package/dist/lib/project-task-list-ensure.d.ts.map +1 -0
- package/dist/mcp/index.js +1085 -5
- package/dist/mcp.js +1 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration.js +491 -35
- package/dist/registry.js +35 -1
- 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 +21 -0
- package/dist/sdk/v1.generated.d.ts +43 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1179 -99
- package/dist/server/openapi.d.ts +311 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +11 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage.js +36 -1
- package/dist/types/index.d.ts +29 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -11646,6 +11646,40 @@ function deleteTaskList(id, db) {
|
|
|
11646
11646
|
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
11647
11647
|
})();
|
|
11648
11648
|
}
|
|
11649
|
+
function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
|
|
11650
|
+
const d = db || getDatabase();
|
|
11651
|
+
return d.transaction(() => {
|
|
11652
|
+
const current = getTaskList(id, d);
|
|
11653
|
+
if (!current) {
|
|
11654
|
+
return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
|
|
11655
|
+
}
|
|
11656
|
+
const changed = current.project_id !== expected.project_id || current.slug !== expected.slug || current.name !== expected.name || current.description !== expected.description || current.updated_at !== expected.updated_at || JSON.stringify(current.metadata) !== JSON.stringify(expected.metadata);
|
|
11657
|
+
if (changed) {
|
|
11658
|
+
return { status: "changed", task_dependents: 0, plan_dependents: 0 };
|
|
11659
|
+
}
|
|
11660
|
+
const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
|
|
11661
|
+
const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
|
|
11662
|
+
if (taskDependents > 0 || planDependents > 0) {
|
|
11663
|
+
return {
|
|
11664
|
+
status: "has_dependents",
|
|
11665
|
+
task_dependents: taskDependents,
|
|
11666
|
+
plan_dependents: planDependents
|
|
11667
|
+
};
|
|
11668
|
+
}
|
|
11669
|
+
recordStorageTombstone({
|
|
11670
|
+
object_type: "task_lists",
|
|
11671
|
+
object_id: id,
|
|
11672
|
+
payload: current
|
|
11673
|
+
}, d);
|
|
11674
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
11675
|
+
const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
11676
|
+
return {
|
|
11677
|
+
status: deleted ? "deleted" : "not_found",
|
|
11678
|
+
task_dependents: 0,
|
|
11679
|
+
plan_dependents: 0
|
|
11680
|
+
};
|
|
11681
|
+
})();
|
|
11682
|
+
}
|
|
11649
11683
|
function ensureTaskList(name, slug, projectId, db) {
|
|
11650
11684
|
const d = db || getDatabase();
|
|
11651
11685
|
const existing = getTaskListBySlug(slug, projectId, d);
|
|
@@ -20769,12 +20803,16 @@ function protectRemoteClient(client) {
|
|
|
20769
20803
|
function remoteAuthorityBase(client) {
|
|
20770
20804
|
return client.baseUrl.replace(/\/v1\/?$/, "");
|
|
20771
20805
|
}
|
|
20772
|
-
async function requiredRemoteRoute(client, route, request) {
|
|
20806
|
+
async function requiredRemoteRoute(client, route, request, recognized404Codes = []) {
|
|
20773
20807
|
try {
|
|
20774
20808
|
return await request();
|
|
20775
20809
|
} catch (error) {
|
|
20776
20810
|
const status = error && typeof error === "object" ? error.status : undefined;
|
|
20777
20811
|
if (status === 404) {
|
|
20812
|
+
const body = error && typeof error === "object" ? error.body : undefined;
|
|
20813
|
+
const code = body && typeof body === "object" && !Array.isArray(body) ? body.code : undefined;
|
|
20814
|
+
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
20815
|
+
throw error;
|
|
20778
20816
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} does not expose ${route}; ` + "deploy the @hasna/todos /v1 server contract before retrying; local SQLite fallback is disabled", { cause: error });
|
|
20779
20817
|
}
|
|
20780
20818
|
throw error;
|
|
@@ -35176,7 +35214,7 @@ var package_default;
|
|
|
35176
35214
|
var init_package = __esm(() => {
|
|
35177
35215
|
package_default = {
|
|
35178
35216
|
name: "@hasna/todos",
|
|
35179
|
-
version: "0.15.
|
|
35217
|
+
version: "0.15.9",
|
|
35180
35218
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35181
35219
|
type: "module",
|
|
35182
35220
|
main: "dist/index.js",
|
|
@@ -45071,7 +45109,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
45071
45109
|
getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
|
|
45072
45110
|
list: (projectId) => listTaskLists(projectId, database()),
|
|
45073
45111
|
update: (id, input) => updateTaskList(id, input, database()),
|
|
45074
|
-
delete: (id) => deleteTaskList(id, database())
|
|
45112
|
+
delete: (id) => deleteTaskList(id, database()),
|
|
45113
|
+
deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
|
|
45075
45114
|
},
|
|
45076
45115
|
templates: {
|
|
45077
45116
|
create: (input) => createTemplate(input, database()),
|
|
@@ -48377,8 +48416,592 @@ var init_postgres2 = __esm(() => {
|
|
|
48377
48416
|
});
|
|
48378
48417
|
|
|
48379
48418
|
// src/project-registration/sqlite.ts
|
|
48380
|
-
|
|
48419
|
+
function sameSqliteValue(left, right) {
|
|
48420
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
48421
|
+
}
|
|
48422
|
+
function taskListFromRow(row) {
|
|
48423
|
+
return {
|
|
48424
|
+
...row,
|
|
48425
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
48426
|
+
};
|
|
48427
|
+
}
|
|
48428
|
+
function selectProject(db, id) {
|
|
48429
|
+
return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
|
|
48430
|
+
}
|
|
48431
|
+
function selectTaskList(db, id) {
|
|
48432
|
+
const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
|
|
48433
|
+
return row ? taskListFromRow(row) : null;
|
|
48434
|
+
}
|
|
48435
|
+
function selectProjectConflict(db, path, taskListSlug) {
|
|
48436
|
+
return db.query(`
|
|
48437
|
+
SELECT * FROM projects
|
|
48438
|
+
WHERE path = ? OR task_list_id = ?
|
|
48439
|
+
ORDER BY created_at ASC, id ASC
|
|
48440
|
+
LIMIT 1
|
|
48441
|
+
`).get(path, taskListSlug);
|
|
48442
|
+
}
|
|
48443
|
+
function selectTaskListConflict(db, projectId, slug) {
|
|
48444
|
+
const row = db.query(`
|
|
48445
|
+
SELECT * FROM task_lists
|
|
48446
|
+
WHERE project_id = ? AND slug = ?
|
|
48447
|
+
LIMIT 1
|
|
48448
|
+
`).get(projectId, slug);
|
|
48449
|
+
return row ? taskListFromRow(row) : null;
|
|
48450
|
+
}
|
|
48451
|
+
function quoteSqliteIdentifier(value) {
|
|
48452
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
48453
|
+
}
|
|
48454
|
+
function hasSqliteDependents(db, resourceKind, targetId) {
|
|
48455
|
+
const targetTable = resourceKind === "project" ? "projects" : "task_lists";
|
|
48456
|
+
const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
|
|
48457
|
+
const tables = db.query(`
|
|
48458
|
+
SELECT name FROM sqlite_schema
|
|
48459
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
48460
|
+
ORDER BY name
|
|
48461
|
+
`).all();
|
|
48462
|
+
for (const { name: tableName } of tables) {
|
|
48463
|
+
const quotedTable = quoteSqliteIdentifier(tableName);
|
|
48464
|
+
const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
|
|
48465
|
+
const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
|
|
48466
|
+
const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
|
|
48467
|
+
for (const columnName of referenceColumns) {
|
|
48468
|
+
const row = db.query(`
|
|
48469
|
+
SELECT 1 AS found
|
|
48470
|
+
FROM ${quotedTable}
|
|
48471
|
+
WHERE ${quoteSqliteIdentifier(columnName)} = ?
|
|
48472
|
+
LIMIT 1
|
|
48473
|
+
`).get(targetId);
|
|
48474
|
+
if (row)
|
|
48475
|
+
return true;
|
|
48476
|
+
}
|
|
48477
|
+
}
|
|
48478
|
+
return false;
|
|
48479
|
+
}
|
|
48480
|
+
function receiptFromRow2(row) {
|
|
48481
|
+
return {
|
|
48482
|
+
...row,
|
|
48483
|
+
authority: "todos",
|
|
48484
|
+
created_by_operation: Number(row["created_by_operation"]) === 1
|
|
48485
|
+
};
|
|
48486
|
+
}
|
|
48487
|
+
function bindingFromRow2(row) {
|
|
48488
|
+
return row;
|
|
48489
|
+
}
|
|
48490
|
+
|
|
48491
|
+
class SqliteTodosProjectRegistrationTransaction {
|
|
48492
|
+
db;
|
|
48493
|
+
storage;
|
|
48494
|
+
constructor(db) {
|
|
48495
|
+
this.db = db;
|
|
48496
|
+
this.storage = createLocalSqliteTodosStorageAdapter({ db });
|
|
48497
|
+
}
|
|
48498
|
+
async lockStep(_identity) {}
|
|
48499
|
+
async getReceiptForLookup(identity) {
|
|
48500
|
+
const row = this.db.query(`
|
|
48501
|
+
SELECT * FROM todos_project_registration_receipts
|
|
48502
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48503
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
48504
|
+
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
48505
|
+
ORDER BY CASE outcome
|
|
48506
|
+
WHEN 'terminal_nonacceptance' THEN 0
|
|
48507
|
+
WHEN 'duplicate_of_accepted' THEN 1
|
|
48508
|
+
ELSE 2
|
|
48509
|
+
END, created_at DESC, receipt_id DESC
|
|
48510
|
+
LIMIT 1
|
|
48511
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
48512
|
+
return row ? receiptFromRow2(row) : null;
|
|
48513
|
+
}
|
|
48514
|
+
async getReceiptById(receiptId) {
|
|
48515
|
+
const row = this.db.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = ? LIMIT 1").get(receiptId);
|
|
48516
|
+
return row ? receiptFromRow2(row) : null;
|
|
48517
|
+
}
|
|
48518
|
+
async getAcceptedReceiptForStep(identity) {
|
|
48519
|
+
const row = this.db.query(`
|
|
48520
|
+
SELECT * FROM todos_project_registration_receipts
|
|
48521
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48522
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
48523
|
+
AND direction = ? AND outcome = 'accepted'
|
|
48524
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
48525
|
+
LIMIT 1
|
|
48526
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction);
|
|
48527
|
+
return row ? receiptFromRow2(row) : null;
|
|
48528
|
+
}
|
|
48529
|
+
async insertReceipt(receipt) {
|
|
48530
|
+
const result = this.db.query(`
|
|
48531
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
48532
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
48533
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
48534
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
48535
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
48536
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
48537
|
+
created_by_operation, created_at
|
|
48538
|
+
) VALUES (
|
|
48539
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
48540
|
+
)
|
|
48541
|
+
`).run(receipt.receipt_id, receipt.authority, receipt.route, receipt.package_version, receipt.authority_id, receipt.tenant_id, receipt.corpus_id, receipt.operation_id, receipt.step_id, receipt.resource_kind, receipt.direction, receipt.target_selector, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.normalized_call_digest, receipt.outcome, receipt.reason, receipt.target_id, receipt.result_revision, receipt.result_digest, receipt.duplicate_of_receipt_id, receipt.accepted_receipt_id, receipt.created_by_operation ? 1 : 0, receipt.created_at);
|
|
48542
|
+
return result.changes === 1;
|
|
48543
|
+
}
|
|
48544
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
48545
|
+
const row = this.db.query(`
|
|
48546
|
+
SELECT * FROM todos_project_registration_bindings
|
|
48547
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48548
|
+
AND resource_kind = ? AND target_selector = ?
|
|
48549
|
+
LIMIT 1
|
|
48550
|
+
`).get(scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48551
|
+
return row ? bindingFromRow2(row) : null;
|
|
48552
|
+
}
|
|
48553
|
+
async claimBinding(binding) {
|
|
48554
|
+
const result = this.db.query(`
|
|
48555
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
48556
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
48557
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
48558
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
48559
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
48560
|
+
created_at, updated_at
|
|
48561
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
48562
|
+
`).run(binding.authority_id, binding.tenant_id, binding.corpus_id, binding.resource_kind, binding.target_selector, binding.operation_id, binding.step_id, binding.direction, binding.idempotency_key, binding.request_digest, binding.precondition_digest, binding.normalized_call_digest, binding.state, binding.target_id, binding.accepted_receipt_id, binding.result_revision, binding.result_digest, binding.removed_receipt_id, binding.created_at, binding.updated_at);
|
|
48563
|
+
return result.changes === 1;
|
|
48564
|
+
}
|
|
48565
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
48566
|
+
const result = this.db.query(`
|
|
48567
|
+
UPDATE todos_project_registration_bindings
|
|
48568
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
48569
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
48570
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48571
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48572
|
+
`).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48573
|
+
if (result.changes !== 1) {
|
|
48574
|
+
throw new Error("Todos project registration binding was not pending at acceptance");
|
|
48575
|
+
}
|
|
48576
|
+
}
|
|
48577
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
48578
|
+
this.db.query(`
|
|
48579
|
+
UPDATE todos_project_registration_bindings
|
|
48580
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
48581
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48582
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48583
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48584
|
+
}
|
|
48585
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
48586
|
+
const result = this.db.query(`
|
|
48587
|
+
UPDATE todos_project_registration_bindings
|
|
48588
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
48589
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48590
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
48591
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48592
|
+
if (result.changes !== 1) {
|
|
48593
|
+
throw new Error("Todos project registration binding was not accepted at removal");
|
|
48594
|
+
}
|
|
48595
|
+
}
|
|
48596
|
+
async findProjectConflict(path, taskListSlug) {
|
|
48597
|
+
const row = this.db.query(`
|
|
48598
|
+
SELECT * FROM projects
|
|
48599
|
+
WHERE path = ? OR task_list_id = ?
|
|
48600
|
+
ORDER BY created_at ASC, id ASC
|
|
48601
|
+
LIMIT 1
|
|
48602
|
+
`).get(path, taskListSlug);
|
|
48603
|
+
return row ?? null;
|
|
48604
|
+
}
|
|
48605
|
+
async findTaskListConflict(projectId, slug) {
|
|
48606
|
+
return await this.storage.taskLists.getBySlug(slug, projectId);
|
|
48607
|
+
}
|
|
48608
|
+
async createProject(input) {
|
|
48609
|
+
return await this.storage.projects.create(input);
|
|
48610
|
+
}
|
|
48611
|
+
async createTaskList(input) {
|
|
48612
|
+
return await this.storage.taskLists.create(input);
|
|
48613
|
+
}
|
|
48614
|
+
async getProject(id) {
|
|
48615
|
+
return await this.storage.projects.get(id);
|
|
48616
|
+
}
|
|
48617
|
+
async getTaskList(id) {
|
|
48618
|
+
return await this.storage.taskLists.get(id);
|
|
48619
|
+
}
|
|
48620
|
+
async lockCompensationWrites() {}
|
|
48621
|
+
async hasDependents(resourceKind, targetId) {
|
|
48622
|
+
return hasSqliteDependents(this.db, resourceKind, targetId);
|
|
48623
|
+
}
|
|
48624
|
+
async deleteProject(id) {
|
|
48625
|
+
return await this.storage.projects.delete(id);
|
|
48626
|
+
}
|
|
48627
|
+
async deleteTaskList(id) {
|
|
48628
|
+
return await this.storage.taskLists.delete(id);
|
|
48629
|
+
}
|
|
48630
|
+
}
|
|
48631
|
+
|
|
48632
|
+
class StagedSqliteTodosProjectRegistrationTransaction {
|
|
48633
|
+
db;
|
|
48634
|
+
direct;
|
|
48635
|
+
validators = [];
|
|
48636
|
+
mutations = [];
|
|
48637
|
+
receipts = new Map;
|
|
48638
|
+
bindings = new Map;
|
|
48639
|
+
projects = new Map;
|
|
48640
|
+
taskLists = new Map;
|
|
48641
|
+
constructor(db) {
|
|
48642
|
+
this.db = db;
|
|
48643
|
+
this.direct = new SqliteTodosProjectRegistrationTransaction(db);
|
|
48644
|
+
}
|
|
48645
|
+
commit() {
|
|
48646
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
48647
|
+
try {
|
|
48648
|
+
for (const validate of this.validators) {
|
|
48649
|
+
if (!validate()) {
|
|
48650
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
|
|
48651
|
+
}
|
|
48652
|
+
}
|
|
48653
|
+
for (const mutate of this.mutations)
|
|
48654
|
+
mutate();
|
|
48655
|
+
this.db.exec("COMMIT");
|
|
48656
|
+
} catch (error) {
|
|
48657
|
+
try {
|
|
48658
|
+
this.db.exec("ROLLBACK");
|
|
48659
|
+
} catch {}
|
|
48660
|
+
throw error;
|
|
48661
|
+
}
|
|
48662
|
+
}
|
|
48663
|
+
async lockStep(_identity) {}
|
|
48664
|
+
async getReceiptForLookup(identity) {
|
|
48665
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
48666
|
+
const stored = await this.direct.getReceiptForLookup(identity);
|
|
48667
|
+
if (stored)
|
|
48668
|
+
staged.push(stored);
|
|
48669
|
+
const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
|
|
48670
|
+
return staged.sort((left, right) => outcomeRank(left) - outcomeRank(right) || right.created_at.localeCompare(left.created_at) || right.receipt_id.localeCompare(left.receipt_id))[0] ?? null;
|
|
48671
|
+
}
|
|
48672
|
+
async getReceiptById(receiptId) {
|
|
48673
|
+
return this.receipts.get(receiptId) ?? this.direct.getReceiptById(receiptId);
|
|
48674
|
+
}
|
|
48675
|
+
async getAcceptedReceiptForStep(identity) {
|
|
48676
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.outcome === "accepted");
|
|
48677
|
+
const stored = await this.direct.getAcceptedReceiptForStep(identity);
|
|
48678
|
+
if (stored)
|
|
48679
|
+
staged.push(stored);
|
|
48680
|
+
return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
|
|
48681
|
+
}
|
|
48682
|
+
async insertReceipt(receipt) {
|
|
48683
|
+
if (this.receipts.has(receipt.receipt_id))
|
|
48684
|
+
return false;
|
|
48685
|
+
if (await this.direct.getReceiptById(receipt.receipt_id))
|
|
48686
|
+
return false;
|
|
48687
|
+
const planned = { ...receipt };
|
|
48688
|
+
this.receipts.set(planned.receipt_id, planned);
|
|
48689
|
+
this.mutations.push(() => {
|
|
48690
|
+
try {
|
|
48691
|
+
const result = this.db.query(`
|
|
48692
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
48693
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
48694
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
48695
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
48696
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
48697
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
48698
|
+
created_by_operation, created_at
|
|
48699
|
+
) VALUES (
|
|
48700
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
48701
|
+
)
|
|
48702
|
+
`).run(planned.receipt_id, planned.authority, planned.route, planned.package_version, planned.authority_id, planned.tenant_id, planned.corpus_id, planned.operation_id, planned.step_id, planned.resource_kind, planned.direction, planned.target_selector, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.outcome, planned.reason, planned.target_id, planned.result_revision, planned.result_digest, planned.duplicate_of_receipt_id, planned.accepted_receipt_id, planned.created_by_operation ? 1 : 0, planned.created_at);
|
|
48703
|
+
if (result.changes !== 1) {
|
|
48704
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
|
|
48705
|
+
}
|
|
48706
|
+
} catch (error) {
|
|
48707
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48708
|
+
throw error;
|
|
48709
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
|
|
48710
|
+
}
|
|
48711
|
+
});
|
|
48712
|
+
return true;
|
|
48713
|
+
}
|
|
48714
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
48715
|
+
const key = this.bindingKey(scope, resourceKind, targetSelector);
|
|
48716
|
+
return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
|
|
48717
|
+
}
|
|
48718
|
+
async claimBinding(binding) {
|
|
48719
|
+
const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
|
|
48720
|
+
if (this.bindings.has(key))
|
|
48721
|
+
return false;
|
|
48722
|
+
if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
|
|
48723
|
+
return false;
|
|
48724
|
+
}
|
|
48725
|
+
const planned = { ...binding };
|
|
48726
|
+
this.bindings.set(key, planned);
|
|
48727
|
+
this.mutations.push(() => {
|
|
48728
|
+
try {
|
|
48729
|
+
const result = this.db.query(`
|
|
48730
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
48731
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
48732
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
48733
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
48734
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
48735
|
+
created_at, updated_at
|
|
48736
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
48737
|
+
`).run(planned.authority_id, planned.tenant_id, planned.corpus_id, planned.resource_kind, planned.target_selector, planned.operation_id, planned.step_id, planned.direction, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.state, planned.target_id, planned.accepted_receipt_id, planned.result_revision, planned.result_digest, planned.removed_receipt_id, planned.created_at, planned.updated_at);
|
|
48738
|
+
if (result.changes !== 1) {
|
|
48739
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
|
|
48740
|
+
}
|
|
48741
|
+
} catch (error) {
|
|
48742
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48743
|
+
throw error;
|
|
48744
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
|
|
48745
|
+
}
|
|
48746
|
+
});
|
|
48747
|
+
return true;
|
|
48748
|
+
}
|
|
48749
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
48750
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
48751
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
48752
|
+
...binding,
|
|
48753
|
+
state: "accepted",
|
|
48754
|
+
target_id: update.target_id,
|
|
48755
|
+
accepted_receipt_id: update.accepted_receipt_id,
|
|
48756
|
+
result_revision: update.result_revision,
|
|
48757
|
+
result_digest: update.result_digest,
|
|
48758
|
+
updated_at: update.updated_at
|
|
48759
|
+
});
|
|
48760
|
+
this.mutations.push(() => {
|
|
48761
|
+
const result = this.db.query(`
|
|
48762
|
+
UPDATE todos_project_registration_bindings
|
|
48763
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
48764
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
48765
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48766
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48767
|
+
`).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48768
|
+
if (result.changes !== 1) {
|
|
48769
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
48770
|
+
}
|
|
48771
|
+
});
|
|
48772
|
+
}
|
|
48773
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
48774
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
48775
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
48776
|
+
...binding,
|
|
48777
|
+
state: "terminal_nonacceptance",
|
|
48778
|
+
updated_at: updatedAt
|
|
48779
|
+
});
|
|
48780
|
+
this.mutations.push(() => {
|
|
48781
|
+
const result = this.db.query(`
|
|
48782
|
+
UPDATE todos_project_registration_bindings
|
|
48783
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
48784
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48785
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48786
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48787
|
+
if (result.changes !== 1) {
|
|
48788
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
48789
|
+
}
|
|
48790
|
+
});
|
|
48791
|
+
}
|
|
48792
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
48793
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
|
|
48794
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
48795
|
+
...binding,
|
|
48796
|
+
state: "removed",
|
|
48797
|
+
removed_receipt_id: removedReceiptId,
|
|
48798
|
+
updated_at: updatedAt
|
|
48799
|
+
});
|
|
48800
|
+
this.mutations.push(() => {
|
|
48801
|
+
const result = this.db.query(`
|
|
48802
|
+
UPDATE todos_project_registration_bindings
|
|
48803
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
48804
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48805
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
48806
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48807
|
+
if (result.changes !== 1) {
|
|
48808
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
|
|
48809
|
+
}
|
|
48810
|
+
});
|
|
48811
|
+
}
|
|
48812
|
+
async findProjectConflict(path, taskListSlug) {
|
|
48813
|
+
const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
|
|
48814
|
+
if (planned)
|
|
48815
|
+
return planned;
|
|
48816
|
+
const observed = selectProjectConflict(this.db, path, taskListSlug);
|
|
48817
|
+
this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
|
|
48818
|
+
return observed;
|
|
48819
|
+
}
|
|
48820
|
+
async findTaskListConflict(projectId, slug) {
|
|
48821
|
+
const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
|
|
48822
|
+
if (planned)
|
|
48823
|
+
return planned;
|
|
48824
|
+
const observed = selectTaskListConflict(this.db, projectId, slug);
|
|
48825
|
+
this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
|
|
48826
|
+
return observed;
|
|
48827
|
+
}
|
|
48828
|
+
async createProject(input) {
|
|
48829
|
+
const derivedSlug = normalizeSlug(input.name);
|
|
48830
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
|
|
48831
|
+
if (!derivedSlug || !taskListId) {
|
|
48832
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
48833
|
+
}
|
|
48834
|
+
const project = {
|
|
48835
|
+
id: uuid(),
|
|
48836
|
+
name: input.name,
|
|
48837
|
+
path: input.path,
|
|
48838
|
+
description: input.description || null,
|
|
48839
|
+
task_list_id: taskListId,
|
|
48840
|
+
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
48841
|
+
task_counter: 0,
|
|
48842
|
+
created_at: now(),
|
|
48843
|
+
updated_at: now(),
|
|
48844
|
+
machine_id: currentStorageMachineId(this.db)
|
|
48845
|
+
};
|
|
48846
|
+
project.updated_at = project.created_at;
|
|
48847
|
+
this.projects.set(project.id, project);
|
|
48848
|
+
this.mutations.push(() => {
|
|
48849
|
+
try {
|
|
48850
|
+
const result = this.db.run(`INSERT INTO projects (
|
|
48851
|
+
id, name, path, description, task_list_id, task_prefix,
|
|
48852
|
+
task_counter, created_at, updated_at, machine_id
|
|
48853
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
48854
|
+
project.id,
|
|
48855
|
+
project.name,
|
|
48856
|
+
project.path,
|
|
48857
|
+
project.description,
|
|
48858
|
+
project.task_list_id,
|
|
48859
|
+
project.task_prefix,
|
|
48860
|
+
project.created_at,
|
|
48861
|
+
project.updated_at,
|
|
48862
|
+
project.machine_id ?? null
|
|
48863
|
+
]);
|
|
48864
|
+
if (result.changes < 1) {
|
|
48865
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
|
|
48866
|
+
}
|
|
48867
|
+
} catch (error) {
|
|
48868
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48869
|
+
throw error;
|
|
48870
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
|
|
48871
|
+
}
|
|
48872
|
+
});
|
|
48873
|
+
return project;
|
|
48874
|
+
}
|
|
48875
|
+
async createTaskList(input) {
|
|
48876
|
+
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
48877
|
+
if (!slug)
|
|
48878
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
48879
|
+
const taskList = {
|
|
48880
|
+
id: uuid(),
|
|
48881
|
+
project_id: input.project_id || null,
|
|
48882
|
+
slug,
|
|
48883
|
+
name: input.name,
|
|
48884
|
+
description: input.description || null,
|
|
48885
|
+
metadata: input.metadata ?? {},
|
|
48886
|
+
created_at: now(),
|
|
48887
|
+
updated_at: now(),
|
|
48888
|
+
machine_id: currentStorageMachineId(this.db)
|
|
48889
|
+
};
|
|
48890
|
+
taskList.updated_at = taskList.created_at;
|
|
48891
|
+
this.taskLists.set(taskList.id, taskList);
|
|
48892
|
+
this.mutations.push(() => {
|
|
48893
|
+
try {
|
|
48894
|
+
const result = this.db.run(`INSERT INTO task_lists (
|
|
48895
|
+
id, project_id, slug, name, description, metadata,
|
|
48896
|
+
created_at, updated_at, machine_id
|
|
48897
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
48898
|
+
taskList.id,
|
|
48899
|
+
taskList.project_id,
|
|
48900
|
+
taskList.slug,
|
|
48901
|
+
taskList.name,
|
|
48902
|
+
taskList.description,
|
|
48903
|
+
JSON.stringify(taskList.metadata),
|
|
48904
|
+
taskList.created_at,
|
|
48905
|
+
taskList.updated_at,
|
|
48906
|
+
taskList.machine_id ?? null
|
|
48907
|
+
]);
|
|
48908
|
+
if (result.changes < 1) {
|
|
48909
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
|
|
48910
|
+
}
|
|
48911
|
+
} catch (error) {
|
|
48912
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48913
|
+
throw error;
|
|
48914
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
|
|
48915
|
+
}
|
|
48916
|
+
});
|
|
48917
|
+
return taskList;
|
|
48918
|
+
}
|
|
48919
|
+
async getProject(id) {
|
|
48920
|
+
if (this.projects.has(id))
|
|
48921
|
+
return this.projects.get(id) ?? null;
|
|
48922
|
+
const observed = selectProject(this.db, id);
|
|
48923
|
+
this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
|
|
48924
|
+
return observed;
|
|
48925
|
+
}
|
|
48926
|
+
async getTaskList(id) {
|
|
48927
|
+
if (this.taskLists.has(id))
|
|
48928
|
+
return this.taskLists.get(id) ?? null;
|
|
48929
|
+
const observed = selectTaskList(this.db, id);
|
|
48930
|
+
this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
|
|
48931
|
+
return observed;
|
|
48932
|
+
}
|
|
48933
|
+
async lockCompensationWrites() {}
|
|
48934
|
+
async hasDependents(resourceKind, targetId) {
|
|
48935
|
+
const observed = hasSqliteDependents(this.db, resourceKind, targetId);
|
|
48936
|
+
this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
|
|
48937
|
+
return observed;
|
|
48938
|
+
}
|
|
48939
|
+
async deleteProject(id) {
|
|
48940
|
+
const project = await this.getProject(id);
|
|
48941
|
+
if (!project)
|
|
48942
|
+
return false;
|
|
48943
|
+
this.projects.set(id, null);
|
|
48944
|
+
this.mutations.push(() => {
|
|
48945
|
+
recordStorageTombstone({
|
|
48946
|
+
object_type: "projects",
|
|
48947
|
+
object_id: id,
|
|
48948
|
+
payload: project
|
|
48949
|
+
}, this.db);
|
|
48950
|
+
if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
|
|
48951
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
|
|
48952
|
+
}
|
|
48953
|
+
});
|
|
48954
|
+
return true;
|
|
48955
|
+
}
|
|
48956
|
+
async deleteTaskList(id) {
|
|
48957
|
+
const taskList = await this.getTaskList(id);
|
|
48958
|
+
if (!taskList)
|
|
48959
|
+
return false;
|
|
48960
|
+
this.taskLists.set(id, null);
|
|
48961
|
+
this.mutations.push(() => {
|
|
48962
|
+
recordStorageTombstone({
|
|
48963
|
+
object_type: "task_lists",
|
|
48964
|
+
object_id: id,
|
|
48965
|
+
payload: taskList
|
|
48966
|
+
}, this.db);
|
|
48967
|
+
if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
|
|
48968
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
|
|
48969
|
+
}
|
|
48970
|
+
});
|
|
48971
|
+
return true;
|
|
48972
|
+
}
|
|
48973
|
+
bindingKey(scope, resourceKind, targetSelector) {
|
|
48974
|
+
return JSON.stringify([
|
|
48975
|
+
scope.authority_id,
|
|
48976
|
+
scope.tenant_id,
|
|
48977
|
+
scope.corpus_id,
|
|
48978
|
+
resourceKind,
|
|
48979
|
+
targetSelector
|
|
48980
|
+
]);
|
|
48981
|
+
}
|
|
48982
|
+
async requireBinding(scope, resourceKind, targetSelector, state) {
|
|
48983
|
+
const binding = await this.getBinding(scope, resourceKind, targetSelector);
|
|
48984
|
+
if (!binding || binding.state !== state) {
|
|
48985
|
+
throw new Error(`Todos project registration binding was not ${state}`);
|
|
48986
|
+
}
|
|
48987
|
+
return binding;
|
|
48988
|
+
}
|
|
48989
|
+
availableProjectPrefix(name) {
|
|
48990
|
+
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
48991
|
+
const prefix = words.length >= 3 ? words.slice(0, 3).map((word) => word[0].toUpperCase()).join("") : words.length === 2 ? (words[0].slice(0, 2) + words[1][0]).toUpperCase() : words[0].slice(0, 3).toUpperCase();
|
|
48992
|
+
let candidate = prefix;
|
|
48993
|
+
let suffix = 1;
|
|
48994
|
+
while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
|
|
48995
|
+
suffix += 1;
|
|
48996
|
+
candidate = `${prefix}${suffix}`;
|
|
48997
|
+
}
|
|
48998
|
+
return candidate;
|
|
48999
|
+
}
|
|
49000
|
+
}
|
|
49001
|
+
var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS, SqliteRegistrationOptimisticConflict;
|
|
48381
49002
|
var init_sqlite = __esm(() => {
|
|
49003
|
+
init_database();
|
|
49004
|
+
init_storage_tombstones();
|
|
48382
49005
|
init_local_sqlite();
|
|
48383
49006
|
sqliteTransactionTails = new WeakMap;
|
|
48384
49007
|
PROJECT_REFERENCE_COLUMNS = new Set([
|
|
@@ -48388,6 +49011,12 @@ var init_sqlite = __esm(() => {
|
|
|
48388
49011
|
"external_project_id"
|
|
48389
49012
|
]);
|
|
48390
49013
|
TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
|
|
49014
|
+
SqliteRegistrationOptimisticConflict = class SqliteRegistrationOptimisticConflict extends Error {
|
|
49015
|
+
constructor(message, options = {}) {
|
|
49016
|
+
super(message, options);
|
|
49017
|
+
this.name = "SqliteRegistrationOptimisticConflict";
|
|
49018
|
+
}
|
|
49019
|
+
};
|
|
48391
49020
|
});
|
|
48392
49021
|
|
|
48393
49022
|
// src/project-registration/authority.ts
|
|
@@ -50892,6 +51521,9 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
50892
51521
|
Task: taskSchema,
|
|
50893
51522
|
Project: projectSchema,
|
|
50894
51523
|
TaskList: taskListSchema,
|
|
51524
|
+
ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
|
|
51525
|
+
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
51526
|
+
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
50895
51527
|
TaskComment: taskCommentSchema,
|
|
50896
51528
|
Plan: planSchema,
|
|
50897
51529
|
Template: templateSchema,
|
|
@@ -50969,6 +51601,29 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
50969
51601
|
name: { type: "string", minLength: 1 }
|
|
50970
51602
|
}
|
|
50971
51603
|
},
|
|
51604
|
+
ProjectTaskListEnsureApplyInput: {
|
|
51605
|
+
type: "object",
|
|
51606
|
+
additionalProperties: false,
|
|
51607
|
+
required: ["expected_project_revision"],
|
|
51608
|
+
properties: {
|
|
51609
|
+
expected_project_revision: { type: "string", minLength: 1 },
|
|
51610
|
+
idempotency_key: {
|
|
51611
|
+
type: "string",
|
|
51612
|
+
minLength: 8,
|
|
51613
|
+
maxLength: 128,
|
|
51614
|
+
pattern: "^[A-Za-z0-9._:-]+$"
|
|
51615
|
+
}
|
|
51616
|
+
}
|
|
51617
|
+
},
|
|
51618
|
+
ProjectTaskListRollbackInput: {
|
|
51619
|
+
type: "object",
|
|
51620
|
+
additionalProperties: false,
|
|
51621
|
+
required: ["receipt_id", "expected_task_list_revision"],
|
|
51622
|
+
properties: {
|
|
51623
|
+
receipt_id: { type: "string", minLength: 1 },
|
|
51624
|
+
expected_task_list_revision: { type: "string", minLength: 1 }
|
|
51625
|
+
}
|
|
51626
|
+
},
|
|
50972
51627
|
ErrorResponse: {
|
|
50973
51628
|
type: "object",
|
|
50974
51629
|
required: ["error"],
|
|
@@ -52071,6 +52726,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
52071
52726
|
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
52072
52727
|
}
|
|
52073
52728
|
},
|
|
52729
|
+
"/v1/projects/{id}/task-list/ensure": {
|
|
52730
|
+
get: {
|
|
52731
|
+
operationId: "planProjectTaskListEnsure",
|
|
52732
|
+
summary: "Plan a non-mutating repair of a project's declared task list",
|
|
52733
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
52734
|
+
responses: {
|
|
52735
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
52736
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52737
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
52738
|
+
}
|
|
52739
|
+
},
|
|
52740
|
+
post: {
|
|
52741
|
+
operationId: "ensureProjectTaskList",
|
|
52742
|
+
summary: "Idempotently create an existing project's declared task list",
|
|
52743
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
52744
|
+
requestBody: {
|
|
52745
|
+
required: true,
|
|
52746
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureApplyInput" } } }
|
|
52747
|
+
},
|
|
52748
|
+
responses: {
|
|
52749
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
52750
|
+
"201": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
52751
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52752
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52753
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
52754
|
+
}
|
|
52755
|
+
}
|
|
52756
|
+
},
|
|
52757
|
+
"/v1/projects/{id}/task-list/rollback": {
|
|
52758
|
+
post: {
|
|
52759
|
+
operationId: "rollbackProjectTaskListEnsure",
|
|
52760
|
+
summary: "Conditionally remove an unchanged task list created by an accepted ensure receipt",
|
|
52761
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
52762
|
+
requestBody: {
|
|
52763
|
+
required: true,
|
|
52764
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackInput" } } }
|
|
52765
|
+
},
|
|
52766
|
+
responses: {
|
|
52767
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackResult" } } } },
|
|
52768
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52769
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52770
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
52771
|
+
}
|
|
52772
|
+
}
|
|
52773
|
+
},
|
|
52074
52774
|
"/v1/projects/{id}/rename": {
|
|
52075
52775
|
post: {
|
|
52076
52776
|
operationId: "renameProject",
|
|
@@ -52352,7 +53052,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
52352
53052
|
}
|
|
52353
53053
|
};
|
|
52354
53054
|
}
|
|
52355
|
-
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
53055
|
+
var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
52356
53056
|
var init_openapi = __esm(() => {
|
|
52357
53057
|
init_package_version();
|
|
52358
53058
|
init_types();
|
|
@@ -52400,6 +53100,80 @@ var init_openapi = __esm(() => {
|
|
|
52400
53100
|
updated_at: { type: "string" }
|
|
52401
53101
|
}
|
|
52402
53102
|
};
|
|
53103
|
+
projectTaskListEnsureReceiptSchema = {
|
|
53104
|
+
type: "object",
|
|
53105
|
+
additionalProperties: false,
|
|
53106
|
+
required: [
|
|
53107
|
+
"schema_version",
|
|
53108
|
+
"receipt_id",
|
|
53109
|
+
"idempotency_key",
|
|
53110
|
+
"project_id",
|
|
53111
|
+
"task_list_id",
|
|
53112
|
+
"slug",
|
|
53113
|
+
"created_by_operation",
|
|
53114
|
+
"result_revision",
|
|
53115
|
+
"result_digest",
|
|
53116
|
+
"rollback_supported",
|
|
53117
|
+
"created_at"
|
|
53118
|
+
],
|
|
53119
|
+
properties: {
|
|
53120
|
+
schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
|
|
53121
|
+
receipt_id: { type: "string" },
|
|
53122
|
+
idempotency_key: { type: "string" },
|
|
53123
|
+
project_id: { type: "string" },
|
|
53124
|
+
task_list_id: { type: "string" },
|
|
53125
|
+
slug: { type: "string" },
|
|
53126
|
+
created_by_operation: { type: "boolean" },
|
|
53127
|
+
result_revision: { type: "string" },
|
|
53128
|
+
result_digest: { type: "string" },
|
|
53129
|
+
rollback_supported: { type: "boolean" },
|
|
53130
|
+
created_at: { type: "string", format: "date-time" }
|
|
53131
|
+
}
|
|
53132
|
+
};
|
|
53133
|
+
projectTaskListEnsureResultSchema = {
|
|
53134
|
+
type: "object",
|
|
53135
|
+
additionalProperties: false,
|
|
53136
|
+
required: ["mode", "action", "project", "task_list", "receipt"],
|
|
53137
|
+
properties: {
|
|
53138
|
+
mode: { type: "string", enum: ["plan", "apply"] },
|
|
53139
|
+
action: { type: "string", enum: ["would_create", "created", "already_present"] },
|
|
53140
|
+
project: { $ref: "#/components/schemas/Project" },
|
|
53141
|
+
task_list: {
|
|
53142
|
+
oneOf: [
|
|
53143
|
+
{ $ref: "#/components/schemas/TaskList" },
|
|
53144
|
+
{ type: "null" }
|
|
53145
|
+
]
|
|
53146
|
+
},
|
|
53147
|
+
receipt: {
|
|
53148
|
+
oneOf: [
|
|
53149
|
+
{ $ref: "#/components/schemas/ProjectTaskListEnsureReceipt" },
|
|
53150
|
+
{ type: "null" }
|
|
53151
|
+
]
|
|
53152
|
+
}
|
|
53153
|
+
}
|
|
53154
|
+
};
|
|
53155
|
+
projectTaskListRollbackResultSchema = {
|
|
53156
|
+
type: "object",
|
|
53157
|
+
additionalProperties: false,
|
|
53158
|
+
required: [
|
|
53159
|
+
"schema_version",
|
|
53160
|
+
"action",
|
|
53161
|
+
"project_id",
|
|
53162
|
+
"task_list_id",
|
|
53163
|
+
"accepted_receipt_id",
|
|
53164
|
+
"rollback_receipt_id",
|
|
53165
|
+
"removed_at"
|
|
53166
|
+
],
|
|
53167
|
+
properties: {
|
|
53168
|
+
schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
|
|
53169
|
+
action: { type: "string", enum: ["removed"] },
|
|
53170
|
+
project_id: { type: "string" },
|
|
53171
|
+
task_list_id: { type: "string" },
|
|
53172
|
+
accepted_receipt_id: { type: "string" },
|
|
53173
|
+
rollback_receipt_id: { type: "string" },
|
|
53174
|
+
removed_at: { type: "string", format: "date-time" }
|
|
53175
|
+
}
|
|
53176
|
+
};
|
|
52403
53177
|
taskCommentSchema = {
|
|
52404
53178
|
type: "object",
|
|
52405
53179
|
required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
|
|
@@ -52633,6 +53407,261 @@ function decodeCommentCursor(value) {
|
|
|
52633
53407
|
}
|
|
52634
53408
|
var MAX_COMMENT_CURSOR_LENGTH = 1024;
|
|
52635
53409
|
|
|
53410
|
+
// src/lib/project-task-list-ensure.ts
|
|
53411
|
+
import { createHash as createHash15 } from "crypto";
|
|
53412
|
+
function canonicalJson(value) {
|
|
53413
|
+
if (value === null || typeof value !== "object")
|
|
53414
|
+
return JSON.stringify(value);
|
|
53415
|
+
if (Array.isArray(value))
|
|
53416
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
53417
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
53418
|
+
}
|
|
53419
|
+
function digest(value) {
|
|
53420
|
+
return createHash15("sha256").update(canonicalJson(value)).digest("hex");
|
|
53421
|
+
}
|
|
53422
|
+
function deriveIdempotencyKey(projectId, slug) {
|
|
53423
|
+
return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
53424
|
+
}
|
|
53425
|
+
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
53426
|
+
const key = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
53427
|
+
if (key.length < 8 || key.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(key)) {
|
|
53428
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID", "idempotency_key must be 8-128 ASCII letters, digits, dots, underscores, colons, or hyphens");
|
|
53429
|
+
}
|
|
53430
|
+
return key;
|
|
53431
|
+
}
|
|
53432
|
+
function receiptId2(projectId, slug, idempotencyKey) {
|
|
53433
|
+
return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
|
|
53434
|
+
}
|
|
53435
|
+
function semanticListDigest(list) {
|
|
53436
|
+
const metadata = { ...list.metadata ?? {} };
|
|
53437
|
+
delete metadata[RECEIPT_METADATA_KEY];
|
|
53438
|
+
return digest({
|
|
53439
|
+
project_id: list.project_id,
|
|
53440
|
+
slug: list.slug,
|
|
53441
|
+
name: list.name,
|
|
53442
|
+
description: list.description,
|
|
53443
|
+
metadata
|
|
53444
|
+
});
|
|
53445
|
+
}
|
|
53446
|
+
function storedMarker(list) {
|
|
53447
|
+
const value = list.metadata?.[RECEIPT_METADATA_KEY];
|
|
53448
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
53449
|
+
return null;
|
|
53450
|
+
const marker = value;
|
|
53451
|
+
if (marker.schema_version !== PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION || typeof marker.receipt_id !== "string" || typeof marker.idempotency_key !== "string" || typeof marker.project_id !== "string" || typeof marker.slug !== "string" || typeof marker.result_digest !== "string" || typeof marker.created_at !== "string")
|
|
53452
|
+
return null;
|
|
53453
|
+
return marker;
|
|
53454
|
+
}
|
|
53455
|
+
function receiptFor(store, project, list, idempotencyKey) {
|
|
53456
|
+
const marker = storedMarker(list);
|
|
53457
|
+
const owned = marker?.project_id === project.id && marker.slug === list.slug;
|
|
53458
|
+
if (owned && marker.idempotency_key !== idempotencyKey) {
|
|
53459
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
|
|
53460
|
+
project_id: project.id,
|
|
53461
|
+
task_list_id: list.id,
|
|
53462
|
+
receipt_id: marker.receipt_id
|
|
53463
|
+
});
|
|
53464
|
+
}
|
|
53465
|
+
return {
|
|
53466
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
53467
|
+
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
|
|
53468
|
+
idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
|
|
53469
|
+
project_id: project.id,
|
|
53470
|
+
task_list_id: list.id,
|
|
53471
|
+
slug: list.slug,
|
|
53472
|
+
created_by_operation: owned,
|
|
53473
|
+
result_revision: list.updated_at,
|
|
53474
|
+
result_digest: owned ? marker.result_digest : semanticListDigest(list),
|
|
53475
|
+
rollback_supported: Boolean(owned && semanticListDigest(list) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused),
|
|
53476
|
+
created_at: owned ? marker.created_at : list.created_at
|
|
53477
|
+
};
|
|
53478
|
+
}
|
|
53479
|
+
async function exactProjectState(store, projectId) {
|
|
53480
|
+
const project = await store.projects.get(projectId);
|
|
53481
|
+
if (!project) {
|
|
53482
|
+
throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, { project_id: projectId });
|
|
53483
|
+
}
|
|
53484
|
+
const slug = project.task_list_id?.trim();
|
|
53485
|
+
if (!slug) {
|
|
53486
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_NOT_DECLARED", "Project does not declare a canonical task_list_id slug", { project_id: project.id });
|
|
53487
|
+
}
|
|
53488
|
+
const all = await store.taskLists.list();
|
|
53489
|
+
const scopedMatches = all.filter((list) => list.project_id === project.id && list.slug === slug);
|
|
53490
|
+
if (scopedMatches.length > 1) {
|
|
53491
|
+
throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "More than one task list matches the project's exact id and declared slug", { project_id: project.id, slug, task_list_ids: scopedMatches.map((list) => list.id) });
|
|
53492
|
+
}
|
|
53493
|
+
const globalMatches = all.filter((list) => list.project_id === null && list.slug === slug);
|
|
53494
|
+
if (globalMatches.length > 0 && scopedMatches.length === 0) {
|
|
53495
|
+
throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "A legacy global task list already owns the declared slug; refusing to create a second locator", { project_id: project.id, slug, task_list_ids: globalMatches.map((list) => list.id) });
|
|
53496
|
+
}
|
|
53497
|
+
return { project, scoped: scopedMatches[0] ?? null, globalCollision: globalMatches[0] ?? null };
|
|
53498
|
+
}
|
|
53499
|
+
async function planProjectTaskListEnsure(store, projectId) {
|
|
53500
|
+
const { project, scoped } = await exactProjectState(store, projectId);
|
|
53501
|
+
return {
|
|
53502
|
+
mode: "plan",
|
|
53503
|
+
action: scoped ? "already_present" : "would_create",
|
|
53504
|
+
project,
|
|
53505
|
+
task_list: scoped,
|
|
53506
|
+
receipt: null
|
|
53507
|
+
};
|
|
53508
|
+
}
|
|
53509
|
+
async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
53510
|
+
const state = await exactProjectState(store, projectId);
|
|
53511
|
+
const { project } = state;
|
|
53512
|
+
if (project.updated_at !== options.expected_project_revision) {
|
|
53513
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed after the ensure plan; fetch a fresh plan before applying", {
|
|
53514
|
+
project_id: project.id,
|
|
53515
|
+
expected_project_revision: options.expected_project_revision,
|
|
53516
|
+
current_project_revision: project.updated_at
|
|
53517
|
+
});
|
|
53518
|
+
}
|
|
53519
|
+
const slug = project.task_list_id;
|
|
53520
|
+
const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
|
|
53521
|
+
if (state.scoped) {
|
|
53522
|
+
return {
|
|
53523
|
+
mode: "apply",
|
|
53524
|
+
action: "already_present",
|
|
53525
|
+
project,
|
|
53526
|
+
task_list: state.scoped,
|
|
53527
|
+
receipt: receiptFor(store, project, state.scoped, idempotencyKey)
|
|
53528
|
+
};
|
|
53529
|
+
}
|
|
53530
|
+
const marker = {
|
|
53531
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
53532
|
+
receipt_id: receiptId2(project.id, slug, idempotencyKey),
|
|
53533
|
+
idempotency_key: idempotencyKey,
|
|
53534
|
+
project_id: project.id,
|
|
53535
|
+
slug,
|
|
53536
|
+
result_digest: semanticListDigest({
|
|
53537
|
+
project_id: project.id,
|
|
53538
|
+
slug,
|
|
53539
|
+
name: project.name,
|
|
53540
|
+
description: null,
|
|
53541
|
+
metadata: {}
|
|
53542
|
+
}),
|
|
53543
|
+
created_at: new Date().toISOString()
|
|
53544
|
+
};
|
|
53545
|
+
let list;
|
|
53546
|
+
try {
|
|
53547
|
+
list = await store.taskLists.create({
|
|
53548
|
+
name: project.name,
|
|
53549
|
+
slug,
|
|
53550
|
+
project_id: project.id,
|
|
53551
|
+
metadata: { [RECEIPT_METADATA_KEY]: marker }
|
|
53552
|
+
});
|
|
53553
|
+
} catch (error) {
|
|
53554
|
+
if (!(error instanceof ResourceConflictError))
|
|
53555
|
+
throw error;
|
|
53556
|
+
const raced = await exactProjectState(store, projectId);
|
|
53557
|
+
if (!raced.scoped)
|
|
53558
|
+
throw error;
|
|
53559
|
+
if (raced.project.updated_at !== options.expected_project_revision || raced.project.task_list_id !== slug) {
|
|
53560
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed while the task list was being created; fetch a fresh plan before retrying", {
|
|
53561
|
+
project_id: raced.project.id,
|
|
53562
|
+
expected_project_revision: options.expected_project_revision,
|
|
53563
|
+
current_project_revision: raced.project.updated_at
|
|
53564
|
+
});
|
|
53565
|
+
}
|
|
53566
|
+
return {
|
|
53567
|
+
mode: "apply",
|
|
53568
|
+
action: "already_present",
|
|
53569
|
+
project: raced.project,
|
|
53570
|
+
task_list: raced.scoped,
|
|
53571
|
+
receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
|
|
53572
|
+
};
|
|
53573
|
+
}
|
|
53574
|
+
const projectReadback = await store.projects.get(project.id);
|
|
53575
|
+
if (!projectReadback || projectReadback.updated_at !== options.expected_project_revision || projectReadback.task_list_id !== slug) {
|
|
53576
|
+
let compensated = false;
|
|
53577
|
+
const unchanged = await store.taskLists.get(list.id);
|
|
53578
|
+
const unchangedMarker = unchanged ? storedMarker(unchanged) : null;
|
|
53579
|
+
if (unchanged && unchangedMarker?.receipt_id === marker.receipt_id && semanticListDigest(unchanged) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused) {
|
|
53580
|
+
const deletion = await store.taskLists.deleteIfUnchangedAndUnused(list.id, {
|
|
53581
|
+
project_id: unchanged.project_id,
|
|
53582
|
+
slug: unchanged.slug,
|
|
53583
|
+
name: unchanged.name,
|
|
53584
|
+
description: unchanged.description,
|
|
53585
|
+
metadata: unchanged.metadata,
|
|
53586
|
+
updated_at: unchanged.updated_at
|
|
53587
|
+
});
|
|
53588
|
+
compensated = deletion.status === "deleted";
|
|
53589
|
+
}
|
|
53590
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", compensated ? "Project changed while the task list was being created; the new list was rolled back" : "Project changed while the task list was being created; the new list was retained because safe conditional rollback could not be proven", { project_id: project.id, task_list_id: list.id, compensated });
|
|
53591
|
+
}
|
|
53592
|
+
const readback = await store.taskLists.get(list.id);
|
|
53593
|
+
if (!readback || readback.project_id !== project.id || readback.slug !== slug) {
|
|
53594
|
+
throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "Task-list create did not preserve the exact project id and declared slug", { project_id: project.id, task_list_id: list.id, slug });
|
|
53595
|
+
}
|
|
53596
|
+
return {
|
|
53597
|
+
mode: "apply",
|
|
53598
|
+
action: "created",
|
|
53599
|
+
project: projectReadback,
|
|
53600
|
+
task_list: readback,
|
|
53601
|
+
receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
|
|
53602
|
+
};
|
|
53603
|
+
}
|
|
53604
|
+
async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
53605
|
+
const conditionalDelete = store.taskLists.deleteIfUnchangedAndUnused;
|
|
53606
|
+
if (!conditionalDelete) {
|
|
53607
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "This storage backend cannot guarantee atomic conditional rollback; refusing to delete", { project_id: projectId, receipt_id: options.receipt_id });
|
|
53608
|
+
}
|
|
53609
|
+
const project = await store.projects.get(projectId);
|
|
53610
|
+
if (!project) {
|
|
53611
|
+
throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`);
|
|
53612
|
+
}
|
|
53613
|
+
const candidates = (await store.taskLists.list(project.id)).filter((list2) => storedMarker(list2)?.receipt_id === options.receipt_id);
|
|
53614
|
+
if (candidates.length !== 1) {
|
|
53615
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_RECEIPT_NOT_FOUND", "No exact operation-owned task list matches this rollback receipt", { project_id: project.id, receipt_id: options.receipt_id });
|
|
53616
|
+
}
|
|
53617
|
+
const list = candidates[0];
|
|
53618
|
+
const marker = storedMarker(list);
|
|
53619
|
+
if (marker.project_id !== project.id || marker.slug !== list.slug || list.project_id !== project.id || list.updated_at !== options.expected_task_list_revision || semanticListDigest(list) !== marker.result_digest) {
|
|
53620
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "The operation-owned task list drifted; refusing conditional rollback", { project_id: project.id, task_list_id: list.id, receipt_id: options.receipt_id });
|
|
53621
|
+
}
|
|
53622
|
+
const deletion = await conditionalDelete.call(store.taskLists, list.id, {
|
|
53623
|
+
project_id: list.project_id,
|
|
53624
|
+
slug: list.slug,
|
|
53625
|
+
name: list.name,
|
|
53626
|
+
description: list.description,
|
|
53627
|
+
metadata: list.metadata,
|
|
53628
|
+
updated_at: list.updated_at
|
|
53629
|
+
});
|
|
53630
|
+
if (deletion.status === "has_dependents") {
|
|
53631
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_HAS_DEPENDENTS", "The operation-owned task list has dependents; refusing conditional rollback", {
|
|
53632
|
+
task_list_id: list.id,
|
|
53633
|
+
task_dependents: deletion.task_dependents,
|
|
53634
|
+
plan_dependents: deletion.plan_dependents
|
|
53635
|
+
});
|
|
53636
|
+
}
|
|
53637
|
+
if (deletion.status !== "deleted" || await store.taskLists.get(list.id)) {
|
|
53638
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "Conditional rollback did not remove the exact task list", { task_list_id: list.id });
|
|
53639
|
+
}
|
|
53640
|
+
return {
|
|
53641
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
53642
|
+
action: "removed",
|
|
53643
|
+
project_id: project.id,
|
|
53644
|
+
task_list_id: list.id,
|
|
53645
|
+
accepted_receipt_id: options.receipt_id,
|
|
53646
|
+
rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
53647
|
+
removed_at: new Date().toISOString()
|
|
53648
|
+
};
|
|
53649
|
+
}
|
|
53650
|
+
var PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION = "todos.project-task-list-ensure.v1", RECEIPT_METADATA_KEY = "todos_project_task_list_ensure", ProjectTaskListEnsureError;
|
|
53651
|
+
var init_project_task_list_ensure = __esm(() => {
|
|
53652
|
+
init_types();
|
|
53653
|
+
ProjectTaskListEnsureError = class ProjectTaskListEnsureError extends Error {
|
|
53654
|
+
code;
|
|
53655
|
+
details;
|
|
53656
|
+
constructor(code, message, details = {}) {
|
|
53657
|
+
super(message);
|
|
53658
|
+
this.code = code;
|
|
53659
|
+
this.details = details;
|
|
53660
|
+
this.name = "ProjectTaskListEnsureError";
|
|
53661
|
+
}
|
|
53662
|
+
};
|
|
53663
|
+
});
|
|
53664
|
+
|
|
52636
53665
|
// src/server/v1.ts
|
|
52637
53666
|
var exports_v1 = {};
|
|
52638
53667
|
__export(exports_v1, {
|
|
@@ -53420,6 +54449,52 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
53420
54449
|
}
|
|
53421
54450
|
return error(405, `method ${method} not allowed on /v1/projects`);
|
|
53422
54451
|
}
|
|
54452
|
+
if (action === "task-list" && subId === "ensure") {
|
|
54453
|
+
if (method === "GET") {
|
|
54454
|
+
return json4(await planProjectTaskListEnsure(store, id));
|
|
54455
|
+
}
|
|
54456
|
+
if (method !== "POST") {
|
|
54457
|
+
return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/ensure`);
|
|
54458
|
+
}
|
|
54459
|
+
const body = await readJson3(req);
|
|
54460
|
+
if (!body)
|
|
54461
|
+
return error(400, "invalid JSON body");
|
|
54462
|
+
const unknown = Object.keys(body).find((key) => !["expected_project_revision", "idempotency_key"].includes(key));
|
|
54463
|
+
if (unknown)
|
|
54464
|
+
return error(400, `unknown task-list ensure field: ${unknown}`);
|
|
54465
|
+
if (typeof body.expected_project_revision !== "string" || !body.expected_project_revision.trim()) {
|
|
54466
|
+
return error(400, "expected_project_revision must be a non-empty string from a fresh ensure plan");
|
|
54467
|
+
}
|
|
54468
|
+
if (body.idempotency_key !== undefined && typeof body.idempotency_key !== "string") {
|
|
54469
|
+
return error(400, "idempotency_key must be a string");
|
|
54470
|
+
}
|
|
54471
|
+
const result = await applyProjectTaskListEnsure(store, id, {
|
|
54472
|
+
expected_project_revision: body.expected_project_revision,
|
|
54473
|
+
...typeof body.idempotency_key === "string" ? { idempotency_key: body.idempotency_key } : {}
|
|
54474
|
+
});
|
|
54475
|
+
return json4(result, result.action === "created" ? 201 : 200);
|
|
54476
|
+
}
|
|
54477
|
+
if (action === "task-list" && subId === "rollback") {
|
|
54478
|
+
if (method !== "POST") {
|
|
54479
|
+
return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/rollback`);
|
|
54480
|
+
}
|
|
54481
|
+
const body = await readJson3(req);
|
|
54482
|
+
if (!body)
|
|
54483
|
+
return error(400, "invalid JSON body");
|
|
54484
|
+
const unknown = Object.keys(body).find((key) => !["receipt_id", "expected_task_list_revision"].includes(key));
|
|
54485
|
+
if (unknown)
|
|
54486
|
+
return error(400, `unknown task-list rollback field: ${unknown}`);
|
|
54487
|
+
if (typeof body.receipt_id !== "string" || !body.receipt_id.trim()) {
|
|
54488
|
+
return error(400, "receipt_id must be a non-empty string");
|
|
54489
|
+
}
|
|
54490
|
+
if (typeof body.expected_task_list_revision !== "string" || !body.expected_task_list_revision.trim()) {
|
|
54491
|
+
return error(400, "expected_task_list_revision must be a non-empty string from the accepted receipt");
|
|
54492
|
+
}
|
|
54493
|
+
return json4(await rollbackProjectTaskListEnsure(store, id, {
|
|
54494
|
+
receipt_id: body.receipt_id,
|
|
54495
|
+
expected_task_list_revision: body.expected_task_list_revision
|
|
54496
|
+
}));
|
|
54497
|
+
}
|
|
53423
54498
|
if (action === "rename") {
|
|
53424
54499
|
if (method !== "POST")
|
|
53425
54500
|
return error(405, `method ${method} not allowed on /v1/projects/:id/rename`);
|
|
@@ -53758,6 +54833,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
53758
54833
|
}
|
|
53759
54834
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
53760
54835
|
} catch (e) {
|
|
54836
|
+
if (e instanceof ProjectTaskListEnsureError) {
|
|
54837
|
+
const status = e.code === "PROJECT_NOT_FOUND" || e.code === "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND" ? 404 : e.code === "PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID" ? 400 : 409;
|
|
54838
|
+
return error(status, e.message, { code: e.code, conflict: status === 409, ...e.details });
|
|
54839
|
+
}
|
|
53761
54840
|
if (e instanceof TaskReferenceAmbiguousError) {
|
|
53762
54841
|
return error(409, e.message, {
|
|
53763
54842
|
code: TaskReferenceAmbiguousError.code,
|
|
@@ -53784,6 +54863,7 @@ var init_v1 = __esm(() => {
|
|
|
53784
54863
|
init_pr_groups();
|
|
53785
54864
|
init_project_registration();
|
|
53786
54865
|
init_redaction();
|
|
54866
|
+
init_project_task_list_ensure();
|
|
53787
54867
|
JSON_HEADERS3 = { "Content-Type": "application/json" };
|
|
53788
54868
|
});
|
|
53789
54869
|
|