@hasna/todos 0.15.34 → 0.15.35

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/contracts.js CHANGED
@@ -1884,6 +1884,10 @@ var init_migrations = __esm(() => {
1884
1884
  );
1885
1885
  INSERT OR IGNORE INTO _migrations (id) VALUES (70);
1886
1886
  COMMIT;
1887
+ `,
1888
+ `BEGIN;
1889
+ INSERT OR IGNORE INTO _migrations (id) VALUES (71);
1890
+ COMMIT;
1887
1891
  `
1888
1892
  ];
1889
1893
  });
@@ -2557,6 +2561,8 @@ function ensureSchema(db) {
2557
2561
  ensureColumn("projects", "task_list_id", "TEXT");
2558
2562
  ensureColumn("projects", "task_prefix", "TEXT");
2559
2563
  ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
2564
+ ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
2565
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
2560
2566
  ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
2561
2567
  ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
2562
2568
  ensureColumn("tasks", "short_id", "TEXT");
@@ -5045,17 +5051,24 @@ function createProject(input, db) {
5045
5051
  const id = uuid();
5046
5052
  const timestamp2 = now();
5047
5053
  const derivedSlug = slugify(input.name);
5048
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
5054
+ const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
5049
5055
  if (!derivedSlug || !taskListId)
5050
5056
  throw new Error("Project name and task-list slug must be non-empty");
5051
5057
  const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
5052
5058
  if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
5053
5059
  throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
5054
5060
  }
5061
+ const parentId = input.parent_id ?? null;
5062
+ if (parentId !== null) {
5063
+ const parent = getProject(parentId, d);
5064
+ if (!parent)
5065
+ throw new ProjectNotFoundError(parentId);
5066
+ assertNotProjectAncestor(id, parentId, d);
5067
+ }
5055
5068
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
5056
5069
  const machineId = currentStorageMachineId(d);
5057
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
5058
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
5070
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
5071
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
5059
5072
  return getProject(id, d);
5060
5073
  })();
5061
5074
  }
@@ -5080,6 +5093,60 @@ function listProjects(db) {
5080
5093
  const d = db || getDatabase();
5081
5094
  return d.query("SELECT * FROM projects ORDER BY name").all();
5082
5095
  }
5096
+ function orderProjectsParentFirst(projects) {
5097
+ const projectId = (project) => {
5098
+ const id = project.id;
5099
+ return typeof id === "string" && id.length > 0 ? id : null;
5100
+ };
5101
+ const parentId = (project) => {
5102
+ const parent = project.parent_id;
5103
+ return parent == null ? null : String(parent);
5104
+ };
5105
+ const byId = new Set;
5106
+ for (const project of projects) {
5107
+ const id = projectId(project);
5108
+ if (id !== null)
5109
+ byId.add(id);
5110
+ }
5111
+ const ordered = [];
5112
+ const emitted = new Set;
5113
+ let remaining = [...projects];
5114
+ let progress = true;
5115
+ while (progress && remaining.length > 0) {
5116
+ progress = false;
5117
+ const deferred = [];
5118
+ for (const project of remaining) {
5119
+ const parent = parentId(project);
5120
+ if (parent === null || emitted.has(parent) || !byId.has(parent)) {
5121
+ ordered.push(project);
5122
+ const id = projectId(project);
5123
+ if (id !== null)
5124
+ emitted.add(id);
5125
+ progress = true;
5126
+ } else {
5127
+ deferred.push(project);
5128
+ }
5129
+ }
5130
+ remaining = deferred;
5131
+ }
5132
+ ordered.push(...remaining);
5133
+ return ordered;
5134
+ }
5135
+ function assertNotProjectAncestor(projectId, ancestorId, db) {
5136
+ const d = db || getDatabase();
5137
+ let cursor = ancestorId;
5138
+ const seen = new Set;
5139
+ while (cursor !== null) {
5140
+ if (cursor === projectId) {
5141
+ throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
5142
+ }
5143
+ if (seen.has(cursor))
5144
+ break;
5145
+ seen.add(cursor);
5146
+ const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
5147
+ cursor = row?.parent_id ?? null;
5148
+ }
5149
+ }
5083
5150
  function updateProject(id, input, db) {
5084
5151
  const d = db || getDatabase();
5085
5152
  const project = getProject(id, d);
@@ -5102,6 +5169,16 @@ function updateProject(id, input, db) {
5102
5169
  sets.push("path = ?");
5103
5170
  params.push(input.path);
5104
5171
  }
5172
+ if (input.parent_id !== undefined) {
5173
+ if (input.parent_id !== null) {
5174
+ const parent = getProject(input.parent_id, d);
5175
+ if (!parent)
5176
+ throw new ProjectNotFoundError(input.parent_id);
5177
+ assertNotProjectAncestor(id, input.parent_id, d);
5178
+ }
5179
+ sets.push("parent_id = ?");
5180
+ params.push(input.parent_id);
5181
+ }
5105
5182
  params.push(id);
5106
5183
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
5107
5184
  return getProject(id, d);
@@ -12728,7 +12805,7 @@ var init_tasks = __esm(() => {
12728
12805
  // package.json
12729
12806
  var package_default = {
12730
12807
  name: "@hasna/todos",
12731
- version: "0.15.34",
12808
+ version: "0.15.35",
12732
12809
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12733
12810
  type: "module",
12734
12811
  main: "dist/index.js",
@@ -12803,6 +12880,9 @@ var package_default = {
12803
12880
  "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
12804
12881
  "verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
12805
12882
  "test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
12883
+ "emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
12884
+ "verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
12885
+ "test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
12806
12886
  "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
12807
12887
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
12808
12888
  postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
@@ -13491,6 +13571,7 @@ var TODOS_JSON_CONTRACTS = [
13491
13571
  task_list_id: nullableIdField,
13492
13572
  task_prefix: field(["string", "null"], "Optional task prefix.", true),
13493
13573
  task_counter: field("integer", "Monotonic project task counter."),
13574
+ parent_id: field(["string", "null"], "Optional parent project id; null means top-level.", true),
13494
13575
  created_at: isoDateField,
13495
13576
  updated_at: isoDateField
13496
13577
  },
@@ -16776,6 +16857,7 @@ function createAgentProjectDemoBundle() {
16776
16857
  task_list_id: ids.list,
16777
16858
  task_prefix: "DEMO",
16778
16859
  task_counter: 4,
16860
+ parent_id: null,
16779
16861
  created_at: createdAt,
16780
16862
  updated_at: completedAt
16781
16863
  });
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,UAAU,UA0qDtB,CAAC"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,UAAU,UAwrDtB,CAAC"}
@@ -5,6 +5,29 @@ export declare function createProject(input: CreateProjectInput, db?: Database):
5
5
  export declare function getProject(id: string, db?: Database): Project | null;
6
6
  export declare function getProjectByPath(path: string, db?: Database): Project | null;
7
7
  export declare function listProjects(db?: Database): Project[];
8
+ export declare function listChildProjects(parentId: string, db?: Database): Project[];
9
+ /**
10
+ * Order projects so every parent precedes its children, regardless of name.
11
+ *
12
+ * projects.parent_id is an immediate foreign key, so importing a child before
13
+ * its parent fails the constraint and drops the child — and everything that
14
+ * points at it, such as its task rows — into the import's error list. Both
15
+ * round-trip import paths (SQLite snapshot restore and bundle import) rely on
16
+ * this ordering and must not depend on the export side's `ORDER BY name`.
17
+ *
18
+ * Stable and cycle-safe: rows whose parent is null, already emitted, or
19
+ * outside the given set keep their original relative order; a malformed input
20
+ * whose parent chain loops falls back to original order instead of hanging or
21
+ * dropping rows silently — the FK constraint then rejects the offending rows
22
+ * per-row and the import records the errors.
23
+ */
24
+ export declare function orderProjectsParentFirst<T extends object>(projects: readonly T[]): T[];
25
+ /**
26
+ * Reject making `projectId` a descendant of `ancestorId`. Walks the ancestor
27
+ * chain of the candidate parent; if `projectId` appears anywhere in it, the
28
+ * new link would create a cycle (a project that is its own ancestor).
29
+ */
30
+ export declare function assertNotProjectAncestor(projectId: string, ancestorId: string, db?: Database): void;
8
31
  export declare function updateProject(id: string, input: UpdateProjectInput, db?: Database): Project;
9
32
  /**
10
33
  * Rename a project: update name and/or task_list_id (slug).
@@ -1 +1 @@
1
- {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/db/projects.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/db/projects.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAoChD;AA8BD,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAs/C/C;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA4BnD"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAoChD;AA8BD,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAw/C/C;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA4BnD"}