@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.
@@ -70,7 +70,7 @@ var package_default;
70
70
  var init_package = __esm(() => {
71
71
  package_default = {
72
72
  name: "@hasna/todos",
73
- version: "0.15.7",
73
+ version: "0.15.9",
74
74
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
75
75
  type: "module",
76
76
  main: "dist/index.js",
@@ -9943,6 +9943,80 @@ var init_database = __esm(() => {
9943
9943
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
9944
9944
  });
9945
9945
 
9946
+ // src/db/storage-tombstones.ts
9947
+ function recordStorageTombstone(input, db) {
9948
+ const d = db ?? getDatabase();
9949
+ const deletedAt = input.deleted_at ?? now();
9950
+ const machineId = input.source_machine_id ?? currentStorageMachineId(d);
9951
+ d.run(`INSERT INTO storage_tombstones (
9952
+ id, object_type, object_id, deleted_at, updated_at, source_machine_id, payload, version
9953
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
9954
+ ON CONFLICT(object_type, object_id) DO UPDATE SET
9955
+ deleted_at = excluded.deleted_at,
9956
+ updated_at = excluded.updated_at,
9957
+ source_machine_id = excluded.source_machine_id,
9958
+ payload = excluded.payload,
9959
+ version = excluded.version
9960
+ WHERE storage_tombstones.updated_at IS NULL OR storage_tombstones.updated_at <= excluded.updated_at`, [
9961
+ uuid(),
9962
+ input.object_type,
9963
+ input.object_id,
9964
+ deletedAt,
9965
+ deletedAt,
9966
+ machineId,
9967
+ input.payload ? JSON.stringify(input.payload) : null,
9968
+ input.version ?? null
9969
+ ]);
9970
+ return getStorageTombstone(input.object_type, input.object_id, d);
9971
+ }
9972
+ function getStorageTombstone(objectType, objectId, db) {
9973
+ const d = db ?? getDatabase();
9974
+ const row = d.query("SELECT * FROM storage_tombstones WHERE object_type = ? AND object_id = ?").get(objectType, objectId);
9975
+ return row ? rowToStorageTombstone(row) : null;
9976
+ }
9977
+ function listStorageTombstones(db) {
9978
+ const d = db ?? getDatabase();
9979
+ return d.query("SELECT * FROM storage_tombstones ORDER BY updated_at ASC, object_type ASC, object_id ASC").all().map(rowToStorageTombstone);
9980
+ }
9981
+ function shouldApplyStorageTombstone(tombstone, existingUpdatedAt) {
9982
+ const tombstoneClock = Date.parse(tombstone.updated_at || tombstone.deleted_at);
9983
+ if (!existingUpdatedAt)
9984
+ return true;
9985
+ const existingClock = Date.parse(existingUpdatedAt);
9986
+ if (Number.isNaN(tombstoneClock))
9987
+ return true;
9988
+ if (Number.isNaN(existingClock))
9989
+ return true;
9990
+ return tombstoneClock >= existingClock;
9991
+ }
9992
+ function rowToStorageTombstone(row) {
9993
+ return {
9994
+ ...row,
9995
+ payload: parsePayload2(row.payload)
9996
+ };
9997
+ }
9998
+ function parsePayload2(value) {
9999
+ if (!value)
10000
+ return null;
10001
+ try {
10002
+ const parsed = JSON.parse(value);
10003
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
10004
+ } catch {
10005
+ return null;
10006
+ }
10007
+ }
10008
+ function currentStorageMachineId(db) {
10009
+ try {
10010
+ return getMachineId(db);
10011
+ } catch {
10012
+ return null;
10013
+ }
10014
+ }
10015
+ var init_storage_tombstones = __esm(() => {
10016
+ init_database();
10017
+ init_machines();
10018
+ });
10019
+
9946
10020
  // src/lib/search.ts
9947
10021
  var exports_search = {};
9948
10022
  __export(exports_search, {
@@ -10102,80 +10176,6 @@ var init_search = __esm(() => {
10102
10176
  init_database();
10103
10177
  });
10104
10178
 
10105
- // src/db/storage-tombstones.ts
10106
- function recordStorageTombstone(input, db) {
10107
- const d = db ?? getDatabase();
10108
- const deletedAt = input.deleted_at ?? now();
10109
- const machineId = input.source_machine_id ?? currentStorageMachineId(d);
10110
- d.run(`INSERT INTO storage_tombstones (
10111
- id, object_type, object_id, deleted_at, updated_at, source_machine_id, payload, version
10112
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
10113
- ON CONFLICT(object_type, object_id) DO UPDATE SET
10114
- deleted_at = excluded.deleted_at,
10115
- updated_at = excluded.updated_at,
10116
- source_machine_id = excluded.source_machine_id,
10117
- payload = excluded.payload,
10118
- version = excluded.version
10119
- WHERE storage_tombstones.updated_at IS NULL OR storage_tombstones.updated_at <= excluded.updated_at`, [
10120
- uuid(),
10121
- input.object_type,
10122
- input.object_id,
10123
- deletedAt,
10124
- deletedAt,
10125
- machineId,
10126
- input.payload ? JSON.stringify(input.payload) : null,
10127
- input.version ?? null
10128
- ]);
10129
- return getStorageTombstone(input.object_type, input.object_id, d);
10130
- }
10131
- function getStorageTombstone(objectType, objectId, db) {
10132
- const d = db ?? getDatabase();
10133
- const row = d.query("SELECT * FROM storage_tombstones WHERE object_type = ? AND object_id = ?").get(objectType, objectId);
10134
- return row ? rowToStorageTombstone(row) : null;
10135
- }
10136
- function listStorageTombstones(db) {
10137
- const d = db ?? getDatabase();
10138
- return d.query("SELECT * FROM storage_tombstones ORDER BY updated_at ASC, object_type ASC, object_id ASC").all().map(rowToStorageTombstone);
10139
- }
10140
- function shouldApplyStorageTombstone(tombstone, existingUpdatedAt) {
10141
- const tombstoneClock = Date.parse(tombstone.updated_at || tombstone.deleted_at);
10142
- if (!existingUpdatedAt)
10143
- return true;
10144
- const existingClock = Date.parse(existingUpdatedAt);
10145
- if (Number.isNaN(tombstoneClock))
10146
- return true;
10147
- if (Number.isNaN(existingClock))
10148
- return true;
10149
- return tombstoneClock >= existingClock;
10150
- }
10151
- function rowToStorageTombstone(row) {
10152
- return {
10153
- ...row,
10154
- payload: parsePayload2(row.payload)
10155
- };
10156
- }
10157
- function parsePayload2(value) {
10158
- if (!value)
10159
- return null;
10160
- try {
10161
- const parsed = JSON.parse(value);
10162
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
10163
- } catch {
10164
- return null;
10165
- }
10166
- }
10167
- function currentStorageMachineId(db) {
10168
- try {
10169
- return getMachineId(db);
10170
- } catch {
10171
- return null;
10172
- }
10173
- }
10174
- var init_storage_tombstones = __esm(() => {
10175
- init_database();
10176
- init_machines();
10177
- });
10178
-
10179
10179
  // src/db/slug-claims.ts
10180
10180
  function taskListSlugScopeKey(projectId) {
10181
10181
  return projectId ? `project:${projectId}` : "standalone:";
@@ -11906,6 +11906,40 @@ function deleteTaskList(id, db) {
11906
11906
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
11907
11907
  })();
11908
11908
  }
11909
+ function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
11910
+ const d = db || getDatabase();
11911
+ return d.transaction(() => {
11912
+ const current = getTaskList(id, d);
11913
+ if (!current) {
11914
+ return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
11915
+ }
11916
+ 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);
11917
+ if (changed) {
11918
+ return { status: "changed", task_dependents: 0, plan_dependents: 0 };
11919
+ }
11920
+ const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
11921
+ const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
11922
+ if (taskDependents > 0 || planDependents > 0) {
11923
+ return {
11924
+ status: "has_dependents",
11925
+ task_dependents: taskDependents,
11926
+ plan_dependents: planDependents
11927
+ };
11928
+ }
11929
+ recordStorageTombstone({
11930
+ object_type: "task_lists",
11931
+ object_id: id,
11932
+ payload: current
11933
+ }, d);
11934
+ releaseCanonicalSlugClaims("task_list", id, d);
11935
+ const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
11936
+ return {
11937
+ status: deleted ? "deleted" : "not_found",
11938
+ task_dependents: 0,
11939
+ plan_dependents: 0
11940
+ };
11941
+ })();
11942
+ }
11909
11943
  function ensureTaskList(name, slug, projectId, db) {
11910
11944
  const d = db || getDatabase();
11911
11945
  const existing = getTaskListBySlug(slug, projectId, d);
@@ -18829,7 +18863,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
18829
18863
  getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
18830
18864
  list: (projectId) => listTaskLists(projectId, database()),
18831
18865
  update: (id, input) => updateTaskList2(id, input, database()),
18832
- delete: (id) => deleteTaskList(id, database())
18866
+ delete: (id) => deleteTaskList(id, database()),
18867
+ deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
18833
18868
  },
18834
18869
  templates: {
18835
18870
  create: (input) => createTemplate2(input, database()),
@@ -18894,8 +18929,592 @@ var init_local_sqlite = __esm(() => {
18894
18929
  });
18895
18930
 
18896
18931
  // src/project-registration/sqlite.ts
18897
- var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS;
18932
+ function sameSqliteValue(left, right) {
18933
+ return JSON.stringify(left) === JSON.stringify(right);
18934
+ }
18935
+ function taskListFromRow(row) {
18936
+ return {
18937
+ ...row,
18938
+ metadata: JSON.parse(row.metadata || "{}")
18939
+ };
18940
+ }
18941
+ function selectProject(db, id) {
18942
+ return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
18943
+ }
18944
+ function selectTaskList(db, id) {
18945
+ const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
18946
+ return row ? taskListFromRow(row) : null;
18947
+ }
18948
+ function selectProjectConflict(db, path, taskListSlug) {
18949
+ return db.query(`
18950
+ SELECT * FROM projects
18951
+ WHERE path = ? OR task_list_id = ?
18952
+ ORDER BY created_at ASC, id ASC
18953
+ LIMIT 1
18954
+ `).get(path, taskListSlug);
18955
+ }
18956
+ function selectTaskListConflict(db, projectId, slug) {
18957
+ const row = db.query(`
18958
+ SELECT * FROM task_lists
18959
+ WHERE project_id = ? AND slug = ?
18960
+ LIMIT 1
18961
+ `).get(projectId, slug);
18962
+ return row ? taskListFromRow(row) : null;
18963
+ }
18964
+ function quoteSqliteIdentifier(value) {
18965
+ return `"${value.replaceAll('"', '""')}"`;
18966
+ }
18967
+ function hasSqliteDependents(db, resourceKind, targetId) {
18968
+ const targetTable = resourceKind === "project" ? "projects" : "task_lists";
18969
+ const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
18970
+ const tables = db.query(`
18971
+ SELECT name FROM sqlite_schema
18972
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
18973
+ ORDER BY name
18974
+ `).all();
18975
+ for (const { name: tableName } of tables) {
18976
+ const quotedTable = quoteSqliteIdentifier(tableName);
18977
+ const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
18978
+ const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
18979
+ const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
18980
+ for (const columnName of referenceColumns) {
18981
+ const row = db.query(`
18982
+ SELECT 1 AS found
18983
+ FROM ${quotedTable}
18984
+ WHERE ${quoteSqliteIdentifier(columnName)} = ?
18985
+ LIMIT 1
18986
+ `).get(targetId);
18987
+ if (row)
18988
+ return true;
18989
+ }
18990
+ }
18991
+ return false;
18992
+ }
18993
+ function receiptFromRow2(row) {
18994
+ return {
18995
+ ...row,
18996
+ authority: "todos",
18997
+ created_by_operation: Number(row["created_by_operation"]) === 1
18998
+ };
18999
+ }
19000
+ function bindingFromRow2(row) {
19001
+ return row;
19002
+ }
19003
+
19004
+ class SqliteTodosProjectRegistrationTransaction {
19005
+ db;
19006
+ storage;
19007
+ constructor(db) {
19008
+ this.db = db;
19009
+ this.storage = createLocalSqliteTodosStorageAdapter({ db });
19010
+ }
19011
+ async lockStep(_identity) {}
19012
+ async getReceiptForLookup(identity) {
19013
+ const row = this.db.query(`
19014
+ SELECT * FROM todos_project_registration_receipts
19015
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19016
+ AND operation_id = ? AND step_id = ? AND resource_kind = ?
19017
+ AND direction = ? AND idempotency_key = ? AND target_selector = ?
19018
+ ORDER BY CASE outcome
19019
+ WHEN 'terminal_nonacceptance' THEN 0
19020
+ WHEN 'duplicate_of_accepted' THEN 1
19021
+ ELSE 2
19022
+ END, created_at DESC, receipt_id DESC
19023
+ LIMIT 1
19024
+ `).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);
19025
+ return row ? receiptFromRow2(row) : null;
19026
+ }
19027
+ async getReceiptById(receiptId) {
19028
+ const row = this.db.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = ? LIMIT 1").get(receiptId);
19029
+ return row ? receiptFromRow2(row) : null;
19030
+ }
19031
+ async getAcceptedReceiptForStep(identity) {
19032
+ const row = this.db.query(`
19033
+ SELECT * FROM todos_project_registration_receipts
19034
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19035
+ AND operation_id = ? AND step_id = ? AND resource_kind = ?
19036
+ AND direction = ? AND outcome = 'accepted'
19037
+ ORDER BY created_at ASC, receipt_id ASC
19038
+ LIMIT 1
19039
+ `).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction);
19040
+ return row ? receiptFromRow2(row) : null;
19041
+ }
19042
+ async insertReceipt(receipt) {
19043
+ const result = this.db.query(`
19044
+ INSERT OR IGNORE INTO todos_project_registration_receipts (
19045
+ receipt_id, authority, route, package_version, authority_id, tenant_id,
19046
+ corpus_id, operation_id, step_id, resource_kind, direction,
19047
+ target_selector, idempotency_key, request_digest, precondition_digest,
19048
+ normalized_call_digest, outcome, reason, target_id, result_revision,
19049
+ result_digest, duplicate_of_receipt_id, accepted_receipt_id,
19050
+ created_by_operation, created_at
19051
+ ) VALUES (
19052
+ ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
19053
+ )
19054
+ `).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);
19055
+ return result.changes === 1;
19056
+ }
19057
+ async getBinding(scope, resourceKind, targetSelector) {
19058
+ const row = this.db.query(`
19059
+ SELECT * FROM todos_project_registration_bindings
19060
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19061
+ AND resource_kind = ? AND target_selector = ?
19062
+ LIMIT 1
19063
+ `).get(scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
19064
+ return row ? bindingFromRow2(row) : null;
19065
+ }
19066
+ async claimBinding(binding) {
19067
+ const result = this.db.query(`
19068
+ INSERT OR IGNORE INTO todos_project_registration_bindings (
19069
+ authority_id, tenant_id, corpus_id, resource_kind, target_selector,
19070
+ operation_id, step_id, direction, idempotency_key, request_digest,
19071
+ precondition_digest, normalized_call_digest, state, target_id,
19072
+ accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
19073
+ created_at, updated_at
19074
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
19075
+ `).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);
19076
+ return result.changes === 1;
19077
+ }
19078
+ async setBindingAccepted(scope, resourceKind, targetSelector, update) {
19079
+ const result = this.db.query(`
19080
+ UPDATE todos_project_registration_bindings
19081
+ SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
19082
+ result_revision = ?, result_digest = ?, updated_at = ?
19083
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19084
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
19085
+ `).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);
19086
+ if (result.changes !== 1) {
19087
+ throw new Error("Todos project registration binding was not pending at acceptance");
19088
+ }
19089
+ }
19090
+ async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
19091
+ this.db.query(`
19092
+ UPDATE todos_project_registration_bindings
19093
+ SET state = 'terminal_nonacceptance', updated_at = ?
19094
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19095
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
19096
+ `).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
19097
+ }
19098
+ async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
19099
+ const result = this.db.query(`
19100
+ UPDATE todos_project_registration_bindings
19101
+ SET state = 'removed', removed_receipt_id = ?, updated_at = ?
19102
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19103
+ AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
19104
+ `).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
19105
+ if (result.changes !== 1) {
19106
+ throw new Error("Todos project registration binding was not accepted at removal");
19107
+ }
19108
+ }
19109
+ async findProjectConflict(path, taskListSlug) {
19110
+ const row = this.db.query(`
19111
+ SELECT * FROM projects
19112
+ WHERE path = ? OR task_list_id = ?
19113
+ ORDER BY created_at ASC, id ASC
19114
+ LIMIT 1
19115
+ `).get(path, taskListSlug);
19116
+ return row ?? null;
19117
+ }
19118
+ async findTaskListConflict(projectId, slug) {
19119
+ return await this.storage.taskLists.getBySlug(slug, projectId);
19120
+ }
19121
+ async createProject(input) {
19122
+ return await this.storage.projects.create(input);
19123
+ }
19124
+ async createTaskList(input) {
19125
+ return await this.storage.taskLists.create(input);
19126
+ }
19127
+ async getProject(id) {
19128
+ return await this.storage.projects.get(id);
19129
+ }
19130
+ async getTaskList(id) {
19131
+ return await this.storage.taskLists.get(id);
19132
+ }
19133
+ async lockCompensationWrites() {}
19134
+ async hasDependents(resourceKind, targetId) {
19135
+ return hasSqliteDependents(this.db, resourceKind, targetId);
19136
+ }
19137
+ async deleteProject(id) {
19138
+ return await this.storage.projects.delete(id);
19139
+ }
19140
+ async deleteTaskList(id) {
19141
+ return await this.storage.taskLists.delete(id);
19142
+ }
19143
+ }
19144
+
19145
+ class StagedSqliteTodosProjectRegistrationTransaction {
19146
+ db;
19147
+ direct;
19148
+ validators = [];
19149
+ mutations = [];
19150
+ receipts = new Map;
19151
+ bindings = new Map;
19152
+ projects = new Map;
19153
+ taskLists = new Map;
19154
+ constructor(db) {
19155
+ this.db = db;
19156
+ this.direct = new SqliteTodosProjectRegistrationTransaction(db);
19157
+ }
19158
+ commit() {
19159
+ this.db.exec("BEGIN IMMEDIATE");
19160
+ try {
19161
+ for (const validate of this.validators) {
19162
+ if (!validate()) {
19163
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
19164
+ }
19165
+ }
19166
+ for (const mutate of this.mutations)
19167
+ mutate();
19168
+ this.db.exec("COMMIT");
19169
+ } catch (error) {
19170
+ try {
19171
+ this.db.exec("ROLLBACK");
19172
+ } catch {}
19173
+ throw error;
19174
+ }
19175
+ }
19176
+ async lockStep(_identity) {}
19177
+ async getReceiptForLookup(identity) {
19178
+ 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);
19179
+ const stored = await this.direct.getReceiptForLookup(identity);
19180
+ if (stored)
19181
+ staged.push(stored);
19182
+ const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
19183
+ 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;
19184
+ }
19185
+ async getReceiptById(receiptId) {
19186
+ return this.receipts.get(receiptId) ?? this.direct.getReceiptById(receiptId);
19187
+ }
19188
+ async getAcceptedReceiptForStep(identity) {
19189
+ 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");
19190
+ const stored = await this.direct.getAcceptedReceiptForStep(identity);
19191
+ if (stored)
19192
+ staged.push(stored);
19193
+ return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
19194
+ }
19195
+ async insertReceipt(receipt) {
19196
+ if (this.receipts.has(receipt.receipt_id))
19197
+ return false;
19198
+ if (await this.direct.getReceiptById(receipt.receipt_id))
19199
+ return false;
19200
+ const planned = { ...receipt };
19201
+ this.receipts.set(planned.receipt_id, planned);
19202
+ this.mutations.push(() => {
19203
+ try {
19204
+ const result = this.db.query(`
19205
+ INSERT OR IGNORE INTO todos_project_registration_receipts (
19206
+ receipt_id, authority, route, package_version, authority_id, tenant_id,
19207
+ corpus_id, operation_id, step_id, resource_kind, direction,
19208
+ target_selector, idempotency_key, request_digest, precondition_digest,
19209
+ normalized_call_digest, outcome, reason, target_id, result_revision,
19210
+ result_digest, duplicate_of_receipt_id, accepted_receipt_id,
19211
+ created_by_operation, created_at
19212
+ ) VALUES (
19213
+ ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
19214
+ )
19215
+ `).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);
19216
+ if (result.changes !== 1) {
19217
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
19218
+ }
19219
+ } catch (error) {
19220
+ if (error instanceof SqliteRegistrationOptimisticConflict)
19221
+ throw error;
19222
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
19223
+ }
19224
+ });
19225
+ return true;
19226
+ }
19227
+ async getBinding(scope, resourceKind, targetSelector) {
19228
+ const key = this.bindingKey(scope, resourceKind, targetSelector);
19229
+ return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
19230
+ }
19231
+ async claimBinding(binding) {
19232
+ const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
19233
+ if (this.bindings.has(key))
19234
+ return false;
19235
+ if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
19236
+ return false;
19237
+ }
19238
+ const planned = { ...binding };
19239
+ this.bindings.set(key, planned);
19240
+ this.mutations.push(() => {
19241
+ try {
19242
+ const result = this.db.query(`
19243
+ INSERT OR IGNORE INTO todos_project_registration_bindings (
19244
+ authority_id, tenant_id, corpus_id, resource_kind, target_selector,
19245
+ operation_id, step_id, direction, idempotency_key, request_digest,
19246
+ precondition_digest, normalized_call_digest, state, target_id,
19247
+ accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
19248
+ created_at, updated_at
19249
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
19250
+ `).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);
19251
+ if (result.changes !== 1) {
19252
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
19253
+ }
19254
+ } catch (error) {
19255
+ if (error instanceof SqliteRegistrationOptimisticConflict)
19256
+ throw error;
19257
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
19258
+ }
19259
+ });
19260
+ return true;
19261
+ }
19262
+ async setBindingAccepted(scope, resourceKind, targetSelector, update) {
19263
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
19264
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
19265
+ ...binding,
19266
+ state: "accepted",
19267
+ target_id: update.target_id,
19268
+ accepted_receipt_id: update.accepted_receipt_id,
19269
+ result_revision: update.result_revision,
19270
+ result_digest: update.result_digest,
19271
+ updated_at: update.updated_at
19272
+ });
19273
+ this.mutations.push(() => {
19274
+ const result = this.db.query(`
19275
+ UPDATE todos_project_registration_bindings
19276
+ SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
19277
+ result_revision = ?, result_digest = ?, updated_at = ?
19278
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19279
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
19280
+ `).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);
19281
+ if (result.changes !== 1) {
19282
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
19283
+ }
19284
+ });
19285
+ }
19286
+ async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
19287
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
19288
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
19289
+ ...binding,
19290
+ state: "terminal_nonacceptance",
19291
+ updated_at: updatedAt
19292
+ });
19293
+ this.mutations.push(() => {
19294
+ const result = this.db.query(`
19295
+ UPDATE todos_project_registration_bindings
19296
+ SET state = 'terminal_nonacceptance', updated_at = ?
19297
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19298
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
19299
+ `).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
19300
+ if (result.changes !== 1) {
19301
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
19302
+ }
19303
+ });
19304
+ }
19305
+ async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
19306
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
19307
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
19308
+ ...binding,
19309
+ state: "removed",
19310
+ removed_receipt_id: removedReceiptId,
19311
+ updated_at: updatedAt
19312
+ });
19313
+ this.mutations.push(() => {
19314
+ const result = this.db.query(`
19315
+ UPDATE todos_project_registration_bindings
19316
+ SET state = 'removed', removed_receipt_id = ?, updated_at = ?
19317
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
19318
+ AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
19319
+ `).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
19320
+ if (result.changes !== 1) {
19321
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
19322
+ }
19323
+ });
19324
+ }
19325
+ async findProjectConflict(path, taskListSlug) {
19326
+ const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
19327
+ if (planned)
19328
+ return planned;
19329
+ const observed = selectProjectConflict(this.db, path, taskListSlug);
19330
+ this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
19331
+ return observed;
19332
+ }
19333
+ async findTaskListConflict(projectId, slug) {
19334
+ const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
19335
+ if (planned)
19336
+ return planned;
19337
+ const observed = selectTaskListConflict(this.db, projectId, slug);
19338
+ this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
19339
+ return observed;
19340
+ }
19341
+ async createProject(input) {
19342
+ const derivedSlug = normalizeSlug(input.name);
19343
+ const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
19344
+ if (!derivedSlug || !taskListId) {
19345
+ throw new Error("Project name and task-list slug must be non-empty");
19346
+ }
19347
+ const project = {
19348
+ id: uuid(),
19349
+ name: input.name,
19350
+ path: input.path,
19351
+ description: input.description || null,
19352
+ task_list_id: taskListId,
19353
+ task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
19354
+ task_counter: 0,
19355
+ created_at: now(),
19356
+ updated_at: now(),
19357
+ machine_id: currentStorageMachineId(this.db)
19358
+ };
19359
+ project.updated_at = project.created_at;
19360
+ this.projects.set(project.id, project);
19361
+ this.mutations.push(() => {
19362
+ try {
19363
+ const result = this.db.run(`INSERT INTO projects (
19364
+ id, name, path, description, task_list_id, task_prefix,
19365
+ task_counter, created_at, updated_at, machine_id
19366
+ ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
19367
+ project.id,
19368
+ project.name,
19369
+ project.path,
19370
+ project.description,
19371
+ project.task_list_id,
19372
+ project.task_prefix,
19373
+ project.created_at,
19374
+ project.updated_at,
19375
+ project.machine_id ?? null
19376
+ ]);
19377
+ if (result.changes < 1) {
19378
+ throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
19379
+ }
19380
+ } catch (error) {
19381
+ if (error instanceof SqliteRegistrationOptimisticConflict)
19382
+ throw error;
19383
+ throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
19384
+ }
19385
+ });
19386
+ return project;
19387
+ }
19388
+ async createTaskList(input) {
19389
+ const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
19390
+ if (!slug)
19391
+ throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
19392
+ const taskList = {
19393
+ id: uuid(),
19394
+ project_id: input.project_id || null,
19395
+ slug,
19396
+ name: input.name,
19397
+ description: input.description || null,
19398
+ metadata: input.metadata ?? {},
19399
+ created_at: now(),
19400
+ updated_at: now(),
19401
+ machine_id: currentStorageMachineId(this.db)
19402
+ };
19403
+ taskList.updated_at = taskList.created_at;
19404
+ this.taskLists.set(taskList.id, taskList);
19405
+ this.mutations.push(() => {
19406
+ try {
19407
+ const result = this.db.run(`INSERT INTO task_lists (
19408
+ id, project_id, slug, name, description, metadata,
19409
+ created_at, updated_at, machine_id
19410
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
19411
+ taskList.id,
19412
+ taskList.project_id,
19413
+ taskList.slug,
19414
+ taskList.name,
19415
+ taskList.description,
19416
+ JSON.stringify(taskList.metadata),
19417
+ taskList.created_at,
19418
+ taskList.updated_at,
19419
+ taskList.machine_id ?? null
19420
+ ]);
19421
+ if (result.changes < 1) {
19422
+ throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
19423
+ }
19424
+ } catch (error) {
19425
+ if (error instanceof SqliteRegistrationOptimisticConflict)
19426
+ throw error;
19427
+ throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
19428
+ }
19429
+ });
19430
+ return taskList;
19431
+ }
19432
+ async getProject(id) {
19433
+ if (this.projects.has(id))
19434
+ return this.projects.get(id) ?? null;
19435
+ const observed = selectProject(this.db, id);
19436
+ this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
19437
+ return observed;
19438
+ }
19439
+ async getTaskList(id) {
19440
+ if (this.taskLists.has(id))
19441
+ return this.taskLists.get(id) ?? null;
19442
+ const observed = selectTaskList(this.db, id);
19443
+ this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
19444
+ return observed;
19445
+ }
19446
+ async lockCompensationWrites() {}
19447
+ async hasDependents(resourceKind, targetId) {
19448
+ const observed = hasSqliteDependents(this.db, resourceKind, targetId);
19449
+ this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
19450
+ return observed;
19451
+ }
19452
+ async deleteProject(id) {
19453
+ const project = await this.getProject(id);
19454
+ if (!project)
19455
+ return false;
19456
+ this.projects.set(id, null);
19457
+ this.mutations.push(() => {
19458
+ recordStorageTombstone({
19459
+ object_type: "projects",
19460
+ object_id: id,
19461
+ payload: project
19462
+ }, this.db);
19463
+ if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
19464
+ throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
19465
+ }
19466
+ });
19467
+ return true;
19468
+ }
19469
+ async deleteTaskList(id) {
19470
+ const taskList = await this.getTaskList(id);
19471
+ if (!taskList)
19472
+ return false;
19473
+ this.taskLists.set(id, null);
19474
+ this.mutations.push(() => {
19475
+ recordStorageTombstone({
19476
+ object_type: "task_lists",
19477
+ object_id: id,
19478
+ payload: taskList
19479
+ }, this.db);
19480
+ if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
19481
+ throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
19482
+ }
19483
+ });
19484
+ return true;
19485
+ }
19486
+ bindingKey(scope, resourceKind, targetSelector) {
19487
+ return JSON.stringify([
19488
+ scope.authority_id,
19489
+ scope.tenant_id,
19490
+ scope.corpus_id,
19491
+ resourceKind,
19492
+ targetSelector
19493
+ ]);
19494
+ }
19495
+ async requireBinding(scope, resourceKind, targetSelector, state) {
19496
+ const binding = await this.getBinding(scope, resourceKind, targetSelector);
19497
+ if (!binding || binding.state !== state) {
19498
+ throw new Error(`Todos project registration binding was not ${state}`);
19499
+ }
19500
+ return binding;
19501
+ }
19502
+ availableProjectPrefix(name) {
19503
+ const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
19504
+ 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();
19505
+ let candidate = prefix;
19506
+ let suffix = 1;
19507
+ while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
19508
+ suffix += 1;
19509
+ candidate = `${prefix}${suffix}`;
19510
+ }
19511
+ return candidate;
19512
+ }
19513
+ }
19514
+ var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS, SqliteRegistrationOptimisticConflict;
18898
19515
  var init_sqlite = __esm(() => {
19516
+ init_database();
19517
+ init_storage_tombstones();
18899
19518
  init_local_sqlite();
18900
19519
  sqliteTransactionTails = new WeakMap;
18901
19520
  PROJECT_REFERENCE_COLUMNS = new Set([
@@ -18905,6 +19524,12 @@ var init_sqlite = __esm(() => {
18905
19524
  "external_project_id"
18906
19525
  ]);
18907
19526
  TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
19527
+ SqliteRegistrationOptimisticConflict = class SqliteRegistrationOptimisticConflict extends Error {
19528
+ constructor(message, options = {}) {
19529
+ super(message, options);
19530
+ this.name = "SqliteRegistrationOptimisticConflict";
19531
+ }
19532
+ };
18908
19533
  });
18909
19534
 
18910
19535
  // src/project-registration/authority.ts
@@ -22248,6 +22873,9 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
22248
22873
  Task: taskSchema,
22249
22874
  Project: projectSchema,
22250
22875
  TaskList: taskListSchema,
22876
+ ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
22877
+ ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
22878
+ ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
22251
22879
  TaskComment: taskCommentSchema,
22252
22880
  Plan: planSchema,
22253
22881
  Template: templateSchema,
@@ -22325,6 +22953,29 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
22325
22953
  name: { type: "string", minLength: 1 }
22326
22954
  }
22327
22955
  },
