run-dmcp 0.1.0 → 0.2.0

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 (135) hide show
  1. package/README.md +76 -10
  2. package/dist/bin/run-dmcp.d.ts +2 -0
  3. package/dist/bin/run-dmcp.js +55 -0
  4. package/dist/db/connection.d.ts +32 -0
  5. package/dist/db/connection.js +38 -16
  6. package/dist/db/schema.d.ts +29 -1
  7. package/dist/db/schema.js +439 -7
  8. package/dist/http/server.js +3 -3
  9. package/dist/index.d.ts +36 -2
  10. package/dist/index.js +184 -92
  11. package/dist/mcp-server.d.ts +49 -0
  12. package/dist/mcp-server.js +127 -0
  13. package/dist/reader/turnReader.d.ts +185 -0
  14. package/dist/reader/turnReader.js +288 -0
  15. package/dist/register/batch.js +5 -79
  16. package/dist/register/mcp-resources.d.ts +9 -0
  17. package/dist/register/mcp-resources.js +16 -62
  18. package/dist/register/render.d.ts +17 -0
  19. package/dist/register/render.js +50 -0
  20. package/dist/register/resolve.d.ts +14 -0
  21. package/dist/register/resolve.js +102 -0
  22. package/dist/register/resources.js +11 -4
  23. package/dist/register/timeline.d.ts +2 -0
  24. package/dist/register/timeline.js +311 -0
  25. package/dist/rpg/index.d.ts +29 -0
  26. package/dist/rpg/index.js +55 -0
  27. package/dist/rpg/register/abilities.d.ts +2 -0
  28. package/dist/rpg/register/abilities.js +165 -0
  29. package/dist/rpg/register/batch.d.ts +2 -0
  30. package/dist/rpg/register/batch.js +92 -0
  31. package/dist/rpg/register/combat.d.ts +2 -0
  32. package/dist/rpg/register/combat.js +207 -0
  33. package/dist/rpg/register/mcp-prompts.d.ts +2 -0
  34. package/dist/rpg/register/mcp-prompts.js +684 -0
  35. package/dist/rpg/register/mcp-resources.d.ts +2 -0
  36. package/dist/rpg/register/mcp-resources.js +61 -0
  37. package/dist/rpg/register/quests.d.ts +2 -0
  38. package/dist/rpg/register/quests.js +118 -0
  39. package/dist/rpg/register/status.d.ts +2 -0
  40. package/dist/rpg/register/status.js +130 -0
  41. package/dist/rpg/register/tables.d.ts +2 -0
  42. package/dist/rpg/register/tables.js +146 -0
  43. package/dist/rpg/tools/ability.d.ts +48 -0
  44. package/dist/rpg/tools/ability.js +238 -0
  45. package/dist/rpg/tools/combat.d.ts +13 -0
  46. package/dist/rpg/tools/combat.js +195 -0
  47. package/dist/rpg/tools/dice.d.ts +23 -0
  48. package/dist/rpg/tools/dice.js +111 -0
  49. package/dist/rpg/tools/quest.d.ts +34 -0
  50. package/dist/rpg/tools/quest.js +164 -0
  51. package/dist/rpg/tools/status.d.ts +36 -0
  52. package/dist/rpg/tools/status.js +218 -0
  53. package/dist/rpg/tools/tables.d.ts +33 -0
  54. package/dist/rpg/tools/tables.js +209 -0
  55. package/dist/schemas/index.d.ts +12 -12
  56. package/dist/timeline/adjudication.d.ts +150 -0
  57. package/dist/timeline/adjudication.js +174 -0
  58. package/dist/timeline/changes.d.ts +100 -0
  59. package/dist/timeline/changes.js +161 -0
  60. package/dist/timeline/checkpoint.d.ts +69 -0
  61. package/dist/timeline/checkpoint.js +131 -0
  62. package/dist/timeline/clock.d.ts +89 -0
  63. package/dist/timeline/clock.js +173 -0
  64. package/dist/timeline/constrained.d.ts +220 -0
  65. package/dist/timeline/constrained.js +671 -0
  66. package/dist/timeline/export.d.ts +171 -0
  67. package/dist/timeline/export.js +329 -0
  68. package/dist/timeline/irreversible.d.ts +85 -0
  69. package/dist/timeline/irreversible.js +108 -0
  70. package/dist/timeline/kinds.d.ts +14 -0
  71. package/dist/timeline/kinds.js +22 -0
  72. package/dist/timeline/narration.d.ts +175 -0
  73. package/dist/timeline/narration.js +259 -0
  74. package/dist/timeline/projection.d.ts +97 -0
  75. package/dist/timeline/projection.js +330 -0
  76. package/dist/timeline/provenance.d.ts +66 -0
  77. package/dist/timeline/provenance.js +45 -0
  78. package/dist/timeline/registry.d.ts +95 -0
  79. package/dist/timeline/registry.js +124 -0
  80. package/dist/timeline/render.d.ts +121 -0
  81. package/dist/timeline/render.js +187 -0
  82. package/dist/timeline/replay.d.ts +64 -0
  83. package/dist/timeline/replay.js +104 -0
  84. package/dist/timeline/resolve.d.ts +262 -0
  85. package/dist/timeline/resolve.js +226 -0
  86. package/dist/timeline/schema.d.ts +13 -0
  87. package/dist/timeline/schema.js +262 -0
  88. package/dist/timeline/t.d.ts +80 -0
  89. package/dist/timeline/t.js +37 -0
  90. package/dist/tools/constraint.d.ts +44 -80
  91. package/dist/tools/constraint.js +115 -124
  92. package/dist/tools/relationship.d.ts +83 -2
  93. package/dist/tools/relationship.js +139 -62
  94. package/dist/tools/resource.d.ts +31 -6
  95. package/dist/tools/resource.js +106 -153
  96. package/dist/types/index.d.ts +19 -1
  97. package/dist/utils/output-schemas.d.ts +593 -2
  98. package/dist/utils/output-schemas.js +3 -0
  99. package/dist/utils/webui.d.ts +32 -0
  100. package/dist/utils/webui.js +54 -1
  101. package/package.json +20 -4
  102. package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
  103. package/dist/__tests__/engineVocabulary.test.js +0 -147
  104. package/dist/db/__tests__/connection.test.d.ts +0 -1
  105. package/dist/db/__tests__/connection.test.js +0 -72
  106. package/dist/db/__tests__/testDb.d.ts +0 -33
  107. package/dist/db/__tests__/testDb.js +0 -41
  108. package/dist/test-setup.d.ts +0 -1
  109. package/dist/test-setup.js +0 -13
  110. package/dist/tools/__tests__/audio.test.d.ts +0 -1
  111. package/dist/tools/__tests__/audio.test.js +0 -59
  112. package/dist/tools/__tests__/conserved.test.d.ts +0 -1
  113. package/dist/tools/__tests__/conserved.test.js +0 -488
  114. package/dist/tools/__tests__/constraint.test.d.ts +0 -1
  115. package/dist/tools/__tests__/constraint.test.js +0 -212
  116. package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
  117. package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
  118. package/dist/tools/__tests__/images.test.d.ts +0 -1
  119. package/dist/tools/__tests__/images.test.js +0 -59
  120. package/dist/tools/__tests__/relationship.test.d.ts +0 -1
  121. package/dist/tools/__tests__/relationship.test.js +0 -132
  122. package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
  123. package/dist/tools/__tests__/resource-constraints.test.js +0 -131
  124. package/dist/tools/__tests__/resource.test.d.ts +0 -1
  125. package/dist/tools/__tests__/resource.test.js +0 -190
  126. package/dist/tools/__tests__/time.test.d.ts +0 -1
  127. package/dist/tools/__tests__/time.test.js +0 -404
  128. package/dist/tools/__tests__/timers.test.d.ts +0 -1
  129. package/dist/tools/__tests__/timers.test.js +0 -426
  130. package/dist/tools/__tests__/world.test.d.ts +0 -1
  131. package/dist/tools/__tests__/world.test.js +0 -70
  132. package/dist/utils/__tests__/json.test.d.ts +0 -1
  133. package/dist/utils/__tests__/json.test.js +0 -55
  134. package/dist/utils/__tests__/validation.test.d.ts +0 -1
  135. package/dist/utils/__tests__/validation.test.js +0 -90
