@danypops/papyrus 0.41.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 (78) hide show
  1. package/README.md +8 -11
  2. package/package.json +2 -2
  3. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  4. package/src/adapters/sqlite-artifact-store.ts +7 -5
  5. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  6. package/src/adapters/sqlite-gate-runner.ts +1 -1
  7. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  8. package/src/adapters/sqlite-log-store.ts +36 -17
  9. package/src/adapters/sqlite-note-event-store.ts +20 -16
  10. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  11. package/src/adapters/sqlite-task-event-store.ts +29 -21
  12. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  13. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  14. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  15. package/src/artifact-relationship-view.ts +4 -4
  16. package/src/artifact-subtree.ts +4 -2
  17. package/src/authority-registry.ts +2 -1
  18. package/src/cli.ts +794 -354
  19. package/src/client.ts +6 -3
  20. package/src/constants.ts +34 -56
  21. package/src/daemon-state.ts +4 -12
  22. package/src/daemon.ts +31 -9
  23. package/src/db.ts +153 -105
  24. package/src/discussion-service.ts +109 -44
  25. package/src/domain/artifact-event.ts +18 -5
  26. package/src/domain/artifact.ts +3 -1
  27. package/src/domain/blueprint-definition.ts +268 -0
  28. package/src/domain/checklist.ts +20 -17
  29. package/src/domain/discussion.ts +37 -18
  30. package/src/domain/gate.ts +7 -7
  31. package/src/domain/log-entry.ts +1 -1
  32. package/src/domain/note-event.ts +20 -7
  33. package/src/domain/task-event.ts +17 -7
  34. package/src/domain-services.ts +362 -288
  35. package/src/graph-projection-service.ts +34 -8
  36. package/src/id-migration.ts +17 -4
  37. package/src/index.ts +16 -11
  38. package/src/log-service.ts +6 -5
  39. package/src/log.ts +19 -0
  40. package/src/modules/discuss.ts +63 -28
  41. package/src/modules/docs.ts +74 -17
  42. package/src/modules/graph-projection.ts +20 -9
  43. package/src/modules/logs.ts +34 -22
  44. package/src/modules/notes.ts +66 -28
  45. package/src/modules/playbooks.ts +93 -32
  46. package/src/modules/rules.ts +57 -15
  47. package/src/modules/session-identity.ts +6 -2
  48. package/src/modules/tasks.ts +142 -67
  49. package/src/note-service.ts +11 -7
  50. package/src/ops.ts +134 -69
  51. package/src/playbook-definition.ts +124 -39
  52. package/src/playbook-execution.ts +18 -25
  53. package/src/ports/artifact-scope-store.ts +1 -1
  54. package/src/ports/note-event-store.ts +6 -4
  55. package/src/ports/task-event-store.ts +9 -6
  56. package/src/ports/task-focus-store.ts +17 -4
  57. package/src/ports/task-lease-store.ts +9 -4
  58. package/src/ports/task-scope-store.ts +3 -1
  59. package/src/service.ts +148 -110
  60. package/src/session-identity-service.ts +10 -2
  61. package/src/task-context.ts +28 -16
  62. package/src/task-execution.ts +4 -12
  63. package/src/task-graph-view.ts +12 -12
  64. package/src/task-relationship-view.ts +1 -3
  65. package/src/task-service.ts +168 -73
  66. package/src/vehicle/artifact-trash-vehicle.ts +26 -14
  67. package/src/vehicle/artifact-vehicle-shared.ts +32 -13
  68. package/src/vehicle/docs-vehicle.ts +50 -18
  69. package/src/vehicle/notes-vehicle.ts +26 -8
  70. package/src/vehicle/papyrus-vehicle.ts +16 -8
  71. package/src/vehicle/playbooks-vehicle.ts +88 -19
  72. package/src/vehicle/rules-vehicle.ts +58 -21
  73. package/src/vehicle/tasks-vehicle.ts +366 -54
  74. package/src/version.ts +1 -1
  75. package/src/workflow-execution.ts +198 -109
  76. package/src/domain/skill-definition.ts +0 -270
  77. package/src/modules/skills.ts +0 -158
  78. package/src/vehicle/skills-vehicle.ts +0 -194
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
 