22956
+ ProjectTaskListEnsureApplyInput: {
22957
+ type: "object",
22958
+ additionalProperties: false,
22959
+ required: ["expected_project_revision"],
22960
+ properties: {
22961
+ expected_project_revision: { type: "string", minLength: 1 },
22962
+ idempotency_key: {
22963
+ type: "string",
22964
+ minLength: 8,
22965
+ maxLength: 128,
22966
+ pattern: "^[A-Za-z0-9._:-]+$"
22967
+ }
22968
+ }
22969
+ },
22970
+ ProjectTaskListRollbackInput: {
22971
+ type: "object",
22972
+ additionalProperties: false,
22973
+ required: ["receipt_id", "expected_task_list_revision"],
22974
+ properties: {
22975
+ receipt_id: { type: "string", minLength: 1 },
22976
+ expected_task_list_revision: { type: "string", minLength: 1 }
22977
+ }
22978
+ },
22328
22979
  ErrorResponse: {
22329
22980
  type: "object",
22330
22981
  required: ["error"],
@@ -23427,6 +24078,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
23427
24078
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
23428
24079
  }
23429
24080
  },
24081
+ "/v1/projects/{id}/task-list/ensure": {
24082
+ get: {
24083
+ operationId: "planProjectTaskListEnsure",
24084
+ summary: "Plan a non-mutating repair of a project's declared task list",
24085
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
24086
+ responses: {
24087
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
24088
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
24089
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
24090
+ }
24091
+ },
24092
+ post: {
24093
+ operationId: "ensureProjectTaskList",
24094
+ summary: "Idempotently create an existing project's declared task list",
24095
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
24096
+ requestBody: {
24097
+ required: true,
24098
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureApplyInput" } } }
24099
+ },
24100
+ responses: {
24101
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
24102
+ "201": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
24103
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
24104
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
24105
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
24106
+ }
24107
+ }
24108
+ },
24109
+ "/v1/projects/{id}/task-list/rollback": {
24110
+ post: {
24111
+ operationId: "rollbackProjectTaskListEnsure",
24112
+ summary: "Conditionally remove an unchanged task list created by an accepted ensure receipt",
24113
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
24114
+ requestBody: {
24115
+ required: true,
24116
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackInput" } } }
24117
+ },
24118
+ responses: {
24119
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackResult" } } } },
24120
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
24121
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
24122
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
24123
+ }
24124
+ }
24125
+ },
23430
24126
  "/v1/projects/{id}/rename": {
23431
24127
  post: {
23432
24128
  operationId: "renameProject",
@@ -23708,7 +24404,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
23708
24404
  }
23709
24405
  };
