@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/cli/index.js
CHANGED
|
@@ -2123,7 +2123,7 @@ var package_default;
|
|
|
2123
2123
|
var init_package = __esm(() => {
|
|
2124
2124
|
package_default = {
|
|
2125
2125
|
name: "@hasna/todos",
|
|
2126
|
-
version: "0.15.
|
|
2126
|
+
version: "0.15.9",
|
|
2127
2127
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
2128
2128
|
type: "module",
|
|
2129
2129
|
main: "dist/index.js",
|
|
@@ -5273,12 +5273,16 @@ function protectRemoteClient(client) {
|
|
|
5273
5273
|
function remoteAuthorityBase(client) {
|
|
5274
5274
|
return client.baseUrl.replace(/\/v1\/?$/, "");
|
|
5275
5275
|
}
|
|
5276
|
-
async function requiredRemoteRoute(client, route, request) {
|
|
5276
|
+
async function requiredRemoteRoute(client, route, request, recognized404Codes = []) {
|
|
5277
5277
|
try {
|
|
5278
5278
|
return await request();
|
|
5279
5279
|
} catch (error) {
|
|
5280
5280
|
const status = error && typeof error === "object" ? error.status : undefined;
|
|
5281
5281
|
if (status === 404) {
|
|
5282
|
+
const body = error && typeof error === "object" ? error.body : undefined;
|
|
5283
|
+
const code = body && typeof body === "object" && !Array.isArray(body) ? body.code : undefined;
|
|
5284
|
+
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
5285
|
+
throw error;
|
|
5282
5286
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} does not expose ${route}; ` + "deploy the @hasna/todos /v1 server contract before retrying; local SQLite fallback is disabled", { cause: error });
|
|
5283
5287
|
}
|
|
5284
5288
|
throw error;
|
|
@@ -5693,6 +5697,15 @@ async function cloudDeleteProject(client, id) {
|
|
|
5693
5697
|
}
|
|
5694
5698
|
return true;
|
|
5695
5699
|
}
|
|
5700
|
+
async function cloudPlanProjectTaskListEnsure(client, projectId) {
|
|
5701
|
+
return requiredRemoteRoute(client, "/v1/projects/:id/task-list/ensure", () => client.transport.get(`/projects/${encodeURIComponent(projectId)}/task-list/ensure`), ["PROJECT_NOT_FOUND"]);
|
|
5702
|
+
}
|
|
5703
|
+
async function cloudApplyProjectTaskListEnsure(client, projectId, input) {
|
|
5704
|
+
return requiredRemoteRoute(client, "/v1/projects/:id/task-list/ensure", () => client.transport.post(`/projects/${encodeURIComponent(projectId)}/task-list/ensure`, input), ["PROJECT_NOT_FOUND"]);
|
|
5705
|
+
}
|
|
5706
|
+
async function cloudRollbackProjectTaskListEnsure(client, projectId, input) {
|
|
5707
|
+
return requiredRemoteRoute(client, "/v1/projects/:id/task-list/rollback", () => client.transport.post(`/projects/${encodeURIComponent(projectId)}/task-list/rollback`, input), ["PROJECT_NOT_FOUND", "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND"]);
|
|
5708
|
+
}
|
|
5696
5709
|
async function cloudListPlans(client, projectId) {
|
|
5697
5710
|
const query = projectId ? { project_id: projectId } : {};
|
|
5698
5711
|
const res = await requiredRemoteRoute(client, "/v1/plans", () => client.list("plans", { query }));
|
|
@@ -13716,6 +13729,40 @@ function deleteTaskList(id, db) {
|
|
|
13716
13729
|
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
13717
13730
|
})();
|
|
13718
13731
|
}
|
|
13732
|
+
function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
|
|
13733
|
+
const d = db || getDatabase();
|
|
13734
|
+
return d.transaction(() => {
|
|
13735
|
+
const current = getTaskList(id, d);
|
|
13736
|
+
if (!current) {
|
|
13737
|
+
return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
|
|
13738
|
+
}
|
|
13739
|
+
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);
|
|
13740
|
+
if (changed) {
|
|
13741
|
+
return { status: "changed", task_dependents: 0, plan_dependents: 0 };
|
|
13742
|
+
}
|
|
13743
|
+
const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
|
|
13744
|
+
const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
|
|
13745
|
+
if (taskDependents > 0 || planDependents > 0) {
|
|
13746
|
+
return {
|
|
13747
|
+
status: "has_dependents",
|
|
13748
|
+
task_dependents: taskDependents,
|
|
13749
|
+
plan_dependents: planDependents
|
|
13750
|
+
};
|
|
13751
|
+
}
|
|
13752
|
+
recordStorageTombstone({
|
|
13753
|
+
object_type: "task_lists",
|
|
13754
|
+
object_id: id,
|
|
13755
|
+
payload: current
|
|
13756
|
+
}, d);
|
|
13757
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
13758
|
+
const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
13759
|
+
return {
|
|
13760
|
+
status: deleted ? "deleted" : "not_found",
|
|
13761
|
+
task_dependents: 0,
|
|
13762
|
+
plan_dependents: 0
|
|
13763
|
+
};
|
|
13764
|
+
})();
|
|
13765
|
+
}
|
|
13719
13766
|
function ensureTaskList(name, slug, projectId, db) {
|
|
13720
13767
|
const d = db || getDatabase();
|
|
13721
13768
|
const existing = getTaskListBySlug(slug, projectId, d);
|
|
@@ -19673,6 +19720,23 @@ function normalizeAgentNameInput(name) {
|
|
|
19673
19720
|
import { readFileSync as readFileSync4 } from "fs";
|
|
19674
19721
|
import { homedir as homedir3 } from "os";
|
|
19675
19722
|
import { join as join8 } from "path";
|
|
19723
|
+
function describeAssigneeFilter(input, ctx) {
|
|
19724
|
+
const raw = input.trim();
|
|
19725
|
+
const normalized = normalizeAgentNameInput(raw);
|
|
19726
|
+
if (!normalized)
|
|
19727
|
+
return { kind: "ok" };
|
|
19728
|
+
if (ctx.agents.some((a) => a.id.toLowerCase() === normalized))
|
|
19729
|
+
return { kind: "ok" };
|
|
19730
|
+
const byName = ctx.agents.filter((a) => normalizeAgentNameInput(a.name) === normalized);
|
|
19731
|
+
if (byName.length <= 1)
|
|
19732
|
+
return { kind: "ok" };
|
|
19733
|
+
const ids = byName.map((a) => a.id).sort();
|
|
19734
|
+
return {
|
|
19735
|
+
kind: "ambiguous",
|
|
19736
|
+
candidates: byName,
|
|
19737
|
+
message: `'${raw}' names ${byName.length} registered agents (${ids.join(", ")}), so this result is INCOMPLETE \u2014 ` + `rows whose assignee was stored as one of the other ids are missing from it. ` + `Re-run with an agent ID instead to get that agent's full queue.`
|
|
19738
|
+
};
|
|
19739
|
+
}
|
|
19676
19740
|
function defaultSeatRosterPath() {
|
|
19677
19741
|
return process.env["TODOS_SEAT_ROSTER_PATH"] || join8(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
|
|
19678
19742
|
}
|
|
@@ -21337,6 +21401,7 @@ function registerTaskCommands(program2) {
|
|
|
21337
21401
|
}
|
|
21338
21402
|
if (assignedFilter !== undefined)
|
|
21339
21403
|
filter["assigned_to"] = assignedFilter;
|
|
21404
|
+
const rosterPromise = assignedWasTyped && assignedFilter ? loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), true).catch(() => ({ agents: [], seats: new Set, allowSeat: true, degraded: true })) : undefined;
|
|
21340
21405
|
if (opts.recurring)
|
|
21341
21406
|
filter["has_recurrence"] = true;
|
|
21342
21407
|
if (opts.limit !== undefined) {
|
|
@@ -21407,15 +21472,22 @@ function registerTaskCommands(program2) {
|
|
|
21407
21472
|
}
|
|
21408
21473
|
if (withholdLimit && requestedLimit !== undefined)
|
|
21409
21474
|
tasks = tasks.slice(0, requestedLimit);
|
|
21410
|
-
if (
|
|
21475
|
+
if (rosterPromise && assignedFilter) {
|
|
21411
21476
|
try {
|
|
21412
|
-
const roster = await
|
|
21477
|
+
const roster = await rosterPromise;
|
|
21413
21478
|
if (!roster.degraded) {
|
|
21414
|
-
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
|
|
21479
|
+
if (tasks.length === 0) {
|
|
21480
|
+
const target = canonicalAgentRef(assignedFilter);
|
|
21481
|
+
const known = roster.agents.some((a) => canonicalAgentRef(a.name) === target || canonicalAgentRef(a.id) === target);
|
|
21482
|
+
if (!known) {
|
|
21483
|
+
console.error(chalk3.yellow(`Warning: no agent named '${assignedFilter}' is registered, so this empty result may be a
|
|
21418
21484
|
` + ` mistyped name rather than an empty queue. Check with 'todos agents'.`));
|
|
21485
|
+
}
|
|
21486
|
+
} else {
|
|
21487
|
+
const notice = describeAssigneeFilter(assignedFilter, { agents: roster.agents });
|
|
21488
|
+
if (notice.kind === "ambiguous") {
|
|
21489
|
+
console.error(chalk3.yellow(`Warning: ${notice.message}`));
|
|
21490
|
+
}
|
|
21419
21491
|
}
|
|
21420
21492
|
}
|
|
21421
21493
|
} catch {}
|
|
@@ -22333,6 +22405,7 @@ var init_task_commands = __esm(() => {
|
|
|
22333
22405
|
init_claim_guard();
|
|
22334
22406
|
init_assignee_guard();
|
|
22335
22407
|
init_assignee_context();
|
|
22408
|
+
init_assignee_validation();
|
|
22336
22409
|
init_agents();
|
|
22337
22410
|
init_helpers();
|
|
22338
22411
|
init_output_redaction();
|
|
@@ -25036,267 +25109,1365 @@ var init_sync = __esm(() => {
|
|
|
25036
25109
|
init_config();
|
|
25037
25110
|
});
|
|
25038
25111
|
|
|
25039
|
-
// src/lib/
|
|
25040
|
-
|
|
25041
|
-
|
|
25042
|
-
|
|
25043
|
-
|
|
25044
|
-
|
|
25045
|
-
|
|
25046
|
-
import { basename as basename4, dirname as dirname6, resolve as resolve11 } from "path";
|
|
25047
|
-
function safeStat(path) {
|
|
25048
|
-
try {
|
|
25049
|
-
return statSync4(path);
|
|
25050
|
-
} catch {
|
|
25051
|
-
return null;
|
|
25052
|
-
}
|
|
25053
|
-
}
|
|
25054
|
-
function canonicalPath(input) {
|
|
25055
|
-
const resolved = resolve11(input);
|
|
25056
|
-
const stats = safeStat(resolved);
|
|
25057
|
-
if (stats?.isFile())
|
|
25058
|
-
return dirname6(resolved);
|
|
25059
|
-
return resolved;
|
|
25060
|
-
}
|
|
25061
|
-
function findUp(start, marker) {
|
|
25062
|
-
let current = canonicalPath(start);
|
|
25063
|
-
while (true) {
|
|
25064
|
-
if (existsSync13(resolve11(current, marker)))
|
|
25065
|
-
return current;
|
|
25066
|
-
const parent = dirname6(current);
|
|
25067
|
-
if (parent === current)
|
|
25068
|
-
return null;
|
|
25069
|
-
current = parent;
|
|
25070
|
-
}
|
|
25112
|
+
// src/lib/integrity.ts
|
|
25113
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
25114
|
+
if (spec.kind === "dangling")
|
|
25115
|
+
return "error";
|
|
25116
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
25117
|
+
return "error";
|
|
25118
|
+
return spec.base_severity;
|
|
25071
25119
|
}
|
|
25072
|
-
function
|
|
25073
|
-
|
|
25074
|
-
|
|
25075
|
-
const file = resolve11(path, "package.json");
|
|
25076
|
-
if (!existsSync13(file))
|
|
25077
|
-
return null;
|
|
25078
|
-
try {
|
|
25079
|
-
const parsed = JSON.parse(readFileSync7(file, "utf-8"));
|
|
25080
|
-
return parsed && typeof parsed === "object" ? parsed : null;
|
|
25081
|
-
} catch {
|
|
25082
|
-
return null;
|
|
25083
|
-
}
|
|
25120
|
+
function plural(entity, count) {
|
|
25121
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
25122
|
+
return count === 1 ? one : many;
|
|
25084
25123
|
}
|
|
25085
|
-
function
|
|
25086
|
-
|
|
25087
|
-
|
|
25088
|
-
const
|
|
25089
|
-
return
|
|
25124
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
25125
|
+
const noun = plural(spec.entity, measurement.count);
|
|
25126
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
25127
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
25128
|
+
return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
|
|
25090
25129
|
}
|
|
25091
|
-
function
|
|
25092
|
-
|
|
25093
|
-
|
|
25094
|
-
|
|
25095
|
-
|
|
25096
|
-
|
|
25097
|
-
|
|
25098
|
-
|
|
25099
|
-
|
|
25100
|
-
|
|
25101
|
-
|
|
25102
|
-
|
|
25130
|
+
function measuredCondition(spec, measurement, source) {
|
|
25131
|
+
return {
|
|
25132
|
+
id: spec.id,
|
|
25133
|
+
entity: spec.entity,
|
|
25134
|
+
field: spec.field,
|
|
25135
|
+
kind: spec.kind,
|
|
25136
|
+
count: measurement.count,
|
|
25137
|
+
open_count: measurement.open_count,
|
|
25138
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
25139
|
+
verified: true,
|
|
25140
|
+
source,
|
|
25141
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
25142
|
+
impact: spec.impact
|
|
25143
|
+
};
|
|
25103
25144
|
}
|
|
25104
|
-
function
|
|
25105
|
-
const input = canonicalPath(inputPath);
|
|
25106
|
-
const gitRoot = findUp(input, ".git");
|
|
25107
|
-
const packageRoot = findUp(input, "package.json");
|
|
25108
|
-
const rootPackage = readPackageJson(gitRoot);
|
|
25109
|
-
const packageMeta = readPackageJson(packageRoot);
|
|
25110
|
-
const workspace = workspaceMarker(gitRoot, rootPackage);
|
|
25111
|
-
const monorepo = Boolean(gitRoot && packageRoot && packageRoot !== gitRoot && workspace.kind);
|
|
25112
|
-
const projectPath = monorepo ? packageRoot : gitRoot ?? packageRoot ?? input;
|
|
25113
|
-
const projectName = packageDisplayName(packageMeta?.name ?? rootPackage?.name ?? null, projectPath);
|
|
25145
|
+
function unverifiedCondition(spec, reason) {
|
|
25114
25146
|
return {
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
|
|
25119
|
-
|
|
25120
|
-
|
|
25121
|
-
|
|
25122
|
-
|
|
25123
|
-
|
|
25124
|
-
|
|
25147
|
+
id: spec.id,
|
|
25148
|
+
entity: spec.entity,
|
|
25149
|
+
field: spec.field,
|
|
25150
|
+
kind: spec.kind,
|
|
25151
|
+
count: null,
|
|
25152
|
+
open_count: null,
|
|
25153
|
+
severity: null,
|
|
25154
|
+
verified: false,
|
|
25155
|
+
source: "unverified",
|
|
25156
|
+
unverified_reason: reason,
|
|
25157
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
25158
|
+
impact: spec.impact
|
|
25125
25159
|
};
|
|
25126
25160
|
}
|
|
25127
|
-
function
|
|
25128
|
-
|
|
25161
|
+
function summarizeIntegrity(conditions) {
|
|
25162
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
25163
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
25164
|
+
const unverified = conditions.length - measured.length;
|
|
25165
|
+
return {
|
|
25166
|
+
ok: unverified === 0 && findings.length === 0,
|
|
25167
|
+
findings: findings.length,
|
|
25168
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
25169
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
25170
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
25171
|
+
unverified,
|
|
25172
|
+
complete: unverified === 0
|
|
25173
|
+
};
|
|
25129
25174
|
}
|
|
25130
|
-
function
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25175
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
25176
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
25177
|
+
return {
|
|
25178
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
25179
|
+
generated_at: generatedAt,
|
|
25180
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
25181
|
+
conditions,
|
|
25182
|
+
summary: summarizeIntegrity(conditions)
|
|
25183
|
+
};
|
|
25134
25184
|
}
|
|
25135
|
-
function
|
|
25136
|
-
const
|
|
25137
|
-
|
|
25138
|
-
|
|
25139
|
-
|
|
25140
|
-
|
|
25141
|
-
dryRun: true,
|
|
25142
|
-
discovery: { ...discovery, projectName: options.name || discovery.projectName },
|
|
25143
|
-
project: null,
|
|
25144
|
-
taskList: null,
|
|
25145
|
-
sources: [],
|
|
25146
|
-
created: { project: false, taskList: false, sources: [] }
|
|
25147
|
-
};
|
|
25148
|
-
}
|
|
25149
|
-
const beforeProject = getProjectByCanonicalPath(discovery.projectPath, d);
|
|
25150
|
-
let project = ensureProject(options.name || discovery.projectName, discovery.projectPath, d);
|
|
25151
|
-
const createdProject = !beforeProject;
|
|
25152
|
-
if (project.task_list_id !== taskListSlug || options.name && project.name !== options.name) {
|
|
25153
|
-
project = renameProject(project.id, {
|
|
25154
|
-
name: options.name ?? project.name,
|
|
25155
|
-
new_slug: taskListSlug
|
|
25156
|
-
}, d).project;
|
|
25157
|
-
}
|
|
25158
|
-
setMachineLocalPath(project.id, discovery.projectPath, d);
|
|
25159
|
-
const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug);
|
|
25160
|
-
let taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
|
|
25161
|
-
if (options.routeEnabled && taskList.metadata.route_enabled !== true) {
|
|
25162
|
-
taskList = updateTaskList(taskList.id, {
|
|
25163
|
-
metadata: {
|
|
25164
|
-
...taskList.metadata,
|
|
25165
|
-
route_enabled: true,
|
|
25166
|
-
automation: {
|
|
25167
|
-
...taskList.metadata.automation && typeof taskList.metadata.automation === "object" && !Array.isArray(taskList.metadata.automation) ? taskList.metadata.automation : {},
|
|
25168
|
-
no_auto: false
|
|
25169
|
-
}
|
|
25170
|
-
}
|
|
25171
|
-
}, d);
|
|
25172
|
-
}
|
|
25173
|
-
const createdSources = [];
|
|
25174
|
-
for (const source of [
|
|
25175
|
-
addSourceOnce(project.id, "local", "Project root", discovery.projectPath, { role: "project-root" }, d),
|
|
25176
|
-
addSourceOnce(project.id, "git", "Git root", discovery.gitRoot, { role: "git-root" }, d),
|
|
25177
|
-
addSourceOnce(project.id, "workspace", "Workspace root", discovery.workspaceRoot, {
|
|
25178
|
-
role: "workspace-root",
|
|
25179
|
-
kind: discovery.workspaceKind,
|
|
25180
|
-
markers: discovery.markers,
|
|
25181
|
-
monorepo: discovery.monorepo
|
|
25182
|
-
}, d)
|
|
25183
|
-
]) {
|
|
25184
|
-
if (source) {
|
|
25185
|
-
createdSources.push(source.type);
|
|
25185
|
+
function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
|
|
25186
|
+
const received = new Map;
|
|
25187
|
+
if (Array.isArray(raw.conditions)) {
|
|
25188
|
+
for (const entry of raw.conditions) {
|
|
25189
|
+
if (entry && typeof entry["id"] === "string")
|
|
25190
|
+
received.set(entry["id"], entry);
|
|
25186
25191
|
}
|
|
25187
25192
|
}
|
|
25188
|
-
|
|
25189
|
-
|
|
25190
|
-
|
|
25191
|
-
|
|
25192
|
-
|
|
25193
|
-
|
|
25194
|
-
created: {
|
|
25195
|
-
project: createdProject,
|
|
25196
|
-
taskList: !beforeTaskList,
|
|
25197
|
-
sources: createdSources
|
|
25193
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
25194
|
+
const entry = received.get(spec.id);
|
|
25195
|
+
const count = entry?.["count"];
|
|
25196
|
+
const verified = entry?.["verified"];
|
|
25197
|
+
if (!entry || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
|
|
25198
|
+
return unverifiedCondition(spec, entry ? typeof entry["unverified_reason"] === "string" ? String(entry["unverified_reason"]) : "authority reported this condition without a usable count" : "authority did not report this condition");
|
|
25198
25199
|
}
|
|
25199
|
-
|
|
25200
|
+
const openRaw = entry["open_count"];
|
|
25201
|
+
return measuredCondition(spec, {
|
|
25202
|
+
count,
|
|
25203
|
+
open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
|
|
25204
|
+
}, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
|
|
25205
|
+
});
|
|
25206
|
+
const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
|
|
25207
|
+
return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
|
|
25200
25208
|
}
|
|
25201
|
-
function
|
|
25202
|
-
return
|
|
25209
|
+
function sqliteOpenStatusList() {
|
|
25210
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
25203
25211
|
}
|
|
25204
|
-
function
|
|
25205
|
-
|
|
25212
|
+
function buildSqliteIntegritySql(spec) {
|
|
25213
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
25214
|
+
const target = SQLITE_TABLE[spec.target];
|
|
25215
|
+
const column = `t."${spec.field}"`;
|
|
25216
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
|
|
25217
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
25218
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
25206
25219
|
}
|
|
25207
|
-
function
|
|
25208
|
-
const
|
|
25209
|
-
|
|
25210
|
-
|
|
25211
|
-
|
|
25212
|
-
|
|
25213
|
-
|
|
25214
|
-
|
|
25220
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
25221
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
25222
|
+
const p = (value) => {
|
|
25223
|
+
params.push(value);
|
|
25224
|
+
return `$${params.length}`;
|
|
25225
|
+
};
|
|
25226
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
25227
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
|
|
25228
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
25229
|
+
const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
|
|
25230
|
+
return { sql, params };
|
|
25215
25231
|
}
|
|
25216
|
-
|
|
25217
|
-
|
|
25218
|
-
|
|
25219
|
-
|
|
25220
|
-
|
|
25221
|
-
|
|
25222
|
-
// src/lib/project-panel.ts
|
|
25223
|
-
var exports_project_panel = {};
|
|
25224
|
-
__export(exports_project_panel, {
|
|
25225
|
-
createTodosProjectPanel: () => createTodosProjectPanel
|
|
25226
|
-
});
|
|
25227
|
-
import {
|
|
25228
|
-
parseContract,
|
|
25229
|
-
SCHEMA_IDS
|
|
25230
|
-
} from "@hasna/contracts";
|
|
25231
|
-
function clampLimit(limit) {
|
|
25232
|
-
if (!Number.isFinite(limit ?? 0))
|
|
25233
|
-
return 20;
|
|
25234
|
-
return Math.max(1, Math.min(100, Math.trunc(limit ?? 20)));
|
|
25232
|
+
function referenceOf(row, field) {
|
|
25233
|
+
const raw = row[field];
|
|
25234
|
+
if (typeof raw !== "string")
|
|
25235
|
+
return null;
|
|
25236
|
+
const trimmed = raw.trim();
|
|
25237
|
+
return trimmed === "" ? null : trimmed;
|
|
25235
25238
|
}
|
|
25236
|
-
function
|
|
25237
|
-
|
|
25239
|
+
function measureIntegrityRows(spec, sets) {
|
|
25240
|
+
const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
|
|
25241
|
+
if (!rows)
|
|
25242
|
+
return null;
|
|
25243
|
+
const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
|
|
25244
|
+
if (spec.kind === "dangling" && !registered)
|
|
25245
|
+
return null;
|
|
25246
|
+
let count = 0;
|
|
25247
|
+
let open = 0;
|
|
25248
|
+
for (const row of rows) {
|
|
25249
|
+
const reference = referenceOf(row, spec.field);
|
|
25250
|
+
const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
|
|
25251
|
+
if (!matches)
|
|
25252
|
+
continue;
|
|
25253
|
+
count++;
|
|
25254
|
+
if (spec.entity === "task") {
|
|
25255
|
+
const status = row.status;
|
|
25256
|
+
if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
|
|
25257
|
+
open++;
|
|
25258
|
+
}
|
|
25259
|
+
}
|
|
25260
|
+
return { count, open_count: spec.entity === "task" ? open : null };
|
|
25238
25261
|
}
|
|
25239
|
-
|
|
25240
|
-
|
|
25241
|
-
|
|
25242
|
-
|
|
25243
|
-
|
|
25244
|
-
|
|
25245
|
-
|
|
25246
|
-
|
|
25247
|
-
|
|
25262
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
|
|
25263
|
+
var init_integrity = __esm(() => {
|
|
25264
|
+
init_types();
|
|
25265
|
+
TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
25266
|
+
OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
25267
|
+
INTEGRITY_CONDITIONS = [
|
|
25268
|
+
{
|
|
25269
|
+
id: "tasks_without_project",
|
|
25270
|
+
entity: "task",
|
|
25271
|
+
field: "project_id",
|
|
25272
|
+
target: "project",
|
|
25273
|
+
kind: "missing",
|
|
25274
|
+
base_severity: "warn",
|
|
25275
|
+
escalate_when_open: true,
|
|
25276
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
25277
|
+
},
|
|
25278
|
+
{
|
|
25279
|
+
id: "tasks_without_task_list",
|
|
25280
|
+
entity: "task",
|
|
25281
|
+
field: "task_list_id",
|
|
25282
|
+
target: "task_list",
|
|
25283
|
+
kind: "missing",
|
|
25284
|
+
base_severity: "warn",
|
|
25285
|
+
escalate_when_open: true,
|
|
25286
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
25287
|
+
},
|
|
25288
|
+
{
|
|
25289
|
+
id: "tasks_with_unregistered_project",
|
|
25290
|
+
entity: "task",
|
|
25291
|
+
field: "project_id",
|
|
25292
|
+
target: "project",
|
|
25293
|
+
kind: "dangling",
|
|
25294
|
+
base_severity: "error",
|
|
25295
|
+
escalate_when_open: false,
|
|
25296
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
25297
|
+
},
|
|
25298
|
+
{
|
|
25299
|
+
id: "tasks_with_unregistered_task_list",
|
|
25300
|
+
entity: "task",
|
|
25301
|
+
field: "task_list_id",
|
|
25302
|
+
target: "task_list",
|
|
25303
|
+
kind: "dangling",
|
|
25304
|
+
base_severity: "error",
|
|
25305
|
+
escalate_when_open: false,
|
|
25306
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
25307
|
+
},
|
|
25308
|
+
{
|
|
25309
|
+
id: "task_lists_without_project",
|
|
25310
|
+
entity: "task_list",
|
|
25311
|
+
field: "project_id",
|
|
25312
|
+
target: "project",
|
|
25313
|
+
kind: "missing",
|
|
25314
|
+
base_severity: "warn",
|
|
25315
|
+
escalate_when_open: false,
|
|
25316
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
25317
|
+
},
|
|
25318
|
+
{
|
|
25319
|
+
id: "task_lists_with_unregistered_project",
|
|
25320
|
+
entity: "task_list",
|
|
25321
|
+
field: "project_id",
|
|
25322
|
+
target: "project",
|
|
25323
|
+
kind: "dangling",
|
|
25324
|
+
base_severity: "error",
|
|
25325
|
+
escalate_when_open: false,
|
|
25326
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
25327
|
+
}
|
|
25328
|
+
];
|
|
25329
|
+
ENTITY_LABEL = {
|
|
25330
|
+
task: ["task", "tasks"],
|
|
25331
|
+
task_list: ["task list", "task lists"]
|
|
25248
25332
|
};
|
|
25249
|
-
|
|
25250
|
-
|
|
25251
|
-
|
|
25252
|
-
|
|
25253
|
-
id,
|
|
25254
|
-
name: name ?? undefined,
|
|
25255
|
-
externalId: id,
|
|
25256
|
-
sourcePackage: SOURCE_PACKAGE
|
|
25333
|
+
SQLITE_TABLE = {
|
|
25334
|
+
task: "tasks",
|
|
25335
|
+
task_list: "task_lists",
|
|
25336
|
+
project: "projects"
|
|
25257
25337
|
};
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
kind: "project",
|
|
25263
|
-
id: projectSlug,
|
|
25264
|
-
name: project.name,
|
|
25265
|
-
uri: `project://${projectSlug}`,
|
|
25266
|
-
externalId: project.id,
|
|
25267
|
-
sourcePackage: SOURCE_PACKAGE
|
|
25338
|
+
RECORD_TYPE = {
|
|
25339
|
+
task: "tasks",
|
|
25340
|
+
task_list: "task_lists",
|
|
25341
|
+
project: "projects"
|
|
25268
25342
|
};
|
|
25343
|
+
});
|
|
25344
|
+
|
|
25345
|
+
// src/db/integrity.ts
|
|
25346
|
+
function tableExists(db, table) {
|
|
25347
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
25269
25348
|
}
|
|
25270
|
-
function
|
|
25271
|
-
const
|
|
25272
|
-
|
|
25349
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
25350
|
+
const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
|
|
25351
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
25352
|
+
if (missing.length > 0) {
|
|
25353
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
25354
|
+
}
|
|
25355
|
+
try {
|
|
25356
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
25357
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
25358
|
+
} catch (error) {
|
|
25359
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
25360
|
+
}
|
|
25361
|
+
});
|
|
25362
|
+
return buildIntegrityReport(conditions, now());
|
|
25273
25363
|
}
|
|
25274
|
-
|
|
25364
|
+
var REQUIRED_TABLES;
|
|
25365
|
+
var init_integrity2 = __esm(() => {
|
|
25366
|
+
init_database();
|
|
25367
|
+
init_integrity();
|
|
25368
|
+
REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
|
|
25369
|
+
});
|
|
25370
|
+
|
|
25371
|
+
// src/storage/sqlite-snapshot.ts
|
|
25372
|
+
function exportSqliteTodosStorageSnapshot(db) {
|
|
25373
|
+
const d = db ?? getDatabase();
|
|
25275
25374
|
return {
|
|
25276
|
-
|
|
25277
|
-
|
|
25278
|
-
|
|
25279
|
-
|
|
25280
|
-
|
|
25375
|
+
exportedAt: new Date().toISOString(),
|
|
25376
|
+
source: "sqlite",
|
|
25377
|
+
tasks: listTasks({ include_archived: true }, d),
|
|
25378
|
+
projects: listProjects(d),
|
|
25379
|
+
projectMachinePaths: listProjectMachinePaths(d),
|
|
25380
|
+
plans: listPlans(undefined, d),
|
|
25381
|
+
agents: listAgents({ include_archived: true }, d),
|
|
25382
|
+
taskLists: listTaskLists(undefined, d),
|
|
25383
|
+
templates: listTemplates(d),
|
|
25384
|
+
templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
|
|
25385
|
+
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
|
|
25386
|
+
tombstones: listStorageTombstones(d)
|
|
25281
25387
|
};
|
|
25282
25388
|
}
|
|
25283
|
-
function
|
|
25284
|
-
|
|
25285
|
-
|
|
25286
|
-
|
|
25287
|
-
|
|
25288
|
-
|
|
25389
|
+
function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
25390
|
+
const d = db ?? getDatabase();
|
|
25391
|
+
const result = {
|
|
25392
|
+
inserted: 0,
|
|
25393
|
+
updated: 0,
|
|
25394
|
+
deleted: 0,
|
|
25395
|
+
skipped: 0,
|
|
25396
|
+
errors: []
|
|
25289
25397
|
};
|
|
25290
|
-
|
|
25291
|
-
|
|
25292
|
-
|
|
25293
|
-
|
|
25294
|
-
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
|
|
25298
|
-
|
|
25299
|
-
|
|
25398
|
+
result.errors.push(...validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists));
|
|
25399
|
+
if (result.errors.length === 0) {
|
|
25400
|
+
const existingProjects = d.query("SELECT id, task_list_id FROM projects").all();
|
|
25401
|
+
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
25402
|
+
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
25403
|
+
}
|
|
25404
|
+
if (result.errors.length > 0)
|
|
25405
|
+
return result;
|
|
25406
|
+
const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
25407
|
+
for (const row of rows) {
|
|
25408
|
+
try {
|
|
25409
|
+
const record = asRecord(row);
|
|
25410
|
+
const tombstone = typeof record["id"] === "string" ? getStorageTombstone(objectType, record["id"], d) : null;
|
|
25411
|
+
if (tombstone && shouldApplyStorageTombstone(tombstone, rowClock(record, updateClockColumn))) {
|
|
25412
|
+
result.skipped += 1;
|
|
25413
|
+
continue;
|
|
25414
|
+
}
|
|
25415
|
+
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
25416
|
+
if (state === "inserted")
|
|
25417
|
+
result.inserted += 1;
|
|
25418
|
+
else if (state === "updated")
|
|
25419
|
+
result.updated += 1;
|
|
25420
|
+
else
|
|
25421
|
+
result.skipped += 1;
|
|
25422
|
+
afterUpsert?.(record, state !== "skipped");
|
|
25423
|
+
} catch (error) {
|
|
25424
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
25425
|
+
}
|
|
25426
|
+
}
|
|
25427
|
+
};
|
|
25428
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
25429
|
+
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
25430
|
+
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
25431
|
+
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
25432
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
25433
|
+
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
25434
|
+
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
25435
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
|
|
25436
|
+
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
25437
|
+
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
25438
|
+
}
|
|
25439
|
+
});
|
|
25440
|
+
applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
|
|
25441
|
+
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
25442
|
+
return result;
|
|
25443
|
+
}
|
|
25444
|
+
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
25445
|
+
const id = row["id"];
|
|
25446
|
+
if (typeof id !== "string" || !id)
|
|
25447
|
+
throw new Error(`${table} row is missing id`);
|
|
25448
|
+
const presentColumns = columns.filter((column) => (column in row));
|
|
25449
|
+
if (!presentColumns.includes("id"))
|
|
25450
|
+
presentColumns.unshift("id");
|
|
25451
|
+
const existing = existsById(db, table, id);
|
|
25452
|
+
const placeholders = presentColumns.map(() => "?").join(", ");
|
|
25453
|
+
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
25454
|
+
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
25455
|
+
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
25456
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
25457
|
+
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
|
|
25458
|
+
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
|
|
25459
|
+
const changes = db.run(sql, values).changes;
|
|
25460
|
+
if (changes === 0)
|
|
25461
|
+
return "skipped";
|
|
25462
|
+
return existing ? "updated" : "inserted";
|
|
25463
|
+
}
|
|
25464
|
+
function existsById(db, table, id) {
|
|
25465
|
+
return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
|
|
25466
|
+
}
|
|
25467
|
+
function valueForColumn(column, value) {
|
|
25468
|
+
if (BOOLEAN_COLUMNS.has(column))
|
|
25469
|
+
return value ? 1 : 0;
|
|
25470
|
+
if (JSON_COLUMNS.has(column))
|
|
25471
|
+
return JSON.stringify(value ?? (column === "tags" || column === "permissions" || column === "capabilities" || column === "variables" ? [] : {}));
|
|
25472
|
+
return value === undefined ? null : value;
|
|
25473
|
+
}
|
|
25474
|
+
function asRecord(value) {
|
|
25475
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
25476
|
+
throw new Error("snapshot rows must be objects");
|
|
25477
|
+
}
|
|
25478
|
+
return value;
|
|
25479
|
+
}
|
|
25480
|
+
function sortedTasks(tasks) {
|
|
25481
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
25482
|
+
const seen = new Set;
|
|
25483
|
+
const result = [];
|
|
25484
|
+
const visit = (task) => {
|
|
25485
|
+
if (seen.has(task.id))
|
|
25486
|
+
return;
|
|
25487
|
+
if (task.parent_id && byId.has(task.parent_id))
|
|
25488
|
+
visit(byId.get(task.parent_id));
|
|
25489
|
+
seen.add(task.id);
|
|
25490
|
+
result.push(task);
|
|
25491
|
+
};
|
|
25492
|
+
for (const task of tasks)
|
|
25493
|
+
visit(task);
|
|
25494
|
+
return result;
|
|
25495
|
+
}
|
|
25496
|
+
function applyTombstones(db, tombstones, result) {
|
|
25497
|
+
for (const tombstone of tombstones) {
|
|
25498
|
+
try {
|
|
25499
|
+
recordStorageTombstone({
|
|
25500
|
+
object_type: tombstone.object_type,
|
|
25501
|
+
object_id: tombstone.object_id,
|
|
25502
|
+
deleted_at: tombstone.deleted_at,
|
|
25503
|
+
source_machine_id: tombstone.source_machine_id ?? null,
|
|
25504
|
+
payload: tombstone.payload ?? null,
|
|
25505
|
+
version: tombstone.version ?? null
|
|
25506
|
+
}, db);
|
|
25507
|
+
const table = tableForTombstone(tombstone.object_type);
|
|
25508
|
+
const existing = existingClock(db, table, tombstone.object_id);
|
|
25509
|
+
if (!shouldApplyStorageTombstone(tombstone, existing)) {
|
|
25510
|
+
result.skipped += 1;
|
|
25511
|
+
continue;
|
|
25512
|
+
}
|
|
25513
|
+
const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
|
|
25514
|
+
const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
|
|
25515
|
+
if (deleted > 0 || deletedTags > 0)
|
|
25516
|
+
result.deleted = (result.deleted ?? 0) + 1;
|
|
25517
|
+
else
|
|
25518
|
+
result.skipped += 1;
|
|
25519
|
+
} catch (error) {
|
|
25520
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
25521
|
+
}
|
|
25522
|
+
}
|
|
25523
|
+
}
|
|
25524
|
+
function tableForTombstone(objectType) {
|
|
25525
|
+
if (objectType === "tasks")
|
|
25526
|
+
return "tasks";
|
|
25527
|
+
if (objectType === "projects")
|
|
25528
|
+
return "projects";
|
|
25529
|
+
if (objectType === "project_machine_paths")
|
|
25530
|
+
return "project_machine_paths";
|
|
25531
|
+
if (objectType === "plans")
|
|
25532
|
+
return "plans";
|
|
25533
|
+
if (objectType === "agents")
|
|
25534
|
+
return "agents";
|
|
25535
|
+
if (objectType === "task_lists")
|
|
25536
|
+
return "task_lists";
|
|
25537
|
+
if (objectType === "templates")
|
|
25538
|
+
return "task_templates";
|
|
25539
|
+
if (objectType === "template_tasks")
|
|
25540
|
+
return "template_tasks";
|
|
25541
|
+
return "task_history";
|
|
25542
|
+
}
|
|
25543
|
+
function listRows(db, table, columns) {
|
|
25544
|
+
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
25545
|
+
}
|
|
25546
|
+
function listProjectMachinePaths(db) {
|
|
25547
|
+
return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
|
|
25548
|
+
id: String(row.id),
|
|
25549
|
+
project_id: String(row.project_id),
|
|
25550
|
+
machine_id: String(row.machine_id),
|
|
25551
|
+
path: String(row.path),
|
|
25552
|
+
created_at: String(row.created_at),
|
|
25553
|
+
updated_at: String(row.updated_at)
|
|
25554
|
+
}));
|
|
25555
|
+
}
|
|
25556
|
+
function existingClock(db, table, id) {
|
|
25557
|
+
const clockColumns = clockColumnsForTable(table);
|
|
25558
|
+
const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
|
|
25559
|
+
return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
|
|
25560
|
+
}
|
|
25561
|
+
function rowClock(row, updateClockColumn) {
|
|
25562
|
+
const value = updateClockColumn ? row[updateClockColumn] : null;
|
|
25563
|
+
return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
|
|
25564
|
+
}
|
|
25565
|
+
function stringClock(value) {
|
|
25566
|
+
return typeof value === "string" && value ? value : null;
|
|
25567
|
+
}
|
|
25568
|
+
function clockColumnsForTable(table) {
|
|
25569
|
+
if (table === "agents")
|
|
25570
|
+
return ["last_seen_at", "created_at"];
|
|
25571
|
+
if (table === "task_templates")
|
|
25572
|
+
return ["created_at"];
|
|
25573
|
+
if (table === "task_history")
|
|
25574
|
+
return ["created_at"];
|
|
25575
|
+
return ["updated_at", "created_at"];
|
|
25576
|
+
}
|
|
25577
|
+
var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TEMPLATE_TASK_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
|
|
25578
|
+
var init_sqlite_snapshot = __esm(() => {
|
|
25579
|
+
init_database();
|
|
25580
|
+
init_agents();
|
|
25581
|
+
init_audit();
|
|
25582
|
+
init_plans();
|
|
25583
|
+
init_projects();
|
|
25584
|
+
init_task_lists();
|
|
25585
|
+
init_tasks();
|
|
25586
|
+
init_templates();
|
|
25587
|
+
init_storage_tombstones();
|
|
25588
|
+
PROJECT_COLUMNS = [
|
|
25589
|
+
"id",
|
|
25590
|
+
"name",
|
|
25591
|
+
"path",
|
|
25592
|
+
"description",
|
|
25593
|
+
"task_list_id",
|
|
25594
|
+
"task_prefix",
|
|
25595
|
+
"task_counter",
|
|
25596
|
+
"created_at",
|
|
25597
|
+
"updated_at",
|
|
25598
|
+
"machine_id",
|
|
25599
|
+
"synced_at"
|
|
25600
|
+
];
|
|
25601
|
+
PROJECT_MACHINE_PATH_COLUMNS = [
|
|
25602
|
+
"id",
|
|
25603
|
+
"project_id",
|
|
25604
|
+
"machine_id",
|
|
25605
|
+
"path",
|
|
25606
|
+
"created_at",
|
|
25607
|
+
"updated_at"
|
|
25608
|
+
];
|
|
25609
|
+
TASK_LIST_COLUMNS = [
|
|
25610
|
+
"id",
|
|
25611
|
+
"project_id",
|
|
25612
|
+
"slug",
|
|
25613
|
+
"name",
|
|
25614
|
+
"description",
|
|
25615
|
+
"metadata",
|
|
25616
|
+
"created_at",
|
|
25617
|
+
"updated_at",
|
|
25618
|
+
"machine_id",
|
|
25619
|
+
"synced_at"
|
|
25620
|
+
];
|
|
25621
|
+
PLAN_COLUMNS = [
|
|
25622
|
+
"id",
|
|
25623
|
+
"project_id",
|
|
25624
|
+
"task_list_id",
|
|
25625
|
+
"agent_id",
|
|
25626
|
+
"name",
|
|
25627
|
+
"description",
|
|
25628
|
+
"status",
|
|
25629
|
+
"created_at",
|
|
25630
|
+
"updated_at",
|
|
25631
|
+
"machine_id",
|
|
25632
|
+
"synced_at"
|
|
25633
|
+
];
|
|
25634
|
+
AGENT_COLUMNS = [
|
|
25635
|
+
"id",
|
|
25636
|
+
"name",
|
|
25637
|
+
"description",
|
|
25638
|
+
"role",
|
|
25639
|
+
"title",
|
|
25640
|
+
"level",
|
|
25641
|
+
"permissions",
|
|
25642
|
+
"capabilities",
|
|
25643
|
+
"reports_to",
|
|
25644
|
+
"org_id",
|
|
25645
|
+
"metadata",
|
|
25646
|
+
"status",
|
|
25647
|
+
"created_at",
|
|
25648
|
+
"last_seen_at",
|
|
25649
|
+
"session_id",
|
|
25650
|
+
"working_dir",
|
|
25651
|
+
"active_project_id",
|
|
25652
|
+
"machine_id",
|
|
25653
|
+
"synced_at"
|
|
25654
|
+
];
|
|
25655
|
+
TEMPLATE_COLUMNS = [
|
|
25656
|
+
"id",
|
|
25657
|
+
"name",
|
|
25658
|
+
"title_pattern",
|
|
25659
|
+
"description",
|
|
25660
|
+
"priority",
|
|
25661
|
+
"tags",
|
|
25662
|
+
"variables",
|
|
25663
|
+
"project_id",
|
|
25664
|
+
"plan_id",
|
|
25665
|
+
"metadata",
|
|
25666
|
+
"version",
|
|
25667
|
+
"created_at",
|
|
25668
|
+
"machine_id",
|
|
25669
|
+
"synced_at"
|
|
25670
|
+
];
|
|
25671
|
+
TEMPLATE_TASK_COLUMNS = [
|
|
25672
|
+
"id",
|
|
25673
|
+
"template_id",
|
|
25674
|
+
"position",
|
|
25675
|
+
"title_pattern",
|
|
25676
|
+
"description",
|
|
25677
|
+
"priority",
|
|
25678
|
+
"tags",
|
|
25679
|
+
"task_type",
|
|
25680
|
+
"condition",
|
|
25681
|
+
"include_template_id",
|
|
25682
|
+
"depends_on_positions",
|
|
25683
|
+
"metadata",
|
|
25684
|
+
"created_at"
|
|
25685
|
+
];
|
|
25686
|
+
TASK_COLUMNS = [
|
|
25687
|
+
"id",
|
|
25688
|
+
"short_id",
|
|
25689
|
+
"project_id",
|
|
25690
|
+
"parent_id",
|
|
25691
|
+
"plan_id",
|
|
25692
|
+
"task_list_id",
|
|
25693
|
+
"title",
|
|
25694
|
+
"description",
|
|
25695
|
+
"status",
|
|
25696
|
+
"priority",
|
|
25697
|
+
"agent_id",
|
|
25698
|
+
"assigned_to",
|
|
25699
|
+
"session_id",
|
|
25700
|
+
"working_dir",
|
|
25701
|
+
"tags",
|
|
25702
|
+
"metadata",
|
|
25703
|
+
"version",
|
|
25704
|
+
"locked_by",
|
|
25705
|
+
"locked_at",
|
|
25706
|
+
"created_at",
|
|
25707
|
+
"updated_at",
|
|
25708
|
+
"started_at",
|
|
25709
|
+
"completed_at",
|
|
25710
|
+
"due_at",
|
|
25711
|
+
"estimated_minutes",
|
|
25712
|
+
"actual_minutes",
|
|
25713
|
+
"requires_approval",
|
|
25714
|
+
"approved_by",
|
|
25715
|
+
"approved_at",
|
|
25716
|
+
"recurrence_rule",
|
|
25717
|
+
"recurrence_parent_id",
|
|
25718
|
+
"spawns_template_id",
|
|
25719
|
+
"confidence",
|
|
25720
|
+
"reason",
|
|
25721
|
+
"spawned_from_session",
|
|
25722
|
+
"assigned_by",
|
|
25723
|
+
"assigned_from_project",
|
|
25724
|
+
"task_type",
|
|
25725
|
+
"cost_tokens",
|
|
25726
|
+
"cost_usd",
|
|
25727
|
+
"delegated_from",
|
|
25728
|
+
"delegation_depth",
|
|
25729
|
+
"retry_count",
|
|
25730
|
+
"max_retries",
|
|
25731
|
+
"retry_after",
|
|
25732
|
+
"sla_minutes",
|
|
25733
|
+
"runner_id",
|
|
25734
|
+
"runner_started_at",
|
|
25735
|
+
"runner_completed_at",
|
|
25736
|
+
"current_step",
|
|
25737
|
+
"total_steps",
|
|
25738
|
+
"cycle_id",
|
|
25739
|
+
"machine_id",
|
|
25740
|
+
"synced_at",
|
|
25741
|
+
"archived_at"
|
|
25742
|
+
];
|
|
25743
|
+
AUDIT_COLUMNS = [
|
|
25744
|
+
"id",
|
|
25745
|
+
"task_id",
|
|
25746
|
+
"action",
|
|
25747
|
+
"field",
|
|
25748
|
+
"old_value",
|
|
25749
|
+
"new_value",
|
|
25750
|
+
"agent_id",
|
|
25751
|
+
"created_at",
|
|
25752
|
+
"machine_id"
|
|
25753
|
+
];
|
|
25754
|
+
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
|
|
25755
|
+
BOOLEAN_COLUMNS = new Set(["requires_approval"]);
|
|
25756
|
+
});
|
|
25757
|
+
|
|
25758
|
+
// src/storage/local-sqlite.ts
|
|
25759
|
+
function resolveTaskRefLocal(db, ref) {
|
|
25760
|
+
const raw = ref.trim().toLowerCase();
|
|
25761
|
+
if (!raw)
|
|
25762
|
+
return null;
|
|
25763
|
+
if (TASK_UUID_RE2.test(raw))
|
|
25764
|
+
return getTask(raw, db);
|
|
25765
|
+
const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
25766
|
+
if (prefixRows.length > 1) {
|
|
25767
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
25768
|
+
}
|
|
25769
|
+
if (prefixRows.length === 1)
|
|
25770
|
+
return getTask(prefixRows[0].id, db);
|
|
25771
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
25772
|
+
if (shortIdRows.length > 1) {
|
|
25773
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
25774
|
+
}
|
|
25775
|
+
if (shortIdRows.length === 1)
|
|
25776
|
+
return getTask(shortIdRows[0].id, db);
|
|
25777
|
+
return null;
|
|
25778
|
+
}
|
|
25779
|
+
function isSearchQuery(filter) {
|
|
25780
|
+
const q = filter.query?.trim();
|
|
25781
|
+
return !!q && q !== "*";
|
|
25782
|
+
}
|
|
25783
|
+
function matchesExtraFilters(task, filter) {
|
|
25784
|
+
if (filter.ids && !filter.ids.includes(task.id))
|
|
25785
|
+
return false;
|
|
25786
|
+
if (filter.parent_id !== undefined && (task.parent_id ?? null) !== filter.parent_id)
|
|
25787
|
+
return false;
|
|
25788
|
+
if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
|
|
25789
|
+
return false;
|
|
25790
|
+
if (filter.session_id !== undefined && task.session_id !== filter.session_id)
|
|
25791
|
+
return false;
|
|
25792
|
+
if (filter.has_recurrence !== undefined && Boolean(task.recurrence_rule) !== filter.has_recurrence)
|
|
25793
|
+
return false;
|
|
25794
|
+
if (filter.task_type !== undefined) {
|
|
25795
|
+
const allowed = Array.isArray(filter.task_type) ? filter.task_type : [filter.task_type];
|
|
25796
|
+
if (!allowed.includes(task.task_type ?? ""))
|
|
25797
|
+
return false;
|
|
25798
|
+
}
|
|
25799
|
+
if (filter.tags?.length) {
|
|
25800
|
+
const taskTags = new Set(task.tags ?? []);
|
|
25801
|
+
if (!filter.tags.every((tag) => taskTags.has(tag)))
|
|
25802
|
+
return false;
|
|
25803
|
+
}
|
|
25804
|
+
if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
|
|
25805
|
+
return false;
|
|
25806
|
+
return true;
|
|
25807
|
+
}
|
|
25808
|
+
function listTasksMaybeSearch(filter, db) {
|
|
25809
|
+
if (!isSearchQuery(filter))
|
|
25810
|
+
return listTasks(filter, db);
|
|
25811
|
+
const matched = searchTasks({
|
|
25812
|
+
query: filter.query,
|
|
25813
|
+
project_id: filter.project_id,
|
|
25814
|
+
task_list_id: filter.task_list_id,
|
|
25815
|
+
status: filter.status,
|
|
25816
|
+
priority: filter.priority,
|
|
25817
|
+
assigned_to: filter.assigned_to,
|
|
25818
|
+
agent_id: filter.agent_id
|
|
25819
|
+
}, undefined, undefined, db).filter((task) => matchesExtraFilters(task, filter));
|
|
25820
|
+
const offset = filter.offset && filter.offset > 0 ? Math.trunc(filter.offset) : 0;
|
|
25821
|
+
if (filter.limit !== undefined && filter.limit >= 0)
|
|
25822
|
+
return matched.slice(offset, offset + filter.limit);
|
|
25823
|
+
return offset ? matched.slice(offset) : matched;
|
|
25824
|
+
}
|
|
25825
|
+
function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
25826
|
+
const database = () => options.db ?? getDatabase();
|
|
25827
|
+
let adapter;
|
|
25828
|
+
adapter = {
|
|
25829
|
+
kind: "sqlite",
|
|
25830
|
+
capabilities: {
|
|
25831
|
+
localPersistence: true,
|
|
25832
|
+
remotePersistence: false,
|
|
25833
|
+
transactions: true,
|
|
25834
|
+
auditLog: true,
|
|
25835
|
+
sync: true
|
|
25836
|
+
},
|
|
25837
|
+
tasks: {
|
|
25838
|
+
create: (input, context) => createTask({
|
|
25839
|
+
...input,
|
|
25840
|
+
agent_id: input.agent_id ?? context?.agentId,
|
|
25841
|
+
created_by: input.created_by ?? input.agent_id ?? context?.agentId
|
|
25842
|
+
}, database()),
|
|
25843
|
+
get: (id) => getTask(id, database()),
|
|
25844
|
+
resolveRef: (ref) => resolveTaskRefLocal(database(), ref),
|
|
25845
|
+
list: (filter = {}) => listTasksMaybeSearch(filter, database()),
|
|
25846
|
+
count: (filter = {}) => isSearchQuery(filter) ? listTasksMaybeSearch({ ...filter, limit: undefined, offset: undefined }, database()).length : countTasks(filter, database()),
|
|
25847
|
+
update: (id, input) => updateTask(id, input, database()),
|
|
25848
|
+
unlock: (id, agentId) => {
|
|
25849
|
+
unlockTask(id, agentId, database());
|
|
25850
|
+
return true;
|
|
25851
|
+
},
|
|
25852
|
+
delete: (id) => deleteTask(id, database()),
|
|
25853
|
+
start: (id, agentId) => startTask(id, agentId, database()),
|
|
25854
|
+
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
25855
|
+
fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
|
|
25856
|
+
claimNext: (agentId, filters) => claimNextTask(agentId, filters, database()),
|
|
25857
|
+
getNext: (agentId, filters) => getNextTask(agentId, filters, database()),
|
|
25858
|
+
getActiveWork: (filters) => getActiveWork(filters, database()),
|
|
25859
|
+
getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
|
|
25860
|
+
},
|
|
25861
|
+
projects: {
|
|
25862
|
+
create: (input) => createProject(input, database()),
|
|
25863
|
+
get: (id) => getProject(id, database()),
|
|
25864
|
+
getByPath: (path) => getProjectByPath(path, database()),
|
|
25865
|
+
list: () => listProjects(database()),
|
|
25866
|
+
update: (id, input) => updateProject(id, input, database()),
|
|
25867
|
+
rename: (id, input) => renameProject(id, input, database()),
|
|
25868
|
+
delete: (id) => deleteProject(id, database())
|
|
25869
|
+
},
|
|
25870
|
+
plans: {
|
|
25871
|
+
create: (input) => createPlan(input, database()),
|
|
25872
|
+
get: (id) => getPlan(id, database()),
|
|
25873
|
+
list: (projectId) => listPlans(projectId, database()),
|
|
25874
|
+
update: (id, input) => updatePlan(id, input, database()),
|
|
25875
|
+
delete: (id) => deletePlan(id, database())
|
|
25876
|
+
},
|
|
25877
|
+
agents: {
|
|
25878
|
+
register: (input) => registerAgent(input, database()),
|
|
25879
|
+
get: (id) => getAgent(id, database()),
|
|
25880
|
+
getByName: (name) => getAgentByName(name, database()),
|
|
25881
|
+
list: (options2) => listAgents(options2, database()),
|
|
25882
|
+
update: (id, input) => updateAgent(id, input, database())
|
|
25883
|
+
},
|
|
25884
|
+
taskLists: {
|
|
25885
|
+
create: (input) => createTaskList(input, database()),
|
|
25886
|
+
get: (id) => getTaskList(id, database()),
|
|
25887
|
+
getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
|
|
25888
|
+
list: (projectId) => listTaskLists(projectId, database()),
|
|
25889
|
+
update: (id, input) => updateTaskList(id, input, database()),
|
|
25890
|
+
delete: (id) => deleteTaskList(id, database()),
|
|
25891
|
+
deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
|
|
25892
|
+
},
|
|
25893
|
+
templates: {
|
|
25894
|
+
create: (input) => createTemplate(input, database()),
|
|
25895
|
+
get: (id) => getTemplate(id, database()),
|
|
25896
|
+
list: () => listTemplates(database()),
|
|
25897
|
+
update: (id, input) => updateTemplate(id, input, database()),
|
|
25898
|
+
delete: (id) => deleteTemplate(id, database()),
|
|
25899
|
+
getWithTasks: (id) => getTemplateWithTasks(id, database())
|
|
25900
|
+
},
|
|
25901
|
+
audit: {
|
|
25902
|
+
logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
|
|
25903
|
+
addComment: (input) => addComment(input, database()),
|
|
25904
|
+
getComments: (taskId) => listComments(taskId, database()),
|
|
25905
|
+
getCommentsPage: (taskId, options2) => {
|
|
25906
|
+
if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
|
|
25907
|
+
throw new Error("Comment limit must be an integer between 1 and 1001");
|
|
25908
|
+
}
|
|
25909
|
+
let comments = listComments(taskId, database());
|
|
25910
|
+
comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
|
|
25911
|
+
if (options2?.before) {
|
|
25912
|
+
const before = options2.before;
|
|
25913
|
+
comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
|
|
25914
|
+
}
|
|
25915
|
+
if (options2?.limit !== undefined)
|
|
25916
|
+
comments = comments.slice(-options2.limit);
|
|
25917
|
+
return comments;
|
|
25918
|
+
},
|
|
25919
|
+
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
25920
|
+
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
25921
|
+
},
|
|
25922
|
+
sync: {
|
|
25923
|
+
getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database()),
|
|
25924
|
+
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
25925
|
+
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
25926
|
+
},
|
|
25927
|
+
integrity: {
|
|
25928
|
+
report: () => scanSqliteIntegrity(database())
|
|
25929
|
+
},
|
|
25930
|
+
transaction: (fn) => {
|
|
25931
|
+
const tx = database().transaction(() => fn(adapter));
|
|
25932
|
+
return tx();
|
|
25933
|
+
}
|
|
25934
|
+
};
|
|
25935
|
+
return adapter;
|
|
25936
|
+
}
|
|
25937
|
+
var TASK_UUID_RE2;
|
|
25938
|
+
var init_local_sqlite = __esm(() => {
|
|
25939
|
+
init_types();
|
|
25940
|
+
init_search();
|
|
25941
|
+
init_tasks();
|
|
25942
|
+
init_projects();
|
|
25943
|
+
init_plans();
|
|
25944
|
+
init_agents();
|
|
25945
|
+
init_task_lists();
|
|
25946
|
+
init_templates();
|
|
25947
|
+
init_audit();
|
|
25948
|
+
init_comments();
|
|
25949
|
+
init_database();
|
|
25950
|
+
init_integrity2();
|
|
25951
|
+
init_sqlite_snapshot();
|
|
25952
|
+
TASK_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
25953
|
+
});
|
|
25954
|
+
|
|
25955
|
+
// src/lib/project-task-list-ensure.ts
|
|
25956
|
+
import { createHash as createHash5 } from "crypto";
|
|
25957
|
+
function canonicalJson(value) {
|
|
25958
|
+
if (value === null || typeof value !== "object")
|
|
25959
|
+
return JSON.stringify(value);
|
|
25960
|
+
if (Array.isArray(value))
|
|
25961
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
25962
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
25963
|
+
}
|
|
25964
|
+
function digest(value) {
|
|
25965
|
+
return createHash5("sha256").update(canonicalJson(value)).digest("hex");
|
|
25966
|
+
}
|
|
25967
|
+
function deriveIdempotencyKey(projectId, slug) {
|
|
25968
|
+
return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
25969
|
+
}
|
|
25970
|
+
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
25971
|
+
const key = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
25972
|
+
if (key.length < 8 || key.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(key)) {
|
|
25973
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID", "idempotency_key must be 8-128 ASCII letters, digits, dots, underscores, colons, or hyphens");
|
|
25974
|
+
}
|
|
25975
|
+
return key;
|
|
25976
|
+
}
|
|
25977
|
+
function receiptId(projectId, slug, idempotencyKey) {
|
|
25978
|
+
return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
|
|
25979
|
+
}
|
|
25980
|
+
function semanticListDigest(list) {
|
|
25981
|
+
const metadata = { ...list.metadata ?? {} };
|
|
25982
|
+
delete metadata[RECEIPT_METADATA_KEY];
|
|
25983
|
+
return digest({
|
|
25984
|
+
project_id: list.project_id,
|
|
25985
|
+
slug: list.slug,
|
|
25986
|
+
name: list.name,
|
|
25987
|
+
description: list.description,
|
|
25988
|
+
metadata
|
|
25989
|
+
});
|
|
25990
|
+
}
|
|
25991
|
+
function storedMarker(list) {
|
|
25992
|
+
const value = list.metadata?.[RECEIPT_METADATA_KEY];
|
|
25993
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
25994
|
+
return null;
|
|
25995
|
+
const marker = value;
|
|
25996
|
+
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")
|
|
25997
|
+
return null;
|
|
25998
|
+
return marker;
|
|
25999
|
+
}
|
|
26000
|
+
function receiptFor(store, project, list, idempotencyKey) {
|
|
26001
|
+
const marker = storedMarker(list);
|
|
26002
|
+
const owned = marker?.project_id === project.id && marker.slug === list.slug;
|
|
26003
|
+
if (owned && marker.idempotency_key !== idempotencyKey) {
|
|
26004
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
|
|
26005
|
+
project_id: project.id,
|
|
26006
|
+
task_list_id: list.id,
|
|
26007
|
+
receipt_id: marker.receipt_id
|
|
26008
|
+
});
|
|
26009
|
+
}
|
|
26010
|
+
return {
|
|
26011
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
26012
|
+
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
|
|
26013
|
+
idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
|
|
26014
|
+
project_id: project.id,
|
|
26015
|
+
task_list_id: list.id,
|
|
26016
|
+
slug: list.slug,
|
|
26017
|
+
created_by_operation: owned,
|
|
26018
|
+
result_revision: list.updated_at,
|
|
26019
|
+
result_digest: owned ? marker.result_digest : semanticListDigest(list),
|
|
26020
|
+
rollback_supported: Boolean(owned && semanticListDigest(list) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused),
|
|
26021
|
+
created_at: owned ? marker.created_at : list.created_at
|
|
26022
|
+
};
|
|
26023
|
+
}
|
|
26024
|
+
async function exactProjectState(store, projectId) {
|
|
26025
|
+
const project = await store.projects.get(projectId);
|
|
26026
|
+
if (!project) {
|
|
26027
|
+
throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, { project_id: projectId });
|
|
26028
|
+
}
|
|
26029
|
+
const slug = project.task_list_id?.trim();
|
|
26030
|
+
if (!slug) {
|
|
26031
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_NOT_DECLARED", "Project does not declare a canonical task_list_id slug", { project_id: project.id });
|
|
26032
|
+
}
|
|
26033
|
+
const all = await store.taskLists.list();
|
|
26034
|
+
const scopedMatches = all.filter((list) => list.project_id === project.id && list.slug === slug);
|
|
26035
|
+
if (scopedMatches.length > 1) {
|
|
26036
|
+
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) });
|
|
26037
|
+
}
|
|
26038
|
+
const globalMatches = all.filter((list) => list.project_id === null && list.slug === slug);
|
|
26039
|
+
if (globalMatches.length > 0 && scopedMatches.length === 0) {
|
|
26040
|
+
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) });
|
|
26041
|
+
}
|
|
26042
|
+
return { project, scoped: scopedMatches[0] ?? null, globalCollision: globalMatches[0] ?? null };
|
|
26043
|
+
}
|
|
26044
|
+
async function planProjectTaskListEnsure(store, projectId) {
|
|
26045
|
+
const { project, scoped } = await exactProjectState(store, projectId);
|
|
26046
|
+
return {
|
|
26047
|
+
mode: "plan",
|
|
26048
|
+
action: scoped ? "already_present" : "would_create",
|
|
26049
|
+
project,
|
|
26050
|
+
task_list: scoped,
|
|
26051
|
+
receipt: null
|
|
26052
|
+
};
|
|
26053
|
+
}
|
|
26054
|
+
async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
26055
|
+
const state = await exactProjectState(store, projectId);
|
|
26056
|
+
const { project } = state;
|
|
26057
|
+
if (project.updated_at !== options.expected_project_revision) {
|
|
26058
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed after the ensure plan; fetch a fresh plan before applying", {
|
|
26059
|
+
project_id: project.id,
|
|
26060
|
+
expected_project_revision: options.expected_project_revision,
|
|
26061
|
+
current_project_revision: project.updated_at
|
|
26062
|
+
});
|
|
26063
|
+
}
|
|
26064
|
+
const slug = project.task_list_id;
|
|
26065
|
+
const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
|
|
26066
|
+
if (state.scoped) {
|
|
26067
|
+
return {
|
|
26068
|
+
mode: "apply",
|
|
26069
|
+
action: "already_present",
|
|
26070
|
+
project,
|
|
26071
|
+
task_list: state.scoped,
|
|
26072
|
+
receipt: receiptFor(store, project, state.scoped, idempotencyKey)
|
|
26073
|
+
};
|
|
26074
|
+
}
|
|
26075
|
+
const marker = {
|
|
26076
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
26077
|
+
receipt_id: receiptId(project.id, slug, idempotencyKey),
|
|
26078
|
+
idempotency_key: idempotencyKey,
|
|
26079
|
+
project_id: project.id,
|
|
26080
|
+
slug,
|
|
26081
|
+
result_digest: semanticListDigest({
|
|
26082
|
+
project_id: project.id,
|
|
26083
|
+
slug,
|
|
26084
|
+
name: project.name,
|
|
26085
|
+
description: null,
|
|
26086
|
+
metadata: {}
|
|
26087
|
+
}),
|
|
26088
|
+
created_at: new Date().toISOString()
|
|
26089
|
+
};
|
|
26090
|
+
let list;
|
|
26091
|
+
try {
|
|
26092
|
+
list = await store.taskLists.create({
|
|
26093
|
+
name: project.name,
|
|
26094
|
+
slug,
|
|
26095
|
+
project_id: project.id,
|
|
26096
|
+
metadata: { [RECEIPT_METADATA_KEY]: marker }
|
|
26097
|
+
});
|
|
26098
|
+
} catch (error) {
|
|
26099
|
+
if (!(error instanceof ResourceConflictError))
|
|
26100
|
+
throw error;
|
|
26101
|
+
const raced = await exactProjectState(store, projectId);
|
|
26102
|
+
if (!raced.scoped)
|
|
26103
|
+
throw error;
|
|
26104
|
+
if (raced.project.updated_at !== options.expected_project_revision || raced.project.task_list_id !== slug) {
|
|
26105
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed while the task list was being created; fetch a fresh plan before retrying", {
|
|
26106
|
+
project_id: raced.project.id,
|
|
26107
|
+
expected_project_revision: options.expected_project_revision,
|
|
26108
|
+
current_project_revision: raced.project.updated_at
|
|
26109
|
+
});
|
|
26110
|
+
}
|
|
26111
|
+
return {
|
|
26112
|
+
mode: "apply",
|
|
26113
|
+
action: "already_present",
|
|
26114
|
+
project: raced.project,
|
|
26115
|
+
task_list: raced.scoped,
|
|
26116
|
+
receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
|
|
26117
|
+
};
|
|
26118
|
+
}
|
|
26119
|
+
const projectReadback = await store.projects.get(project.id);
|
|
26120
|
+
if (!projectReadback || projectReadback.updated_at !== options.expected_project_revision || projectReadback.task_list_id !== slug) {
|
|
26121
|
+
let compensated = false;
|
|
26122
|
+
const unchanged = await store.taskLists.get(list.id);
|
|
26123
|
+
const unchangedMarker = unchanged ? storedMarker(unchanged) : null;
|
|
26124
|
+
if (unchanged && unchangedMarker?.receipt_id === marker.receipt_id && semanticListDigest(unchanged) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused) {
|
|
26125
|
+
const deletion = await store.taskLists.deleteIfUnchangedAndUnused(list.id, {
|
|
26126
|
+
project_id: unchanged.project_id,
|
|
26127
|
+
slug: unchanged.slug,
|
|
26128
|
+
name: unchanged.name,
|
|
26129
|
+
description: unchanged.description,
|
|
26130
|
+
metadata: unchanged.metadata,
|
|
26131
|
+
updated_at: unchanged.updated_at
|
|
26132
|
+
});
|
|
26133
|
+
compensated = deletion.status === "deleted";
|
|
26134
|
+
}
|
|
26135
|
+
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 });
|
|
26136
|
+
}
|
|
26137
|
+
const readback = await store.taskLists.get(list.id);
|
|
26138
|
+
if (!readback || readback.project_id !== project.id || readback.slug !== slug) {
|
|
26139
|
+
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 });
|
|
26140
|
+
}
|
|
26141
|
+
return {
|
|
26142
|
+
mode: "apply",
|
|
26143
|
+
action: "created",
|
|
26144
|
+
project: projectReadback,
|
|
26145
|
+
task_list: readback,
|
|
26146
|
+
receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
|
|
26147
|
+
};
|
|
26148
|
+
}
|
|
26149
|
+
async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
26150
|
+
const conditionalDelete = store.taskLists.deleteIfUnchangedAndUnused;
|
|
26151
|
+
if (!conditionalDelete) {
|
|
26152
|
+
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 });
|
|
26153
|
+
}
|
|
26154
|
+
const project = await store.projects.get(projectId);
|
|
26155
|
+
if (!project) {
|
|
26156
|
+
throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`);
|
|
26157
|
+
}
|
|
26158
|
+
const candidates = (await store.taskLists.list(project.id)).filter((list2) => storedMarker(list2)?.receipt_id === options.receipt_id);
|
|
26159
|
+
if (candidates.length !== 1) {
|
|
26160
|
+
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 });
|
|
26161
|
+
}
|
|
26162
|
+
const list = candidates[0];
|
|
26163
|
+
const marker = storedMarker(list);
|
|
26164
|
+
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) {
|
|
26165
|
+
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 });
|
|
26166
|
+
}
|
|
26167
|
+
const deletion = await conditionalDelete.call(store.taskLists, list.id, {
|
|
26168
|
+
project_id: list.project_id,
|
|
26169
|
+
slug: list.slug,
|
|
26170
|
+
name: list.name,
|
|
26171
|
+
description: list.description,
|
|
26172
|
+
metadata: list.metadata,
|
|
26173
|
+
updated_at: list.updated_at
|
|
26174
|
+
});
|
|
26175
|
+
if (deletion.status === "has_dependents") {
|
|
26176
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_HAS_DEPENDENTS", "The operation-owned task list has dependents; refusing conditional rollback", {
|
|
26177
|
+
task_list_id: list.id,
|
|
26178
|
+
task_dependents: deletion.task_dependents,
|
|
26179
|
+
plan_dependents: deletion.plan_dependents
|
|
26180
|
+
});
|
|
26181
|
+
}
|
|
26182
|
+
if (deletion.status !== "deleted" || await store.taskLists.get(list.id)) {
|
|
26183
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "Conditional rollback did not remove the exact task list", { task_list_id: list.id });
|
|
26184
|
+
}
|
|
26185
|
+
return {
|
|
26186
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
26187
|
+
action: "removed",
|
|
26188
|
+
project_id: project.id,
|
|
26189
|
+
task_list_id: list.id,
|
|
26190
|
+
accepted_receipt_id: options.receipt_id,
|
|
26191
|
+
rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
26192
|
+
removed_at: new Date().toISOString()
|
|
26193
|
+
};
|
|
26194
|
+
}
|
|
26195
|
+
var PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION = "todos.project-task-list-ensure.v1", RECEIPT_METADATA_KEY = "todos_project_task_list_ensure", ProjectTaskListEnsureError;
|
|
26196
|
+
var init_project_task_list_ensure = __esm(() => {
|
|
26197
|
+
init_types();
|
|
26198
|
+
ProjectTaskListEnsureError = class ProjectTaskListEnsureError extends Error {
|
|
26199
|
+
code;
|
|
26200
|
+
details;
|
|
26201
|
+
constructor(code, message, details = {}) {
|
|
26202
|
+
super(message);
|
|
26203
|
+
this.code = code;
|
|
26204
|
+
this.details = details;
|
|
26205
|
+
this.name = "ProjectTaskListEnsureError";
|
|
26206
|
+
}
|
|
26207
|
+
};
|
|
26208
|
+
});
|
|
26209
|
+
|
|
26210
|
+
// src/lib/project-bootstrap.ts
|
|
26211
|
+
var exports_project_bootstrap = {};
|
|
26212
|
+
__export(exports_project_bootstrap, {
|
|
26213
|
+
discoverProjectWorkspace: () => discoverProjectWorkspace,
|
|
26214
|
+
bootstrapProject: () => bootstrapProject
|
|
26215
|
+
});
|
|
26216
|
+
import { existsSync as existsSync13, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
26217
|
+
import { basename as basename4, dirname as dirname6, resolve as resolve11 } from "path";
|
|
26218
|
+
function safeStat(path) {
|
|
26219
|
+
try {
|
|
26220
|
+
return statSync4(path);
|
|
26221
|
+
} catch {
|
|
26222
|
+
return null;
|
|
26223
|
+
}
|
|
26224
|
+
}
|
|
26225
|
+
function canonicalPath(input) {
|
|
26226
|
+
const resolved = resolve11(input);
|
|
26227
|
+
const stats = safeStat(resolved);
|
|
26228
|
+
if (stats?.isFile())
|
|
26229
|
+
return dirname6(resolved);
|
|
26230
|
+
return resolved;
|
|
26231
|
+
}
|
|
26232
|
+
function findUp(start, marker) {
|
|
26233
|
+
let current = canonicalPath(start);
|
|
26234
|
+
while (true) {
|
|
26235
|
+
if (existsSync13(resolve11(current, marker)))
|
|
26236
|
+
return current;
|
|
26237
|
+
const parent = dirname6(current);
|
|
26238
|
+
if (parent === current)
|
|
26239
|
+
return null;
|
|
26240
|
+
current = parent;
|
|
26241
|
+
}
|
|
26242
|
+
}
|
|
26243
|
+
function readPackageJson(path) {
|
|
26244
|
+
if (!path)
|
|
26245
|
+
return null;
|
|
26246
|
+
const file = resolve11(path, "package.json");
|
|
26247
|
+
if (!existsSync13(file))
|
|
26248
|
+
return null;
|
|
26249
|
+
try {
|
|
26250
|
+
const parsed = JSON.parse(readFileSync7(file, "utf-8"));
|
|
26251
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
26252
|
+
} catch {
|
|
26253
|
+
return null;
|
|
26254
|
+
}
|
|
26255
|
+
}
|
|
26256
|
+
function packageDisplayName(name, fallbackPath) {
|
|
26257
|
+
if (!name)
|
|
26258
|
+
return basename4(fallbackPath);
|
|
26259
|
+
const withoutScope = name.startsWith("@") ? name.split("/")[1] : name;
|
|
26260
|
+
return withoutScope || basename4(fallbackPath);
|
|
26261
|
+
}
|
|
26262
|
+
function workspaceMarker(root, rootPackage) {
|
|
26263
|
+
if (!root)
|
|
26264
|
+
return { kind: null, markers: [] };
|
|
26265
|
+
const markers = [];
|
|
26266
|
+
if (rootPackage?.workspaces)
|
|
26267
|
+
markers.push("package.json#workspaces");
|
|
26268
|
+
for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
|
|
26269
|
+
if (existsSync13(resolve11(root, marker)))
|
|
26270
|
+
markers.push(marker);
|
|
26271
|
+
}
|
|
26272
|
+
const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
|
|
26273
|
+
return { kind, markers };
|
|
26274
|
+
}
|
|
26275
|
+
function discoverProjectWorkspace(inputPath = process.cwd()) {
|
|
26276
|
+
const input = canonicalPath(inputPath);
|
|
26277
|
+
const gitRoot = findUp(input, ".git");
|
|
26278
|
+
const packageRoot = findUp(input, "package.json");
|
|
26279
|
+
const rootPackage = readPackageJson(gitRoot);
|
|
26280
|
+
const packageMeta = readPackageJson(packageRoot);
|
|
26281
|
+
const workspace = workspaceMarker(gitRoot, rootPackage);
|
|
26282
|
+
const monorepo = Boolean(gitRoot && packageRoot && packageRoot !== gitRoot && workspace.kind);
|
|
26283
|
+
const projectPath = monorepo ? packageRoot : gitRoot ?? packageRoot ?? input;
|
|
26284
|
+
const projectName = packageDisplayName(packageMeta?.name ?? rootPackage?.name ?? null, projectPath);
|
|
26285
|
+
return {
|
|
26286
|
+
inputPath: input,
|
|
26287
|
+
projectPath,
|
|
26288
|
+
projectName,
|
|
26289
|
+
gitRoot,
|
|
26290
|
+
packageRoot,
|
|
26291
|
+
packageName: packageMeta?.name ?? null,
|
|
26292
|
+
workspaceRoot: workspace.kind ? gitRoot : null,
|
|
26293
|
+
workspaceKind: workspace.kind,
|
|
26294
|
+
monorepo,
|
|
26295
|
+
markers: workspace.markers
|
|
26296
|
+
};
|
|
26297
|
+
}
|
|
26298
|
+
function sourceExists(projectId, type, uri, db) {
|
|
26299
|
+
return listProjectSources(projectId, db).some((source) => source.type === type && source.uri === uri);
|
|
26300
|
+
}
|
|
26301
|
+
function addSourceOnce(projectId, type, name, uri, metadata, db) {
|
|
26302
|
+
if (!uri || sourceExists(projectId, type, uri, db))
|
|
26303
|
+
return null;
|
|
26304
|
+
return addProjectSource({ project_id: projectId, type, name, uri, metadata }, db);
|
|
26305
|
+
}
|
|
26306
|
+
function bootstrapProject(options = {}, db) {
|
|
26307
|
+
const d = db || getDatabase();
|
|
26308
|
+
const discovery = discoverProjectWorkspace(options.path);
|
|
26309
|
+
const taskListSlug = options.taskListSlug || `todos-${slugify(options.name || discovery.projectName)}`;
|
|
26310
|
+
if (options.dryRun) {
|
|
26311
|
+
return {
|
|
26312
|
+
dryRun: true,
|
|
26313
|
+
discovery: { ...discovery, projectName: options.name || discovery.projectName },
|
|
26314
|
+
project: null,
|
|
26315
|
+
taskList: null,
|
|
26316
|
+
sources: [],
|
|
26317
|
+
created: { project: false, taskList: false, sources: [] }
|
|
26318
|
+
};
|
|
26319
|
+
}
|
|
26320
|
+
const beforeProject = getProjectByCanonicalPath(discovery.projectPath, d);
|
|
26321
|
+
let project = ensureProject(options.name || discovery.projectName, discovery.projectPath, d);
|
|
26322
|
+
const createdProject = !beforeProject;
|
|
26323
|
+
if (project.task_list_id !== taskListSlug || options.name && project.name !== options.name) {
|
|
26324
|
+
project = renameProject(project.id, {
|
|
26325
|
+
name: options.name ?? project.name,
|
|
26326
|
+
new_slug: taskListSlug
|
|
26327
|
+
}, d).project;
|
|
26328
|
+
}
|
|
26329
|
+
setMachineLocalPath(project.id, discovery.projectPath, d);
|
|
26330
|
+
const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug);
|
|
26331
|
+
let taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
|
|
26332
|
+
if (options.routeEnabled && taskList.metadata.route_enabled !== true) {
|
|
26333
|
+
taskList = updateTaskList(taskList.id, {
|
|
26334
|
+
metadata: {
|
|
26335
|
+
...taskList.metadata,
|
|
26336
|
+
route_enabled: true,
|
|
26337
|
+
automation: {
|
|
26338
|
+
...taskList.metadata.automation && typeof taskList.metadata.automation === "object" && !Array.isArray(taskList.metadata.automation) ? taskList.metadata.automation : {},
|
|
26339
|
+
no_auto: false
|
|
26340
|
+
}
|
|
26341
|
+
}
|
|
26342
|
+
}, d);
|
|
26343
|
+
}
|
|
26344
|
+
const createdSources = [];
|
|
26345
|
+
for (const source of [
|
|
26346
|
+
addSourceOnce(project.id, "local", "Project root", discovery.projectPath, { role: "project-root" }, d),
|
|
26347
|
+
addSourceOnce(project.id, "git", "Git root", discovery.gitRoot, { role: "git-root" }, d),
|
|
26348
|
+
addSourceOnce(project.id, "workspace", "Workspace root", discovery.workspaceRoot, {
|
|
26349
|
+
role: "workspace-root",
|
|
26350
|
+
kind: discovery.workspaceKind,
|
|
26351
|
+
markers: discovery.markers,
|
|
26352
|
+
monorepo: discovery.monorepo
|
|
26353
|
+
}, d)
|
|
26354
|
+
]) {
|
|
26355
|
+
if (source) {
|
|
26356
|
+
createdSources.push(source.type);
|
|
26357
|
+
}
|
|
26358
|
+
}
|
|
26359
|
+
return {
|
|
26360
|
+
dryRun: false,
|
|
26361
|
+
discovery: { ...discovery, projectName: options.name || discovery.projectName },
|
|
26362
|
+
project,
|
|
26363
|
+
taskList,
|
|
26364
|
+
sources: listProjectSources(project.id, d),
|
|
26365
|
+
created: {
|
|
26366
|
+
project: createdProject,
|
|
26367
|
+
taskList: !beforeTaskList,
|
|
26368
|
+
sources: createdSources
|
|
26369
|
+
}
|
|
26370
|
+
};
|
|
26371
|
+
}
|
|
26372
|
+
function getProjectByCanonicalPath(path, db) {
|
|
26373
|
+
return getProjectByExactPath(path, db) ?? null;
|
|
26374
|
+
}
|
|
26375
|
+
function getProjectByExactPath(path, db) {
|
|
26376
|
+
return getProjectByPathForBootstrap(path, db);
|
|
26377
|
+
}
|
|
26378
|
+
function getProjectByPathForBootstrap(path, db) {
|
|
26379
|
+
const row = db.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
26380
|
+
if (row)
|
|
26381
|
+
return row;
|
|
26382
|
+
const machineRow = db.query(`SELECT p.* FROM projects p
|
|
26383
|
+
JOIN project_machine_paths pmp ON pmp.project_id = p.id
|
|
26384
|
+
WHERE pmp.path = ?`).get(path);
|
|
26385
|
+
return machineRow ?? null;
|
|
26386
|
+
}
|
|
26387
|
+
var init_project_bootstrap = __esm(() => {
|
|
26388
|
+
init_database();
|
|
26389
|
+
init_projects();
|
|
26390
|
+
init_task_lists();
|
|
26391
|
+
});
|
|
26392
|
+
|
|
26393
|
+
// src/lib/project-panel.ts
|
|
26394
|
+
var exports_project_panel = {};
|
|
26395
|
+
__export(exports_project_panel, {
|
|
26396
|
+
createTodosProjectPanel: () => createTodosProjectPanel
|
|
26397
|
+
});
|
|
26398
|
+
import {
|
|
26399
|
+
parseContract,
|
|
26400
|
+
SCHEMA_IDS
|
|
26401
|
+
} from "@hasna/contracts";
|
|
26402
|
+
function clampLimit(limit) {
|
|
26403
|
+
if (!Number.isFinite(limit ?? 0))
|
|
26404
|
+
return 20;
|
|
26405
|
+
return Math.max(1, Math.min(100, Math.trunc(limit ?? 20)));
|
|
26406
|
+
}
|
|
26407
|
+
function taskUri(id) {
|
|
26408
|
+
return `todo://tasks/${id}`;
|
|
26409
|
+
}
|
|
26410
|
+
function taskResource(task) {
|
|
26411
|
+
return {
|
|
26412
|
+
kind: "task",
|
|
26413
|
+
id: task.id,
|
|
26414
|
+
name: task.title,
|
|
26415
|
+
uri: taskUri(task.id),
|
|
26416
|
+
externalId: task.id,
|
|
26417
|
+
sourcePackage: SOURCE_PACKAGE,
|
|
26418
|
+
tags: task.tags
|
|
26419
|
+
};
|
|
26420
|
+
}
|
|
26421
|
+
function planResource(id, name) {
|
|
26422
|
+
return {
|
|
26423
|
+
kind: "workflow",
|
|
26424
|
+
id,
|
|
26425
|
+
name: name ?? undefined,
|
|
26426
|
+
externalId: id,
|
|
26427
|
+
sourcePackage: SOURCE_PACKAGE
|
|
26428
|
+
};
|
|
26429
|
+
}
|
|
26430
|
+
function projectResource(project) {
|
|
26431
|
+
const projectSlug = projectSlugForPanel(project);
|
|
26432
|
+
return {
|
|
26433
|
+
kind: "project",
|
|
26434
|
+
id: projectSlug,
|
|
26435
|
+
name: project.name,
|
|
26436
|
+
uri: `project://${projectSlug}`,
|
|
26437
|
+
externalId: project.id,
|
|
26438
|
+
sourcePackage: SOURCE_PACKAGE
|
|
26439
|
+
};
|
|
26440
|
+
}
|
|
26441
|
+
function projectSlugForPanel(project) {
|
|
26442
|
+
const taskListSlug = project.task_list_id?.replace(/^todos-/, "");
|
|
26443
|
+
return taskListSlug && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(taskListSlug) ? taskListSlug : slugify(project.name) || project.id.toLowerCase();
|
|
26444
|
+
}
|
|
26445
|
+
function countByStatus(tasks) {
|
|
26446
|
+
return {
|
|
26447
|
+
pending: tasks.filter((task) => task.status === "pending").length,
|
|
26448
|
+
in_progress: tasks.filter((task) => task.status === "in_progress").length,
|
|
26449
|
+
completed: tasks.filter((task) => task.status === "completed").length,
|
|
26450
|
+
failed: tasks.filter((task) => task.status === "failed").length,
|
|
26451
|
+
cancelled: tasks.filter((task) => task.status === "cancelled").length
|
|
26452
|
+
};
|
|
26453
|
+
}
|
|
26454
|
+
function countByPriority(tasks) {
|
|
26455
|
+
return {
|
|
26456
|
+
critical: tasks.filter((task) => task.priority === "critical").length,
|
|
26457
|
+
high: tasks.filter((task) => task.priority === "high").length,
|
|
26458
|
+
medium: tasks.filter((task) => task.priority === "medium").length,
|
|
26459
|
+
low: tasks.filter((task) => task.priority === "low").length
|
|
26460
|
+
};
|
|
26461
|
+
}
|
|
26462
|
+
function isOverdue(task, generatedAt) {
|
|
26463
|
+
if (!task.due_at || TERMINAL_STATUSES.has(task.status))
|
|
26464
|
+
return false;
|
|
26465
|
+
const due = Date.parse(task.due_at);
|
|
26466
|
+
return Number.isFinite(due) && due < Date.parse(generatedAt);
|
|
26467
|
+
}
|
|
26468
|
+
function taskSummary(task, blockers) {
|
|
26469
|
+
if (blockers.length > 0) {
|
|
26470
|
+
return `Blocked by ${blockers.map((blocker) => blocker.id.slice(0, 8)).join(", ")}`;
|
|
25300
26471
|
}
|
|
25301
26472
|
const firstLine = task.description?.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
25302
26473
|
return firstLine || undefined;
|
|
@@ -25430,10 +26601,10 @@ __export(exports_extract, {
|
|
|
25430
26601
|
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
25431
26602
|
});
|
|
25432
26603
|
import { existsSync as existsSync14, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
|
|
25433
|
-
import { createHash as
|
|
26604
|
+
import { createHash as createHash6 } from "crypto";
|
|
25434
26605
|
import { relative as relative3, resolve as resolve12, join as join13 } from "path";
|
|
25435
26606
|
function stableHash(value) {
|
|
25436
|
-
return
|
|
26607
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
25437
26608
|
}
|
|
25438
26609
|
function normalizePathForMatch(value) {
|
|
25439
26610
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -25974,7 +27145,7 @@ function validateLocalBridgeBundle(value) {
|
|
|
25974
27145
|
}
|
|
25975
27146
|
return { ok: issues.length === 0, issues };
|
|
25976
27147
|
}
|
|
25977
|
-
function
|
|
27148
|
+
function existsById2(db, table, id) {
|
|
25978
27149
|
return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
|
|
25979
27150
|
}
|
|
25980
27151
|
function dependencyExists(db, row) {
|
|
@@ -25990,7 +27161,7 @@ function plannedIdSets(data) {
|
|
|
25990
27161
|
return result;
|
|
25991
27162
|
}
|
|
25992
27163
|
function hasExistingOrPlannedId(db, tableKey, id, plannedIds) {
|
|
25993
|
-
return
|
|
27164
|
+
return existsById2(db, tableByKey[tableKey], id) || Boolean(plannedIds.get(tableKey)?.has(id));
|
|
25994
27165
|
}
|
|
25995
27166
|
function missingDependency(db, tableKey, row, plannedIds) {
|
|
25996
27167
|
const taskId = typeof row.task_id === "string" ? row.task_id : null;
|
|
@@ -26067,7 +27238,7 @@ function insertRecord(db, tableKey, row) {
|
|
|
26067
27238
|
}
|
|
26068
27239
|
return result.changes > 0;
|
|
26069
27240
|
}
|
|
26070
|
-
function
|
|
27241
|
+
function sortedTasks2(tasks) {
|
|
26071
27242
|
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
26072
27243
|
const visited = new Set;
|
|
26073
27244
|
const result = [];
|
|
@@ -26188,7 +27359,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
|
|
|
26188
27359
|
const data = {
|
|
26189
27360
|
...bundle.data,
|
|
26190
27361
|
plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
|
|
26191
|
-
tasks:
|
|
27362
|
+
tasks: sortedTasks2(bundle.data.tasks),
|
|
26192
27363
|
saved_views: bundle.data.saved_views ?? [],
|
|
26193
27364
|
task_boards: bundle.data.task_boards ?? [],
|
|
26194
27365
|
local_calendar_items: bundle.data.local_calendar_items ?? []
|
|
@@ -26198,7 +27369,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
|
|
|
26198
27369
|
for (const row of data[key]) {
|
|
26199
27370
|
const table = tableByKey[key];
|
|
26200
27371
|
const id = key === "task_dependencies" ? `${row.task_id}->${row.depends_on}` : String(row.id);
|
|
26201
|
-
const exists = key === "task_dependencies" ? dependencyExists(d, row) :
|
|
27372
|
+
const exists = key === "task_dependencies" ? dependencyExists(d, row) : existsById2(d, table, id);
|
|
26202
27373
|
if (exists) {
|
|
26203
27374
|
if (key === "tasks" && conflictStrategy === "safe_merge") {
|
|
26204
27375
|
const merge = safeMergeTask(d, row, { dryRun });
|
|
@@ -26413,12 +27584,12 @@ __export(exports_local_encryption, {
|
|
|
26413
27584
|
DEFAULT_ENCRYPTION_PROFILE: () => DEFAULT_ENCRYPTION_PROFILE,
|
|
26414
27585
|
DEFAULT_ENCRYPTION_KEY_ENV: () => DEFAULT_ENCRYPTION_KEY_ENV
|
|
26415
27586
|
});
|
|
26416
|
-
import { createCipheriv, createDecipheriv, createHash as
|
|
27587
|
+
import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
26417
27588
|
function now3() {
|
|
26418
27589
|
return new Date().toISOString();
|
|
26419
27590
|
}
|
|
26420
27591
|
function sha2563(value) {
|
|
26421
|
-
return
|
|
27592
|
+
return createHash7("sha256").update(value).digest("hex");
|
|
26422
27593
|
}
|
|
26423
27594
|
function normalizeProfileName(value) {
|
|
26424
27595
|
const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
|
|
@@ -27474,9 +28645,69 @@ function registerProjectCommands(program2) {
|
|
|
27474
28645
|
}
|
|
27475
28646
|
}
|
|
27476
28647
|
});
|
|
27477
|
-
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").action(async (opts) => {
|
|
28648
|
+
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").option("--ensure-task-list <project>", "Plan or apply creation of an existing project's declared task list").option("--rollback-task-list <project>", "Conditionally roll back a task list created by --ensure-task-list").option("--apply", "Apply --ensure-task-list or --rollback-task-list; ensure plans by default").option("--idempotency-key <key>", "Stable idempotency key for --ensure-task-list --apply").option("--receipt <id>", "Accepted ensure receipt for --rollback-task-list --apply").action(async (opts) => {
|
|
27478
28649
|
const globalOpts = program2.opts();
|
|
27479
28650
|
const cloud = getTodosCloudClient();
|
|
28651
|
+
if (opts.ensureTaskList && opts.rollbackTaskList) {
|
|
28652
|
+
handleError(new Error("Choose either --ensure-task-list or --rollback-task-list, not both"));
|
|
28653
|
+
}
|
|
28654
|
+
if (opts.apply && !opts.ensureTaskList && !opts.rollbackTaskList) {
|
|
28655
|
+
handleError(new Error("projects --apply requires --ensure-task-list or --rollback-task-list"));
|
|
28656
|
+
}
|
|
28657
|
+
if (opts.dryRun && opts.apply && (opts.ensureTaskList || opts.rollbackTaskList)) {
|
|
28658
|
+
handleError(new Error("Choose either --dry-run or --apply, not both"));
|
|
28659
|
+
}
|
|
28660
|
+
if (opts.ensureTaskList) {
|
|
28661
|
+
const project = cloud ? await cloudResolveProject(cloud, opts.ensureTaskList) : resolveExplicitProject(opts.ensureTaskList);
|
|
28662
|
+
const store = cloud ? null : createLocalSqliteTodosStorageAdapter({ db: getDatabase() });
|
|
28663
|
+
const plan = cloud ? await cloudPlanProjectTaskListEnsure(cloud, project.id) : await planProjectTaskListEnsure(store, project.id);
|
|
28664
|
+
const result = opts.apply ? cloud ? await cloudApplyProjectTaskListEnsure(cloud, project.id, {
|
|
28665
|
+
expected_project_revision: plan.project.updated_at,
|
|
28666
|
+
...opts.idempotencyKey ? { idempotency_key: opts.idempotencyKey } : {}
|
|
28667
|
+
}) : await applyProjectTaskListEnsure(store, project.id, {
|
|
28668
|
+
expected_project_revision: plan.project.updated_at,
|
|
28669
|
+
...opts.idempotencyKey ? { idempotency_key: opts.idempotencyKey } : {}
|
|
28670
|
+
}) : plan;
|
|
28671
|
+
if (globalOpts.json) {
|
|
28672
|
+
output(result, true);
|
|
28673
|
+
} else {
|
|
28674
|
+
const list = result.task_list ? `${result.task_list.slug} (${result.task_list.id})` : result.project.task_list_id;
|
|
28675
|
+
console.log(chalk5.green(`${result.mode === "plan" ? "Task-list plan" : "Task-list ensure"}: ${result.action}`));
|
|
28676
|
+
console.log(chalk5.dim(` Project: ${result.project.name} (${result.project.id})`));
|
|
28677
|
+
console.log(chalk5.dim(` Task list: ${list}`));
|
|
28678
|
+
if (result.receipt)
|
|
28679
|
+
console.log(chalk5.dim(` Receipt: ${result.receipt.receipt_id}`));
|
|
28680
|
+
}
|
|
28681
|
+
return;
|
|
28682
|
+
}
|
|
28683
|
+
if (opts.rollbackTaskList) {
|
|
28684
|
+
if (!opts.apply) {
|
|
28685
|
+
handleError(new Error("projects --rollback-task-list requires --apply"));
|
|
28686
|
+
}
|
|
28687
|
+
if (!opts.receipt) {
|
|
28688
|
+
handleError(new Error("projects --rollback-task-list requires --receipt"));
|
|
28689
|
+
}
|
|
28690
|
+
const project = cloud ? await cloudResolveProject(cloud, opts.rollbackTaskList) : resolveExplicitProject(opts.rollbackTaskList);
|
|
28691
|
+
const store = cloud ? null : createLocalSqliteTodosStorageAdapter({ db: getDatabase() });
|
|
28692
|
+
const plan = cloud ? await cloudPlanProjectTaskListEnsure(cloud, project.id) : await planProjectTaskListEnsure(store, project.id);
|
|
28693
|
+
if (!plan.task_list) {
|
|
28694
|
+
handleError(new Error("The project has no exact declared task list to roll back"));
|
|
28695
|
+
}
|
|
28696
|
+
const input = {
|
|
28697
|
+
receipt_id: opts.receipt,
|
|
28698
|
+
expected_task_list_revision: plan.task_list.updated_at
|
|
28699
|
+
};
|
|
28700
|
+
const result = cloud ? await cloudRollbackProjectTaskListEnsure(cloud, project.id, input) : await rollbackProjectTaskListEnsure(store, project.id, input);
|
|
28701
|
+
if (globalOpts.json) {
|
|
28702
|
+
output(result, true);
|
|
28703
|
+
} else {
|
|
28704
|
+
console.log(chalk5.green(`Task-list rollback: ${result.action}`));
|
|
28705
|
+
console.log(chalk5.dim(` Project: ${result.project_id}`));
|
|
28706
|
+
console.log(chalk5.dim(` Removed task list: ${result.task_list_id}`));
|
|
28707
|
+
console.log(chalk5.dim(` Rollback receipt: ${result.rollback_receipt_id}`));
|
|
28708
|
+
}
|
|
28709
|
+
return;
|
|
28710
|
+
}
|
|
27480
28711
|
if (opts.show) {
|
|
27481
28712
|
const project = cloud ? await cloudResolveProject(cloud, opts.show) : resolveExplicitProject(opts.show);
|
|
27482
28713
|
outputRecord(project, Boolean(globalOpts.json), "Project:");
|
|
@@ -27976,6 +29207,8 @@ var init_project_commands = __esm(() => {
|
|
|
27976
29207
|
init_config();
|
|
27977
29208
|
init_helpers();
|
|
27978
29209
|
init_output_redaction();
|
|
29210
|
+
init_local_sqlite();
|
|
29211
|
+
init_project_task_list_ensure();
|
|
27979
29212
|
SEARCH_SCOPE_FLAG = {
|
|
27980
29213
|
name: "--scope",
|
|
27981
29214
|
vocabulary: SAVED_SEARCH_SCOPES,
|
|
@@ -28662,12 +29895,12 @@ function unique4(values) {
|
|
|
28662
29895
|
function placeholders(values) {
|
|
28663
29896
|
return values.map(() => "?").join(", ");
|
|
28664
29897
|
}
|
|
28665
|
-
function
|
|
29898
|
+
function tableExists2(db, table) {
|
|
28666
29899
|
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
28667
29900
|
return Boolean(row);
|
|
28668
29901
|
}
|
|
28669
29902
|
function tableColumns(db, table) {
|
|
28670
|
-
if (!
|
|
29903
|
+
if (!tableExists2(db, table))
|
|
28671
29904
|
return new Set;
|
|
28672
29905
|
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
28673
29906
|
return new Set(rows.map((row) => row.name));
|
|
@@ -28683,7 +29916,7 @@ function normalizeScope2(options) {
|
|
|
28683
29916
|
};
|
|
28684
29917
|
}
|
|
28685
29918
|
function scopedCommentTaskIds(db, commentIds) {
|
|
28686
|
-
if (commentIds.length === 0 || !
|
|
29919
|
+
if (commentIds.length === 0 || !tableExists2(db, "task_comments"))
|
|
28687
29920
|
return [];
|
|
28688
29921
|
const rows = db.query(`SELECT DISTINCT task_id FROM task_comments WHERE id IN (${placeholders(commentIds)})`).all(...commentIds);
|
|
28689
29922
|
return rows.map((row) => row.task_id).filter(Boolean);
|
|
@@ -29896,7 +31129,7 @@ __export(exports_local_extensions, {
|
|
|
29896
31129
|
getLocalExtension: () => getLocalExtension,
|
|
29897
31130
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
29898
31131
|
});
|
|
29899
|
-
import { createHash as
|
|
31132
|
+
import { createHash as createHash8, createVerify } from "crypto";
|
|
29900
31133
|
import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
29901
31134
|
import { basename as basename6, join as join16, resolve as resolve15 } from "path";
|
|
29902
31135
|
function isObject(value) {
|
|
@@ -29982,7 +31215,7 @@ function parseJson(path) {
|
|
|
29982
31215
|
return JSON.parse(readFileSync10(path, "utf8"));
|
|
29983
31216
|
}
|
|
29984
31217
|
function sha2564(bytes) {
|
|
29985
|
-
return `sha256:${
|
|
31218
|
+
return `sha256:${createHash8("sha256").update(bytes).digest("hex")}`;
|
|
29986
31219
|
}
|
|
29987
31220
|
function compareVersions(a, b) {
|
|
29988
31221
|
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -31339,7 +32572,7 @@ var init_terminal_notifications = __esm(() => {
|
|
|
31339
32572
|
});
|
|
31340
32573
|
|
|
31341
32574
|
// src/db/api-keys.ts
|
|
31342
|
-
import { createHash as
|
|
32575
|
+
import { createHash as createHash9, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
31343
32576
|
function rowToRecord(row) {
|
|
31344
32577
|
return {
|
|
31345
32578
|
id: row.id,
|
|
@@ -31353,7 +32586,7 @@ function rowToRecord(row) {
|
|
|
31353
32586
|
};
|
|
31354
32587
|
}
|
|
31355
32588
|
function hashApiKey(key) {
|
|
31356
|
-
return
|
|
32589
|
+
return createHash9("sha256").update(key).digest("hex");
|
|
31357
32590
|
}
|
|
31358
32591
|
function safeEqualHex(a, b) {
|
|
31359
32592
|
if (a.length !== b.length)
|
|
@@ -31361,8 +32594,8 @@ function safeEqualHex(a, b) {
|
|
|
31361
32594
|
return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
31362
32595
|
}
|
|
31363
32596
|
function safeEqualStrings(a, b) {
|
|
31364
|
-
const ah =
|
|
31365
|
-
const bh =
|
|
32597
|
+
const ah = createHash9("sha256").update(a, "utf8").digest();
|
|
32598
|
+
const bh = createHash9("sha256").update(b, "utf8").digest();
|
|
31366
32599
|
return timingSafeEqual3(ah, bh);
|
|
31367
32600
|
}
|
|
31368
32601
|
function generatePlaintextKey() {
|
|
@@ -31889,239 +33122,6 @@ var init_postgres_sync = __esm(() => {
|
|
|
31889
33122
|
};
|
|
31890
33123
|
});
|
|
31891
33124
|
|
|
31892
|
-
// src/lib/integrity.ts
|
|
31893
|
-
function resolveIntegritySeverity(spec, measurement) {
|
|
31894
|
-
if (spec.kind === "dangling")
|
|
31895
|
-
return "error";
|
|
31896
|
-
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
31897
|
-
return "error";
|
|
31898
|
-
return spec.base_severity;
|
|
31899
|
-
}
|
|
31900
|
-
function plural(entity, count) {
|
|
31901
|
-
const [one, many] = ENTITY_LABEL[entity];
|
|
31902
|
-
return count === 1 ? one : many;
|
|
31903
|
-
}
|
|
31904
|
-
function formatIntegrityMessage(spec, measurement) {
|
|
31905
|
-
const noun = plural(spec.entity, measurement.count);
|
|
31906
|
-
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
31907
|
-
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
31908
|
-
return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
|
|
31909
|
-
}
|
|
31910
|
-
function measuredCondition(spec, measurement, source2) {
|
|
31911
|
-
return {
|
|
31912
|
-
id: spec.id,
|
|
31913
|
-
entity: spec.entity,
|
|
31914
|
-
field: spec.field,
|
|
31915
|
-
kind: spec.kind,
|
|
31916
|
-
count: measurement.count,
|
|
31917
|
-
open_count: measurement.open_count,
|
|
31918
|
-
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
31919
|
-
verified: true,
|
|
31920
|
-
source: source2,
|
|
31921
|
-
message: formatIntegrityMessage(spec, measurement),
|
|
31922
|
-
impact: spec.impact
|
|
31923
|
-
};
|
|
31924
|
-
}
|
|
31925
|
-
function unverifiedCondition(spec, reason) {
|
|
31926
|
-
return {
|
|
31927
|
-
id: spec.id,
|
|
31928
|
-
entity: spec.entity,
|
|
31929
|
-
field: spec.field,
|
|
31930
|
-
kind: spec.kind,
|
|
31931
|
-
count: null,
|
|
31932
|
-
open_count: null,
|
|
31933
|
-
severity: null,
|
|
31934
|
-
verified: false,
|
|
31935
|
-
source: "unverified",
|
|
31936
|
-
unverified_reason: reason,
|
|
31937
|
-
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
31938
|
-
impact: spec.impact
|
|
31939
|
-
};
|
|
31940
|
-
}
|
|
31941
|
-
function summarizeIntegrity(conditions) {
|
|
31942
|
-
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
31943
|
-
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
31944
|
-
const unverified = conditions.length - measured.length;
|
|
31945
|
-
return {
|
|
31946
|
-
ok: unverified === 0 && findings.length === 0,
|
|
31947
|
-
findings: findings.length,
|
|
31948
|
-
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
31949
|
-
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
31950
|
-
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
31951
|
-
unverified,
|
|
31952
|
-
complete: unverified === 0
|
|
31953
|
-
};
|
|
31954
|
-
}
|
|
31955
|
-
function buildIntegrityReport(conditions, generatedAt) {
|
|
31956
|
-
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source2) => source2 !== "unverified");
|
|
31957
|
-
return {
|
|
31958
|
-
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
31959
|
-
generated_at: generatedAt,
|
|
31960
|
-
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
31961
|
-
conditions,
|
|
31962
|
-
summary: summarizeIntegrity(conditions)
|
|
31963
|
-
};
|
|
31964
|
-
}
|
|
31965
|
-
function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
|
|
31966
|
-
const received = new Map;
|
|
31967
|
-
if (Array.isArray(raw.conditions)) {
|
|
31968
|
-
for (const entry2 of raw.conditions) {
|
|
31969
|
-
if (entry2 && typeof entry2["id"] === "string")
|
|
31970
|
-
received.set(entry2["id"], entry2);
|
|
31971
|
-
}
|
|
31972
|
-
}
|
|
31973
|
-
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
31974
|
-
const entry2 = received.get(spec.id);
|
|
31975
|
-
const count = entry2?.["count"];
|
|
31976
|
-
const verified = entry2?.["verified"];
|
|
31977
|
-
if (!entry2 || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
|
|
31978
|
-
return unverifiedCondition(spec, entry2 ? typeof entry2["unverified_reason"] === "string" ? String(entry2["unverified_reason"]) : "authority reported this condition without a usable count" : "authority did not report this condition");
|
|
31979
|
-
}
|
|
31980
|
-
const openRaw = entry2["open_count"];
|
|
31981
|
-
return measuredCondition(spec, {
|
|
31982
|
-
count,
|
|
31983
|
-
open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
|
|
31984
|
-
}, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
|
|
31985
|
-
});
|
|
31986
|
-
const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
|
|
31987
|
-
return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
|
|
31988
|
-
}
|
|
31989
|
-
function sqliteOpenStatusList() {
|
|
31990
|
-
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
31991
|
-
}
|
|
31992
|
-
function buildSqliteIntegritySql(spec) {
|
|
31993
|
-
const table = SQLITE_TABLE[spec.entity];
|
|
31994
|
-
const target = SQLITE_TABLE[spec.target];
|
|
31995
|
-
const column = `t."${spec.field}"`;
|
|
31996
|
-
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
|
|
31997
|
-
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
31998
|
-
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
31999
|
-
}
|
|
32000
|
-
function buildPostgresIntegritySql(spec, options) {
|
|
32001
|
-
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
32002
|
-
const p = (value) => {
|
|
32003
|
-
params.push(value);
|
|
32004
|
-
return `$${params.length}`;
|
|
32005
|
-
};
|
|
32006
|
-
const column = `t.payload->>'${spec.field}'`;
|
|
32007
|
-
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
|
|
32008
|
-
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
32009
|
-
const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
|
|
32010
|
-
return { sql, params };
|
|
32011
|
-
}
|
|
32012
|
-
function referenceOf(row, field) {
|
|
32013
|
-
const raw = row[field];
|
|
32014
|
-
if (typeof raw !== "string")
|
|
32015
|
-
return null;
|
|
32016
|
-
const trimmed = raw.trim();
|
|
32017
|
-
return trimmed === "" ? null : trimmed;
|
|
32018
|
-
}
|
|
32019
|
-
function measureIntegrityRows(spec, sets) {
|
|
32020
|
-
const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
|
|
32021
|
-
if (!rows)
|
|
32022
|
-
return null;
|
|
32023
|
-
const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
|
|
32024
|
-
if (spec.kind === "dangling" && !registered)
|
|
32025
|
-
return null;
|
|
32026
|
-
let count = 0;
|
|
32027
|
-
let open = 0;
|
|
32028
|
-
for (const row of rows) {
|
|
32029
|
-
const reference = referenceOf(row, spec.field);
|
|
32030
|
-
const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
|
|
32031
|
-
if (!matches)
|
|
32032
|
-
continue;
|
|
32033
|
-
count++;
|
|
32034
|
-
if (spec.entity === "task") {
|
|
32035
|
-
const status = row.status;
|
|
32036
|
-
if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
|
|
32037
|
-
open++;
|
|
32038
|
-
}
|
|
32039
|
-
}
|
|
32040
|
-
return { count, open_count: spec.entity === "task" ? open : null };
|
|
32041
|
-
}
|
|
32042
|
-
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
|
|
32043
|
-
var init_integrity = __esm(() => {
|
|
32044
|
-
init_types();
|
|
32045
|
-
TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
32046
|
-
OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
32047
|
-
INTEGRITY_CONDITIONS = [
|
|
32048
|
-
{
|
|
32049
|
-
id: "tasks_without_project",
|
|
32050
|
-
entity: "task",
|
|
32051
|
-
field: "project_id",
|
|
32052
|
-
target: "project",
|
|
32053
|
-
kind: "missing",
|
|
32054
|
-
base_severity: "warn",
|
|
32055
|
-
escalate_when_open: true,
|
|
32056
|
-
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
32057
|
-
},
|
|
32058
|
-
{
|
|
32059
|
-
id: "tasks_without_task_list",
|
|
32060
|
-
entity: "task",
|
|
32061
|
-
field: "task_list_id",
|
|
32062
|
-
target: "task_list",
|
|
32063
|
-
kind: "missing",
|
|
32064
|
-
base_severity: "warn",
|
|
32065
|
-
escalate_when_open: true,
|
|
32066
|
-
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
32067
|
-
},
|
|
32068
|
-
{
|
|
32069
|
-
id: "tasks_with_unregistered_project",
|
|
32070
|
-
entity: "task",
|
|
32071
|
-
field: "project_id",
|
|
32072
|
-
target: "project",
|
|
32073
|
-
kind: "dangling",
|
|
32074
|
-
base_severity: "error",
|
|
32075
|
-
escalate_when_open: false,
|
|
32076
|
-
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
32077
|
-
},
|
|
32078
|
-
{
|
|
32079
|
-
id: "tasks_with_unregistered_task_list",
|
|
32080
|
-
entity: "task",
|
|
32081
|
-
field: "task_list_id",
|
|
32082
|
-
target: "task_list",
|
|
32083
|
-
kind: "dangling",
|
|
32084
|
-
base_severity: "error",
|
|
32085
|
-
escalate_when_open: false,
|
|
32086
|
-
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
32087
|
-
},
|
|
32088
|
-
{
|
|
32089
|
-
id: "task_lists_without_project",
|
|
32090
|
-
entity: "task_list",
|
|
32091
|
-
field: "project_id",
|
|
32092
|
-
target: "project",
|
|
32093
|
-
kind: "missing",
|
|
32094
|
-
base_severity: "warn",
|
|
32095
|
-
escalate_when_open: false,
|
|
32096
|
-
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
32097
|
-
},
|
|
32098
|
-
{
|
|
32099
|
-
id: "task_lists_with_unregistered_project",
|
|
32100
|
-
entity: "task_list",
|
|
32101
|
-
field: "project_id",
|
|
32102
|
-
target: "project",
|
|
32103
|
-
kind: "dangling",
|
|
32104
|
-
base_severity: "error",
|
|
32105
|
-
escalate_when_open: false,
|
|
32106
|
-
impact: "points at a project id that does not exist; the list can never be reached"
|
|
32107
|
-
}
|
|
32108
|
-
];
|
|
32109
|
-
ENTITY_LABEL = {
|
|
32110
|
-
task: ["task", "tasks"],
|
|
32111
|
-
task_list: ["task list", "task lists"]
|
|
32112
|
-
};
|
|
32113
|
-
SQLITE_TABLE = {
|
|
32114
|
-
task: "tasks",
|
|
32115
|
-
task_list: "task_lists",
|
|
32116
|
-
project: "projects"
|
|
32117
|
-
};
|
|
32118
|
-
RECORD_TYPE = {
|
|
32119
|
-
task: "tasks",
|
|
32120
|
-
task_list: "task_lists",
|
|
32121
|
-
project: "projects"
|
|
32122
|
-
};
|
|
32123
|
-
});
|
|
32124
|
-
|
|
32125
33125
|
// src/storage/postgres-adapter.ts
|
|
32126
33126
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
32127
33127
|
function createPostgresTodosStorageAdapter(options) {
|
|
@@ -34271,8 +35271,8 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
34271
35271
|
]);
|
|
34272
35272
|
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
34273
35273
|
}
|
|
34274
|
-
async getReceiptById(
|
|
34275
|
-
const result = await this.client.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = $1 LIMIT 1", [
|
|
35274
|
+
async getReceiptById(receiptId2) {
|
|
35275
|
+
const result = await this.client.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = $1 LIMIT 1", [receiptId2]);
|
|
34276
35276
|
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
34277
35277
|
}
|
|
34278
35278
|
async getAcceptedReceiptForStep(identity) {
|
|
@@ -34552,8 +35552,8 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
34552
35552
|
async getReceiptForLookup(identity) {
|
|
34553
35553
|
return (await this.direct()).getReceiptForLookup(identity);
|
|
34554
35554
|
}
|
|
34555
|
-
async getReceiptById(
|
|
34556
|
-
return (await this.direct()).getReceiptById(
|
|
35555
|
+
async getReceiptById(receiptId2) {
|
|
35556
|
+
return (await this.direct()).getReceiptById(receiptId2);
|
|
34557
35557
|
}
|
|
34558
35558
|
async getBinding(scope, resourceKind, targetSelector) {
|
|
34559
35559
|
return (await this.direct()).getBinding(scope, resourceKind, targetSelector);
|
|
@@ -34571,618 +35571,593 @@ var init_postgres2 = __esm(() => {
|
|
|
34571
35571
|
init_types3();
|
|
34572
35572
|
});
|
|
34573
35573
|
|
|
34574
|
-
// src/
|
|
34575
|
-
function
|
|
34576
|
-
return
|
|
34577
|
-
}
|
|
34578
|
-
function scanSqliteIntegrity(db = getDatabase()) {
|
|
34579
|
-
const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists2(db, table));
|
|
34580
|
-
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
34581
|
-
if (missing.length > 0) {
|
|
34582
|
-
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
34583
|
-
}
|
|
34584
|
-
try {
|
|
34585
|
-
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
34586
|
-
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
34587
|
-
} catch (error) {
|
|
34588
|
-
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
34589
|
-
}
|
|
34590
|
-
});
|
|
34591
|
-
return buildIntegrityReport(conditions, now());
|
|
35574
|
+
// src/project-registration/sqlite.ts
|
|
35575
|
+
function sameSqliteValue(left, right) {
|
|
35576
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
34592
35577
|
}
|
|
34593
|
-
|
|
34594
|
-
var init_integrity2 = __esm(() => {
|
|
34595
|
-
init_database();
|
|
34596
|
-
init_integrity();
|
|
34597
|
-
REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
|
|
34598
|
-
});
|
|
34599
|
-
|
|
34600
|
-
// src/storage/sqlite-snapshot.ts
|
|
34601
|
-
function exportSqliteTodosStorageSnapshot(db) {
|
|
34602
|
-
const d = db ?? getDatabase();
|
|
35578
|
+
function taskListFromRow(row) {
|
|
34603
35579
|
return {
|
|
34604
|
-
|
|
34605
|
-
|
|
34606
|
-
|
|
34607
|
-
|
|
34608
|
-
|
|
34609
|
-
|
|
34610
|
-
|
|
34611
|
-
|
|
34612
|
-
|
|
34613
|
-
|
|
34614
|
-
|
|
34615
|
-
|
|
34616
|
-
|
|
34617
|
-
|
|
34618
|
-
|
|
34619
|
-
|
|
34620
|
-
|
|
34621
|
-
|
|
34622
|
-
|
|
34623
|
-
|
|
34624
|
-
|
|
34625
|
-
|
|
34626
|
-
|
|
34627
|
-
|
|
34628
|
-
|
|
34629
|
-
|
|
34630
|
-
|
|
34631
|
-
|
|
34632
|
-
}
|
|
34633
|
-
|
|
34634
|
-
|
|
34635
|
-
const
|
|
34636
|
-
|
|
34637
|
-
|
|
34638
|
-
|
|
34639
|
-
|
|
34640
|
-
|
|
34641
|
-
|
|
34642
|
-
|
|
34643
|
-
|
|
34644
|
-
|
|
34645
|
-
|
|
34646
|
-
|
|
34647
|
-
|
|
34648
|
-
|
|
34649
|
-
|
|
34650
|
-
|
|
34651
|
-
|
|
34652
|
-
|
|
34653
|
-
|
|
34654
|
-
|
|
34655
|
-
|
|
34656
|
-
|
|
34657
|
-
|
|
34658
|
-
|
|
34659
|
-
|
|
34660
|
-
|
|
34661
|
-
|
|
34662
|
-
|
|
34663
|
-
|
|
34664
|
-
|
|
34665
|
-
|
|
34666
|
-
|
|
34667
|
-
|
|
34668
|
-
|
|
34669
|
-
|
|
34670
|
-
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
34671
|
-
return result;
|
|
34672
|
-
}
|
|
34673
|
-
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
34674
|
-
const id = row["id"];
|
|
34675
|
-
if (typeof id !== "string" || !id)
|
|
34676
|
-
throw new Error(`${table} row is missing id`);
|
|
34677
|
-
const presentColumns = columns.filter((column) => (column in row));
|
|
34678
|
-
if (!presentColumns.includes("id"))
|
|
34679
|
-
presentColumns.unshift("id");
|
|
34680
|
-
const existing = existsById2(db, table, id);
|
|
34681
|
-
const placeholders3 = presentColumns.map(() => "?").join(", ");
|
|
34682
|
-
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
34683
|
-
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
34684
|
-
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
34685
|
-
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
34686
|
-
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders3})
|
|
34687
|
-
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders3})`;
|
|
34688
|
-
const changes = db.run(sql, values).changes;
|
|
34689
|
-
if (changes === 0)
|
|
34690
|
-
return "skipped";
|
|
34691
|
-
return existing ? "updated" : "inserted";
|
|
34692
|
-
}
|
|
34693
|
-
function existsById2(db, table, id) {
|
|
34694
|
-
return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
|
|
34695
|
-
}
|
|
34696
|
-
function valueForColumn(column, value) {
|
|
34697
|
-
if (BOOLEAN_COLUMNS.has(column))
|
|
34698
|
-
return value ? 1 : 0;
|
|
34699
|
-
if (JSON_COLUMNS.has(column))
|
|
34700
|
-
return JSON.stringify(value ?? (column === "tags" || column === "permissions" || column === "capabilities" || column === "variables" ? [] : {}));
|
|
34701
|
-
return value === undefined ? null : value;
|
|
34702
|
-
}
|
|
34703
|
-
function asRecord(value) {
|
|
34704
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
34705
|
-
throw new Error("snapshot rows must be objects");
|
|
34706
|
-
}
|
|
34707
|
-
return value;
|
|
34708
|
-
}
|
|
34709
|
-
function sortedTasks2(tasks) {
|
|
34710
|
-
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
34711
|
-
const seen = new Set;
|
|
34712
|
-
const result = [];
|
|
34713
|
-
const visit = (task) => {
|
|
34714
|
-
if (seen.has(task.id))
|
|
34715
|
-
return;
|
|
34716
|
-
if (task.parent_id && byId.has(task.parent_id))
|
|
34717
|
-
visit(byId.get(task.parent_id));
|
|
34718
|
-
seen.add(task.id);
|
|
34719
|
-
result.push(task);
|
|
34720
|
-
};
|
|
34721
|
-
for (const task of tasks)
|
|
34722
|
-
visit(task);
|
|
34723
|
-
return result;
|
|
34724
|
-
}
|
|
34725
|
-
function applyTombstones(db, tombstones, result) {
|
|
34726
|
-
for (const tombstone of tombstones) {
|
|
34727
|
-
try {
|
|
34728
|
-
recordStorageTombstone({
|
|
34729
|
-
object_type: tombstone.object_type,
|
|
34730
|
-
object_id: tombstone.object_id,
|
|
34731
|
-
deleted_at: tombstone.deleted_at,
|
|
34732
|
-
source_machine_id: tombstone.source_machine_id ?? null,
|
|
34733
|
-
payload: tombstone.payload ?? null,
|
|
34734
|
-
version: tombstone.version ?? null
|
|
34735
|
-
}, db);
|
|
34736
|
-
const table = tableForTombstone(tombstone.object_type);
|
|
34737
|
-
const existing = existingClock(db, table, tombstone.object_id);
|
|
34738
|
-
if (!shouldApplyStorageTombstone(tombstone, existing)) {
|
|
34739
|
-
result.skipped += 1;
|
|
34740
|
-
continue;
|
|
34741
|
-
}
|
|
34742
|
-
const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
|
|
34743
|
-
const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
|
|
34744
|
-
if (deleted > 0 || deletedTags > 0)
|
|
34745
|
-
result.deleted = (result.deleted ?? 0) + 1;
|
|
34746
|
-
else
|
|
34747
|
-
result.skipped += 1;
|
|
34748
|
-
} catch (error) {
|
|
34749
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
34750
|
-
}
|
|
34751
|
-
}
|
|
34752
|
-
}
|
|
34753
|
-
function tableForTombstone(objectType) {
|
|
34754
|
-
if (objectType === "tasks")
|
|
34755
|
-
return "tasks";
|
|
34756
|
-
if (objectType === "projects")
|
|
34757
|
-
return "projects";
|
|
34758
|
-
if (objectType === "project_machine_paths")
|
|
34759
|
-
return "project_machine_paths";
|
|
34760
|
-
if (objectType === "plans")
|
|
34761
|
-
return "plans";
|
|
34762
|
-
if (objectType === "agents")
|
|
34763
|
-
return "agents";
|
|
34764
|
-
if (objectType === "task_lists")
|
|
34765
|
-
return "task_lists";
|
|
34766
|
-
if (objectType === "templates")
|
|
34767
|
-
return "task_templates";
|
|
34768
|
-
if (objectType === "template_tasks")
|
|
34769
|
-
return "template_tasks";
|
|
34770
|
-
return "task_history";
|
|
34771
|
-
}
|
|
34772
|
-
function listRows(db, table, columns) {
|
|
34773
|
-
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
34774
|
-
}
|
|
34775
|
-
function listProjectMachinePaths(db) {
|
|
34776
|
-
return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
|
|
34777
|
-
id: String(row.id),
|
|
34778
|
-
project_id: String(row.project_id),
|
|
34779
|
-
machine_id: String(row.machine_id),
|
|
34780
|
-
path: String(row.path),
|
|
34781
|
-
created_at: String(row.created_at),
|
|
34782
|
-
updated_at: String(row.updated_at)
|
|
34783
|
-
}));
|
|
34784
|
-
}
|
|
34785
|
-
function existingClock(db, table, id) {
|
|
34786
|
-
const clockColumns = clockColumnsForTable(table);
|
|
34787
|
-
const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
|
|
34788
|
-
return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
|
|
34789
|
-
}
|
|
34790
|
-
function rowClock(row, updateClockColumn) {
|
|
34791
|
-
const value = updateClockColumn ? row[updateClockColumn] : null;
|
|
34792
|
-
return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
|
|
34793
|
-
}
|
|
34794
|
-
function stringClock(value) {
|
|
34795
|
-
return typeof value === "string" && value ? value : null;
|
|
34796
|
-
}
|
|
34797
|
-
function clockColumnsForTable(table) {
|
|
34798
|
-
if (table === "agents")
|
|
34799
|
-
return ["last_seen_at", "created_at"];
|
|
34800
|
-
if (table === "task_templates")
|
|
34801
|
-
return ["created_at"];
|
|
34802
|
-
if (table === "task_history")
|
|
34803
|
-
return ["created_at"];
|
|
34804
|
-
return ["updated_at", "created_at"];
|
|
34805
|
-
}
|
|
34806
|
-
var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TEMPLATE_TASK_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
|
|
34807
|
-
var init_sqlite_snapshot = __esm(() => {
|
|
34808
|
-
init_database();
|
|
34809
|
-
init_agents();
|
|
34810
|
-
init_audit();
|
|
34811
|
-
init_plans();
|
|
34812
|
-
init_projects();
|
|
34813
|
-
init_task_lists();
|
|
34814
|
-
init_tasks();
|
|
34815
|
-
init_templates();
|
|
34816
|
-
init_storage_tombstones();
|
|
34817
|
-
PROJECT_COLUMNS = [
|
|
34818
|
-
"id",
|
|
34819
|
-
"name",
|
|
34820
|
-
"path",
|
|
34821
|
-
"description",
|
|
34822
|
-
"task_list_id",
|
|
34823
|
-
"task_prefix",
|
|
34824
|
-
"task_counter",
|
|
34825
|
-
"created_at",
|
|
34826
|
-
"updated_at",
|
|
34827
|
-
"machine_id",
|
|
34828
|
-
"synced_at"
|
|
34829
|
-
];
|
|
34830
|
-
PROJECT_MACHINE_PATH_COLUMNS = [
|
|
34831
|
-
"id",
|
|
34832
|
-
"project_id",
|
|
34833
|
-
"machine_id",
|
|
34834
|
-
"path",
|
|
34835
|
-
"created_at",
|
|
34836
|
-
"updated_at"
|
|
34837
|
-
];
|
|
34838
|
-
TASK_LIST_COLUMNS = [
|
|
34839
|
-
"id",
|
|
34840
|
-
"project_id",
|
|
34841
|
-
"slug",
|
|
34842
|
-
"name",
|
|
34843
|
-
"description",
|
|
34844
|
-
"metadata",
|
|
34845
|
-
"created_at",
|
|
34846
|
-
"updated_at",
|
|
34847
|
-
"machine_id",
|
|
34848
|
-
"synced_at"
|
|
34849
|
-
];
|
|
34850
|
-
PLAN_COLUMNS = [
|
|
34851
|
-
"id",
|
|
34852
|
-
"project_id",
|
|
34853
|
-
"task_list_id",
|
|
34854
|
-
"agent_id",
|
|
34855
|
-
"name",
|
|
34856
|
-
"description",
|
|
34857
|
-
"status",
|
|
34858
|
-
"created_at",
|
|
34859
|
-
"updated_at",
|
|
34860
|
-
"machine_id",
|
|
34861
|
-
"synced_at"
|
|
34862
|
-
];
|
|
34863
|
-
AGENT_COLUMNS = [
|
|
34864
|
-
"id",
|
|
34865
|
-
"name",
|
|
34866
|
-
"description",
|
|
34867
|
-
"role",
|
|
34868
|
-
"title",
|
|
34869
|
-
"level",
|
|
34870
|
-
"permissions",
|
|
34871
|
-
"capabilities",
|
|
34872
|
-
"reports_to",
|
|
34873
|
-
"org_id",
|
|
34874
|
-
"metadata",
|
|
34875
|
-
"status",
|
|
34876
|
-
"created_at",
|
|
34877
|
-
"last_seen_at",
|
|
34878
|
-
"session_id",
|
|
34879
|
-
"working_dir",
|
|
34880
|
-
"active_project_id",
|
|
34881
|
-
"machine_id",
|
|
34882
|
-
"synced_at"
|
|
34883
|
-
];
|
|
34884
|
-
TEMPLATE_COLUMNS = [
|
|
34885
|
-
"id",
|
|
34886
|
-
"name",
|
|
34887
|
-
"title_pattern",
|
|
34888
|
-
"description",
|
|
34889
|
-
"priority",
|
|
34890
|
-
"tags",
|
|
34891
|
-
"variables",
|
|
34892
|
-
"project_id",
|
|
34893
|
-
"plan_id",
|
|
34894
|
-
"metadata",
|
|
34895
|
-
"version",
|
|
34896
|
-
"created_at",
|
|
34897
|
-
"machine_id",
|
|
34898
|
-
"synced_at"
|
|
34899
|
-
];
|
|
34900
|
-
TEMPLATE_TASK_COLUMNS = [
|
|
34901
|
-
"id",
|
|
34902
|
-
"template_id",
|
|
34903
|
-
"position",
|
|
34904
|
-
"title_pattern",
|
|
34905
|
-
"description",
|
|
34906
|
-
"priority",
|
|
34907
|
-
"tags",
|
|
34908
|
-
"task_type",
|
|
34909
|
-
"condition",
|
|
34910
|
-
"include_template_id",
|
|
34911
|
-
"depends_on_positions",
|
|
34912
|
-
"metadata",
|
|
34913
|
-
"created_at"
|
|
34914
|
-
];
|
|
34915
|
-
TASK_COLUMNS = [
|
|
34916
|
-
"id",
|
|
34917
|
-
"short_id",
|
|
34918
|
-
"project_id",
|
|
34919
|
-
"parent_id",
|
|
34920
|
-
"plan_id",
|
|
34921
|
-
"task_list_id",
|
|
34922
|
-
"title",
|
|
34923
|
-
"description",
|
|
34924
|
-
"status",
|
|
34925
|
-
"priority",
|
|
34926
|
-
"agent_id",
|
|
34927
|
-
"assigned_to",
|
|
34928
|
-
"session_id",
|
|
34929
|
-
"working_dir",
|
|
34930
|
-
"tags",
|
|
34931
|
-
"metadata",
|
|
34932
|
-
"version",
|
|
34933
|
-
"locked_by",
|
|
34934
|
-
"locked_at",
|
|
34935
|
-
"created_at",
|
|
34936
|
-
"updated_at",
|
|
34937
|
-
"started_at",
|
|
34938
|
-
"completed_at",
|
|
34939
|
-
"due_at",
|
|
34940
|
-
"estimated_minutes",
|
|
34941
|
-
"actual_minutes",
|
|
34942
|
-
"requires_approval",
|
|
34943
|
-
"approved_by",
|
|
34944
|
-
"approved_at",
|
|
34945
|
-
"recurrence_rule",
|
|
34946
|
-
"recurrence_parent_id",
|
|
34947
|
-
"spawns_template_id",
|
|
34948
|
-
"confidence",
|
|
34949
|
-
"reason",
|
|
34950
|
-
"spawned_from_session",
|
|
34951
|
-
"assigned_by",
|
|
34952
|
-
"assigned_from_project",
|
|
34953
|
-
"task_type",
|
|
34954
|
-
"cost_tokens",
|
|
34955
|
-
"cost_usd",
|
|
34956
|
-
"delegated_from",
|
|
34957
|
-
"delegation_depth",
|
|
34958
|
-
"retry_count",
|
|
34959
|
-
"max_retries",
|
|
34960
|
-
"retry_after",
|
|
34961
|
-
"sla_minutes",
|
|
34962
|
-
"runner_id",
|
|
34963
|
-
"runner_started_at",
|
|
34964
|
-
"runner_completed_at",
|
|
34965
|
-
"current_step",
|
|
34966
|
-
"total_steps",
|
|
34967
|
-
"cycle_id",
|
|
34968
|
-
"machine_id",
|
|
34969
|
-
"synced_at",
|
|
34970
|
-
"archived_at"
|
|
34971
|
-
];
|
|
34972
|
-
AUDIT_COLUMNS = [
|
|
34973
|
-
"id",
|
|
34974
|
-
"task_id",
|
|
34975
|
-
"action",
|
|
34976
|
-
"field",
|
|
34977
|
-
"old_value",
|
|
34978
|
-
"new_value",
|
|
34979
|
-
"agent_id",
|
|
34980
|
-
"created_at",
|
|
34981
|
-
"machine_id"
|
|
34982
|
-
];
|
|
34983
|
-
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
|
|
34984
|
-
BOOLEAN_COLUMNS = new Set(["requires_approval"]);
|
|
34985
|
-
});
|
|
35580
|
+
...row,
|
|
35581
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
35582
|
+
};
|
|
35583
|
+
}
|
|
35584
|
+
function selectProject(db, id) {
|
|
35585
|
+
return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
|
|
35586
|
+
}
|
|
35587
|
+
function selectTaskList(db, id) {
|
|
35588
|
+
const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
|
|
35589
|
+
return row ? taskListFromRow(row) : null;
|
|
35590
|
+
}
|
|
35591
|
+
function selectProjectConflict(db, path, taskListSlug) {
|
|
35592
|
+
return db.query(`
|
|
35593
|
+
SELECT * FROM projects
|
|
35594
|
+
WHERE path = ? OR task_list_id = ?
|
|
35595
|
+
ORDER BY created_at ASC, id ASC
|
|
35596
|
+
LIMIT 1
|
|
35597
|
+
`).get(path, taskListSlug);
|
|
35598
|
+
}
|
|
35599
|
+
function selectTaskListConflict(db, projectId, slug) {
|
|
35600
|
+
const row = db.query(`
|
|
35601
|
+
SELECT * FROM task_lists
|
|
35602
|
+
WHERE project_id = ? AND slug = ?
|
|
35603
|
+
LIMIT 1
|
|
35604
|
+
`).get(projectId, slug);
|
|
35605
|
+
return row ? taskListFromRow(row) : null;
|
|
35606
|
+
}
|
|
35607
|
+
function quoteSqliteIdentifier(value) {
|
|
35608
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
35609
|
+
}
|
|
35610
|
+
function hasSqliteDependents(db, resourceKind, targetId) {
|
|
35611
|
+
const targetTable = resourceKind === "project" ? "projects" : "task_lists";
|
|
35612
|
+
const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
|
|
35613
|
+
const tables = db.query(`
|
|
35614
|
+
SELECT name FROM sqlite_schema
|
|
35615
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
35616
|
+
ORDER BY name
|
|
35617
|
+
`).all();
|
|
35618
|
+
for (const { name: tableName } of tables) {
|
|
35619
|
+
const quotedTable = quoteSqliteIdentifier(tableName);
|
|
35620
|
+
const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
|
|
35621
|
+
const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
|
|
35622
|
+
const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
|
|
35623
|
+
for (const columnName of referenceColumns) {
|
|
35624
|
+
const row = db.query(`
|
|
35625
|
+
SELECT 1 AS found
|
|
35626
|
+
FROM ${quotedTable}
|
|
35627
|
+
WHERE ${quoteSqliteIdentifier(columnName)} = ?
|
|
35628
|
+
LIMIT 1
|
|
35629
|
+
`).get(targetId);
|
|
35630
|
+
if (row)
|
|
35631
|
+
return true;
|
|
35632
|
+
}
|
|
35633
|
+
}
|
|
35634
|
+
return false;
|
|
35635
|
+
}
|
|
35636
|
+
function receiptFromRow2(row) {
|
|
35637
|
+
return {
|
|
35638
|
+
...row,
|
|
35639
|
+
authority: "todos",
|
|
35640
|
+
created_by_operation: Number(row["created_by_operation"]) === 1
|
|
35641
|
+
};
|
|
35642
|
+
}
|
|
35643
|
+
function bindingFromRow2(row) {
|
|
35644
|
+
return row;
|
|
35645
|
+
}
|
|
34986
35646
|
|
|
34987
|
-
|
|
34988
|
-
|
|
34989
|
-
|
|
34990
|
-
|
|
34991
|
-
|
|
34992
|
-
|
|
34993
|
-
return getTask(raw, db);
|
|
34994
|
-
const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
34995
|
-
if (prefixRows.length > 1) {
|
|
34996
|
-
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
35647
|
+
class SqliteTodosProjectRegistrationTransaction {
|
|
35648
|
+
db;
|
|
35649
|
+
storage;
|
|
35650
|
+
constructor(db) {
|
|
35651
|
+
this.db = db;
|
|
35652
|
+
this.storage = createLocalSqliteTodosStorageAdapter({ db });
|
|
34997
35653
|
}
|
|
34998
|
-
|
|
34999
|
-
|
|
35000
|
-
|
|
35001
|
-
|
|
35002
|
-
|
|
35654
|
+
async lockStep(_identity) {}
|
|
35655
|
+
async getReceiptForLookup(identity) {
|
|
35656
|
+
const row = this.db.query(`
|
|
35657
|
+
SELECT * FROM todos_project_registration_receipts
|
|
35658
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35659
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
35660
|
+
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
35661
|
+
ORDER BY CASE outcome
|
|
35662
|
+
WHEN 'terminal_nonacceptance' THEN 0
|
|
35663
|
+
WHEN 'duplicate_of_accepted' THEN 1
|
|
35664
|
+
ELSE 2
|
|
35665
|
+
END, created_at DESC, receipt_id DESC
|
|
35666
|
+
LIMIT 1
|
|
35667
|
+
`).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);
|
|
35668
|
+
return row ? receiptFromRow2(row) : null;
|
|
35669
|
+
}
|
|
35670
|
+
async getReceiptById(receiptId2) {
|
|
35671
|
+
const row = this.db.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = ? LIMIT 1").get(receiptId2);
|
|
35672
|
+
return row ? receiptFromRow2(row) : null;
|
|
35673
|
+
}
|
|
35674
|
+
async getAcceptedReceiptForStep(identity) {
|
|
35675
|
+
const row = this.db.query(`
|
|
35676
|
+
SELECT * FROM todos_project_registration_receipts
|
|
35677
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35678
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
35679
|
+
AND direction = ? AND outcome = 'accepted'
|
|
35680
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
35681
|
+
LIMIT 1
|
|
35682
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction);
|
|
35683
|
+
return row ? receiptFromRow2(row) : null;
|
|
35684
|
+
}
|
|
35685
|
+
async insertReceipt(receipt) {
|
|
35686
|
+
const result = this.db.query(`
|
|
35687
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
35688
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
35689
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
35690
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
35691
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
35692
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
35693
|
+
created_by_operation, created_at
|
|
35694
|
+
) VALUES (
|
|
35695
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
35696
|
+
)
|
|
35697
|
+
`).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);
|
|
35698
|
+
return result.changes === 1;
|
|
35699
|
+
}
|
|
35700
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
35701
|
+
const row = this.db.query(`
|
|
35702
|
+
SELECT * FROM todos_project_registration_bindings
|
|
35703
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35704
|
+
AND resource_kind = ? AND target_selector = ?
|
|
35705
|
+
LIMIT 1
|
|
35706
|
+
`).get(scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
35707
|
+
return row ? bindingFromRow2(row) : null;
|
|
35708
|
+
}
|
|
35709
|
+
async claimBinding(binding) {
|
|
35710
|
+
const result = this.db.query(`
|
|
35711
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
35712
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
35713
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
35714
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
35715
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
35716
|
+
created_at, updated_at
|
|
35717
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
35718
|
+
`).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);
|
|
35719
|
+
return result.changes === 1;
|
|
35720
|
+
}
|
|
35721
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
35722
|
+
const result = this.db.query(`
|
|
35723
|
+
UPDATE todos_project_registration_bindings
|
|
35724
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
35725
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
35726
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35727
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
35728
|
+
`).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);
|
|
35729
|
+
if (result.changes !== 1) {
|
|
35730
|
+
throw new Error("Todos project registration binding was not pending at acceptance");
|
|
35731
|
+
}
|
|
35732
|
+
}
|
|
35733
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
35734
|
+
this.db.query(`
|
|
35735
|
+
UPDATE todos_project_registration_bindings
|
|
35736
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
35737
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35738
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
35739
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
35740
|
+
}
|
|
35741
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
35742
|
+
const result = this.db.query(`
|
|
35743
|
+
UPDATE todos_project_registration_bindings
|
|
35744
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
35745
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35746
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
35747
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
35748
|
+
if (result.changes !== 1) {
|
|
35749
|
+
throw new Error("Todos project registration binding was not accepted at removal");
|
|
35750
|
+
}
|
|
35751
|
+
}
|
|
35752
|
+
async findProjectConflict(path, taskListSlug) {
|
|
35753
|
+
const row = this.db.query(`
|
|
35754
|
+
SELECT * FROM projects
|
|
35755
|
+
WHERE path = ? OR task_list_id = ?
|
|
35756
|
+
ORDER BY created_at ASC, id ASC
|
|
35757
|
+
LIMIT 1
|
|
35758
|
+
`).get(path, taskListSlug);
|
|
35759
|
+
return row ?? null;
|
|
35760
|
+
}
|
|
35761
|
+
async findTaskListConflict(projectId, slug) {
|
|
35762
|
+
return await this.storage.taskLists.getBySlug(slug, projectId);
|
|
35763
|
+
}
|
|
35764
|
+
async createProject(input) {
|
|
35765
|
+
return await this.storage.projects.create(input);
|
|
35766
|
+
}
|
|
35767
|
+
async createTaskList(input) {
|
|
35768
|
+
return await this.storage.taskLists.create(input);
|
|
35769
|
+
}
|
|
35770
|
+
async getProject(id) {
|
|
35771
|
+
return await this.storage.projects.get(id);
|
|
35772
|
+
}
|
|
35773
|
+
async getTaskList(id) {
|
|
35774
|
+
return await this.storage.taskLists.get(id);
|
|
35775
|
+
}
|
|
35776
|
+
async lockCompensationWrites() {}
|
|
35777
|
+
async hasDependents(resourceKind, targetId) {
|
|
35778
|
+
return hasSqliteDependents(this.db, resourceKind, targetId);
|
|
35779
|
+
}
|
|
35780
|
+
async deleteProject(id) {
|
|
35781
|
+
return await this.storage.projects.delete(id);
|
|
35782
|
+
}
|
|
35783
|
+
async deleteTaskList(id) {
|
|
35784
|
+
return await this.storage.taskLists.delete(id);
|
|
35003
35785
|
}
|
|
35004
|
-
if (shortIdRows.length === 1)
|
|
35005
|
-
return getTask(shortIdRows[0].id, db);
|
|
35006
|
-
return null;
|
|
35007
|
-
}
|
|
35008
|
-
function isSearchQuery(filter) {
|
|
35009
|
-
const q = filter.query?.trim();
|
|
35010
|
-
return !!q && q !== "*";
|
|
35011
35786
|
}
|
|
35012
|
-
|
|
35013
|
-
|
|
35014
|
-
|
|
35015
|
-
|
|
35016
|
-
|
|
35017
|
-
|
|
35018
|
-
|
|
35019
|
-
|
|
35020
|
-
|
|
35021
|
-
|
|
35022
|
-
|
|
35023
|
-
|
|
35024
|
-
|
|
35025
|
-
|
|
35787
|
+
|
|
35788
|
+
class StagedSqliteTodosProjectRegistrationTransaction {
|
|
35789
|
+
db;
|
|
35790
|
+
direct;
|
|
35791
|
+
validators = [];
|
|
35792
|
+
mutations = [];
|
|
35793
|
+
receipts = new Map;
|
|
35794
|
+
bindings = new Map;
|
|
35795
|
+
projects = new Map;
|
|
35796
|
+
taskLists = new Map;
|
|
35797
|
+
constructor(db) {
|
|
35798
|
+
this.db = db;
|
|
35799
|
+
this.direct = new SqliteTodosProjectRegistrationTransaction(db);
|
|
35800
|
+
}
|
|
35801
|
+
commit() {
|
|
35802
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
35803
|
+
try {
|
|
35804
|
+
for (const validate of this.validators) {
|
|
35805
|
+
if (!validate()) {
|
|
35806
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
|
|
35807
|
+
}
|
|
35808
|
+
}
|
|
35809
|
+
for (const mutate of this.mutations)
|
|
35810
|
+
mutate();
|
|
35811
|
+
this.db.exec("COMMIT");
|
|
35812
|
+
} catch (error) {
|
|
35813
|
+
try {
|
|
35814
|
+
this.db.exec("ROLLBACK");
|
|
35815
|
+
} catch {}
|
|
35816
|
+
throw error;
|
|
35817
|
+
}
|
|
35818
|
+
}
|
|
35819
|
+
async lockStep(_identity) {}
|
|
35820
|
+
async getReceiptForLookup(identity) {
|
|
35821
|
+
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);
|
|
35822
|
+
const stored = await this.direct.getReceiptForLookup(identity);
|
|
35823
|
+
if (stored)
|
|
35824
|
+
staged.push(stored);
|
|
35825
|
+
const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
|
|
35826
|
+
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;
|
|
35827
|
+
}
|
|
35828
|
+
async getReceiptById(receiptId2) {
|
|
35829
|
+
return this.receipts.get(receiptId2) ?? this.direct.getReceiptById(receiptId2);
|
|
35830
|
+
}
|
|
35831
|
+
async getAcceptedReceiptForStep(identity) {
|
|
35832
|
+
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");
|
|
35833
|
+
const stored = await this.direct.getAcceptedReceiptForStep(identity);
|
|
35834
|
+
if (stored)
|
|
35835
|
+
staged.push(stored);
|
|
35836
|
+
return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
|
|
35837
|
+
}
|
|
35838
|
+
async insertReceipt(receipt) {
|
|
35839
|
+
if (this.receipts.has(receipt.receipt_id))
|
|
35840
|
+
return false;
|
|
35841
|
+
if (await this.direct.getReceiptById(receipt.receipt_id))
|
|
35026
35842
|
return false;
|
|
35843
|
+
const planned = { ...receipt };
|
|
35844
|
+
this.receipts.set(planned.receipt_id, planned);
|
|
35845
|
+
this.mutations.push(() => {
|
|
35846
|
+
try {
|
|
35847
|
+
const result = this.db.query(`
|
|
35848
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
35849
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
35850
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
35851
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
35852
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
35853
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
35854
|
+
created_by_operation, created_at
|
|
35855
|
+
) VALUES (
|
|
35856
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
35857
|
+
)
|
|
35858
|
+
`).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);
|
|
35859
|
+
if (result.changes !== 1) {
|
|
35860
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
|
|
35861
|
+
}
|
|
35862
|
+
} catch (error) {
|
|
35863
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
35864
|
+
throw error;
|
|
35865
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
|
|
35866
|
+
}
|
|
35867
|
+
});
|
|
35868
|
+
return true;
|
|
35027
35869
|
}
|
|
35028
|
-
|
|
35029
|
-
const
|
|
35030
|
-
|
|
35870
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
35871
|
+
const key = this.bindingKey(scope, resourceKind, targetSelector);
|
|
35872
|
+
return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
|
|
35873
|
+
}
|
|
35874
|
+
async claimBinding(binding) {
|
|
35875
|
+
const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
|
|
35876
|
+
if (this.bindings.has(key))
|
|
35031
35877
|
return false;
|
|
35878
|
+
if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
|
|
35879
|
+
return false;
|
|
35880
|
+
}
|
|
35881
|
+
const planned = { ...binding };
|
|
35882
|
+
this.bindings.set(key, planned);
|
|
35883
|
+
this.mutations.push(() => {
|
|
35884
|
+
try {
|
|
35885
|
+
const result = this.db.query(`
|
|
35886
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
35887
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
35888
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
35889
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
35890
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
35891
|
+
created_at, updated_at
|
|
35892
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
35893
|
+
`).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);
|
|
35894
|
+
if (result.changes !== 1) {
|
|
35895
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
|
|
35896
|
+
}
|
|
35897
|
+
} catch (error) {
|
|
35898
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
35899
|
+
throw error;
|
|
35900
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
|
|
35901
|
+
}
|
|
35902
|
+
});
|
|
35903
|
+
return true;
|
|
35032
35904
|
}
|
|
35033
|
-
|
|
35034
|
-
|
|
35035
|
-
|
|
35036
|
-
|
|
35037
|
-
|
|
35038
|
-
|
|
35039
|
-
|
|
35040
|
-
|
|
35041
|
-
|
|
35042
|
-
|
|
35043
|
-
|
|
35044
|
-
|
|
35045
|
-
|
|
35046
|
-
|
|
35047
|
-
|
|
35048
|
-
|
|
35049
|
-
|
|
35050
|
-
|
|
35051
|
-
|
|
35052
|
-
|
|
35053
|
-
|
|
35054
|
-
|
|
35055
|
-
|
|
35056
|
-
|
|
35057
|
-
|
|
35058
|
-
|
|
35059
|
-
|
|
35060
|
-
|
|
35061
|
-
|
|
35062
|
-
|
|
35063
|
-
|
|
35064
|
-
|
|
35065
|
-
|
|
35066
|
-
|
|
35067
|
-
|
|
35068
|
-
|
|
35069
|
-
|
|
35070
|
-
|
|
35071
|
-
|
|
35072
|
-
|
|
35073
|
-
|
|
35074
|
-
|
|
35075
|
-
|
|
35076
|
-
|
|
35077
|
-
|
|
35078
|
-
|
|
35079
|
-
|
|
35080
|
-
|
|
35081
|
-
|
|
35082
|
-
|
|
35083
|
-
|
|
35084
|
-
|
|
35085
|
-
|
|
35086
|
-
|
|
35087
|
-
|
|
35088
|
-
|
|
35089
|
-
|
|
35090
|
-
|
|
35091
|
-
|
|
35092
|
-
|
|
35093
|
-
|
|
35094
|
-
|
|
35095
|
-
|
|
35096
|
-
|
|
35097
|
-
|
|
35098
|
-
|
|
35099
|
-
|
|
35100
|
-
|
|
35101
|
-
|
|
35102
|
-
|
|
35103
|
-
|
|
35104
|
-
|
|
35105
|
-
|
|
35106
|
-
|
|
35107
|
-
|
|
35108
|
-
|
|
35109
|
-
|
|
35110
|
-
|
|
35111
|
-
|
|
35112
|
-
|
|
35113
|
-
|
|
35114
|
-
|
|
35115
|
-
|
|
35116
|
-
|
|
35117
|
-
|
|
35118
|
-
|
|
35119
|
-
|
|
35120
|
-
|
|
35121
|
-
|
|
35122
|
-
|
|
35123
|
-
|
|
35124
|
-
|
|
35125
|
-
|
|
35126
|
-
|
|
35127
|
-
|
|
35128
|
-
|
|
35129
|
-
|
|
35130
|
-
|
|
35131
|
-
|
|
35132
|
-
|
|
35133
|
-
|
|
35134
|
-
|
|
35135
|
-
|
|
35905
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
35906
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
35907
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
35908
|
+
...binding,
|
|
35909
|
+
state: "accepted",
|
|
35910
|
+
target_id: update.target_id,
|
|
35911
|
+
accepted_receipt_id: update.accepted_receipt_id,
|
|
35912
|
+
result_revision: update.result_revision,
|
|
35913
|
+
result_digest: update.result_digest,
|
|
35914
|
+
updated_at: update.updated_at
|
|
35915
|
+
});
|
|
35916
|
+
this.mutations.push(() => {
|
|
35917
|
+
const result = this.db.query(`
|
|
35918
|
+
UPDATE todos_project_registration_bindings
|
|
35919
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
35920
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
35921
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35922
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
35923
|
+
`).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);
|
|
35924
|
+
if (result.changes !== 1) {
|
|
35925
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
35926
|
+
}
|
|
35927
|
+
});
|
|
35928
|
+
}
|
|
35929
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
35930
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
35931
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
35932
|
+
...binding,
|
|
35933
|
+
state: "terminal_nonacceptance",
|
|
35934
|
+
updated_at: updatedAt
|
|
35935
|
+
});
|
|
35936
|
+
this.mutations.push(() => {
|
|
35937
|
+
const result = this.db.query(`
|
|
35938
|
+
UPDATE todos_project_registration_bindings
|
|
35939
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
35940
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35941
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
35942
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
35943
|
+
if (result.changes !== 1) {
|
|
35944
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
35945
|
+
}
|
|
35946
|
+
});
|
|
35947
|
+
}
|
|
35948
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
35949
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
|
|
35950
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
35951
|
+
...binding,
|
|
35952
|
+
state: "removed",
|
|
35953
|
+
removed_receipt_id: removedReceiptId,
|
|
35954
|
+
updated_at: updatedAt
|
|
35955
|
+
});
|
|
35956
|
+
this.mutations.push(() => {
|
|
35957
|
+
const result = this.db.query(`
|
|
35958
|
+
UPDATE todos_project_registration_bindings
|
|
35959
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
35960
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
35961
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
35962
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
35963
|
+
if (result.changes !== 1) {
|
|
35964
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
|
|
35965
|
+
}
|
|
35966
|
+
});
|
|
35967
|
+
}
|
|
35968
|
+
async findProjectConflict(path, taskListSlug) {
|
|
35969
|
+
const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
|
|
35970
|
+
if (planned)
|
|
35971
|
+
return planned;
|
|
35972
|
+
const observed = selectProjectConflict(this.db, path, taskListSlug);
|
|
35973
|
+
this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
|
|
35974
|
+
return observed;
|
|
35975
|
+
}
|
|
35976
|
+
async findTaskListConflict(projectId, slug) {
|
|
35977
|
+
const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
|
|
35978
|
+
if (planned)
|
|
35979
|
+
return planned;
|
|
35980
|
+
const observed = selectTaskListConflict(this.db, projectId, slug);
|
|
35981
|
+
this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
|
|
35982
|
+
return observed;
|
|
35983
|
+
}
|
|
35984
|
+
async createProject(input) {
|
|
35985
|
+
const derivedSlug = normalizeSlug(input.name);
|
|
35986
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
|
|
35987
|
+
if (!derivedSlug || !taskListId) {
|
|
35988
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
35989
|
+
}
|
|
35990
|
+
const project = {
|
|
35991
|
+
id: uuid(),
|
|
35992
|
+
name: input.name,
|
|
35993
|
+
path: input.path,
|
|
35994
|
+
description: input.description || null,
|
|
35995
|
+
task_list_id: taskListId,
|
|
35996
|
+
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
35997
|
+
task_counter: 0,
|
|
35998
|
+
created_at: now(),
|
|
35999
|
+
updated_at: now(),
|
|
36000
|
+
machine_id: currentStorageMachineId(this.db)
|
|
36001
|
+
};
|
|
36002
|
+
project.updated_at = project.created_at;
|
|
36003
|
+
this.projects.set(project.id, project);
|
|
36004
|
+
this.mutations.push(() => {
|
|
36005
|
+
try {
|
|
36006
|
+
const result = this.db.run(`INSERT INTO projects (
|
|
36007
|
+
id, name, path, description, task_list_id, task_prefix,
|
|
36008
|
+
task_counter, created_at, updated_at, machine_id
|
|
36009
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
36010
|
+
project.id,
|
|
36011
|
+
project.name,
|
|
36012
|
+
project.path,
|
|
36013
|
+
project.description,
|
|
36014
|
+
project.task_list_id,
|
|
36015
|
+
project.task_prefix,
|
|
36016
|
+
project.created_at,
|
|
36017
|
+
project.updated_at,
|
|
36018
|
+
project.machine_id ?? null
|
|
36019
|
+
]);
|
|
36020
|
+
if (result.changes < 1) {
|
|
36021
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
|
|
35136
36022
|
}
|
|
35137
|
-
|
|
35138
|
-
|
|
35139
|
-
|
|
35140
|
-
|
|
35141
|
-
|
|
36023
|
+
} catch (error) {
|
|
36024
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
36025
|
+
throw error;
|
|
36026
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
|
|
36027
|
+
}
|
|
36028
|
+
});
|
|
36029
|
+
return project;
|
|
36030
|
+
}
|
|
36031
|
+
async createTaskList(input) {
|
|
36032
|
+
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
36033
|
+
if (!slug)
|
|
36034
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
36035
|
+
const taskList = {
|
|
36036
|
+
id: uuid(),
|
|
36037
|
+
project_id: input.project_id || null,
|
|
36038
|
+
slug,
|
|
36039
|
+
name: input.name,
|
|
36040
|
+
description: input.description || null,
|
|
36041
|
+
metadata: input.metadata ?? {},
|
|
36042
|
+
created_at: now(),
|
|
36043
|
+
updated_at: now(),
|
|
36044
|
+
machine_id: currentStorageMachineId(this.db)
|
|
36045
|
+
};
|
|
36046
|
+
taskList.updated_at = taskList.created_at;
|
|
36047
|
+
this.taskLists.set(taskList.id, taskList);
|
|
36048
|
+
this.mutations.push(() => {
|
|
36049
|
+
try {
|
|
36050
|
+
const result = this.db.run(`INSERT INTO task_lists (
|
|
36051
|
+
id, project_id, slug, name, description, metadata,
|
|
36052
|
+
created_at, updated_at, machine_id
|
|
36053
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
36054
|
+
taskList.id,
|
|
36055
|
+
taskList.project_id,
|
|
36056
|
+
taskList.slug,
|
|
36057
|
+
taskList.name,
|
|
36058
|
+
taskList.description,
|
|
36059
|
+
JSON.stringify(taskList.metadata),
|
|
36060
|
+
taskList.created_at,
|
|
36061
|
+
taskList.updated_at,
|
|
36062
|
+
taskList.machine_id ?? null
|
|
36063
|
+
]);
|
|
36064
|
+
if (result.changes < 1) {
|
|
36065
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
|
|
35142
36066
|
}
|
|
35143
|
-
|
|
35144
|
-
|
|
35145
|
-
|
|
35146
|
-
|
|
35147
|
-
|
|
35148
|
-
|
|
35149
|
-
|
|
35150
|
-
|
|
35151
|
-
|
|
35152
|
-
|
|
35153
|
-
|
|
35154
|
-
|
|
35155
|
-
|
|
35156
|
-
|
|
35157
|
-
|
|
35158
|
-
|
|
35159
|
-
|
|
35160
|
-
return
|
|
36067
|
+
} catch (error) {
|
|
36068
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
36069
|
+
throw error;
|
|
36070
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
|
|
36071
|
+
}
|
|
36072
|
+
});
|
|
36073
|
+
return taskList;
|
|
36074
|
+
}
|
|
36075
|
+
async getProject(id) {
|
|
36076
|
+
if (this.projects.has(id))
|
|
36077
|
+
return this.projects.get(id) ?? null;
|
|
36078
|
+
const observed = selectProject(this.db, id);
|
|
36079
|
+
this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
|
|
36080
|
+
return observed;
|
|
36081
|
+
}
|
|
36082
|
+
async getTaskList(id) {
|
|
36083
|
+
if (this.taskLists.has(id))
|
|
36084
|
+
return this.taskLists.get(id) ?? null;
|
|
36085
|
+
const observed = selectTaskList(this.db, id);
|
|
36086
|
+
this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
|
|
36087
|
+
return observed;
|
|
36088
|
+
}
|
|
36089
|
+
async lockCompensationWrites() {}
|
|
36090
|
+
async hasDependents(resourceKind, targetId) {
|
|
36091
|
+
const observed = hasSqliteDependents(this.db, resourceKind, targetId);
|
|
36092
|
+
this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
|
|
36093
|
+
return observed;
|
|
36094
|
+
}
|
|
36095
|
+
async deleteProject(id) {
|
|
36096
|
+
const project = await this.getProject(id);
|
|
36097
|
+
if (!project)
|
|
36098
|
+
return false;
|
|
36099
|
+
this.projects.set(id, null);
|
|
36100
|
+
this.mutations.push(() => {
|
|
36101
|
+
recordStorageTombstone({
|
|
36102
|
+
object_type: "projects",
|
|
36103
|
+
object_id: id,
|
|
36104
|
+
payload: project
|
|
36105
|
+
}, this.db);
|
|
36106
|
+
if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
|
|
36107
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
|
|
36108
|
+
}
|
|
36109
|
+
});
|
|
36110
|
+
return true;
|
|
36111
|
+
}
|
|
36112
|
+
async deleteTaskList(id) {
|
|
36113
|
+
const taskList = await this.getTaskList(id);
|
|
36114
|
+
if (!taskList)
|
|
36115
|
+
return false;
|
|
36116
|
+
this.taskLists.set(id, null);
|
|
36117
|
+
this.mutations.push(() => {
|
|
36118
|
+
recordStorageTombstone({
|
|
36119
|
+
object_type: "task_lists",
|
|
36120
|
+
object_id: id,
|
|
36121
|
+
payload: taskList
|
|
36122
|
+
}, this.db);
|
|
36123
|
+
if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
|
|
36124
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
|
|
36125
|
+
}
|
|
36126
|
+
});
|
|
36127
|
+
return true;
|
|
36128
|
+
}
|
|
36129
|
+
bindingKey(scope, resourceKind, targetSelector) {
|
|
36130
|
+
return JSON.stringify([
|
|
36131
|
+
scope.authority_id,
|
|
36132
|
+
scope.tenant_id,
|
|
36133
|
+
scope.corpus_id,
|
|
36134
|
+
resourceKind,
|
|
36135
|
+
targetSelector
|
|
36136
|
+
]);
|
|
36137
|
+
}
|
|
36138
|
+
async requireBinding(scope, resourceKind, targetSelector, state) {
|
|
36139
|
+
const binding = await this.getBinding(scope, resourceKind, targetSelector);
|
|
36140
|
+
if (!binding || binding.state !== state) {
|
|
36141
|
+
throw new Error(`Todos project registration binding was not ${state}`);
|
|
35161
36142
|
}
|
|
35162
|
-
|
|
35163
|
-
|
|
36143
|
+
return binding;
|
|
36144
|
+
}
|
|
36145
|
+
availableProjectPrefix(name) {
|
|
36146
|
+
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
36147
|
+
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();
|
|
36148
|
+
let candidate = prefix;
|
|
36149
|
+
let suffix = 1;
|
|
36150
|
+
while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
|
|
36151
|
+
suffix += 1;
|
|
36152
|
+
candidate = `${prefix}${suffix}`;
|
|
36153
|
+
}
|
|
36154
|
+
return candidate;
|
|
36155
|
+
}
|
|
35164
36156
|
}
|
|
35165
|
-
var
|
|
35166
|
-
var init_local_sqlite = __esm(() => {
|
|
35167
|
-
init_types();
|
|
35168
|
-
init_search();
|
|
35169
|
-
init_tasks();
|
|
35170
|
-
init_projects();
|
|
35171
|
-
init_plans();
|
|
35172
|
-
init_agents();
|
|
35173
|
-
init_task_lists();
|
|
35174
|
-
init_templates();
|
|
35175
|
-
init_audit();
|
|
35176
|
-
init_comments();
|
|
35177
|
-
init_database();
|
|
35178
|
-
init_integrity2();
|
|
35179
|
-
init_sqlite_snapshot();
|
|
35180
|
-
TASK_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
35181
|
-
});
|
|
35182
|
-
|
|
35183
|
-
// src/project-registration/sqlite.ts
|
|
35184
|
-
var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS;
|
|
36157
|
+
var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS, SqliteRegistrationOptimisticConflict;
|
|
35185
36158
|
var init_sqlite = __esm(() => {
|
|
36159
|
+
init_database();
|
|
36160
|
+
init_storage_tombstones();
|
|
35186
36161
|
init_local_sqlite();
|
|
35187
36162
|
sqliteTransactionTails = new WeakMap;
|
|
35188
36163
|
PROJECT_REFERENCE_COLUMNS = new Set([
|
|
@@ -35192,10 +36167,16 @@ var init_sqlite = __esm(() => {
|
|
|
35192
36167
|
"external_project_id"
|
|
35193
36168
|
]);
|
|
35194
36169
|
TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
|
|
36170
|
+
SqliteRegistrationOptimisticConflict = class SqliteRegistrationOptimisticConflict extends Error {
|
|
36171
|
+
constructor(message, options = {}) {
|
|
36172
|
+
super(message, options);
|
|
36173
|
+
this.name = "SqliteRegistrationOptimisticConflict";
|
|
36174
|
+
}
|
|
36175
|
+
};
|
|
35195
36176
|
});
|
|
35196
36177
|
|
|
35197
36178
|
// src/project-registration/authority.ts
|
|
35198
|
-
import { createHash as
|
|
36179
|
+
import { createHash as createHash10 } from "crypto";
|
|
35199
36180
|
function canonicalProjectRegistrationJson(value) {
|
|
35200
36181
|
return JSON.stringify(canonicalize2(value));
|
|
35201
36182
|
}
|
|
@@ -35213,7 +36194,7 @@ function canonicalize2(value) {
|
|
|
35213
36194
|
return out;
|
|
35214
36195
|
}
|
|
35215
36196
|
function digestProjectRegistrationValue(value) {
|
|
35216
|
-
return
|
|
36197
|
+
return createHash10("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
35217
36198
|
}
|
|
35218
36199
|
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
35219
36200
|
return `prk_${digestProjectRegistrationValue({
|
|
@@ -35354,7 +36335,7 @@ function taskListRecord(taskList) {
|
|
|
35354
36335
|
})
|
|
35355
36336
|
};
|
|
35356
36337
|
}
|
|
35357
|
-
function
|
|
36338
|
+
function receiptId2(input) {
|
|
35358
36339
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
35359
36340
|
}
|
|
35360
36341
|
function capabilityMatches(request, capability) {
|
|
@@ -35512,7 +36493,7 @@ function assertInverseRequest(request, capability) {
|
|
|
35512
36493
|
function makeReceipt(input, createdAt) {
|
|
35513
36494
|
return {
|
|
35514
36495
|
...input,
|
|
35515
|
-
receipt_id:
|
|
36496
|
+
receipt_id: receiptId2(input),
|
|
35516
36497
|
created_at: createdAt
|
|
35517
36498
|
};
|
|
35518
36499
|
}
|
|
@@ -38389,6 +39370,9 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
38389
39370
|
Task: taskSchema,
|
|
38390
39371
|
Project: projectSchema,
|
|
38391
39372
|
TaskList: taskListSchema,
|
|
39373
|
+
ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
|
|
39374
|
+
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
39375
|
+
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
38392
39376
|
TaskComment: taskCommentSchema,
|
|
38393
39377
|
Plan: planSchema,
|
|
38394
39378
|
Template: templateSchema,
|
|
@@ -38466,6 +39450,29 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
38466
39450
|
name: { type: "string", minLength: 1 }
|
|
38467
39451
|
}
|
|
38468
39452
|
},
|
|
39453
|
+
ProjectTaskListEnsureApplyInput: {
|
|
39454
|
+
type: "object",
|
|
39455
|
+
additionalProperties: false,
|
|
39456
|
+
required: ["expected_project_revision"],
|
|
39457
|
+
properties: {
|
|
39458
|
+
expected_project_revision: { type: "string", minLength: 1 },
|
|
39459
|
+
idempotency_key: {
|
|
39460
|
+
type: "string",
|
|
39461
|
+
minLength: 8,
|
|
39462
|
+
maxLength: 128,
|
|
39463
|
+
pattern: "^[A-Za-z0-9._:-]+$"
|
|
39464
|
+
}
|
|
39465
|
+
}
|
|
39466
|
+
},
|
|
39467
|
+
ProjectTaskListRollbackInput: {
|
|
39468
|
+
type: "object",
|
|
39469
|
+
additionalProperties: false,
|
|
39470
|
+
required: ["receipt_id", "expected_task_list_revision"],
|
|
39471
|
+
properties: {
|
|
39472
|
+
receipt_id: { type: "string", minLength: 1 },
|
|
39473
|
+
expected_task_list_revision: { type: "string", minLength: 1 }
|
|
39474
|
+
}
|
|
39475
|
+
},
|
|
38469
39476
|
ErrorResponse: {
|
|
38470
39477
|
type: "object",
|
|
38471
39478
|
required: ["error"],
|
|
@@ -39568,6 +40575,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
39568
40575
|
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
39569
40576
|
}
|
|
39570
40577
|
},
|
|
40578
|
+
"/v1/projects/{id}/task-list/ensure": {
|
|
40579
|
+
get: {
|
|
40580
|
+
operationId: "planProjectTaskListEnsure",
|
|
40581
|
+
summary: "Plan a non-mutating repair of a project's declared task list",
|
|
40582
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
40583
|
+
responses: {
|
|
40584
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
40585
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
40586
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
40587
|
+
}
|
|
40588
|
+
},
|
|
40589
|
+
post: {
|
|
40590
|
+
operationId: "ensureProjectTaskList",
|
|
40591
|
+
summary: "Idempotently create an existing project's declared task list",
|
|
40592
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
40593
|
+
requestBody: {
|
|
40594
|
+
required: true,
|
|
40595
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureApplyInput" } } }
|
|
40596
|
+
},
|
|
40597
|
+
responses: {
|
|
40598
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
40599
|
+
"201": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
40600
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
40601
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
40602
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
40603
|
+
}
|
|
40604
|
+
}
|
|
40605
|
+
},
|
|
40606
|
+
"/v1/projects/{id}/task-list/rollback": {
|
|
40607
|
+
post: {
|
|
40608
|
+
operationId: "rollbackProjectTaskListEnsure",
|
|
40609
|
+
summary: "Conditionally remove an unchanged task list created by an accepted ensure receipt",
|
|
40610
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
40611
|
+
requestBody: {
|
|
40612
|
+
required: true,
|
|
40613
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackInput" } } }
|
|
40614
|
+
},
|
|
40615
|
+
responses: {
|
|
40616
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackResult" } } } },
|
|
40617
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
40618
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
40619
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
40620
|
+
}
|
|
40621
|
+
}
|
|
40622
|
+
},
|
|
39571
40623
|
"/v1/projects/{id}/rename": {
|
|
39572
40624
|
post: {
|
|
39573
40625
|
operationId: "renameProject",
|
|
@@ -39849,7 +40901,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
39849
40901
|
}
|
|
39850
40902
|
};
|
|
39851
40903
|
}
|
|
39852
|
-
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
40904
|
+
var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
39853
40905
|
var init_openapi = __esm(() => {
|
|
39854
40906
|
init_package_version();
|
|
39855
40907
|
init_types();
|
|
@@ -39897,6 +40949,80 @@ var init_openapi = __esm(() => {
|
|
|
39897
40949
|
updated_at: { type: "string" }
|
|
39898
40950
|
}
|
|
39899
40951
|
};
|
|
40952
|
+
projectTaskListEnsureReceiptSchema = {
|
|
40953
|
+
type: "object",
|
|
40954
|
+
additionalProperties: false,
|
|
40955
|
+
required: [
|
|
40956
|
+
"schema_version",
|
|
40957
|
+
"receipt_id",
|
|
40958
|
+
"idempotency_key",
|
|
40959
|
+
"project_id",
|
|
40960
|
+
"task_list_id",
|
|
40961
|
+
"slug",
|
|
40962
|
+
"created_by_operation",
|
|
40963
|
+
"result_revision",
|
|
40964
|
+
"result_digest",
|
|
40965
|
+
"rollback_supported",
|
|
40966
|
+
"created_at"
|
|
40967
|
+
],
|
|
40968
|
+
properties: {
|
|
40969
|
+
schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
|
|
40970
|
+
receipt_id: { type: "string" },
|
|
40971
|
+
idempotency_key: { type: "string" },
|
|
40972
|
+
project_id: { type: "string" },
|
|
40973
|
+
task_list_id: { type: "string" },
|
|
40974
|
+
slug: { type: "string" },
|
|
40975
|
+
created_by_operation: { type: "boolean" },
|
|
40976
|
+
result_revision: { type: "string" },
|
|
40977
|
+
result_digest: { type: "string" },
|
|
40978
|
+
rollback_supported: { type: "boolean" },
|
|
40979
|
+
created_at: { type: "string", format: "date-time" }
|
|
40980
|
+
}
|
|
40981
|
+
};
|
|
40982
|
+
projectTaskListEnsureResultSchema = {
|
|
40983
|
+
type: "object",
|
|
40984
|
+
additionalProperties: false,
|
|
40985
|
+
required: ["mode", "action", "project", "task_list", "receipt"],
|
|
40986
|
+
properties: {
|
|
40987
|
+
mode: { type: "string", enum: ["plan", "apply"] },
|
|
40988
|
+
action: { type: "string", enum: ["would_create", "created", "already_present"] },
|
|
40989
|
+
project: { $ref: "#/components/schemas/Project" },
|
|
40990
|
+
task_list: {
|
|
40991
|
+
oneOf: [
|
|
40992
|
+
{ $ref: "#/components/schemas/TaskList" },
|
|
40993
|
+
{ type: "null" }
|
|
40994
|
+
]
|
|
40995
|
+
},
|
|
40996
|
+
receipt: {
|
|
40997
|
+
oneOf: [
|
|
40998
|
+
{ $ref: "#/components/schemas/ProjectTaskListEnsureReceipt" },
|
|
40999
|
+
{ type: "null" }
|
|
41000
|
+
]
|
|
41001
|
+
}
|
|
41002
|
+
}
|
|
41003
|
+
};
|
|
41004
|
+
projectTaskListRollbackResultSchema = {
|
|
41005
|
+
type: "object",
|
|
41006
|
+
additionalProperties: false,
|
|
41007
|
+
required: [
|
|
41008
|
+
"schema_version",
|
|
41009
|
+
"action",
|
|
41010
|
+
"project_id",
|
|
41011
|
+
"task_list_id",
|
|
41012
|
+
"accepted_receipt_id",
|
|
41013
|
+
"rollback_receipt_id",
|
|
41014
|
+
"removed_at"
|
|
41015
|
+
],
|
|
41016
|
+
properties: {
|
|
41017
|
+
schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
|
|
41018
|
+
action: { type: "string", enum: ["removed"] },
|
|
41019
|
+
project_id: { type: "string" },
|
|
41020
|
+
task_list_id: { type: "string" },
|
|
41021
|
+
accepted_receipt_id: { type: "string" },
|
|
41022
|
+
rollback_receipt_id: { type: "string" },
|
|
41023
|
+
removed_at: { type: "string", format: "date-time" }
|
|
41024
|
+
}
|
|
41025
|
+
};
|
|
39900
41026
|
taskCommentSchema = {
|
|
39901
41027
|
type: "object",
|
|
39902
41028
|
required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
|
|
@@ -40894,6 +42020,52 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
40894
42020
|
}
|
|
40895
42021
|
return error(405, `method ${method} not allowed on /v1/projects`);
|
|
40896
42022
|
}
|
|
42023
|
+
if (action === "task-list" && subId === "ensure") {
|
|
42024
|
+
if (method === "GET") {
|
|
42025
|
+
return json4(await planProjectTaskListEnsure(store, id));
|
|
42026
|
+
}
|
|
42027
|
+
if (method !== "POST") {
|
|
42028
|
+
return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/ensure`);
|
|
42029
|
+
}
|
|
42030
|
+
const body = await readJson3(req);
|
|
42031
|
+
if (!body)
|
|
42032
|
+
return error(400, "invalid JSON body");
|
|
42033
|
+
const unknown = Object.keys(body).find((key) => !["expected_project_revision", "idempotency_key"].includes(key));
|
|
42034
|
+
if (unknown)
|
|
42035
|
+
return error(400, `unknown task-list ensure field: ${unknown}`);
|
|
42036
|
+
if (typeof body.expected_project_revision !== "string" || !body.expected_project_revision.trim()) {
|
|
42037
|
+
return error(400, "expected_project_revision must be a non-empty string from a fresh ensure plan");
|
|
42038
|
+
}
|
|
42039
|
+
if (body.idempotency_key !== undefined && typeof body.idempotency_key !== "string") {
|
|
42040
|
+
return error(400, "idempotency_key must be a string");
|
|
42041
|
+
}
|
|
42042
|
+
const result = await applyProjectTaskListEnsure(store, id, {
|
|
42043
|
+
expected_project_revision: body.expected_project_revision,
|
|
42044
|
+
...typeof body.idempotency_key === "string" ? { idempotency_key: body.idempotency_key } : {}
|
|
42045
|
+
});
|
|
42046
|
+
return json4(result, result.action === "created" ? 201 : 200);
|
|
42047
|
+
}
|
|
42048
|
+
if (action === "task-list" && subId === "rollback") {
|
|
42049
|
+
if (method !== "POST") {
|
|
42050
|
+
return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/rollback`);
|
|
42051
|
+
}
|
|
42052
|
+
const body = await readJson3(req);
|
|
42053
|
+
if (!body)
|
|
42054
|
+
return error(400, "invalid JSON body");
|
|
42055
|
+
const unknown = Object.keys(body).find((key) => !["receipt_id", "expected_task_list_revision"].includes(key));
|
|
42056
|
+
if (unknown)
|
|
42057
|
+
return error(400, `unknown task-list rollback field: ${unknown}`);
|
|
42058
|
+
if (typeof body.receipt_id !== "string" || !body.receipt_id.trim()) {
|
|
42059
|
+
return error(400, "receipt_id must be a non-empty string");
|
|
42060
|
+
}
|
|
42061
|
+
if (typeof body.expected_task_list_revision !== "string" || !body.expected_task_list_revision.trim()) {
|
|
42062
|
+
return error(400, "expected_task_list_revision must be a non-empty string from the accepted receipt");
|
|
42063
|
+
}
|
|
42064
|
+
return json4(await rollbackProjectTaskListEnsure(store, id, {
|
|
42065
|
+
receipt_id: body.receipt_id,
|
|
42066
|
+
expected_task_list_revision: body.expected_task_list_revision
|
|
42067
|
+
}));
|
|
42068
|
+
}
|
|
40897
42069
|
if (action === "rename") {
|
|
40898
42070
|
if (method !== "POST")
|
|
40899
42071
|
return error(405, `method ${method} not allowed on /v1/projects/:id/rename`);
|
|
@@ -41232,6 +42404,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
41232
42404
|
}
|
|
41233
42405
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
41234
42406
|
} catch (e) {
|
|
42407
|
+
if (e instanceof ProjectTaskListEnsureError) {
|
|
42408
|
+
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;
|
|
42409
|
+
return error(status, e.message, { code: e.code, conflict: status === 409, ...e.details });
|
|
42410
|
+
}
|
|
41235
42411
|
if (e instanceof TaskReferenceAmbiguousError) {
|
|
41236
42412
|
return error(409, e.message, {
|
|
41237
42413
|
code: TaskReferenceAmbiguousError.code,
|
|
@@ -41258,6 +42434,7 @@ var init_v1 = __esm(() => {
|
|
|
41258
42434
|
init_pr_groups();
|
|
41259
42435
|
init_project_registration();
|
|
41260
42436
|
init_redaction();
|
|
42437
|
+
init_project_task_list_ensure();
|
|
41261
42438
|
JSON_HEADERS3 = { "Content-Type": "application/json" };
|
|
41262
42439
|
});
|
|
41263
42440
|
|
|
@@ -48967,7 +50144,7 @@ __export(exports_audit_ledger, {
|
|
|
48967
50144
|
LOCAL_AUDIT_LEDGER_INITIAL_HASH: () => LOCAL_AUDIT_LEDGER_INITIAL_HASH,
|
|
48968
50145
|
LOCAL_AUDIT_LEDGER_HASH_ALGORITHM: () => LOCAL_AUDIT_LEDGER_HASH_ALGORITHM
|
|
48969
50146
|
});
|
|
48970
|
-
import { createHash as
|
|
50147
|
+
import { createHash as createHash11 } from "crypto";
|
|
48971
50148
|
function canonicalize3(value) {
|
|
48972
50149
|
if (value === null || typeof value !== "object")
|
|
48973
50150
|
return JSON.stringify(value);
|
|
@@ -48977,7 +50154,7 @@ function canonicalize3(value) {
|
|
|
48977
50154
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalize3(object[key])}`).join(",")}}`;
|
|
48978
50155
|
}
|
|
48979
50156
|
function hash(value) {
|
|
48980
|
-
return
|
|
50157
|
+
return createHash11("sha256").update(value).digest("hex");
|
|
48981
50158
|
}
|
|
48982
50159
|
function parsePayload3(value) {
|
|
48983
50160
|
if (!value)
|
|
@@ -57693,7 +58870,7 @@ __export(exports_local_backups, {
|
|
|
57693
58870
|
TODOS_LOCAL_BACKUP_KIND: () => TODOS_LOCAL_BACKUP_KIND,
|
|
57694
58871
|
LOCAL_BACKUP_CHECKSUM_ALGORITHM: () => LOCAL_BACKUP_CHECKSUM_ALGORITHM
|
|
57695
58872
|
});
|
|
57696
|
-
import { createHash as
|
|
58873
|
+
import { createHash as createHash12 } from "crypto";
|
|
57697
58874
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
|
|
57698
58875
|
import { dirname as dirname9, resolve as resolve20 } from "path";
|
|
57699
58876
|
import { mkdirSync as mkdirSync10 } from "fs";
|
|
@@ -57706,7 +58883,7 @@ function stableJson2(value) {
|
|
|
57706
58883
|
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson2(record[key])}`).join(",")}}`;
|
|
57707
58884
|
}
|
|
57708
58885
|
function sha2565(value) {
|
|
57709
|
-
return
|
|
58886
|
+
return createHash12("sha256").update(stableJson2(value)).digest("hex");
|
|
57710
58887
|
}
|
|
57711
58888
|
function sqliteIntegrity(db) {
|
|
57712
58889
|
let quick = "unknown";
|
|
@@ -58387,7 +59564,7 @@ __export(exports_local_snapshots, {
|
|
|
58387
59564
|
getLocalSnapshot: () => getLocalSnapshot,
|
|
58388
59565
|
TODOS_LOCAL_SNAPSHOT_SCHEMA_VERSION: () => TODOS_LOCAL_SNAPSHOT_SCHEMA_VERSION
|
|
58389
59566
|
});
|
|
58390
|
-
import { createHash as
|
|
59567
|
+
import { createHash as createHash13 } from "crypto";
|
|
58391
59568
|
function source2(version) {
|
|
58392
59569
|
return {
|
|
58393
59570
|
packageName: "@hasna/todos",
|
|
@@ -58411,7 +59588,7 @@ function stable(value) {
|
|
|
58411
59588
|
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stable(item)]));
|
|
58412
59589
|
}
|
|
58413
59590
|
function sha2566(value) {
|
|
58414
|
-
return
|
|
59591
|
+
return createHash13("sha256").update(JSON.stringify(stable(value))).digest("hex");
|
|
58415
59592
|
}
|
|
58416
59593
|
function latestTimestamp2(items, fallback) {
|
|
58417
59594
|
const timestamps = [];
|
|
@@ -58983,7 +60160,7 @@ __export(exports_agent_replay_simulator, {
|
|
|
58983
60160
|
simulateAgentReplay: () => simulateAgentReplay,
|
|
58984
60161
|
renderAgentReplaySimulationMarkdown: () => renderAgentReplaySimulationMarkdown
|
|
58985
60162
|
});
|
|
58986
|
-
import { createHash as
|
|
60163
|
+
import { createHash as createHash14 } from "crypto";
|
|
58987
60164
|
import { readFileSync as readFileSync15 } from "fs";
|
|
58988
60165
|
function isObject2(value) {
|
|
58989
60166
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -59005,7 +60182,7 @@ function stable2(value) {
|
|
|
59005
60182
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
|
|
59006
60183
|
}
|
|
59007
60184
|
function fingerprint2(value) {
|
|
59008
|
-
return
|
|
60185
|
+
return createHash14("sha256").update(JSON.stringify(stable2(value))).digest("hex");
|
|
59009
60186
|
}
|
|
59010
60187
|
function unpackFixture(input) {
|
|
59011
60188
|
if (!isObject2(input))
|
|
@@ -59323,7 +60500,7 @@ __export(exports_inbox, {
|
|
|
59323
60500
|
deriveInboxTitle: () => deriveInboxTitle,
|
|
59324
60501
|
createInboxItem: () => createInboxItem
|
|
59325
60502
|
});
|
|
59326
|
-
import { createHash as
|
|
60503
|
+
import { createHash as createHash15 } from "crypto";
|
|
59327
60504
|
function parseMetadata3(value) {
|
|
59328
60505
|
if (!value)
|
|
59329
60506
|
return {};
|
|
@@ -59343,7 +60520,7 @@ function compactWhitespace(value) {
|
|
|
59343
60520
|
function fingerprintInboxInput(input) {
|
|
59344
60521
|
const sourceType = input.source_type || detectInboxSourceType(input.body, input.source_url);
|
|
59345
60522
|
const normalized = compactWhitespace(sanitizePreWriteText(input.body, "inbox.fingerprint")).slice(0, 8000);
|
|
59346
|
-
return
|
|
60523
|
+
return createHash15("sha256").update(`${sourceType}
|
|
59347
60524
|
${input.source_url || ""}
|
|
59348
60525
|
${normalized}`).digest("hex");
|
|
59349
60526
|
}
|
|
@@ -64555,13 +65732,13 @@ __export(exports_environment_snapshots, {
|
|
|
64555
65732
|
compareEnvironmentSnapshotFiles: () => compareEnvironmentSnapshotFiles,
|
|
64556
65733
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
64557
65734
|
});
|
|
64558
|
-
import { createHash as
|
|
65735
|
+
import { createHash as createHash16 } from "crypto";
|
|
64559
65736
|
import { existsSync as existsSync21, readFileSync as readFileSync16, statSync as statSync10 } from "fs";
|
|
64560
65737
|
import { hostname as hostname2, platform, arch } from "os";
|
|
64561
65738
|
import { dirname as dirname10, join as join22, resolve as resolve21 } from "path";
|
|
64562
65739
|
import { tmpdir as tmpdir4 } from "os";
|
|
64563
65740
|
function sha2567(value) {
|
|
64564
|
-
return
|
|
65741
|
+
return createHash16("sha256").update(value).digest("hex");
|
|
64565
65742
|
}
|
|
64566
65743
|
function fileRecord(root, relativePath) {
|
|
64567
65744
|
const path = join22(root, relativePath);
|
|
@@ -64676,8 +65853,8 @@ function defaultSnapshotDir() {
|
|
|
64676
65853
|
return join22(dirname10(resolve21(dbPath)), "environment-snapshots");
|
|
64677
65854
|
}
|
|
64678
65855
|
function snapshotWithId(snapshot) {
|
|
64679
|
-
const
|
|
64680
|
-
return { id: `env_${
|
|
65856
|
+
const digest2 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
|
|
65857
|
+
return { id: `env_${digest2}`, ...snapshot };
|
|
64681
65858
|
}
|
|
64682
65859
|
function captureEnvironmentSnapshot(input = {}) {
|
|
64683
65860
|
const root = resolve21(input.root || process.cwd());
|
|
@@ -69352,15 +70529,15 @@ __export(exports_task_route_sources, {
|
|
|
69352
70529
|
TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION: () => TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION
|
|
69353
70530
|
});
|
|
69354
70531
|
import { Database as Database4 } from "bun:sqlite";
|
|
69355
|
-
import { createHash as
|
|
70532
|
+
import { createHash as createHash17 } from "crypto";
|
|
69356
70533
|
import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync11 } from "fs";
|
|
69357
70534
|
import { basename as basename10, dirname as dirname14, join as join26, resolve as resolve22 } from "path";
|
|
69358
70535
|
function normalizePath6(input) {
|
|
69359
70536
|
return resolve22(input);
|
|
69360
70537
|
}
|
|
69361
70538
|
function sourceStoreId(sourceDbPath) {
|
|
69362
|
-
const
|
|
69363
|
-
return `sqlite:${
|
|
70539
|
+
const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
70540
|
+
return `sqlite:${digest2}`;
|
|
69364
70541
|
}
|
|
69365
70542
|
function inferSourceRepoPath(sourceDbPath) {
|
|
69366
70543
|
const normalized = normalizePath6(sourceDbPath);
|
|
@@ -69691,7 +70868,7 @@ __export(exports_tester_issue_reports, {
|
|
|
69691
70868
|
TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
|
|
69692
70869
|
TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION
|
|
69693
70870
|
});
|
|
69694
|
-
import { createHash as
|
|
70871
|
+
import { createHash as createHash18 } from "crypto";
|
|
69695
70872
|
function asObject3(value) {
|
|
69696
70873
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
69697
70874
|
}
|
|
@@ -69863,7 +71040,7 @@ function fingerprintTesterIssueReport(report) {
|
|
|
69863
71040
|
normalizeText4(report.failure?.message || report.summary || report.title).slice(0, 240),
|
|
69864
71041
|
normalizeText4(stackTop).slice(0, 160)
|
|
69865
71042
|
].join("::");
|
|
69866
|
-
return `testers:${
|
|
71043
|
+
return `testers:${createHash18("sha256").update(raw).digest("hex").slice(0, 16)}`;
|
|
69867
71044
|
}
|
|
69868
71045
|
function priorityForSeverity(severity, fallback) {
|
|
69869
71046
|
return PRIORITIES5.includes(severity) ? severity : fallback;
|
|
@@ -74498,7 +75675,7 @@ var init_dispatch3 = __esm(() => {
|
|
|
74498
75675
|
});
|
|
74499
75676
|
|
|
74500
75677
|
// src/lib/delegation-brief.ts
|
|
74501
|
-
import { createHash as
|
|
75678
|
+
import { createHash as createHash19 } from "crypto";
|
|
74502
75679
|
function resolveDelegationBrief(input, sources) {
|
|
74503
75680
|
const hasPath = typeof input.briefPath === "string" && input.briefPath.length > 0;
|
|
74504
75681
|
const hasText = typeof input.briefText === "string" && input.briefText.length > 0;
|
|
@@ -74558,7 +75735,7 @@ function resolveDelegationBrief(input, sources) {
|
|
|
74558
75735
|
ok: true,
|
|
74559
75736
|
text: text2,
|
|
74560
75737
|
source: source3,
|
|
74561
|
-
sha256:
|
|
75738
|
+
sha256: createHash19("sha256").update(text2, "utf8").digest("hex"),
|
|
74562
75739
|
bytes: Buffer.byteLength(text2, "utf8")
|
|
74563
75740
|
};
|
|
74564
75741
|
}
|
|
@@ -81101,7 +82278,7 @@ var init_hybrid = __esm(() => {
|
|
|
81101
82278
|
});
|
|
81102
82279
|
|
|
81103
82280
|
// src/storage/s3-artifacts.ts
|
|
81104
|
-
import { createHash as
|
|
82281
|
+
import { createHash as createHash20, createHmac as createHmac2 } from "crypto";
|
|
81105
82282
|
function createTodosS3ArtifactStore(options) {
|
|
81106
82283
|
const requestFetch = options.fetch ?? fetch;
|
|
81107
82284
|
const now4 = options.now ?? (() => new Date);
|
|
@@ -81273,7 +82450,7 @@ function toAmzDate(date) {
|
|
|
81273
82450
|
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
81274
82451
|
}
|
|
81275
82452
|
function sha256Hex(value) {
|
|
81276
|
-
return
|
|
82453
|
+
return createHash20("sha256").update(value).digest("hex");
|
|
81277
82454
|
}
|
|
81278
82455
|
function hmac(key, value) {
|
|
81279
82456
|
return createHmac2("sha256", key).update(value).digest();
|