@danypops/papyrus 0.42.0 → 0.42.1

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.
Files changed (73) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  3. package/src/adapters/sqlite-artifact-store.ts +7 -5
  4. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  5. package/src/adapters/sqlite-gate-runner.ts +1 -1
  6. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  7. package/src/adapters/sqlite-log-store.ts +36 -17
  8. package/src/adapters/sqlite-note-event-store.ts +20 -16
  9. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  10. package/src/adapters/sqlite-task-event-store.ts +29 -21
  11. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  12. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  13. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  14. package/src/artifact-relationship-view.ts +3 -3
  15. package/src/artifact-subtree.ts +4 -2
  16. package/src/authority-registry.ts +2 -1
  17. package/src/cli.ts +785 -179
  18. package/src/client.ts +6 -3
  19. package/src/constants.ts +15 -4
  20. package/src/daemon-state.ts +4 -12
  21. package/src/daemon.ts +31 -9
  22. package/src/db.ts +119 -98
  23. package/src/discussion-service.ts +109 -44
  24. package/src/domain/artifact-event.ts +17 -4
  25. package/src/domain/artifact.ts +3 -1
  26. package/src/domain/blueprint-definition.ts +26 -31
  27. package/src/domain/checklist.ts +20 -17
  28. package/src/domain/discussion.ts +37 -18
  29. package/src/domain/gate.ts +7 -7
  30. package/src/domain/log-entry.ts +1 -1
  31. package/src/domain/note-event.ts +20 -7
  32. package/src/domain/task-event.ts +17 -7
  33. package/src/domain-services.ts +241 -110
  34. package/src/graph-projection-service.ts +34 -8
  35. package/src/id-migration.ts +17 -4
  36. package/src/index.ts +16 -11
  37. package/src/log-service.ts +6 -5
  38. package/src/log.ts +19 -0
  39. package/src/modules/discuss.ts +63 -28
  40. package/src/modules/docs.ts +74 -17
  41. package/src/modules/graph-projection.ts +20 -9
  42. package/src/modules/logs.ts +33 -21
  43. package/src/modules/notes.ts +66 -28
  44. package/src/modules/playbooks.ts +88 -29
  45. package/src/modules/rules.ts +57 -15
  46. package/src/modules/session-identity.ts +6 -2
  47. package/src/modules/tasks.ts +142 -67
  48. package/src/note-service.ts +11 -7
  49. package/src/ops.ts +131 -68
  50. package/src/playbook-definition.ts +56 -17
  51. package/src/playbook-execution.ts +13 -3
  52. package/src/ports/note-event-store.ts +6 -4
  53. package/src/ports/task-event-store.ts +9 -6
  54. package/src/ports/task-focus-store.ts +17 -4
  55. package/src/ports/task-lease-store.ts +9 -4
  56. package/src/ports/task-scope-store.ts +3 -1
  57. package/src/service.ts +143 -89
  58. package/src/session-identity-service.ts +10 -2
  59. package/src/task-context.ts +28 -16
  60. package/src/task-execution.ts +4 -12
  61. package/src/task-graph-view.ts +12 -12
  62. package/src/task-relationship-view.ts +1 -3
  63. package/src/task-service.ts +167 -72
  64. package/src/vehicle/artifact-trash-vehicle.ts +25 -13
  65. package/src/vehicle/artifact-vehicle-shared.ts +28 -7
  66. package/src/vehicle/docs-vehicle.ts +48 -16
  67. package/src/vehicle/notes-vehicle.ts +24 -6
  68. package/src/vehicle/papyrus-vehicle.ts +14 -3
  69. package/src/vehicle/playbooks-vehicle.ts +87 -18
  70. package/src/vehicle/rules-vehicle.ts +58 -21
  71. package/src/vehicle/tasks-vehicle.ts +366 -54
  72. package/src/version.ts +1 -1
  73. package/src/workflow-execution.ts +71 -52
package/src/client.ts CHANGED
@@ -2,7 +2,7 @@ import { spawn as spawnProcess } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { connectWithPolicy, spawnDetachedDaemon } from "@danypops/vehicle-client/daemon-client";
4
4
  import { DAEMON_CLIENT_TIMEOUT_MS, DAEMON_DIR_ENV, DAEMON_PROBE_TIMEOUT_MS } from "./constants.ts";
5
- import { daemonStateDir, readDaemonHandle, type DaemonHandle } from "./daemon-state.ts";
5
+ import { type DaemonHandle, daemonStateDir, readDaemonHandle } from "./daemon-state.ts";
6
6
  import type { OperationName, SchemaState } from "./service.ts";