23710
24406
  }
23711
- var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
24407
+ var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
23712
24408
  var init_openapi = __esm(() => {
23713
24409
  init_package_version();
23714
24410
  init_types();
@@ -23756,6 +24452,80 @@ var init_openapi = __esm(() => {
23756
24452
  updated_at: { type: "string" }
23757
24453
  }
23758
24454
  };
24455
+ projectTaskListEnsureReceiptSchema = {
24456
+ type: "object",
24457
+ additionalProperties: false,
24458
+ required: [
24459
+ "schema_version",
24460
+ "receipt_id",
24461
+ "idempotency_key",
24462
+ "project_id",
24463
+ "task_list_id",
24464
+ "slug",
24465
+ "created_by_operation",
24466
+ "result_revision",
24467
+ "result_digest",
24468
+ "rollback_supported",
24469
+ "created_at"
24470
+ ],
24471
+ properties: {
24472
+ schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
24473
+ receipt_id: { type: "string" },
24474
+ idempotency_key: { type: "string" },
24475
+ project_id: { type: "string" },
24476
+ task_list_id: { type: "string" },
24477
+ slug: { type: "string" },
24478
+ created_by_operation: { type: "boolean" },
24479
+ result_revision: { type: "string" },
24480
+ result_digest: { type: "string" },
24481
+ rollback_supported: { type: "boolean" },
24482
+ created_at: { type: "string", format: "date-time" }
24483
+ }
24484
+ };
24485
+ projectTaskListEnsureResultSchema = {
24486
+ type: "object",
24487
+ additionalProperties: false,
24488
+ required: ["mode", "action", "project", "task_list", "receipt"],
24489
+ properties: {
24490
+ mode: { type: "string", enum: ["plan", "apply"] },
24491
+ action: { type: "string", enum: ["would_create", "created", "already_present"] },
24492
+ project: { $ref: "#/components/schemas/Project" },
24493
+ task_list: {
24494
+ oneOf: [
24495
+ { $ref: "#/components/schemas/TaskList" },
24496
+ { type: "null" }
24497
+ ]
24498
+ },
24499
+ receipt: {
24500
+ oneOf: [
24501
+ { $ref: "#/components/schemas/ProjectTaskListEnsureReceipt" },
24502
+ { type: "null" }
24503
+ ]
24504
+ }
24505
+ }
24506
+ };
24507
+ projectTaskListRollbackResultSchema = {
24508
+ type: "object",
24509
+ additionalProperties: false,
24510
+ required: [
24511
+ "schema_version",
24512
+ "action",
24513
+ "project_id",
24514
+ "task_list_id",
24515
+ "accepted_receipt_id",
24516
+ "rollback_receipt_id",
24517
+ "removed_at"
24518
+ ],
24519
+ properties: {
24520
+ schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
24521
+ action: { type: "string", enum: ["removed"] },
24522
+ project_id: { type: "string" },
24523
+ task_list_id: { type: "string" },
24524
+ accepted_receipt_id: { type: "string" },
24525
+ rollback_receipt_id: { type: "string" },
24526
+ removed_at: { type: "string", format: "date-time" }
24527
+ }
24528
+ };
23759
24529
  taskCommentSchema = {
23760
24530
  type: "object",
23761
24531
  required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
@@ -23989,6 +24759,261 @@ function decodeCommentCursor(value) {
23989
24759
  }
23990
24760
  var MAX_COMMENT_CURSOR_LENGTH = 1024;
23991
24761
 
24762
+ // src/lib/project-task-list-ensure.ts
24763
+ import { createHash as createHash7 } from "crypto";
24764
+ function canonicalJson(value) {
24765
+ if (value === null || typeof value !== "object")
24766
+ return JSON.stringify(value);
24767
+ if (Array.isArray(value))
24768
+ return `[${value.map(canonicalJson).join(",")}]`;
24769
+ return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
24770
+ }
24771
+ function digest(value) {
24772
+ return createHash7("sha256").update(canonicalJson(value)).digest("hex");
24773
+ }
24774
+ function deriveIdempotencyKey(projectId, slug) {
24775
+ return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
24776
+ }
24777
+ function normalizeIdempotencyKey(value, projectId, slug) {
24778
+ const key = value?.trim() || deriveIdempotencyKey(projectId, slug);
24779
+ if (key.length < 8 || key.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(key)) {
24780
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID", "idempotency_key must be 8-128 ASCII letters, digits, dots, underscores, colons, or hyphens");
24781
+ }
24782
+ return key;
24783
+ }
24784
+ function receiptId2(projectId, slug, idempotencyKey) {
24785
+ return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
24786
+ }
24787
+ function semanticListDigest(list) {
24788
+ const metadata = { ...list.metadata ?? {} };
24789
+ delete metadata[RECEIPT_METADATA_KEY];
24790
+ return digest({
24791
+ project_id: list.project_id,
24792
+ slug: list.slug,
24793
+ name: list.name,
24794
+ description: list.description,
24795
+ metadata
24796
+ });
24797
+ }
24798
+ function storedMarker(list) {
24799
+ const value = list.metadata?.[RECEIPT_METADATA_KEY];
24800
+ if (!value || typeof value !== "object" || Array.isArray(value))
24801
+ return null;
24802
+ const marker = value;
24803
+ 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")
24804
+ return null;
24805
+ return marker;
24806
+ }
24807
+ function receiptFor(store, project, list, idempotencyKey) {
24808
+ const marker = storedMarker(list);
24809
+ const owned = marker?.project_id === project.id && marker.slug === list.slug;
24810
+ if (owned && marker.idempotency_key !== idempotencyKey) {
24811
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
24812
+ project_id: project.id,
24813
+ task_list_id: list.id,
24814
+ receipt_id: marker.receipt_id
24815
+ });
24816
+ }
24817
+ return {
24818
+ schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
24819
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
24820
+ idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
24821
+ project_id: project.id,
24822
+ task_list_id: list.id,
24823
+ slug: list.slug,
24824
+ created_by_operation: owned,
24825
+ result_revision: list.updated_at,
24826
+ result_digest: owned ? marker.result_digest : semanticListDigest(list),
24827
+ rollback_supported: Boolean(owned && semanticListDigest(list) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused),
24828
+ created_at: owned ? marker.created_at : list.created_at
24829
+ };
24830
+ }
24831
+ async function exactProjectState(store, projectId) {
24832
+ const project = await store.projects.get(projectId);
24833
+ if (!project) {
24834
+ throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, { project_id: projectId });
24835
+ }
24836
+ const slug = project.task_list_id?.trim();
24837
+ if (!slug) {
24838
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_NOT_DECLARED", "Project does not declare a canonical task_list_id slug", { project_id: project.id });
24839
+ }
24840
+ const all = await store.taskLists.list();
24841
+ const scopedMatches = all.filter((list) => list.project_id === project.id && list.slug === slug);
24842
+ if (scopedMatches.length > 1) {
24843
+ 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) });
24844
+ }
24845
+ const globalMatches = all.filter((list) => list.project_id === null && list.slug === slug);
24846
+ if (globalMatches.length > 0 && scopedMatches.length === 0) {
24847
+ 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) });
24848
+ }
24849
+ return { project, scoped: scopedMatches[0] ?? null, globalCollision: globalMatches[0] ?? null };
24850
+ }
24851
+ async function planProjectTaskListEnsure(store, projectId) {
24852
+ const { project, scoped } = await exactProjectState(store, projectId);
24853
+ return {
24854
+ mode: "plan",
24855
+ action: scoped ? "already_present" : "would_create",
24856
+ project,
24857
+ task_list: scoped,
24858
+ receipt: null
24859
+ };
24860
+ }
24861
+ async function applyProjectTaskListEnsure(store, projectId, options) {
24862
+ const state = await exactProjectState(store, projectId);
24863
+ const { project } = state;
24864
+ if (project.updated_at !== options.expected_project_revision) {
24865
+ throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed after the ensure plan; fetch a fresh plan before applying", {
24866
+ project_id: project.id,
24867
+ expected_project_revision: options.expected_project_revision,
24868
+ current_project_revision: project.updated_at
24869
+ });
24870
+ }
24871
+ const slug = project.task_list_id;
24872
+ const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
24873
+ if (state.scoped) {
24874
+ return {
24875
+ mode: "apply",
24876
+ action: "already_present",
24877
+ project,
24878
+ task_list: state.scoped,
24879
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey)
24880
+ };
24881
+ }
24882
+ const marker = {
24883
+ schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
24884
+ receipt_id: receiptId2(project.id, slug, idempotencyKey),
24885
+ idempotency_key: idempotencyKey,
24886
+ project_id: project.id,
24887
+ slug,
24888
+ result_digest: semanticListDigest({
24889
+ project_id: project.id,
24890
+ slug,
24891
+ name: project.name,
24892
+ description: null,
24893
+ metadata: {}
24894
+ }),
24895
+ created_at: new Date().toISOString()
24896
+ };
24897
+ let list;
24898
+ try {
24899
+ list = await store.taskLists.create({
24900
+ name: project.name,
24901
+ slug,
24902
+ project_id: project.id,
24903
+ metadata: { [RECEIPT_METADATA_KEY]: marker }
24904
+ });
24905
+ } catch (error) {
24906
+ if (!(error instanceof ResourceConflictError))
24907
+ throw error;
24908
+ const raced = await exactProjectState(store, projectId);
24909
+ if (!raced.scoped)
24910
+ throw error;
24911
+ if (raced.project.updated_at !== options.expected_project_revision || raced.project.task_list_id !== slug) {
24912
+ throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed while the task list was being created; fetch a fresh plan before retrying", {
24913
+ project_id: raced.project.id,
24914
+ expected_project_revision: options.expected_project_revision,
24915
+ current_project_revision: raced.project.updated_at
24916
+ });
24917
+ }
24918
+ return {
24919
+ mode: "apply",
24920
+ action: "already_present",
24921
+ project: raced.project,
24922
+ task_list: raced.scoped,
24923
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
24924
+ };
24925
+ }
24926
+ const projectReadback = await store.projects.get(project.id);
24927
+ if (!projectReadback || projectReadback.updated_at !== options.expected_project_revision || projectReadback.task_list_id !== slug) {
24928
+ let compensated = false;
24929
+ const unchanged = await store.taskLists.get(list.id);
24930
+ const unchangedMarker = unchanged ? storedMarker(unchanged) : null;
24931
+ if (unchanged && unchangedMarker?.receipt_id === marker.receipt_id && semanticListDigest(unchanged) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused) {
24932
+ const deletion = await store.taskLists.deleteIfUnchangedAndUnused(list.id, {
24933
+ project_id: unchanged.project_id,
24934
+ slug: unchanged.slug,
24935
+ name: unchanged.name,
24936
+ description: unchanged.description,
24937
+ metadata: unchanged.metadata,
24938
+ updated_at: unchanged.updated_at
24939
+ });
24940
+ compensated = deletion.status === "deleted";
24941
+ }
24942
+ 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 });
24943
+ }
24944
+ const readback = await store.taskLists.get(list.id);
24945
+ if (!readback || readback.project_id !== project.id || readback.slug !== slug) {
24946
+ 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 });
24947
+ }
24948
+ return {
24949
+ mode: "apply",
24950
+ action: "created",
24951
+ project: projectReadback,
24952
+ task_list: readback,
24953
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
24954
+ };
24955
+ }
24956
+ async function rollbackProjectTaskListEnsure(store, projectId, options) {
24957
+ const conditionalDelete = store.taskLists.deleteIfUnchangedAndUnused;
24958
+ if (!conditionalDelete) {
24959
+ 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 });
24960
+ }
24961
+ const project = await store.projects.get(projectId);
24962
+ if (!project) {
24963
+ throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`);
24964
+ }
24965
+ const candidates = (await store.taskLists.list(project.id)).filter((list2) => storedMarker(list2)?.receipt_id === options.receipt_id);
24966
+ if (candidates.length !== 1) {
24967
+ 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 });
24968
+ }
24969
+ const list = candidates[0];
24970
+ const marker = storedMarker(list);
24971
+ 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) {
24972
+ 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 });
24973
+ }
24974
+ const deletion = await conditionalDelete.call(store.taskLists, list.id, {
24975
+ project_id: list.project_id,
24976
+ slug: list.slug,
24977
+ name: list.name,
24978
+ description: list.description,
24979
+ metadata: list.metadata,
24980
+ updated_at: list.updated_at
24981
+ });
24982
+ if (deletion.status === "has_dependents") {
24983
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_HAS_DEPENDENTS", "The operation-owned task list has dependents; refusing conditional rollback", {
24984
+ task_list_id: list.id,
24985
+ task_dependents: deletion.task_dependents,
24986
+ plan_dependents: deletion.plan_dependents
24987
+ });
24988
+ }
24989
+ if (deletion.status !== "deleted" || await store.taskLists.get(list.id)) {
24990
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "Conditional rollback did not remove the exact task list", { task_list_id: list.id });
24991
+ }
24992
+ return {
24993
+ schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
24994
+ action: "removed",
24995
+ project_id: project.id,
24996
+ task_list_id: list.id,
24997
+ accepted_receipt_id: options.receipt_id,
24998
+ rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
24999
+ removed_at: new Date().toISOString()
25000
+ };
25001
+ }
25002
+ var PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION = "todos.project-task-list-ensure.v1", RECEIPT_METADATA_KEY = "todos_project_task_list_ensure", ProjectTaskListEnsureError;
25003
+ var init_project_task_list_ensure = __esm(() => {
25004
+ init_types();
25005
+ ProjectTaskListEnsureError = class ProjectTaskListEnsureError extends Error {
25006
+ code;
25007
+ details;
25008
+ constructor(code, message, details = {}) {
25009
+ super(message);
25010
+ this.code = code;
25011
+ this.details = details;
25012
+ this.name = "ProjectTaskListEnsureError";
25013
+ }
25014
+ };
25015
+ });
25016
+
23992
25017
  // src/server/v1.ts
23993
25018
  var exports_v1 = {};
23994
25019
  __export(exports_v1, {
@@ -24776,6 +25801,52 @@ async function handleV1Request(req, url, dependencies = {}) {
24776
25801
  }
24777
25802
  return error(405, `method ${method} not allowed on /v1/projects`);
24778
25803
  }
25804
+ if (action === "task-list" && subId === "ensure") {
25805
+ if (method === "GET") {
25806
+ return json4(await planProjectTaskListEnsure(store, id));
25807
+ }
25808
+ if (method !== "POST") {
25809
+ return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/ensure`);
25810
+ }
25811
+ const body = await readJson3(req);
25812
+ if (!body)
25813
+ return error(400, "invalid JSON body");
25814
+ const unknown = Object.keys(body).find((key) => !["expected_project_revision", "idempotency_key"].includes(key));
25815
+ if (unknown)
25816
+ return error(400, `unknown task-list ensure field: ${unknown}`);
25817
+ if (typeof body.expected_project_revision !== "string" || !body.expected_project_revision.trim()) {
25818
+ return error(400, "expected_project_revision must be a non-empty string from a fresh ensure plan");
25819
+ }
25820
+ if (body.idempotency_key !== undefined && typeof body.idempotency_key !== "string") {
25821
+ return error(400, "idempotency_key must be a string");
25822
+ }
25823
+ const result = await applyProjectTaskListEnsure(store, id, {
25824
+ expected_project_revision: body.expected_project_revision,
25825
+ ...typeof body.idempotency_key === "string" ? { idempotency_key: body.idempotency_key } : {}
25826
+ });
25827
+ return json4(result, result.action === "created" ? 201 : 200);
25828
+ }
25829
+ if (action === "task-list" && subId === "rollback") {
25830
+ if (method !== "POST") {
25831
+ return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/rollback`);
25832
+ }
25833
+ const body = await readJson3(req);
25834
+ if (!body)
25835
+ return error(400, "invalid JSON body");
25836
+ const unknown = Object.keys(body).find((key) => !["receipt_id", "expected_task_list_revision"].includes(key));
25837
+ if (unknown)
25838
+ return error(400, `unknown task-list rollback field: ${unknown}`);
25839
+ if (typeof body.receipt_id !== "string" || !body.receipt_id.trim()) {
25840
+ return error(400, "receipt_id must be a non-empty string");
25841
+ }
25842
+ if (typeof body.expected_task_list_revision !== "string" || !body.expected_task_list_revision.trim()) {
25843
+ return error(400, "expected_task_list_revision must be a non-empty string from the accepted receipt");
25844
+ }
25845
+ return json4(await rollbackProjectTaskListEnsure(store, id, {
25846
+ receipt_id: body.receipt_id,
25847
+ expected_task_list_revision: body.expected_task_list_revision
25848
+ }));
25849
+ }
24779
25850
  if (action === "rename") {
24780
25851
  if (method !== "POST")
24781
25852
  return error(405, `method ${method} not allowed on /v1/projects/:id/rename`);
@@ -25114,6 +26185,10 @@ async function handleV1Request(req, url, dependencies = {}) {
25114
26185
  }
25115
26186
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
25116
26187
  } catch (e) {
26188
+ if (e instanceof ProjectTaskListEnsureError) {
26189
+ 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;
26190
+ return error(status, e.message, { code: e.code, conflict: status === 409, ...e.details });
26191
+ }
25117
26192
  if (e instanceof TaskReferenceAmbiguousError) {
25118
26193
  return error(409, e.message, {
25119
26194
  code: TaskReferenceAmbiguousError.code,
@@ -25140,6 +26215,7 @@ var init_v1 = __esm(() => {
25140
26215
  init_pr_groups();
25141
26216
  init_project_registration();
25142
26217
  init_redaction();
26218
+ init_project_task_list_ensure();
25143
26219
  JSON_HEADERS3 = { "Content-Type": "application/json" };
25144
26220
  });
25145
26221
 
@@ -60082,12 +61158,16 @@ function protectRemoteClient(client) {
60082
61158
  function remoteAuthorityBase(client) {
60083
61159
  return client.baseUrl.replace(/\/v1\/?$/, "");
60084
61160
  }
60085
- async function requiredRemoteRoute(client, route, request) {
61161
+ async function requiredRemoteRoute(client, route, request, recognized404Codes = []) {
60086
61162
  try {
60087
61163
  return await request();
60088
61164
  } catch (error3) {
60089
61165
  const status = error3 && typeof error3 === "object" ? error3.status : undefined;
60090
61166
  if (status === 404) {
61167
+ const body = error3 && typeof error3 === "object" ? error3.body : undefined;
61168
+ const code = body && typeof body === "object" && !Array.isArray(body) ? body.code : undefined;
61169
+ if (typeof code === "string" && recognized404Codes.includes(code))
61170
+ throw error3;
60091
61171
  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: error3 });
60092
61172
  }
60093
61173
  throw error3;
@@ -62790,12 +63870,12 @@ var init_local_notifications = __esm(() => {
62790
63870
  });
62791
63871
 
62792
63872
  // src/lib/local-encryption.ts
62793
- import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes as randomBytes3, scryptSync, timingSafeEqual as timingSafeEqual4 } from "crypto";
63873
+ import { createCipheriv, createDecipheriv, createHash as createHash8, randomBytes as randomBytes3, scryptSync, timingSafeEqual as timingSafeEqual4 } from "crypto";
62794
63874
  function now3() {
62795
63875
  return new Date().toISOString();
62796
63876
  }
62797
63877
  function sha2563(value) {
62798
- return createHash7("sha256").update(value).digest("hex");
63878
+ return createHash8("sha256").update(value).digest("hex");
62799
63879
  }
62800
63880
  function normalizeProfileName(value) {
62801
63881
  const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
@@ -64933,7 +66013,7 @@ var init_capacity_forecasts = __esm(() => {
64933
66013
  });
64934
66014
 
64935
66015
  // src/lib/audit-ledger.ts
64936
- import { createHash as createHash8 } from "crypto";
66016
+ import { createHash as createHash9 } from "crypto";
64937
66017
  function canonicalize2(value) {
64938
66018
  if (value === null || typeof value !== "object")
64939
66019
  return JSON.stringify(value);
@@ -64943,7 +66023,7 @@ function canonicalize2(value) {
64943
66023
  return `{${Object.keys(object4).sort().map((key) => `${JSON.stringify(key)}:${canonicalize2(object4[key])}`).join(",")}}`;
64944
66024
  }
64945
66025
  function hash(value) {
64946
- return createHash8("sha256").update(value).digest("hex");
66026
+ return createHash9("sha256").update(value).digest("hex");
64947
66027
  }
64948
66028
  function parsePayload3(value) {
64949
66029
  if (!value)
@@ -98044,7 +99124,7 @@ var init_local_bridge = __esm(() => {
98044
99124
  });
98045
99125
 
98046
99126
  // src/lib/local-backups.ts
98047
- import { createHash as createHash9 } from "crypto";
99127
+ import { createHash as createHash10 } from "crypto";
98048
99128
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
98049
99129
  import { dirname as dirname7, resolve as resolve13 } from "path";
98050
99130
  import { mkdirSync as mkdirSync6 } from "fs";
@@ -98057,7 +99137,7 @@ function stableJson2(value) {
98057
99137
  return `{${Object.keys(record3).sort().map((key) => `${JSON.stringify(key)}:${stableJson2(record3[key])}`).join(",")}}`;
98058
99138
  }
98059
99139
  function sha2564(value) {
98060
- return createHash9("sha256").update(stableJson2(value)).digest("hex");
99140
+ return createHash10("sha256").update(stableJson2(value)).digest("hex");
98061
99141
  }
98062
99142
  function sqliteIntegrity(db) {
98063
99143
  let quick = "unknown";
@@ -98707,7 +99787,7 @@ var init_onboarding_fixtures = __esm(() => {
98707
99787
  });
98708
99788
 
98709
99789
  // src/lib/local-snapshots.ts
98710
- import { createHash as createHash10 } from "crypto";
99790
+ import { createHash as createHash11 } from "crypto";
98711
99791
  function source(version2) {
98712
99792
  return {
98713
99793
  packageName: "@hasna/todos",
@@ -98731,7 +99811,7 @@ function stable(value) {
98731
99811
  return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stable(item)]));
98732
99812
  }
98733
99813
  function sha2565(value) {
98734
- return createHash10("sha256").update(JSON.stringify(stable(value))).digest("hex");
99814
+ return createHash11("sha256").update(JSON.stringify(stable(value))).digest("hex");
98735
99815
  }
98736
99816
  function latestTimestamp2(items, fallback) {
98737
99817
  const timestamps = [];
@@ -99297,7 +100377,7 @@ var init_retrospectives = __esm(() => {
99297
100377
  });
99298
100378
 
99299
100379
  // src/lib/agent-replay-simulator.ts
99300
- import { createHash as createHash11 } from "crypto";
100380
+ import { createHash as createHash12 } from "crypto";
99301
100381
  function isObject2(value) {
99302
100382
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
99303
100383
  }
@@ -99318,7 +100398,7 @@ function stable2(value) {
99318
100398
  return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
99319
100399
  }
99320
100400
  function fingerprint2(value) {
99321
- return createHash11("sha256").update(JSON.stringify(stable2(value))).digest("hex");
100401
+ return createHash12("sha256").update(JSON.stringify(stable2(value))).digest("hex");
99322
100402
  }
99323
100403
  function unpackFixture(input) {
99324
100404
  if (!isObject2(input))
@@ -99605,7 +100685,7 @@ __export(exports_local_extensions, {
99605
100685
  getLocalExtension: () => getLocalExtension,
99606
100686
  discoverLocalExtensions: () => discoverLocalExtensions
99607
100687
  });
99608
- import { createHash as createHash12, createVerify } from "crypto";
100688
+ import { createHash as createHash13, createVerify } from "crypto";
99609
100689
  import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
99610
100690
  import { basename as basename5, join as join12, resolve as resolve14 } from "path";
99611
100691
  function isObject3(value) {
@@ -99691,7 +100771,7 @@ function parseJson(path) {
99691
100771
  return JSON.parse(readFileSync9(path, "utf8"));
99692
100772
  }
99693
100773
  function sha2566(bytes) {
99694
- return `sha256:${createHash12("sha256").update(bytes).digest("hex")}`;
100774
+ return `sha256:${createHash13("sha256").update(bytes).digest("hex")}`;
99695
100775
  }
99696
100776
  function compareVersions(a, b) {
99697
100777
  const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -100166,7 +101246,7 @@ function issueToTask(issue2, opts) {
100166
101246
  var init_github = () => {};
100167
101247
 
100168
101248
  // src/db/inbox.ts
100169
- import { createHash as createHash13 } from "crypto";
101249
+ import { createHash as createHash14 } from "crypto";
100170
101250
  function parseMetadata3(value) {
100171
101251
  if (!value)
100172
101252
  return {};
@@ -100186,7 +101266,7 @@ function compactWhitespace(value) {
100186
101266
  function fingerprintInboxInput(input) {
100187
101267
  const sourceType = input.source_type || detectInboxSourceType(input.body, input.source_url);
100188
101268
  const normalized = compactWhitespace(sanitizePreWriteText(input.body, "inbox.fingerprint")).slice(0, 8000);
100189
- return createHash13("sha256").update(`${sourceType}
101269
+ return createHash14("sha256").update(`${sourceType}
100190
101270
  ${input.source_url || ""}
100191
101271
  ${normalized}`).digest("hex");
100192
101272
  }
@@ -104386,10 +105466,10 @@ __export(exports_extract, {
104386
105466
  EXTRACT_TAGS: () => EXTRACT_TAGS
104387
105467
  });
104388
105468
  import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
104389
- import { createHash as createHash14 } from "crypto";
105469
+ import { createHash as createHash15 } from "crypto";
104390
105470
  import { relative as relative5, resolve as resolve15, join as join13 } from "path";
104391
105471
  function stableHash(value) {
104392
- return createHash14("sha256").update(value).digest("hex");
105472
+ return createHash15("sha256").update(value).digest("hex");
104393
105473
  }
104394
105474
  function normalizePathForMatch(value) {
104395
105475
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -106113,13 +107193,13 @@ __export(exports_environment_snapshots, {
106113
107193
  compareEnvironmentSnapshotFiles: () => compareEnvironmentSnapshotFiles,
106114
107194
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
106115
107195
  });
106116
- import { createHash as createHash15 } from "crypto";
107196
+ import { createHash as createHash16 } from "crypto";
106117
107197
  import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
106118
107198
  import { hostname as hostname3, platform, arch } from "os";
106119
107199
  import { dirname as dirname8, join as join15, resolve as resolve16 } from "path";
106120
107200
  import { tmpdir as tmpdir3 } from "os";
106121
107201
  function sha2567(value) {
106122
- return createHash15("sha256").update(value).digest("hex");
107202
+ return createHash16("sha256").update(value).digest("hex");
106123
107203
  }
106124
107204
  function fileRecord(root, relativePath) {
106125
107205
  const path = join15(root, relativePath);
@@ -106234,8 +107314,8 @@ function defaultSnapshotDir() {
106234
107314
  return join15(dirname8(resolve16(dbPath)), "environment-snapshots");
106235
107315
  }
106236
107316
  function snapshotWithId(snapshot) {
106237
- const digest = sha2567(JSON.stringify(snapshot)).slice(0, 24);
106238
- return { id: `env_${digest}`, ...snapshot };
107317
+ const digest2 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
107318
+ return { id: `env_${digest2}`, ...snapshot };
106239
107319
  }
106240
107320
  function captureEnvironmentSnapshot(input = {}) {
106241
107321
  const root = resolve16(input.root || process.cwd());