@@ -291,7 +285,6 @@ const SEED_SQL = `
291
285
  INSERT OR IGNORE INTO kinds VALUES ('doc','Knowledge — what we know (specs, decisions, research, designs)');
292
286
  INSERT OR IGNORE INTO kinds VALUES ('task','Work — what we are doing (objectives, steps, checklists)');
293
287
  INSERT OR IGNORE INTO kinds VALUES ('rule','Governance — when doing X, follow Y');
294
- INSERT OR IGNORE INTO kinds VALUES ('skill','Parameterized workflow bundle — inputs and templates load tasks, rules, and docs');
295
288
  INSERT OR IGNORE INTO kinds VALUES ('playbook','Reusable procedure — a trigger and an ordered list of steps an agent reads and follows, not a mechanically instantiated blueprint');
296
289
  INSERT OR IGNORE INTO statuses VALUES ('draft','doc');
297
290
  INSERT OR IGNORE INTO statuses VALUES ('active','doc');
@@ -304,20 +297,18 @@ INSERT OR IGNORE INTO statuses VALUES ('done','task');
304
297
  INSERT OR IGNORE INTO statuses VALUES ('canceled','task');
305
298
  INSERT OR IGNORE INTO statuses VALUES ('active','rule');
306
299
  INSERT OR IGNORE INTO statuses VALUES ('deprecated','rule');
307
- INSERT OR IGNORE INTO statuses VALUES ('active','skill');
308
- INSERT OR IGNORE INTO statuses VALUES ('deprecated','skill');
309
300
  INSERT OR IGNORE INTO statuses VALUES ('active','playbook');
310
301
  INSERT OR IGNORE INTO statuses VALUES ('deprecated','playbook');
311
302
  INSERT OR IGNORE INTO relation_names VALUES ('references','Source material (doc→doc, doc→task, doc→rule)');
312
303
  INSERT OR IGNORE INTO relation_names VALUES ('implements','This work satisfies that (task→doc, task→rule)');
313
- INSERT OR IGNORE INTO relation_names VALUES ('follows','This work obeys that (task→rule, task→skill)');
314
- INSERT OR IGNORE INTO relation_names VALUES ('depends_on','DAG ordering (task→task)');
315
- INSERT OR IGNORE INTO relation_names VALUES ('documents','Describes (doc→task, doc→rule, doc→skill)');
304
+ INSERT OR IGNORE INTO relation_names VALUES ('follows','This work obeys that (task→rule, task→playbook)');
305
+ INSERT OR IGNORE INTO relation_names VALUES ('depends_on','DAG ordering (task→task, playbook→playbook)');
306
+ INSERT OR IGNORE INTO relation_names VALUES ('documents','Describes (doc→task, doc→rule, doc→playbook)');
316
307
  INSERT OR IGNORE INTO relation_names VALUES ('blocks','Blocking relationship (task→task, or an active Discussion doc→task)');
317
308
  INSERT OR IGNORE INTO relation_names VALUES ('supersedes','Replaces (doc→doc, rule→rule)');
318
309
  INSERT OR IGNORE INTO relation_names VALUES ('relates_to','Catch-all (any→any)');
319
310
  INSERT OR IGNORE INTO relation_names VALUES ('gates','This rule gates that task (rule→task)');
320
- INSERT OR IGNORE INTO relation_names VALUES ('triggers','This skill applies to that work (skill→task)');
311
+ INSERT OR IGNORE INTO relation_names VALUES ('triggers','This playbook run applies to that work (playbook→task)');
321
312
  INSERT OR IGNORE INTO relation_names VALUES ('contains','Parent contains a nested artifact (any→any)');
322
313
  INSERT OR IGNORE INTO relation_names VALUES ('part_of','Artifact belongs to a parent artifact (any→any)');
323
314
  CREATE INDEX IF NOT EXISTS edges_to_id_idx ON edges(to_id);
@@ -376,10 +367,22 @@ export function migrationLedger(db: Db): ModuleMigrationRow[] {
376
367
  // has no ledger table yet -- "nothing recorded" is the correct answer, not an error.
377
368
  const tableExists = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'module_migrations'").get() != null;
378
369
  if (!tableExists) return [];
379
- const rows = db.prepare("SELECT module_id, version, name, checksum, applied_at FROM module_migrations ORDER BY module_id, version").all() as Array<{
380
- 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;
381
378
  }>;
382
- 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
+ }));
383
386
  }
384
387
 
385
388
  /**
@@ -410,10 +413,14 @@ function ensureCoreLedger(db: Db, alreadyAtCurrentSchema: boolean): void {
410
413
  `);