7
7
 
8
8
  export type FetchAdapter = (request: Request) => Promise<Response>;
@@ -26,7 +26,7 @@ export class PapyrusClient {
26
26
  signal: init.signal ?? AbortSignal.timeout(this.timeoutMs),
27
27
  });
28
28
  const response = await this.fetchAdapter(request);
29
- const body = await response.json() as { error?: string } & T;
29
+ const body = (await response.json()) as { error?: string } & T;
30
30
  if (!response.ok) throw new Error(body.error ?? `Papyrus daemon HTTP ${response.status}`);
31
31
  return body;
32
32
  }
@@ -87,7 +87,10 @@ export interface ConnectPapyrusClientOptions {
87
87
  * failure (stale, not "never started") and is NOT auto-recovered here -- it still
88
88
  * throws its own actionable "restart manually" error, unchanged from before.
89
89
  */
90
- export async function connectPapyrusClient(dir: string = daemonStateDir(), options: ConnectPapyrusClientOptions = {}): Promise<PapyrusClient> {
90
+ export async function connectPapyrusClient(
91
+ dir: string = daemonStateDir(),
92
+ options: ConnectPapyrusClientOptions = {},
93
+ ): Promise<PapyrusClient> {
91
94
  return connectWithPolicy({
92
95
  readHandle: () => readDaemonHandle(dir) ?? null,
93
96
  buildClient: probedPapyrusClient,
package/src/constants.ts CHANGED
@@ -289,7 +289,7 @@ export const TASK_RECONCILIATION_INSTRUCTION = [
289
289
 
290
290
  /** $XDG_DATA_HOME/papyrus/papyrus.db */
291
291
  export function dbPath(): string {
292
- const xdg = process.env["XDG_DATA_HOME"] || `${process.env["HOME"]}/.local/share`;
292
+ const xdg = process.env.XDG_DATA_HOME || `${process.env.HOME}/.local/share`;
293
293
  return `${xdg}/papyrus/papyrus.db`;
294
294
  }
295
295
 
@@ -324,7 +324,18 @@ export const DEFAULT_STATUS_BY_KIND: Readonly<Record<string, string>> = {
324
324
  * triggers: this playbook run applies to that work (playbook→task)
325
325
  */
326
326
  export const SEED_RELATIONS = [
327
- "references", "implements", "follows", "depends_on",
328
- "documents", "blocks", "supersedes", "relates_to",
329
- "gates", "triggers", "contains", "part_of", "reply_to", "discusses",
327
+ "references",
328
+ "implements",
329
+ "follows",
330
+ "depends_on",
331
+ "documents",
332
+ "blocks",
333
+ "supersedes",
334
+ "relates_to",
335
+ "gates",
336
+ "triggers",
337
+ "contains",
338
+ "part_of",
339
+ "reply_to",
340
+ "discusses",
330
341
  ] as const;
@@ -2,12 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import {
6
- DAEMON_DIR_ENV,
7
- DAEMON_HOST,
8
- DAEMON_PORT_FILE,
9
- DAEMON_TOKEN_FILE,
10
- } from "./constants.ts";
5
+ import { DAEMON_DIR_ENV, DAEMON_HOST, DAEMON_PORT_FILE, DAEMON_TOKEN_FILE } from "./constants.ts";
11
6
 
12
7
  export interface DaemonHandle {
13
8
  baseUrl: string;
@@ -17,13 +12,10 @@ export interface DaemonHandle {
17
12
  pid: number;
18
13
  }
19
14
 
20
- export function daemonStateDir(
21
- env: Record<string, string | undefined> = process.env,
22
- home: string = homedir(),
23
- ): string {
15
+ export function daemonStateDir(env: Record<string, string | undefined> = process.env, home: string = homedir()): string {
24
16
  if (env[DAEMON_DIR_ENV]) return env[DAEMON_DIR_ENV];
25
- if (env["XDG_RUNTIME_DIR"]) return join(env["XDG_RUNTIME_DIR"], "papyrus");
26
- if (env["XDG_STATE_HOME"]) return join(env["XDG_STATE_HOME"], "papyrus");
17
+ if (env.XDG_RUNTIME_DIR) return join(env.XDG_RUNTIME_DIR, "papyrus");
18
+ if (env.XDG_STATE_HOME) return join(env.XDG_STATE_HOME, "papyrus");
27
19
  return join(home, ".local", "state", "papyrus");
28
20
  }
29
21
 
package/src/daemon.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { PushChannel } from "@danypops/vehicle-server/push-channel";
2
- import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, WAL_CHECKPOINT_INTERVAL_MS, dbPath } from "./constants.ts";
2
+ import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, dbPath, WAL_CHECKPOINT_INTERVAL_MS } from "./constants.ts";
3
3
  import { clearDaemonPort, daemonStateDir, loadOrCreateToken, writeDaemonPort } from "./daemon-state.ts";
4
+ import { logEvent, vehicleLogger } from "./log.ts";
4
5
  import { createApp, createPapyrusService } from "./service.ts";
5
- import { logEvent } from "./log.ts";
6
6
 
7
7
  /**
8
8
  * Operations that never change what a Task-graph reader (the pi-papyrus widget's
@@ -13,8 +13,16 @@ import { logEvent } from "./log.ts";
13
13
  * silently-uncovered new mutation.
14
14
  */
15
15
  const TASK_READ_ONLY_OPERATIONS = new Set([
16
- "tasks.active", "tasks.context", "tasks.event_feed", "tasks.focused",
17
- "tasks.graph", "tasks.history", "tasks.list", "tasks.plan", "tasks.scope", "tasks.show",
16
+ "tasks.active",
17
+ "tasks.context",
18
+ "tasks.event_feed",
19
+ "tasks.focused",
20
+ "tasks.graph",
21
+ "tasks.history",
22
+ "tasks.list",
23
+ "tasks.plan",
24
+ "tasks.scope",
25
+ "tasks.show",
18
26
  ]);
19
27
 
20
28
  /** Start the supervised, long-running Papyrus service. */
@@ -31,6 +39,7 @@ export function serveMain(): void {
31
39
  pushChannel.publish("tasks", { operation });
32
40
  }
33
41
  },
42
+ logger: vehicleLogger(),
34
43
  });
35
44
  const server = Bun.serve({
36
45
  hostname: DAEMON_HOST,
@@ -49,10 +58,18 @@ export function serveMain(): void {
49
58
  }
50
59
  writeDaemonPort(stateDir, server.port);
51
60
  const checkpointTimer = setInterval(() => {
52
- try { service.checkpoint(); } catch (error) { logEvent("error", "checkpoint_failed", { message: error instanceof Error ? error.message : String(error) }); }
61
+ try {
62
+ service.checkpoint();
63
+ } catch (error) {
64
+ logEvent("error", "checkpoint_failed", { message: error instanceof Error ? error.message : String(error) });
65
+ }
53
66
  }, WAL_CHECKPOINT_INTERVAL_MS);
54
67
  const optimizeTimer = setInterval(() => {
55
- try { service.optimize(); } catch (error) { logEvent("error", "optimize_failed", { message: error instanceof Error ? error.message : String(error) }); }
68
+ try {
69
+ service.optimize();
70
+ } catch (error) {
71
+ logEvent("error", "optimize_failed", { message: error instanceof Error ? error.message : String(error) });
72
+ }
56
73
  }, DB_OPTIMIZE_INTERVAL_MS);
57
74
  // Daily cadence (reusing DB_OPTIMIZE_INTERVAL_MS) is plenty against a 30-day staleness
58
75
  // threshold (TASK_FOCUS_STALE_AFTER_MS) -- see clean-up-stale-per-session-task-focus-rows-
@@ -61,7 +78,9 @@ export function serveMain(): void {
61
78
  try {
62
79
  const removed = service.reapStaleFocus();
63
80
  if (removed > 0) logEvent("info", "stale_focus_reaped", { removed });
64
- } catch (error) { logEvent("error", "reap_stale_focus_failed", { message: error instanceof Error ? error.message : String(error) }); }
81
+ } catch (error) {
82
+ logEvent("error", "reap_stale_focus_failed", { message: error instanceof Error ? error.message : String(error) });
83
+ }
65
84
  }, DB_OPTIMIZE_INTERVAL_MS);
66
85
  // Same daily cadence: ARTIFACT_TRASH_RETENTION_MS is 30 days, so a daily sweep finds newly
67
86
  // due artifacts promptly without needing its own tighter interval -- see domain/artifact-trash.ts.
@@ -69,7 +88,9 @@ export function serveMain(): void {
69
88
  try {
70
89
  const purged = service.purgeDueTrash();
71
90
  if (purged > 0) logEvent("info", "artifact_trash_purged", { purged });
72
- } catch (error) { logEvent("error", "purge_trash_failed", { message: error instanceof Error ? error.message : String(error) }); }
91
+ } catch (error) {
92
+ logEvent("error", "purge_trash_failed", { message: error instanceof Error ? error.message : String(error) });
93
+ }
73
94
  }, DB_OPTIMIZE_INTERVAL_MS);
74
95
  let stopping = false;
75
96
  const shutdown = () => {
@@ -83,7 +104,8 @@ export function serveMain(): void {
83
104
  service.close();
84
105
  // .finally() re-throws rather than handling a rejection -- catching it first turns a bare
85
106
  // unhandled-rejection warning into a real, queryable shutdown-failure log line.
86
- void server.stop(true)
107
+ void server
108
+ .stop(true)
87
109
  .catch((error) => logEvent("error", "server_stop_failed", { message: error instanceof Error ? error.message : String(error) }))
88
110
  .finally(() => process.exit(0));
89
111
  };
package/src/db.ts CHANGED
@@ -1,12 +1,6 @@
1
- /**
2
- * db.ts — enforced-schema SQLite store for Papyrus.
3
- * Dual-runtime: bun:sqlite (Bun) / node:sqlite (Node/pi host).
4
- * Four kinds (doc/task/rule/skill) are FK-enforced; relations are universal (any→any).
5
- */
6
- import { createHash } from "node:crypto";
7
- import { createRequire } from "node:module";
8
1
  import { mkdirSync } from "node:fs";
9
- import { join, dirname } from "node:path";
2
+ import { createRequire } from "node:module";
3
+ import { dirname } from "node:path";
10
4
  import { runMigrations, type SqliteMigrationRunner } from "@danypops/vehicle-server/storage";
11
5
  import { SQLITE_BUSY_TIMEOUT_MS, SQLITE_SCHEMA_VERSION } from "./constants.ts";
12
6
 
@@ -373,10 +367,22 @@ export function migrationLedger(db: Db): ModuleMigrationRow[] {
373
367
  // has no ledger table yet -- "nothing recorded" is the correct answer, not an error.
374
368
  const tableExists = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'module_migrations'").get() != null;
375
369
  if (!tableExists) return [];
376
- const rows = db.prepare("SELECT module_id, version, name, checksum, applied_at FROM module_migrations ORDER BY module_id, version").all() as Array<{
377
- module_id: string; version: number; name: string; checksum: string; applied_at: string;
370
+ const rows = db
371
+ .prepare("SELECT module_id, version, name, checksum, applied_at FROM module_migrations ORDER BY module_id, version")
372
+ .all() as Array<{
373
+ module_id: string;
374
+ version: number;
375
+ name: string;
376
+ checksum: string;
377
+ applied_at: string;
378
378
  }>;
379
- return rows.map((row) => ({ moduleId: row.module_id, version: row.version, name: row.name, checksum: row.checksum, appliedAt: row.applied_at }));
379
+ return rows.map((row) => ({
380
+ moduleId: row.module_id,
381
+ version: row.version,
382
+ name: row.name,
383
+ checksum: row.checksum,
384
+ appliedAt: row.applied_at,
385
+ }));
380
386
  }
381
387
 
382
388
  /**
@@ -407,10 +413,14 @@ function ensureCoreLedger(db: Db, alreadyAtCurrentSchema: boolean): void {
407
413
  `);
408
414
  inTransaction(db, () => {
409
415
  for (const [index, entry] of CORE_LEDGER_VERSIONS.entries()) {
410
- const existingRow = db.prepare("SELECT checksum FROM module_migrations WHERE module_id = 'core' AND version = ?").get(entry.version) as { checksum: string } | null;
416
+ const existingRow = db
417
+ .prepare("SELECT checksum FROM module_migrations WHERE module_id = 'core' AND version = ?")
418
+ .get(entry.version) as { checksum: string } | null;
411
419
  if (existingRow != null) {
412
420
  if (existingRow.checksum !== entry.checksum) {
413
- throw new Error(`module migration "core" version ${entry.version} checksum mismatch: the frozen definition for this version was edited after it was applied`);
421
+ throw new Error(
422
+ `module migration "core" version ${entry.version} checksum mismatch: the frozen definition for this version was edited after it was applied`,
423
+ );
414
424
  }
415
425
  continue;
416
426
  }
@@ -420,16 +430,18 @@ function ensureCoreLedger(db: Db, alreadyAtCurrentSchema: boolean): void {
420
430
  db.exec(SEED_SQL);
421
431
  db.exec(`PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
422
432
  }
423
- db.prepare("INSERT INTO module_migrations (module_id, version, name, checksum, applied_at) VALUES ('core', ?, ?, ?, ?)")
424
- .run(entry.version, entry.name, entry.checksum, new Date().toISOString());
433
+ db.prepare("INSERT INTO module_migrations (module_id, version, name, checksum, applied_at) VALUES ('core', ?, ?, ?, ?)").run(
434
+ entry.version,
435
+ entry.name,
436
+ entry.checksum,
437
+ new Date().toISOString(),
438
+ );
425
439
  }
426
440
  });
427
441
  }
428
442
 
429
443
  function bootstrapEmptyDatabase(db: Db): void {
430
- const existing = db
431
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1")
432
- .get();
444
+ const existing = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
433
445
  if (existing) throw new Error("database schema is unversioned; refusing to migrate existing data during boot");
434
446
  ensureCoreLedger(db, false);
435
447
  }
@@ -524,7 +536,9 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
524
536
  // older user_version to exercise this migration path must not fail with "duplicate column
525
537
  // name" -- the same class of already-bootstrapped-fixture concern version 9's comment covers.
526
538
  up: (db) => {
527
- const existing = new Set((db.prepare("PRAGMA table_info(discussion_rounds)").all() as Array<{ name: string }>).map((row) => row.name));
539
+ const existing = new Set(
540
+ (db.prepare("PRAGMA table_info(discussion_rounds)").all() as Array<{ name: string }>).map((row) => row.name),
541
+ );
528
542
  for (const column of ["options", "options_mode", "selected"]) {
529
543
  if (!existing.has(column)) db.exec(`ALTER TABLE discussion_rounds ADD COLUMN ${column} TEXT`);
530
544
  }
@@ -568,7 +582,9 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
568
582
  // pattern as version 16's discuss-options -- a round with no per-option description (every
569
583
  // round before this feature existed, and any that simply doesn't need one) stores NULL.
570
584
  up: (db) => {
571
- const existing = new Set((db.prepare("PRAGMA table_info(discussion_rounds)").all() as Array<{ name: string }>).map((row) => row.name));
585
+ const existing = new Set(
586
+ (db.prepare("PRAGMA table_info(discussion_rounds)").all() as Array<{ name: string }>).map((row) => row.name),
587
+ );
572
588
  if (!existing.has("option_descriptions")) db.exec("ALTER TABLE discussion_rounds ADD COLUMN option_descriptions TEXT");
573
589
  },
574
590
  },
@@ -621,7 +637,10 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
621
637
  WHEN NOT EXISTS (SELECT 1 FROM artifact_trash WHERE artifact_id = OLD.note_id AND purge_after <= strftime('%Y-%m-%dT%H:%M:%fZ','now'))
622
638
  BEGIN SELECT RAISE(ABORT, 'note_events are append-only except during an explicit, elapsed-grace-period artifact trash purge'); END;
623
639
  `);
624
- const rows = db.prepare("SELECT id, extra FROM artifacts WHERE kind = 'doc' AND subtype = 'note'").all() as Array<{ id: string; extra: string }>;
640
+ const rows = db.prepare("SELECT id, extra FROM artifacts WHERE kind = 'doc' AND subtype = 'note'").all() as Array<{
641
+ id: string;
642
+ extra: string;
643
+ }>;
625
644
  const insert = db.prepare(`
626
645
  INSERT INTO note_events (note_id, occurred_at, event_type, actor, source, session_id, reason, related_id, disposition, event_schema_version)
627
646
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
@@ -629,20 +648,20 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
629
648
  const strip = db.prepare("UPDATE artifacts SET extra = ? WHERE id = ?");
630
649
  for (const row of rows) {
631
650
  const extra = JSON.parse(row.extra) as Record<string, unknown>;
632
- const noteHistory = extra["noteHistory"];
651
+ const noteHistory = extra.noteHistory;
633
652
  if (Array.isArray(noteHistory)) {
634
653
  for (const raw of noteHistory) {
635
654
  const entry = raw as Record<string, unknown>;
636
655
  insert.run(
637
656
  row.id,
638
- typeof entry["at"] === "string" ? entry["at"] : new Date().toISOString(),
639
- typeof entry["action"] === "string" ? entry["action"] : "captured",
640
- typeof entry["actor"] === "string" ? entry["actor"] : "system",
641
- typeof entry["source"] === "string" ? entry["source"] : "unknown",
642
- typeof entry["sessionId"] === "string" ? entry["sessionId"] : null,
643
- typeof entry["reason"] === "string" ? entry["reason"] : null,
644
- typeof entry["targetId"] === "string" ? entry["targetId"] : null,
645
- typeof entry["disposition"] === "string" ? entry["disposition"] : null,
657
+ typeof entry.at === "string" ? entry.at : new Date().toISOString(),
658
+ typeof entry.action === "string" ? entry.action : "captured",
659
+ typeof entry.actor === "string" ? entry.actor : "system",
660
+ typeof entry.source === "string" ? entry.source : "unknown",
661
+ typeof entry.sessionId === "string" ? entry.sessionId : null,
662
+ typeof entry.reason === "string" ? entry.reason : null,
663
+ typeof entry.targetId === "string" ? entry.targetId : null,
664
+ typeof entry.disposition === "string" ? entry.disposition : null,
646
665
  );
647
666
  }
648
667
  }
@@ -724,9 +743,10 @@ export function migrateDb(db: Db): MigrationResult {
724
743
  // entire frozen chain, not merely fail to match any of its branches -- entering it and
725
744
  // falling through to the final gap check would otherwise misreport a database correctly
726
745
  // mid-way through FUTURE_MIGRATIONS as "no explicit migration path".
727
- if (from < LEGACY_MIGRATION_CHAIN_TARGET_VERSION) inTransaction(db, () => {
728
- if (schemaVersion(db) === 1) {
729
- db.exec(`
746
+ if (from < LEGACY_MIGRATION_CHAIN_TARGET_VERSION)
747
+ inTransaction(db, () => {
748
+ if (schemaVersion(db) === 1) {
749
+ db.exec(`
730
750
  INSERT OR IGNORE INTO statuses VALUES ('todo','task');
731
751
  INSERT OR IGNORE INTO statuses VALUES ('in-progress','task');
732
752
  INSERT OR IGNORE INTO statuses VALUES ('review','task');
@@ -751,10 +771,10 @@ export function migrateDb(db: Db): MigrationResult {
751
771
  DELETE FROM statuses WHERE kind = 'task' AND name IN ('pending', 'active', 'failed');
752
772
  PRAGMA user_version = 2;
753
773
  `);
754
- applied.push("task-lifecycle-and-focus");
755
- }
756
- if (schemaVersion(db) === 2) {
757
- db.exec(`
774
+ applied.push("task-lifecycle-and-focus");
775
+ }
776
+ if (schemaVersion(db) === 2) {
777
+ db.exec(`
758
778
  CREATE TABLE task_events (
759
779
  id INTEGER PRIMARY KEY AUTOINCREMENT,
760
780
  task_id TEXT NOT NULL REFERENCES artifacts(id),
@@ -777,10 +797,10 @@ export function migrateDb(db: Db): MigrationResult {
777
797
  BEGIN SELECT RAISE(ABORT, 'task_events are append-only'); END;
778
798
  PRAGMA user_version = 3;
779
799
  `);
780
- applied.push("task-history");
781
- }
782
- if (schemaVersion(db) === 3) {
783
- db.exec(`
800
+ applied.push("task-history");
801
+ }
802
+ if (schemaVersion(db) === 3) {
803
+ db.exec(`
784
804
  CREATE TABLE task_scopes (
785
805
  task_id TEXT PRIMARY KEY REFERENCES artifacts(id),
786
806
  project_root TEXT,
@@ -800,18 +820,18 @@ export function migrateDb(db: Db): MigrationResult {
800
820
  FROM artifacts WHERE kind = 'task';
801
821
  PRAGMA user_version = 4;
802
822
  `);
803
- applied.push("task-project-scope");
804
- }
805
- if (schemaVersion(db) === 4) {
806
- db.exec(`
823
+ applied.push("task-project-scope");
824
+ }
825
+ if (schemaVersion(db) === 4) {
826
+ db.exec(`
807
827
  ALTER TABLE task_focus ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused'));
808
828
  ALTER TABLE task_focus ADD COLUMN pause_reason TEXT;
809
829
  PRAGMA user_version = 5;
810
830
  `);
811
- applied.push("task-focus-continuation");
812
- }
813
- if (schemaVersion(db) === 5) {
814
- db.exec(`
831
+ applied.push("task-focus-continuation");
832
+ }
833
+ if (schemaVersion(db) === 5) {
834
+ db.exec(`
815
835
  INSERT OR IGNORE INTO relation_names VALUES ('reply_to','Append-only message replies to another message in the same thread');
816
836
  INSERT OR IGNORE INTO relation_names VALUES ('discusses','Message or turn concerns a verified artifact');
817
837
  CREATE TABLE discourse_threads (
@@ -853,10 +873,10 @@ export function migrateDb(db: Db): MigrationResult {
853
873
  BEGIN SELECT RAISE(ABORT, 'discourse Context Mesh artifact type is immutable'); END;
854
874
  PRAGMA user_version = 6;
855
875
  `);
856
- applied.push("discourse-context-mesh");
857
- }
858
- if (schemaVersion(db) === 6) {
859
- db.exec(`
876
+ applied.push("discourse-context-mesh");
877
+ }
878
+ if (schemaVersion(db) === 6) {
879
+ db.exec(`
860
880
  CREATE TABLE artifact_events (
861
881
  id INTEGER PRIMARY KEY AUTOINCREMENT,
862
882
  artifact_id TEXT NOT NULL REFERENCES artifacts(id),
@@ -881,10 +901,10 @@ export function migrateDb(db: Db): MigrationResult {
881
901
  BEGIN SELECT RAISE(ABORT, 'artifact_events are append-only'); END;
882
902
  PRAGMA user_version = 7;
883
903
  `);
884
- applied.push("artifact-event-log");
885
- }
886
- if (schemaVersion(db) === 7) {
887
- db.exec(`
904
+ applied.push("artifact-event-log");
905
+ }
906
+ if (schemaVersion(db) === 7) {
907
+ db.exec(`
888
908
  CREATE TABLE task_focus_v7 (
889
909
  scope TEXT PRIMARY KEY,
890
910
  task_id TEXT NOT NULL REFERENCES artifacts(id),
@@ -897,15 +917,15 @@ export function migrateDb(db: Db): MigrationResult {
897
917
  ALTER TABLE task_focus_v7 RENAME TO task_focus;
898
918
  PRAGMA user_version = 8;
899
919
  `);
900
- applied.push("task-focus-session-scope");
901
- }
902
- if (schemaVersion(db) === 8) {
903
- // IF NOT EXISTS here, unlike earlier migration branches: a fully-bootstrapped
904
- // :memory: fixture (used by unrelated tests that only roll user_version back to
905
- // simulate an older *file* database) already has every table the current bootstrap
906
- // DDL declares, this one included -- so this branch must be safe to run whether or
907
- // not that already happened, not assume a truly-old database created it first.
908
- db.exec(`
920
+ applied.push("task-focus-session-scope");
921
+ }
922
+ if (schemaVersion(db) === 8) {
923
+ // IF NOT EXISTS here, unlike earlier migration branches: a fully-bootstrapped
924
+ // :memory: fixture (used by unrelated tests that only roll user_version back to
925
+ // simulate an older *file* database) already has every table the current bootstrap
926
+ // DDL declares, this one included -- so this branch must be safe to run whether or
927
+ // not that already happened, not assume a truly-old database created it first.
928
+ db.exec(`
909
929
  CREATE TABLE IF NOT EXISTS graph_projection_checkpoints (
910
930
  producer_id TEXT PRIMARY KEY,
911
931
  last_sequence INTEGER NOT NULL,
@@ -921,10 +941,10 @@ export function migrateDb(db: Db): MigrationResult {
921
941
  CREATE INDEX IF NOT EXISTS graph_projection_identities_artifact_idx ON graph_projection_identities(artifact_id);
922
942
  PRAGMA user_version = 9;
923
943
  `);
924
- applied.push("graph-projection-protocol");
925
- }
926
- if (schemaVersion(db) === 9) {
927
- db.exec(`
944
+ applied.push("graph-projection-protocol");
945
+ }
946
+ if (schemaVersion(db) === 9) {
947
+ db.exec(`
928
948
  CREATE TABLE IF NOT EXISTS artifact_scopes (
929
949
  artifact_id TEXT PRIMARY KEY REFERENCES artifacts(id),
930
950
  project_root TEXT,
@@ -934,10 +954,10 @@ export function migrateDb(db: Db): MigrationResult {
934
954
  CREATE INDEX IF NOT EXISTS artifact_scopes_project_idx ON artifact_scopes(project_root, artifact_id);
935
955
  PRAGMA user_version = 10;
936
956
  `);
937
- applied.push("docs-rules-skills-project-scope");
938
- }
939
- if (schemaVersion(db) === 10) {
940
- db.exec(`
957
+ applied.push("docs-rules-skills-project-scope");
958
+ }
959
+ if (schemaVersion(db) === 10) {
960
+ db.exec(`
941
961
  CREATE TABLE IF NOT EXISTS log_sources (
942
962
  id TEXT PRIMARY KEY,
943
963
  label TEXT NOT NULL,
@@ -961,18 +981,18 @@ export function migrateDb(db: Db): MigrationResult {
961
981
  BEGIN SELECT RAISE(ABORT, 'log_entries are immutable once written; retention trimming is the only supported deletion path'); END;
962
982
  PRAGMA user_version = 11;
963
983
  `);
964
- applied.push("log-domain");
965
- }
966
- if (schemaVersion(db) === 11) {
967
- // Removes Discourse's Papyrus-embedded storage entirely: confirmed zero rows in every
968
- // discourse_* table and zero Docs carrying the reserved context-thread/context-message
969
- // subtypes in the real production database before this was written -- Discourse's real
970
- // home is now the standalone @danypops/discourse package plus host adapters, and
971
- // Papyrus's own copy never had a single real caller since it was built. IF EXISTS
972
- // throughout: a database that never actually reached the v5->v6 discourse-context-mesh
973
- // step in the first place (e.g. a test fixture that starts partway through the chain)
974
- // must not fail here just because there was nothing to remove.
975
- db.exec(`
984
+ applied.push("log-domain");
985
+ }
986
+ if (schemaVersion(db) === 11) {
987
+ // Removes Discourse's Papyrus-embedded storage entirely: confirmed zero rows in every
988
+ // discourse_* table and zero Docs carrying the reserved context-thread/context-message
989
+ // subtypes in the real production database before this was written -- Discourse's real
990
+ // home is now the standalone @danypops/discourse package plus host adapters, and
991
+ // Papyrus's own copy never had a single real caller since it was built. IF EXISTS
992
+ // throughout: a database that never actually reached the v5->v6 discourse-context-mesh
993
+ // step in the first place (e.g. a test fixture that starts partway through the chain)
994
+ // must not fail here just because there was nothing to remove.
995
+ db.exec(`
976
996
  DROP TRIGGER IF EXISTS discourse_artifact_type_immutable;
977
997
  DROP TRIGGER IF EXISTS discourse_posts_artifact_type;
978
998
  DROP TRIGGER IF EXISTS discourse_threads_artifact_type;
@@ -985,14 +1005,14 @@ export function migrateDb(db: Db): MigrationResult {
985
1005
  DELETE FROM relation_names WHERE name IN ('reply_to', 'discusses');
986
1006
  PRAGMA user_version = 12;
987
1007
  `);
988
- applied.push("remove-discourse");
989
- }
990
- if (schemaVersion(db) === 12) {
991
- // See domain/session-identity.ts and verify-caller-identity-behind-papyrus-mutation-
992
- // attribution-koxt: first-touch capability binding for session_id, the one place it is
993
- // behavior-affecting today (Task Focus). Purely additive -- a session_id that never
994
- // registers here behaves exactly as before.
995
- db.exec(`
1008
+ applied.push("remove-discourse");
1009
+ }
1010
+ if (schemaVersion(db) === 12) {
1011
+ // See domain/session-identity.ts and verify-caller-identity-behind-papyrus-mutation-
1012
+ // attribution-koxt: first-touch capability binding for session_id, the one place it is
1013
+ // behavior-affecting today (Task Focus). Purely additive -- a session_id that never
1014
+ // registers here behaves exactly as before.
1015
+ db.exec(`
996
1016
  CREATE TABLE IF NOT EXISTS session_identities (
997
1017
  session_id TEXT PRIMARY KEY,
998
1018
  secret_hash TEXT NOT NULL,
@@ -1001,10 +1021,11 @@ export function migrateDb(db: Db): MigrationResult {
1001
1021
  );
1002
1022
  PRAGMA user_version = 13;
1003
1023
  `);
1004
- applied.push("session-identity");
1005
- }
1006
- if (schemaVersion(db) !== LEGACY_MIGRATION_CHAIN_TARGET_VERSION) throw new Error(`no explicit migration path from database schema ${from}`);
1007
- });
1024
+ applied.push("session-identity");
1025
+ }
1026
+ if (schemaVersion(db) !== LEGACY_MIGRATION_CHAIN_TARGET_VERSION)
1027
+ throw new Error(`no explicit migration path from database schema ${from}`);
1028
+ });
1008
1029
 
1009
1030
  // Guarded by length: runMigrations treats an empty migrations array's "target version" as
1010
1031
  // 0 (see its own sorted.at(-1) ?? 0), which would misreport a database the legacy chain