@danypops/papyrus 0.17.1 → 0.17.2

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 (2) hide show
  1. package/package.json +2 -2
  2. package/src/db.ts +71 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.17.1",
3
+ "version": "0.17.2",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -42,6 +42,6 @@
42
42
  "files": ["src", "extension", "README.md"],
43
43
  "dependencies": {
44
44
  "beautiful-mermaid": "1.1.3",
45
- "@danypops/daemon-kit": "^0.2.0"
45
+ "@danypops/daemon-kit": "^0.2.1"
46
46
  }
47
47
  }
package/src/db.ts CHANGED
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import { createRequire } from "node:module";
8
8
  import { mkdirSync } from "node:fs";
9
9
  import { join, dirname } from "node:path";
10
+ import { runMigrations, type SqliteMigrationRunner } from "@danypops/daemon-kit/storage";
10
11
  import { SQLITE_BUSY_TIMEOUT_MS, SQLITE_SCHEMA_VERSION } from "./constants.ts";
11
12
 
12
13
  const require_ = createRequire(import.meta.url);
@@ -363,6 +364,47 @@ function bootstrapEmptyDatabase(db: Db): void {
363
364
  ensureCoreLedger(db, false);
364
365
  }
365
366
 
367
+ /**
368
+ * Version the hardcoded, sequential if-chain below produces once fully applied. Frozen
369
+ * forever, per this same function's own "deliberately not a hand-enumerated allow-list"
370
+ * history below: that chain is never edited once shipped, only ever extended with a new
371
+ * `if` branch -- except a NEW branch is no longer how migrations beyond this version are
372
+ * added (see FUTURE_MIGRATIONS). The legacy chain itself stays byte-for-byte as it always
373
+ * was: same SQL, same single all-or-nothing transaction, same dynamic post-hoc gap check.
374
+ */
375
+ const LEGACY_MIGRATION_CHAIN_TARGET_VERSION = 13;
376
+
377
+ /**
378
+ * A migration beyond LEGACY_MIGRATION_CHAIN_TARGET_VERSION. Runs through @danypops/
379
+ * daemon-kit's generic runMigrations engine (one transaction per migration, daemon-kit's
380
+ * default) via dbMigrationRunner below, instead of a new branch appended to the legacy
381
+ * if-chain -- the exact reuse daemon-kit's storage module was refactored (v0.2.1) to allow,
382
+ * since Papyrus's dual bun:sqlite/node:sqlite Db abstraction could never satisfy that
383
+ * engine's original bun:sqlite-only signature.
384
+ */
385
+ export interface PapyrusMigration {
386
+ version: number;
387
+ name: string;
388
+ up: (db: Db) => void;
389
+ }
390
+
391
+ /** Currently empty -- no schema version beyond LEGACY_MIGRATION_CHAIN_TARGET_VERSION exists yet. The next migration is appended here, never as a new branch in the legacy chain above. */
392
+ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [];
393
+
394
+ /**
395
+ * Adapts Papyrus's own Db/inTransaction to daemon-kit's storage-agnostic
396
+ * SqliteMigrationRunner port, so its runMigrations engine (written against bun:sqlite's
397
+ * concrete Database) runs unmodified against Papyrus's dual-runtime Db abstraction instead.
398
+ */
399
+ export function dbMigrationRunner(db: Db): SqliteMigrationRunner<Db> {
400
+ return {
401
+ raw: db,
402
+ userVersion: () => schemaVersion(db),
403
+ setUserVersion: (version) => db.exec(`PRAGMA user_version = ${version}`),
404
+ transaction: (fn) => inTransaction(db, fn),
405
+ };
406
+ }
407
+
366
408
  export function migrateDb(db: Db): MigrationResult {
367
409
  const from = schemaVersion(db);
368
410
  if (from > SQLITE_SCHEMA_VERSION) {
@@ -378,11 +420,17 @@ export function migrateDb(db: Db): MigrationResult {
378
420
  // migrating any already-deployed database sitting at schema 8, 9, or 10 (including the real
379
421
  // production database at the time this was found) would have thrown "no explicit migration
380
422
  // path" before ever reaching the migration chain below. Checked dynamically after the chain
381
- // runs instead: if schemaVersion(db) hasn't reached SQLITE_SCHEMA_VERSION once every
382
- // `schemaVersion(db) === N` step below has had its chance to fire, `from` was never a valid
383
- // starting point (a genuine gap in the chain) -- structurally cannot drift out of sync the
384
- // way a separate, parallel enumeration did.
385
- inTransaction(db, () => {
423
+ // runs instead: if schemaVersion(db) hasn't reached LEGACY_MIGRATION_CHAIN_TARGET_VERSION once
424
+ // every `schemaVersion(db) === N` step below has had its chance to fire, `from` was never a
425
+ // valid starting point (a genuine gap in the chain) -- structurally cannot drift out of sync
426
+ // the way a separate, parallel enumeration did.
427
+ //
428
+ // Guarded by `from < LEGACY_MIGRATION_CHAIN_TARGET_VERSION`: a database already at or past
429
+ // that version (only reachable once FUTURE_MIGRATIONS below has entries) must skip this
430
+ // entire frozen chain, not merely fail to match any of its branches -- entering it and
431
+ // falling through to the final gap check would otherwise misreport a database correctly
432
+ // mid-way through FUTURE_MIGRATIONS as "no explicit migration path".
433
+ if (from < LEGACY_MIGRATION_CHAIN_TARGET_VERSION) inTransaction(db, () => {
386
434
  if (schemaVersion(db) === 1) {
387
435
  db.exec(`
388
436
  INSERT OR IGNORE INTO statuses VALUES ('todo','task');
@@ -661,9 +709,25 @@ export function migrateDb(db: Db): MigrationResult {
661
709
  `);
662
710
  applied.push("session-identity");
663
711
  }
664
- if (schemaVersion(db) !== SQLITE_SCHEMA_VERSION) throw new Error(`no explicit migration path from database schema ${from}`);
712
+ if (schemaVersion(db) !== LEGACY_MIGRATION_CHAIN_TARGET_VERSION) throw new Error(`no explicit migration path from database schema ${from}`);
665
713
  });
666
- if (schemaVersion(db) === SQLITE_SCHEMA_VERSION) ensureCoreLedger(db, true);
714
+
715
+ // Guarded by length: runMigrations treats an empty migrations array's "target version" as
716
+ // 0 (see its own sorted.at(-1) ?? 0), which would misreport a database the legacy chain
717
+ // already advanced past 0 as a downgrade. FUTURE_MIGRATIONS is empty only when there is
718
+ // nothing beyond LEGACY_MIGRATION_CHAIN_TARGET_VERSION to apply -- exactly the case where
719
+ // skipping the call is correct, not merely convenient.
720
+ if (FUTURE_MIGRATIONS.length > 0) {
721
+ const beforeFuture = schemaVersion(db);
722
+ runMigrations(dbMigrationRunner(db), [...FUTURE_MIGRATIONS]);
723
+ const afterFuture = schemaVersion(db);
724
+ for (const migration of FUTURE_MIGRATIONS) {
725
+ if (migration.version > beforeFuture && migration.version <= afterFuture) applied.push(migration.name);
726
+ }
727
+ }
728
+
729
+ if (schemaVersion(db) !== SQLITE_SCHEMA_VERSION) throw new Error(`no explicit migration path from database schema ${from}`);
730
+ ensureCoreLedger(db, true);
667
731
  return { from, to: schemaVersion(db), applied };
668
732
  }
669
733