@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/server/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
106238
|
-
return { id: `env_${
|
|
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());
|