package/dist/db/schema.js CHANGED
@@ -1,5 +1,9 @@
1
- import { getDatabase } from "./connection.js";
2
- export function initializeSchema() {
1
+ import { getDatabase, withTransaction } from "./connection.js";
2
+ import { createLogger } from "../utils/logger.js";
3
+ import { initializeTimelineSchema } from "../timeline/schema.js";
4
+ import { initializeAdjudicationSchema } from "../timeline/adjudication.js";
5
+ const log = createLogger("schema");
6
+ export function initializeSchema(options) {
3
7
  const db = getDatabase();
4
8
  // ============================================================================
5
9
  // MIGRATION: Rename 'sessions' to 'games' and 'session_id' to 'game_id'
@@ -215,7 +219,9 @@ export function initializeSchema() {
215
219
  FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
216
220
  )
217
221
  `);
218
- // Resource history table (tracks all changes)
222
+ // Resource history table (tracks all changes) -- FROZEN, see the trigger
223
+ // immediately below. Kept for existing rows only; nothing writes here any
224
+ // more.
219
225
  db.exec(`
220
226
  CREATE TABLE IF NOT EXISTS resource_history (
221
227
  id TEXT PRIMARY KEY,
@@ -227,22 +233,281 @@ export function initializeSchema() {
227
233
  timestamp TEXT NOT NULL,
228
234
  FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
229
235
  )
236
+ `);
237
+ // Design §5.4 option (C) / issue #9, Phase 3: `resource_history` stops
238
+ // being a mechanism. Interval-versioned `facts` (via the timeline's one
239
+ // choke point, writeConstrainedValue/transferConstrainedValue in
240
+ // src/timeline/constrained.ts) are now the ONLY record of what a
241
+ // resource's value used to be -- see valueHistory() there, and
242
+ // getResourceHistory() in src/tools/resource.ts, which reads through it.
243
+ // This trigger makes the second write path this project spent its whole
244
+ // history accumulating (see engineVocabulary.test.ts's epigraph on
245
+ // resource_history/relationship_history) UNCONSTRUCTABLE rather than
246
+ // merely undocumented: any INSERT here -- from old code nobody rewrote,
247
+ // from a copy-pasted query, from anything -- aborts loudly instead of
248
+ // silently reintroducing a second history.
249
+ //
250
+ // The table is frozen, not dropped. This project has no down-migrations
251
+ // (root CLAUDE.md) and no framework beyond "idempotent DDL runs on every
252
+ // startup" -- a migration that DROPped this table would destroy rows a
253
+ // user's existing database may still hold, permanently, the first time
254
+ // they upgraded, which is a strictly worse outcome than a table that
255
+ // simply stops growing. Nothing reads it (getResourceHistory() no longer
256
+ // does) and nothing writes it (this trigger); the rows already on disk
257
+ // are inert history, not a mechanism.
258
+ //
259
+ // DROP TRIGGER IF EXISTS then CREATE, not CREATE TRIGGER IF NOT EXISTS --
260
+ // the same reasoning src/timeline/schema.ts's append-only guards give for
261
+ // their own triggers: IF NOT EXISTS would freeze whatever guard first
262
+ // shipped for a given on-disk database forever, so a later fix to this
263
+ // trigger's logic or message would silently never reach a database that
264
+ // already had an older version installed. Dropping and recreating on
265
+ // every startup keeps the guard a database actually has in sync with the
266
+ // guard this build believes it deployed.
267
+ db.exec(`
268
+ DROP TRIGGER IF EXISTS resource_history_frozen;
269
+ CREATE TRIGGER resource_history_frozen BEFORE INSERT ON resource_history
270
+ BEGIN
271
+ SELECT RAISE(ABORT, 'resource_history is frozen -- interval-versioned facts are now the only record of what a resource value used to be (design section 5.4 option C); write through writeConstrainedValue in src/timeline/constrained.ts instead');
272
+ END;
230
273
  `);
231
274
  // Resource constraints table -- optional, server-enforced invariants on
232
275
  // resource values (see src/tools/constraint.ts). Opt-in: a resource with
233
276
  // no row here (directly or via resource_constraint_members) behaves
234
277
  // exactly as before this feature existed.
235
- db.exec(`
278
+ //
279
+ // `RESOURCE_CONSTRAINTS_DDL` is a shared constant, not a literal inlined
280
+ // here, because issue #13's `resolve_only` (design §5.3's fourth
281
+ // row-based member) needs this exact CHECK-widened text executed in TWO
282
+ // places: here, for a fresh database, and again inside the CHECK-rebuild
283
+ // migration further below, for a database that still carries the OLD
284
+ // three-member CHECK (SQLite cannot ALTER a CHECK constraint -- widening
285
+ // one needs a full table rebuild; see that block's comment for the
286
+ // recipe). Sharing one JS string is what makes "a fresh database and a
287
+ // migrated database converge on byte-identical `sqlite_master.sql` for
288
+ // this table" true BY CONSTRUCTION rather than by two hand-written
289
+ // literals happening to agree today -- resolveOnly.test.ts asserts
290
+ // exactly that equality, and it would be a permanent, silent trap to
291
+ // maintain as two copies.
292
+ const RESOURCE_CONSTRAINTS_DDL = `
236
293
  CREATE TABLE IF NOT EXISTS resource_constraints (
237
294
  id TEXT PRIMARY KEY,
238
295
  game_id TEXT NOT NULL,
239
- kind TEXT NOT NULL CHECK (kind IN ('bounded', 'monotonic', 'conserved')),
296
+ kind TEXT NOT NULL CHECK (kind IN ('bounded', 'monotonic', 'conserved', 'resolve_only')),
240
297
  direction TEXT CHECK (direction IN ('increasing', 'decreasing')),
241
298
  total REAL,
242
299
  created_at TEXT NOT NULL,
243
300
  FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
244
301
  )
245
- `);
302
+ `;
303
+ db.exec(RESOURCE_CONSTRAINTS_DDL);
304
+ // Add fact_key column to resource_constraints (migration, Phase 3 / issue
305
+ // #9 step 1, design §5.4 option (C)). A constraint governs a numeric
306
+ // *fact key* on an entity, not an entire row -- once the generic
307
+ // (entityId, factKey) choke point this column exists to prepare for
308
+ // lands, an entity can carry more than one numeric fact, and a constraint
309
+ // must say which one it governs or it would silently apply to all of
310
+ // them. 'value' is the default because every constraint that exists
311
+ // today (declared through declareBoundedConstraint/declareMonotonicConstraint/
312
+ // declareConservedConstraint in src/tools/constraint.ts) governs
313
+ // `resources.value` -- the sole numeric fact key any of them has ever
314
+ // constrained -- so defaulting means an existing database needs no data
315
+ // migration to keep reading its own constraints correctly. This column belongs on
316
+ // resource_constraints (the constraint), NOT on resource_constraint_members
317
+ // below: a 'conserved' set's members all share one key by construction
318
+ // (they are summed against a single total), and putting it on the member
319
+ // row would permit a set whose members are constrained on different
320
+ // keys -- a generality nothing has asked for.
321
+ //
322
+ // Shared for the same reason RESOURCE_CONSTRAINTS_DDL is above: the
323
+ // CHECK-rebuild migration below re-adds this exact column to its
324
+ // replacement table, and reusing the identical statement text (rather
325
+ // than a second hand-written ALTER) is what makes the fresh and migrated
326
+ // paths' final stored SQL provably identical instead of coincidentally
327
+ // similar.
328
+ const RESOURCE_CONSTRAINTS_ADD_FACT_KEY_DDL = `ALTER TABLE resource_constraints ADD COLUMN fact_key TEXT NOT NULL DEFAULT 'value'`;
329
+ try {
330
+ db.exec(RESOURCE_CONSTRAINTS_ADD_FACT_KEY_DDL);
331
+ }
332
+ catch {
333
+ // Column already exists
334
+ }
335
+ // Issue #13 / design §5.3, §5.4 option (C): widen resource_constraints'
336
+ // `kind` CHECK to admit 'resolve_only', the fourth row-based constraint
337
+ // family member. SQLite cannot ALTER a CHECK constraint -- unlike the
338
+ // fact_key column just above, there is no `ALTER TABLE ... ADD` for
339
+ // "loosen this CHECK" -- so a database that already has resource_constraints
340
+ // rows under the OLD three-member CHECK needs SQLite's own documented
341
+ // recipe for "other kinds of table schema changes": build a replacement
342
+ // table with the new CHECK, copy every row across, drop the old table, and
343
+ // put the replacement in its place. This is the riskiest part of issue #13
344
+ // precisely because it is a real ALTER on a table other code already
345
+ // depends on (resource_constraint_members' own FOREIGN KEY points at it),
346
+ // so every step below is deliberate -- see resolveOnly.test.ts for the
347
+ // "an existing database survives the rebuild with every row and every FK
348
+ // intact" test this comment describes.
349
+ //
350
+ // DETECTION IS IDEMPOTENT AND LITERAL, not a guess: read this codebase's
351
+ // OWN generated DDL back out of `sqlite_master` and check whether it
352
+ // already contains the token 'resolve_only'. Hard rule 4 (never
353
+ // pattern-match meaning) forbids deriving state by matching words or
354
+ // phrases against natural language -- narrative text a model wrote, or a
355
+ // player typed. This is neither: it is a literal substring check against
356
+ // SQL text THIS FUNCTION generated a few lines above, for a token THIS
357
+ // FUNCTION defined (the CHECK's own enum member). Rule 4 explicitly
358
+ // permits exactly that ("a literal check for a token we defined in output
359
+ // we generated is fine; understanding English is not") -- flagged here in
360
+ // as many words because a future reader skimming a string-literal
361
+ // `.includes()` check might otherwise mistake it for the thing rule 4
362
+ // bans. A truly fresh database never takes the branch below:
363
+ // RESOURCE_CONSTRAINTS_DDL's CREATE TABLE IF NOT EXISTS a few lines up
364
+ // already carries the widened CHECK, so by the time this runs, this
365
+ // database's own resource_constraints already contains 'resolve_only'.
366
+ const resourceConstraintsDdl = db
367
+ .prepare(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'resource_constraints'`)
368
+ .get();
369
+ if (resourceConstraintsDdl && !resourceConstraintsDdl.sql.includes("resolve_only")) {
370
+ // DROP THE resolve_only TRIGGER FIRST, UNCONDITIONALLY, IF IT EXISTS --
371
+ // made ORDER-INDEPENDENT rather than relying on "this trigger has never
372
+ // been created yet the one time this branch runs." That reliance was
373
+ // real on today's code (this block runs before initializeTimelineSchema()
374
+ // ever installs timeline_facts_resolve_only for the very first time, so
375
+ // on the one startup that performs a rebuild, the trigger genuinely does
376
+ // not exist yet) -- but it is an ACCIDENT OF ORDERING, not a structural
377
+ // guarantee, and root CLAUDE.md's "test against an existing database,
378
+ // not just a fresh one" warning exists precisely because accidents of
379
+ // ordering are how this kind of bug survives review. The trigger's own
380
+ // `WHEN` clause names `resource_constraints`, and SQLite revalidates
381
+ // every trigger body during schema surgery on a table it references
382
+ // (measured, not assumed -- see resolveOnly.test.ts, whose migration
383
+ // fixture hit exactly this: `ALTER TABLE ... RENAME TO resource_constraints`
384
+ // failed with "no such table: resource_constraints" while this trigger
385
+ // still existed and the table was transiently absent mid-rebuild) --
386
+ // so a database where the trigger was somehow installed before a rebuild
387
+ // runs (a future change that reorders trigger installation, or widens
388
+ // this CHECK a second time some other way) would break at startup with
389
+ // the schema half-rebuilt. Dropping it here costs nothing: the
390
+ // DROP-then-CREATE discipline every trigger in this codebase already
391
+ // follows (see the trigger's own creation site, far below, and the
392
+ // reasoning timeline_facts_irreversible gives for the same pattern)
393
+ // means it is unconditionally reinstalled before this function returns
394
+ // regardless -- this is that discipline paying for itself a second time.
395
+ db.exec(`DROP TRIGGER IF EXISTS timeline_facts_resolve_only`);
396
+ // PRAGMA foreign_keys is a documented no-op when toggled inside a
397
+ // pending transaction, so it brackets withTransaction() below rather
398
+ // than living inside it -- set OFF here (before BEGIN), restored ON
399
+ // after COMMIT. connection.ts turns it ON for every connection at
400
+ // open (`getDatabase()`); this block is the one place in the codebase
401
+ // that deliberately, temporarily, undoes that, and it is responsible
402
+ // for putting it back.
403
+ db.pragma("foreign_keys = OFF");
404
+ withTransaction(() => {
405
+ // EMPIRICALLY MEASURED, NOT ASSUMED (see resolveOnly.test.ts, whose
406
+ // FK-check assertion caught a real bug in an earlier version of this
407
+ // block): `ALTER TABLE ... RENAME TO` does not just rename the table
408
+ // being renamed -- it also rewrites the stored FOREIGN KEY clause of
409
+ // every OTHER table that references it by name (here,
410
+ // resource_constraint_members' `FOREIGN KEY (constraint_id)
411
+ // REFERENCES resource_constraints(id)`), REGARDLESS of the
412
+ // `foreign_keys` pragma above. Renaming the OLD table out of the way
413
+ // (the obvious first move) would therefore silently rewrite
414
+ // resource_constraint_members to reference the OLD table's new,
415
+ // temporary name -- and once that old table is dropped a few steps
416
+ // later, that reference is permanently dangling, with no further
417
+ // rename ever pointed at it to fix it back. `DROP TABLE`, unlike
418
+ // `RENAME TO`, does NOT rewrite other tables' references (there is
419
+ // nothing to rewrite them TO), so this recipe routes around the
420
+ // problem entirely: the old table is copied out of and then DROPped
421
+ // (never renamed), and the FINAL name is produced by a genuine
422
+ // `CREATE TABLE` (also never a rename target) -- so
423
+ // resource_constraint_members' FK text is never touched by anything
424
+ // in this block, and is simply valid again the moment step 4 below
425
+ // recreates a table under the name it always pointed at.
426
+ //
427
+ // 1. Copy the OLD table's rows into a staging table under a temporary
428
+ // name -- widened CHECK, and fact_key included from the start
429
+ // (unlike the final table a few steps down, this one is never
430
+ // compared to RESOURCE_CONSTRAINTS_DDL's stored text, so there is
431
+ // no reason to reproduce the fresh path's "add fact_key later via
432
+ // ALTER" shape here).
433
+ db.exec(`
434
+ CREATE TABLE resource_constraints_staging (
435
+ id TEXT PRIMARY KEY,
436
+ game_id TEXT NOT NULL,
437
+ kind TEXT NOT NULL CHECK (kind IN ('bounded', 'monotonic', 'conserved', 'resolve_only')),
438
+ direction TEXT CHECK (direction IN ('increasing', 'decreasing')),
439
+ total REAL,
440
+ created_at TEXT NOT NULL,
441
+ fact_key TEXT NOT NULL DEFAULT 'value'
442
+ )
443
+ `);
444
+ db.exec(`
445
+ INSERT INTO resource_constraints_staging (id, game_id, kind, direction, total, created_at, fact_key)
446
+ SELECT id, game_id, kind, direction, total, created_at, fact_key FROM resource_constraints
447
+ `);
448
+ // 2. Drop the OLD table outright -- not renamed, per the note above.
449
+ db.exec(`DROP TABLE resource_constraints`);
450
+ // 3. Recreate under the FINAL name using the exact same DDL text the
451
+ // fresh-database path executed above -- RESOURCE_CONSTRAINTS_DDL
452
+ // itself, not a second hand-copied literal. This is a genuine
453
+ // `CREATE TABLE resource_constraints (...)`, never a rename
454
+ // target, so its stored SQL stays unquoted -- matching the
455
+ // fresh-database path byte for byte (verified empirically, see
456
+ // resolveOnly.test.ts's "byte-identical" assertion) rather than
457
+ // picking up RENAME TO's habit of re-quoting the identifier.
458
+ db.exec(RESOURCE_CONSTRAINTS_DDL);
459
+ // 4. Copy every row across from the staging table with an EXPLICIT
460
+ // column list -- never SELECT * -- naming every column except
461
+ // fact_key, which the just-recreated table does not have yet. It
462
+ // is added next, the same way the fresh path adds it.
463
+ db.exec(`
464
+ INSERT INTO resource_constraints (id, game_id, kind, direction, total, created_at)
465
+ SELECT id, game_id, kind, direction, total, created_at FROM resource_constraints_staging
466
+ `);
467
+ // 5. Add fact_key via the IDENTICAL statement text the fresh path
468
+ // uses (RESOURCE_CONSTRAINTS_ADD_FACT_KEY_DDL, not a hand-written
469
+ // equivalent). This is what makes the two paths' stored SQL
470
+ // provably byte-identical after this point: SQLite's ALTER TABLE
471
+ // ADD COLUMN rewrites a table's stored CREATE TABLE text by
472
+ // inserting the new column definition right after the last
473
+ // existing column and before any table-level constraint (verified
474
+ // empirically, not assumed -- see resolveOnly.test.ts) -- running
475
+ // the same statement against two structurally identical tables
476
+ // produces the same rewritten text, with no need to hand-guess
477
+ // that formatting.
478
+ db.exec(RESOURCE_CONSTRAINTS_ADD_FACT_KEY_DDL);
479
+ // 6. The ALTER above just backfilled every just-copied row's fact_key
480
+ // with its DEFAULT 'value' -- correct for every row that can exist
481
+ // today (every declare*Constraint() function other than
482
+ // declareResolveOnlyConstraint hardcodes factKey 'value', and
483
+ // declareResolveOnlyConstraint cannot have written a row before
484
+ // this migration exists to run), but this UPDATE restores each
485
+ // row's ACTUAL prior fact_key from the staging table by id rather
486
+ // than leaning on that staying true forever. An UPDATE never
487
+ // touches sqlite_master, so it cannot disturb the byte-identical
488
+ // DDL text step 5 just produced.
489
+ db.exec(`
490
+ UPDATE resource_constraints
491
+ SET fact_key = (
492
+ SELECT fact_key FROM resource_constraints_staging
493
+ WHERE resource_constraints_staging.id = resource_constraints.id
494
+ )
495
+ `);
496
+ // 7. Drop the staging table. Every id was copied verbatim in step 4,
497
+ // so resource_constraint_members' FOREIGN KEY (constraint_id)
498
+ // REFERENCES resource_constraints(id) -- never touched by any of
499
+ // the steps above -- is satisfied by the replacement table
500
+ // throughout, verified for real by the PRAGMA foreign_key_check
501
+ // below, not merely assumed here.
502
+ db.exec(`DROP TABLE resource_constraints_staging`);
503
+ });
504
+ const fkViolations = db.pragma("foreign_key_check");
505
+ if (fkViolations.length > 0) {
506
+ throw new Error(`resource_constraints CHECK migration (resolve_only, issue #13) left dangling foreign keys: ` +
507
+ JSON.stringify(fkViolations));
508
+ }
509
+ db.pragma("foreign_keys = ON");
510
+ }
246
511
  // Members of a resource constraint. 'bounded' and 'monotonic' constraints
247
512
  // have exactly one member (the resource they govern); 'conserved'
248
513
  // constraints have two or more (the set that must sum to a fixed total).
@@ -255,6 +520,12 @@ export function initializeSchema() {
255
520
  FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
256
521
  )
257
522
  `);
523
+ // The adjudication window (design §5.3, §5.2a; issue #13): created here,
524
+ // BEFORE `timeline_facts_resolve_only` (the trigger that reads it, added
525
+ // near the end of this function once `facts` exists) -- see
526
+ // src/timeline/adjudication.ts for the table's own doc comment, including
527
+ // why its startup pass unconditionally clears every row.
528
+ initializeAdjudicationSchema(db);
258
529
  // Game time table (one per game)
259
530
  db.exec(`
260
531
  CREATE TABLE IF NOT EXISTS game_time (
@@ -366,7 +637,8 @@ export function initializeSchema() {
366
637
  FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
367
638
  )
368
639
  `);
369
- // Relationship history table
640
+ // Relationship history table -- FROZEN, see the trigger immediately below.
641
+ // Kept for existing rows only; nothing writes here any more.
370
642
  db.exec(`
371
643
  CREATE TABLE IF NOT EXISTS relationship_history (
372
644
  id TEXT PRIMARY KEY,
@@ -377,6 +649,25 @@ export function initializeSchema() {
377
649
  timestamp TEXT NOT NULL,
378
650
  FOREIGN KEY (relationship_id) REFERENCES relationships(id) ON DELETE CASCADE
379
651
  )
652
+ `);
653
+ // Design §5.4 option (C) / issue #9, Phase 3 step 3: `relationship_history`
654
+ // stops being a mechanism, same as `resource_history` immediately above --
655
+ // see that trigger's comment for the shared reasoning (why frozen and not
656
+ // dropped, why DROP-then-CREATE on every startup). The one thing specific
657
+ // here: `relationships` was already a `PROJECTED_TABLES` row (projection.ts)
658
+ // before this trigger existed, so its `value` column was already being
659
+ // dual-written into interval-versioned `facts` -- this freeze is what makes
660
+ // that the ONLY record, by routing every relationship value write through
661
+ // writeConstrainedValue() (src/timeline/constrained.ts) and cutting off the
662
+ // second path this table represented. See valueHistory() there, and
663
+ // getRelationshipHistory() in src/tools/relationship.ts, which reads
664
+ // through it.
665
+ db.exec(`
666
+ DROP TRIGGER IF EXISTS relationship_history_frozen;
667
+ CREATE TRIGGER relationship_history_frozen BEFORE INSERT ON relationship_history
668
+ BEGIN
669
+ SELECT RAISE(ABORT, 'relationship_history is frozen -- interval-versioned facts are now the only record of what a relationship value used to be (design section 5.4 option C); write through writeConstrainedValue in src/timeline/constrained.ts instead');
670
+ END;
380
671
  `);
381
672
  // Factions table
382
673
  db.exec(`
@@ -722,4 +1013,145 @@ export function initializeSchema() {
722
1013
  CREATE INDEX IF NOT EXISTS idx_stored_audio_primary ON stored_audio(entity_id, entity_type, is_primary);
723
1014
  CREATE INDEX IF NOT EXISTS idx_stored_audio_voice_ref ON stored_audio(game_id, is_voice_reference);
724
1015
  `);
1016
+ // Consumer-registered migrations (see `SchemaMigration` above) run after
1017
+ // every core table above exists.
1018
+ runConsumerMigrations(db, options?.migrations);
1019
+ // Timeline substrate (design §5.1), and it must be LAST -- after the
1020
+ // consumer's migrations, not merely after this function's own DDL.
1021
+ //
1022
+ // The projection triggers are generated from a live `pragma_table_info`
1023
+ // read of each projected table (see timeline/projection.ts), so whatever
1024
+ // ran most recently is what they are built against. A consumer migration
1025
+ // that adds a column to a projected table therefore gets that column
1026
+ // projected as a fact key, and reconciliation backfills it, with no code
1027
+ // change on either side. Run this before `runConsumerMigrations` instead
1028
+ // and such a column would be silently absent from the timeline until some
1029
+ // later startup happened to regenerate -- which is precisely the drift the
1030
+ // Phase 1 checkpoint exists to catch, arriving through the one door the
1031
+ // engine hands a consumer.
1032
+ initializeTimelineSchema();
1033
+ // Issue #13 / design §5.3, §5.2a: `resolve_only`'s enforcement backstop --
1034
+ // the SQL-level twin of the `resolve_only` branch in assertConstraintsAllow()
1035
+ // (src/timeline/constrained.ts). That JS check covers every write that
1036
+ // goes through writeConstrainedValue/transferConstrainedValue; THIS
1037
+ // trigger is what makes a write that skips those functions entirely --
1038
+ // a raw `UPDATE resources SET value = ...`, old code nobody rewrote, a
1039
+ // copy-pasted query -- unconstructable rather than merely unchecked. It
1040
+ // has to live HERE, not inside initializeTimelineSchema() (src/timeline/schema.ts)
1041
+ // alongside timeline_facts_irreversible (the trigger it is modeled on):
1042
+ // it reads resource_constraints/resource_constraint_members, which are
1043
+ // src/db/schema.ts's own tables, AND facts, which initializeTimelineSchema()
1044
+ // just created a line above -- and initializeTimelineSchema() is the LAST
1045
+ // thing this function calls (see the comment above it), so `facts` does
1046
+ // not exist at any point before that call returns. Creating this trigger
1047
+ // any earlier would `CREATE TRIGGER` against a table that isn't there
1048
+ // yet.
1049
+ //
1050
+ // ORDERING REQUIREMENT, STATED EXPLICITLY SO NOBODY MOVES THIS LATER: this
1051
+ // CREATE must run AFTER resource_constraints, resource_constraint_members
1052
+ // and timeline_adjudications_open all exist (true from here on, since all
1053
+ // three are created earlier in this same function) AND after the CHECK
1054
+ // -rebuild migration above has already run for this call. The rebuild
1055
+ // block enforces the second half of that itself -- it unconditionally
1056
+ // `DROP TRIGGER IF EXISTS`s this exact trigger before doing any table
1057
+ // surgery, precisely so that surgery can never run while this trigger
1058
+ // still exists to have its `WHEN` clause revalidated mid-rebuild against a
1059
+ // table that is transiently missing (see that block's own comment). Do
1060
+ // not reorder this CREATE ahead of that block on the theory that "it
1061
+ // always ran after anyway" -- that theory is exactly what the rebuild
1062
+ // block's DROP now makes irrelevant, and reordering would silently
1063
+ // reintroduce the dependency the DROP exists to remove.
1064
+ //
1065
+ // SCOPED PER (entity_id, key), unlike timeline_facts_irreversible's
1066
+ // per-(entity_id, key) VALUE comparison -- resolve_only has no value to
1067
+ // compare, only a declaration to look up, via the same JOIN
1068
+ // registry.ts's constraintsFor() runs in JS: is there a 'resolve_only'
1069
+ // constraint whose member list includes NEW.entity_id and whose fact_key
1070
+ // matches NEW.key. AND conditional on the window: the second half of the
1071
+ // WHEN clause, `NOT EXISTS (SELECT 1 FROM timeline_adjudications_open)`,
1072
+ // reads the exact same table `adjudicationOpen()` (src/timeline/adjudication.js)
1073
+ // reads in JS -- see that module's doc comment for why this is one source
1074
+ // of truth read in two places, never two independent checks that happen
1075
+ // to agree.
1076
+ //
1077
+ // BEFORE INSERT ON facts, not UPDATE: facts are append-only
1078
+ // (timeline_facts_immutable, src/timeline/schema.ts), so a new value for a
1079
+ // key always arrives as a fresh INSERT. This is deliberately an INSERT
1080
+ // guard for the same reason timeline_facts_irreversible is one: closing an
1081
+ // interval is `UPDATE facts SET valid_to_t = ...`, a different table
1082
+ // event entirely, and is NOT a write of a new value -- it never reaches
1083
+ // this trigger. That is what keeps destroying an entity, or otherwise
1084
+ // closing a resolve_only-governed fact, legal even with no adjudication
1085
+ // window open: the engine records decisions, it does not impose policy on
1086
+ // top of them (hard rule 2), and refusing to let an entity be destroyed
1087
+ // because it happens to carry a resolve_only fact would be exactly that.
1088
+ //
1089
+ // DROP-then-CREATE, never CREATE TRIGGER IF NOT EXISTS -- the same
1090
+ // reasoning timeline_facts_irreversible and every other trigger in this
1091
+ // codebase give for themselves: IF NOT EXISTS would freeze whatever guard
1092
+ // first shipped for a given on-disk database forever, so a later fix to
1093
+ // this trigger's logic or wording would silently never reach a database
1094
+ // that already had an older version installed. Dropping and recreating on
1095
+ // every startup keeps the guard a database actually has in sync with the
1096
+ // guard this build believes it deployed.
1097
+ //
1098
+ // NOTE FOR THE NEXT READER OF reconcileTimeline() (src/timeline/projection.ts):
1099
+ // reconciliation runs at the end of every startup and, per column, closes
1100
+ // a diverged fact and opens a new one -- an INSERT that would hit this
1101
+ // very trigger for a resolve_only-governed key with no window open
1102
+ // (reconciliation is not an adjudicating call). projection.ts's
1103
+ // reconcileTable() carries its own guard for exactly that, modeled on the
1104
+ // one it already had for `irreversible` -- see the comment on its
1105
+ // close-UPDATE and open-INSERT statements.
1106
+ db.exec(`
1107
+ DROP TRIGGER IF EXISTS timeline_facts_resolve_only;
1108
+ CREATE TRIGGER timeline_facts_resolve_only
1109
+ BEFORE INSERT ON facts
1110
+ WHEN EXISTS (
1111
+ SELECT 1 FROM resource_constraints rc
1112
+ JOIN resource_constraint_members rcm ON rcm.constraint_id = rc.id
1113
+ WHERE rc.kind = 'resolve_only'
1114
+ AND rcm.resource_id = NEW.entity_id
1115
+ AND rc.fact_key = NEW.key
1116
+ ) AND NOT EXISTS (SELECT 1 FROM timeline_adjudications_open)
1117
+ BEGIN
1118
+ SELECT RAISE(ABORT, 'timeline: ''' || NEW.key || ''' on this entity is resolve_only-constrained; direct writes are refused -- this value can only change through the adjudicating call that opens the resolution window');
1119
+ END;
1120
+ `);
1121
+ }
1122
+ function runConsumerMigrations(db, migrations) {
1123
+ if (!migrations || migrations.length === 0) {
1124
+ return;
1125
+ }
1126
+ validateMigrations(migrations);
1127
+ for (const migration of migrations) {
1128
+ try {
1129
+ db.transaction(() => {
1130
+ migration.up(db);
1131
+ })();
1132
+ }
1133
+ catch (err) {
1134
+ log.error("Consumer schema migration failed", {
1135
+ migration: migration.name,
1136
+ error: err instanceof Error ? err.message : String(err),
1137
+ });
1138
+ throw new Error(`Schema migration '${migration.name}' failed`, { cause: err });
1139
+ }
1140
+ }
1141
+ }
1142
+ function validateMigrations(migrations) {
1143
+ const seen = new Set();
1144
+ for (const migration of migrations) {
1145
+ const name = migration?.name;
1146
+ if (typeof name !== "string" || name.trim().length === 0) {
1147
+ throw new Error(`Invalid schema migration: 'name' must be a non-empty string, got ${JSON.stringify(name)}`);
1148
+ }
1149
+ if (seen.has(name)) {
1150
+ throw new Error(`Duplicate schema migration name: '${name}'`);
1151
+ }
1152
+ seen.add(name);
1153
+ if (typeof migration.up !== "function") {
1154
+ throw new Error(`Schema migration '${name}' has no 'up' function`);
1155
+ }
1156
+ }
725
1157
  }
@@ -9,18 +9,18 @@ import { getCharacter, listCharacters, renderCharacterSheet, } from "../tools/ch
9
9
  import { getLocation, listLocations, renderMap } from "../tools/world.js";
10
10
  import { getImage, getImageData, listEntityImages, listGameImages, } from "../tools/images.js";
11
11
  import { getInventory, getItem, listGameItems } from "../tools/inventory.js";
12
- import { listQuests, getQuest } from "../tools/quest.js";
12
+ import { listQuests, getQuest } from "../rpg/tools/quest.js";
13
13
  import { getHistory } from "../tools/narrative.js";
14
14
  import { getDisplayConfig, getGameDisplayConfig, hasGameTheme, } from "../tools/display.js";
15
15
  import { listFactions, getFaction } from "../tools/faction.js";
16
16
  import { listResources, getResource } from "../tools/resource.js";
17
17
  import { listNotes, getNote } from "../tools/notes.js";
18
18
  import { listRelationships } from "../tools/relationship.js";
19
- import { listAbilities, getAbility } from "../tools/ability.js";
19
+ import { listAbilities, getAbility } from "../rpg/tools/ability.js";
20
20
  import { listTimers, getTimer } from "../tools/timers.js";
21
21
  import { listSecrets } from "../tools/secrets.js";
22
22
  import { getTime } from "../tools/time.js";
23
- import { getActiveCombat } from "../tools/combat.js";
23
+ import { getActiveCombat } from "../rpg/tools/combat.js";
24
24
  import { gameEvents } from "../events/emitter.js";
25
25
  const __filename = fileURLToPath(import.meta.url);
26
26
  const __dirname = dirname(__filename);
package/dist/index.d.ts CHANGED
@@ -1,2 +1,36 @@
1
- #!/usr/bin/env node
2
- export {};
1
+ export { createCoreMcpServer, SERVER_NAME, SERVER_VERSION } from "./mcp-server.js";
2
+ export { getDatabase, closeDatabase, withTransaction, getDatabasePath, getDataDir, resolveDataPathFrom, } from "./db/connection.js";
3
+ export type { DataPathInputs } from "./db/connection.js";
4
+ export { initializeSchema } from "./db/schema.js";
5
+ export type { SchemaMigration } from "./db/schema.js";
6
+ export { replay } from "./timeline/replay.js";
7
+ export type { Snapshot, ReplayedEntity, ReplayedFact } from "./timeline/replay.js";
8
+ export { declareTimeAxis, setStoryTime, currentStoryTime } from "./timeline/clock.js";
9
+ export type { StoryTime } from "./timeline/clock.js";
10
+ export { changesWithin } from "./timeline/changes.js";
11
+ export type { Change, ChangeSet, EventChange, FactChange } from "./timeline/changes.js";
12
+ export { compareT, assertT } from "./timeline/t.js";
13
+ export type { T, TimeAxis } from "./timeline/t.js";
14
+ export { timelineDivergences } from "./timeline/checkpoint.js";
15
+ export type { Divergence } from "./timeline/checkpoint.js";
16
+ export { declareIrreversible, irreversibleFactFor, listIrreversibleFacts, } from "./timeline/irreversible.js";
17
+ export type { IrreversibleFact } from "./timeline/irreversible.js";
18
+ export { openingEventId } from "./timeline/provenance.js";
19
+ export type { FactProvenance } from "./timeline/provenance.js";
20
+ export { narrationConstraintAt, contradictions, NARRATION_CONSTRAINT_FORMAT_VERSION, } from "./timeline/narration.js";
21
+ export type { NarrationConstraint, ConstraintFact, Claim, Contradiction } from "./timeline/narration.js";
22
+ export { writeConstrainedValue, transferConstrainedValue, valueHistory, } from "./timeline/constrained.js";
23
+ export type { ValueTransition } from "./timeline/constrained.js";
24
+ export { ConstraintViolationError, constraintsFor, conservedConstraintFor } from "./timeline/registry.js";
25
+ export { createResolver, ResolveProtocolError } from "./timeline/resolve.js";
26
+ export type { Mechanic, Resolver, Proposal, Expectation, AdjudicationInput, Adjudication, IntendedChange, IntendedWrite, IntendedTransfer, Outcome, ResolveRefusalReason, } from "./timeline/resolve.js";
27
+ export { createStateRenderer } from "./timeline/render.js";
28
+ export type { RenderVocabulary, VocabularyEntry, StateRenderer, RenderedState, RenderedNoun, UnnamedFact, } from "./timeline/render.js";
29
+ export { createTurnReader } from "./reader/turnReader.js";
30
+ export type { TurnReader, ReaderQuestion, ReaderSource, ReaderTransport, ReadRequest, TransportAnswer, ReaderResult, AnsweredQuestion, RejectedOffer, RejectionReason, } from "./reader/turnReader.js";
31
+ export { exportTimeline, importTimeline, exportTimelineToFile, importTimelineFromFile, TIMELINE_FORMAT_VERSION, } from "./timeline/export.js";
32
+ export type { TimelineExport, TimelineExportEntity, TimelineExportFact, TimelineExportEvent, TimelineExportClock, TimelineImportResult, } from "./timeline/export.js";
33
+ export { ENTITY_KINDS } from "./timeline/kinds.js";
34
+ export type { EntityKind } from "./timeline/kinds.js";
35
+ export { DEFAULT_HTTP_PORT, httpPortFromEnv, webUiEnabled, setHttpPort, getWebUiBaseUrl, } from "./utils/webui.js";
36
+ export type * from "./types/index.js";