411
414
  inTransaction(db, () => {
412
415
  for (const [index, entry] of CORE_LEDGER_VERSIONS.entries()) {
413
- 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;
414
419
  if (existingRow != null) {
415
420
  if (existingRow.checksum !== entry.checksum) {
416
- 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
+ );
417
424
  }
418
425
  continue;
419
426
  }
@@ -423,16 +430,18 @@ function ensureCoreLedger(db: Db, alreadyAtCurrentSchema: boolean): void {
423
430
  db.exec(SEED_SQL);
424
431
  db.exec(`PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
425
432
  }
426
- db.prepare("INSERT INTO module_migrations (module_id, version, name, checksum, applied_at) VALUES ('core', ?, ?, ?, ?)")
427
- .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
+ );
428
439
  }
429
440
  });
430
441
  }
431
442
 
432
443
  function bootstrapEmptyDatabase(db: Db): void {
433
- const existing = db
434
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1")
435
- .get();
444
+ const existing = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
436
445
  if (existing) throw new Error("database schema is unversioned; refusing to migrate existing data during boot");
437
446
  ensureCoreLedger(db, false);
438
447
  }
@@ -527,7 +536,9 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
527
536
  // older user_version to exercise this migration path must not fail with "duplicate column
528
537
  // name" -- the same class of already-bootstrapped-fixture concern version 9's comment covers.
529
538
  up: (db) => {
530
- 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
+ );
531
542
  for (const column of ["options", "options_mode", "selected"]) {
532
543
  if (!existing.has(column)) db.exec(`ALTER TABLE discussion_rounds ADD COLUMN ${column} TEXT`);
533
544
  }
@@ -571,7 +582,9 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
571
582
  // pattern as version 16's discuss-options -- a round with no per-option description (every
572
583
  // round before this feature existed, and any that simply doesn't need one) stores NULL.
573
584
  up: (db) => {
574
- 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
+ );
575
588
  if (!existing.has("option_descriptions")) db.exec("ALTER TABLE discussion_rounds ADD COLUMN option_descriptions TEXT");
576
589
  },
577
590
  },
@@ -624,7 +637,10 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
624
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'))
625
638
  BEGIN SELECT RAISE(ABORT, 'note_events are append-only except during an explicit, elapsed-grace-period artifact trash purge'); END;
626
639
  `);
627
- 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
+ }>;
628
644
  const insert = db.prepare(`
629
645
  INSERT INTO note_events (note_id, occurred_at, event_type, actor, source, session_id, reason, related_id, disposition, event_schema_version)
630
646
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
@@ -632,20 +648,20 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
632
648
  const strip = db.prepare("UPDATE artifacts SET extra = ? WHERE id = ?");
633
649
  for (const row of rows) {
634
650
  const extra = JSON.parse(row.extra) as Record<string, unknown>;
635
- const noteHistory = extra["noteHistory"];
651
+ const noteHistory = extra.noteHistory;
636
652
  if (Array.isArray(noteHistory)) {
637
653
  for (const raw of noteHistory) {
638
654
  const entry = raw as Record<string, unknown>;
639
655
  insert.run(
640
656
  row.id,
641
- typeof entry["at"] === "string" ? entry["at"] : new Date().toISOString(),
642
- typeof entry["action"] === "string" ? entry["action"] : "captured",
643
- typeof entry["actor"] === "string" ? entry["actor"] : "system",
644
- typeof entry["source"] === "string" ? entry["source"] : "unknown",
645
- typeof entry["sessionId"] === "string" ? entry["sessionId"] : null,
646
- typeof entry["reason"] === "string" ? entry["reason"] : null,
647
- typeof entry["targetId"] === "string" ? entry["targetId"] : null,
648
- 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,
649
665
  );
650
666
  }
651
667
  }
@@ -656,6 +672,36 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
656
672
  }
657
673
  },
658
674
  },
675
+ {
676
+ version: 22,
677
+ name: "skill-to-playbook-data-migration",
678
+ // Part of the Skill→Playbook consolidation. Migration 18 already moved subtype-less Skill
679
+ // rows to kind=playbook; this moves everything still left under kind=skill (workflow and
680
+ // artifact-template rows alike, plus any bare stragglers), preserving subtype and status so
681
+ // a later task can still tell which authoring shape each migrated row came from. The
682
+ // 'skill'/'artifact-template' kinds/statuses type rows themselves are deliberately NOT
683
+ // dropped here: src/modules/skills.ts's createSkill (and the CLI/Vehicle/TUI surfaces still
684
+ // wired to it) still writes kind='skill' rows in its own live, not-yet-retired test suite --
685
+ // dropping the kinds row now would break that FK-enforced write path before its own
686
+ // retirement task removes it. Dropping the type rows is that later task's job.
687
+ up: (db) => {
688
+ db.exec(`UPDATE artifacts SET kind = 'playbook' WHERE kind = 'skill'`);
689
+ },
690
+ },
691
+ {
692
+ version: 23,
693
+ name: "retire-skill-kind",
694
+ // Final step of the Skill→Playbook consolidation. Every module/CLI/Vehicle/TUI surface
695
+ // that wrote kind='skill' is now retired (confirmed live: zero real callers anywhere), and
696
+ // migration 22 already moved every existing row off kind=skill. The type rows themselves
697
+ // are now genuinely dead -- safe to drop.
698
+ up: (db) => {
699
+ db.exec(`
700
+ DELETE FROM statuses WHERE kind = 'skill';
701
+ DELETE FROM kinds WHERE name = 'skill';
702
+ `);
703
+ },
704
+ },
659
705
  ];
660
706
 
661
707
  /**
@@ -697,9 +743,10 @@ export function migrateDb(db: Db): MigrationResult {
697
743
  // entire frozen chain, not merely fail to match any of its branches -- entering it and
698
744
  // falling through to the final gap check would otherwise misreport a database correctly
699
745
  // mid-way through FUTURE_MIGRATIONS as "no explicit migration path".
700
- if (from < LEGACY_MIGRATION_CHAIN_TARGET_VERSION) inTransaction(db, () => {
701
- if (schemaVersion(db) === 1) {
702
- db.exec(`
746
+ if (from < LEGACY_MIGRATION_CHAIN_TARGET_VERSION)
747
+ inTransaction(db, () => {
748
+ if (schemaVersion(db) === 1) {
749
+ db.exec(`
703
750
  INSERT OR IGNORE INTO statuses VALUES ('todo','task');
704
751
  INSERT OR IGNORE INTO statuses VALUES ('in-progress','task');
705
752
  INSERT OR IGNORE INTO statuses VALUES ('review','task');
@@ -724,10 +771,10 @@ export function migrateDb(db: Db): MigrationResult {
724
771
  DELETE FROM statuses WHERE kind = 'task' AND name IN ('pending', 'active', 'failed');
725
772
  PRAGMA user_version = 2;
726
773
  `);
727
- applied.push("task-lifecycle-and-focus");
728
- }
729
- if (schemaVersion(db) === 2) {
730
- db.exec(`
774
+ applied.push("task-lifecycle-and-focus");
775
+ }
776
+ if (schemaVersion(db) === 2) {
777
+ db.exec(`
731
778
  CREATE TABLE task_events (
732
779
  id INTEGER PRIMARY KEY AUTOINCREMENT,
733
780
  task_id TEXT NOT NULL REFERENCES artifacts(id),
@@ -750,10 +797,10 @@ export function migrateDb(db: Db): MigrationResult {
750
797
  BEGIN SELECT RAISE(ABORT, 'task_events are append-only'); END;
751
798
  PRAGMA user_version = 3;
752
799
  `);
753
- applied.push("task-history");
754
- }
755
- if (schemaVersion(db) === 3) {
756
- db.exec(`
800
+ applied.push("task-history");
801
+ }
802
+ if (schemaVersion(db) === 3) {
803
+ db.exec(`
757
804
  CREATE TABLE task_scopes (
758
805
  task_id TEXT PRIMARY KEY REFERENCES artifacts(id),
759
806
  project_root TEXT,
@@ -773,18 +820,18 @@ export function migrateDb(db: Db): MigrationResult {
773
820
  FROM artifacts WHERE kind = 'task';
774
821
  PRAGMA user_version = 4;
775
822
  `);
776
- applied.push("task-project-scope");
777
- }
778
- if (schemaVersion(db) === 4) {
779
- db.exec(`
823
+ applied.push("task-project-scope");
824
+ }
825
+ if (schemaVersion(db) === 4) {
826
+ db.exec(`
780
827
  ALTER TABLE task_focus ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused'));
781
828
  ALTER TABLE task_focus ADD COLUMN pause_reason TEXT;
782
829
  PRAGMA user_version = 5;
783
830
  `);
784
- applied.push("task-focus-continuation");
785
- }
786
- if (schemaVersion(db) === 5) {
787
- db.exec(`
831
+ applied.push("task-focus-continuation");
832
+ }
833
+ if (schemaVersion(db) === 5) {
834
+ db.exec(`
788
835
  INSERT OR IGNORE INTO relation_names VALUES ('reply_to','Append-only message replies to another message in the same thread');
789
836
  INSERT OR IGNORE INTO relation_names VALUES ('discusses','Message or turn concerns a verified artifact');
790
837
  CREATE TABLE discourse_threads (
@@ -826,10 +873,10 @@ export function migrateDb(db: Db): MigrationResult {
826
873
  BEGIN SELECT RAISE(ABORT, 'discourse Context Mesh artifact type is immutable'); END;
827
874
  PRAGMA user_version = 6;
828
875
  `);
829
- applied.push("discourse-context-mesh");
830
- }
831
- if (schemaVersion(db) === 6) {
832
- db.exec(`
876
+ applied.push("discourse-context-mesh");
877
+ }
878
+ if (schemaVersion(db) === 6) {
879
+ db.exec(`
833
880
  CREATE TABLE artifact_events (
834
881
  id INTEGER PRIMARY KEY AUTOINCREMENT,
835
882
  artifact_id TEXT NOT NULL REFERENCES artifacts(id),
@@ -854,10 +901,10 @@ export function migrateDb(db: Db): MigrationResult {
854
901
  BEGIN SELECT RAISE(ABORT, 'artifact_events are append-only'); END;
855
902
  PRAGMA user_version = 7;
856
903
  `);
857
- applied.push("artifact-event-log");
858
- }
859
- if (schemaVersion(db) === 7) {
860
- db.exec(`
904
+ applied.push("artifact-event-log");
905
+ }
906
+ if (schemaVersion(db) === 7) {
907
+ db.exec(`
861
908
  CREATE TABLE task_focus_v7 (
862
909
  scope TEXT PRIMARY KEY,
863
910
  task_id TEXT NOT NULL REFERENCES artifacts(id),
@@ -870,15 +917,15 @@ export function migrateDb(db: Db): MigrationResult {
870
917
  ALTER TABLE task_focus_v7 RENAME TO task_focus;
871
918
  PRAGMA user_version = 8;
872
919
  `);
873
- applied.push("task-focus-session-scope");
874
- }
875
- if (schemaVersion(db) === 8) {
876
- // IF NOT EXISTS here, unlike earlier migration branches: a fully-bootstrapped
877
- // :memory: fixture (used by unrelated tests that only roll user_version back to
878
- // simulate an older *file* database) already has every table the current bootstrap
879
- // DDL declares, this one included -- so this branch must be safe to run whether or
880
- // not that already happened, not assume a truly-old database created it first.
881
- 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(`
882
929
  CREATE TABLE IF NOT EXISTS graph_projection_checkpoints (
883
930
  producer_id TEXT PRIMARY KEY,
884
931
  last_sequence INTEGER NOT NULL,
@@ -894,10 +941,10 @@ export function migrateDb(db: Db): MigrationResult {
894
941
  CREATE INDEX IF NOT EXISTS graph_projection_identities_artifact_idx ON graph_projection_identities(artifact_id);
895
942
  PRAGMA user_version = 9;
896
943
  `);
897
- applied.push("graph-projection-protocol");
898
- }
899
- if (schemaVersion(db) === 9) {
900
- db.exec(`
944
+ applied.push("graph-projection-protocol");
945
+ }
946
+ if (schemaVersion(db) === 9) {
947
+ db.exec(`
901
948
  CREATE TABLE IF NOT EXISTS artifact_scopes (
902
949
  artifact_id TEXT PRIMARY KEY REFERENCES artifacts(id),
903
950
  project_root TEXT,
@@ -907,10 +954,10 @@ export function migrateDb(db: Db): MigrationResult {
907
954
  CREATE INDEX IF NOT EXISTS artifact_scopes_project_idx ON artifact_scopes(project_root, artifact_id);
908
955
  PRAGMA user_version = 10;
909
956
  `);
910
- applied.push("docs-rules-skills-project-scope");
911
- }
912
- if (schemaVersion(db) === 10) {
913
- db.exec(`
957
+ applied.push("docs-rules-skills-project-scope");
958
+ }
959
+ if (schemaVersion(db) === 10) {
960
+ db.exec(`
914
961
  CREATE TABLE IF NOT EXISTS log_sources (
915
962
  id TEXT PRIMARY KEY,
916
963
  label TEXT NOT NULL,
@@ -934,18 +981,18 @@ export function migrateDb(db: Db): MigrationResult {
934
981
  BEGIN SELECT RAISE(ABORT, 'log_entries are immutable once written; retention trimming is the only supported deletion path'); END;
935
982
  PRAGMA user_version = 11;
936
983
  `);
937
- applied.push("log-domain");
938
- }
939
- if (schemaVersion(db) === 11) {
940
- // Removes Discourse's Papyrus-embedded storage entirely: confirmed zero rows in every
941
- // discourse_* table and zero Docs carrying the reserved context-thread/context-message
942
- // subtypes in the real production database before this was written -- Discourse's real
943
- // home is now the standalone @danypops/discourse package plus host adapters, and
944
- // Papyrus's own copy never had a single real caller since it was built. IF EXISTS
945
- // throughout: a database that never actually reached the v5->v6 discourse-context-mesh
946
- // step in the first place (e.g. a test fixture that starts partway through the chain)
947
- // must not fail here just because there was nothing to remove.
948
- 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(`
949
996
  DROP TRIGGER IF EXISTS discourse_artifact_type_immutable;
950
997
  DROP TRIGGER IF EXISTS discourse_posts_artifact_type;
951
998
  DROP TRIGGER IF EXISTS discourse_threads_artifact_type;
@@ -958,14 +1005,14 @@ export function migrateDb(db: Db): MigrationResult {
958
1005
  DELETE FROM relation_names WHERE name IN ('reply_to', 'discusses');
959
1006
  PRAGMA user_version = 12;
960
1007
  `);
961
- applied.push("remove-discourse");
962
- }
963
- if (schemaVersion(db) === 12) {
964
- // See domain/session-identity.ts and verify-caller-identity-behind-papyrus-mutation-
965
- // attribution-koxt: first-touch capability binding for session_id, the one place it is
966
- // behavior-affecting today (Task Focus). Purely additive -- a session_id that never
967
- // registers here behaves exactly as before.
968
- 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(`
969
1016
  CREATE TABLE IF NOT EXISTS session_identities (
970
1017
  session_id TEXT PRIMARY KEY,
971
1018
  secret_hash TEXT NOT NULL,
@@ -974,10 +1021,11 @@ export function migrateDb(db: Db): MigrationResult {
974
1021
  );
975
1022
  PRAGMA user_version = 13;
976
1023
  `);
977
- applied.push("session-identity");
978
- }
979
- if (schemaVersion(db) !== LEGACY_MIGRATION_CHAIN_TARGET_VERSION) throw new Error(`no explicit migration path from database schema ${from}`);
980
- });
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
+ });
981
1029
 
982
1030
  // Guarded by length: runMigrations treats an empty migrations array's "target version" as
983
1031
  // 0 (see its own sorted.at(-1) ?? 0), which would misreport a database the legacy chain