run-dmcp 0.1.0 → 0.3.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.
- package/README.md +101 -11
- package/dist/bin/run-dmcp.d.ts +2 -0
- package/dist/bin/run-dmcp.js +55 -0
- package/dist/db/connection.d.ts +32 -0
- package/dist/db/connection.js +38 -16
- package/dist/db/schema.d.ts +29 -1
- package/dist/db/schema.js +594 -10
- package/dist/http/server.js +25 -4
- package/dist/index.d.ts +69 -2
- package/dist/index.js +262 -92
- package/dist/mcp-server.d.ts +49 -0
- package/dist/mcp-server.js +127 -0
- package/dist/reader/turnReader.d.ts +185 -0
- package/dist/reader/turnReader.js +288 -0
- package/dist/register/batch.js +5 -79
- package/dist/register/mcp-resources.d.ts +9 -0
- package/dist/register/mcp-resources.js +16 -62
- package/dist/register/render.d.ts +17 -0
- package/dist/register/render.js +50 -0
- package/dist/register/resolve.d.ts +14 -0
- package/dist/register/resolve.js +102 -0
- package/dist/register/resources.js +13 -6
- package/dist/register/timeline.d.ts +2 -0
- package/dist/register/timeline.js +311 -0
- package/dist/rpg/index.d.ts +29 -0
- package/dist/rpg/index.js +55 -0
- package/dist/rpg/register/abilities.d.ts +2 -0
- package/dist/rpg/register/abilities.js +165 -0
- package/dist/rpg/register/batch.d.ts +2 -0
- package/dist/rpg/register/batch.js +92 -0
- package/dist/rpg/register/combat.d.ts +2 -0
- package/dist/rpg/register/combat.js +207 -0
- package/dist/rpg/register/mcp-prompts.d.ts +2 -0
- package/dist/rpg/register/mcp-prompts.js +684 -0
- package/dist/rpg/register/mcp-resources.d.ts +2 -0
- package/dist/rpg/register/mcp-resources.js +61 -0
- package/dist/rpg/register/quests.d.ts +2 -0
- package/dist/rpg/register/quests.js +118 -0
- package/dist/rpg/register/status.d.ts +2 -0
- package/dist/rpg/register/status.js +130 -0
- package/dist/rpg/register/tables.d.ts +2 -0
- package/dist/rpg/register/tables.js +146 -0
- package/dist/rpg/tools/ability.d.ts +48 -0
- package/dist/rpg/tools/ability.js +238 -0
- package/dist/rpg/tools/combat.d.ts +13 -0
- package/dist/rpg/tools/combat.js +195 -0
- package/dist/rpg/tools/dice.d.ts +23 -0
- package/dist/rpg/tools/dice.js +111 -0
- package/dist/rpg/tools/quest.d.ts +34 -0
- package/dist/rpg/tools/quest.js +164 -0
- package/dist/rpg/tools/status.d.ts +36 -0
- package/dist/rpg/tools/status.js +218 -0
- package/dist/rpg/tools/tables.d.ts +33 -0
- package/dist/rpg/tools/tables.js +209 -0
- package/dist/schemas/index.d.ts +12 -12
- package/dist/timeline/adjudication.d.ts +150 -0
- package/dist/timeline/adjudication.js +174 -0
- package/dist/timeline/changes.d.ts +108 -0
- package/dist/timeline/changes.js +169 -0
- package/dist/timeline/checkpoint.d.ts +69 -0
- package/dist/timeline/checkpoint.js +131 -0
- package/dist/timeline/clock.d.ts +89 -0
- package/dist/timeline/clock.js +173 -0
- package/dist/timeline/constrained.d.ts +220 -0
- package/dist/timeline/constrained.js +671 -0
- package/dist/timeline/export.d.ts +181 -0
- package/dist/timeline/export.js +339 -0
- package/dist/timeline/irreversible.d.ts +87 -0
- package/dist/timeline/irreversible.js +108 -0
- package/dist/timeline/kinds.d.ts +14 -0
- package/dist/timeline/kinds.js +22 -0
- package/dist/timeline/narration.d.ts +175 -0
- package/dist/timeline/narration.js +259 -0
- package/dist/timeline/projection.d.ts +97 -0
- package/dist/timeline/projection.js +330 -0
- package/dist/timeline/provenance.d.ts +66 -0
- package/dist/timeline/provenance.js +45 -0
- package/dist/timeline/registry.d.ts +95 -0
- package/dist/timeline/registry.js +124 -0
- package/dist/timeline/render.d.ts +121 -0
- package/dist/timeline/render.js +187 -0
- package/dist/timeline/replay.d.ts +86 -0
- package/dist/timeline/replay.js +126 -0
- package/dist/timeline/resolve.d.ts +262 -0
- package/dist/timeline/resolve.js +226 -0
- package/dist/timeline/schema.d.ts +13 -0
- package/dist/timeline/schema.js +264 -0
- package/dist/timeline/t.d.ts +80 -0
- package/dist/timeline/t.js +37 -0
- package/dist/tools/audio.js +13 -9
- package/dist/tools/constraint.d.ts +44 -80
- package/dist/tools/constraint.js +115 -124
- package/dist/tools/game.js +33 -1
- package/dist/tools/images.js +17 -10
- package/dist/tools/relationship.d.ts +83 -2
- package/dist/tools/relationship.js +139 -62
- package/dist/tools/resource.d.ts +33 -8
- package/dist/tools/resource.js +106 -153
- package/dist/tools/time.js +18 -3
- package/dist/types/index.d.ts +20 -2
- package/dist/utils/media-path.d.ts +52 -0
- package/dist/utils/media-path.js +106 -0
- package/dist/utils/output-schemas.d.ts +594 -3
- package/dist/utils/output-schemas.js +4 -1
- package/dist/utils/webui.d.ts +32 -0
- package/dist/utils/webui.js +54 -1
- package/package.json +25 -5
- package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
- package/dist/__tests__/engineVocabulary.test.js +0 -147
- package/dist/db/__tests__/connection.test.d.ts +0 -1
- package/dist/db/__tests__/connection.test.js +0 -72
- package/dist/db/__tests__/testDb.d.ts +0 -33
- package/dist/db/__tests__/testDb.js +0 -41
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -13
- package/dist/tools/__tests__/audio.test.d.ts +0 -1
- package/dist/tools/__tests__/audio.test.js +0 -59
- package/dist/tools/__tests__/conserved.test.d.ts +0 -1
- package/dist/tools/__tests__/conserved.test.js +0 -488
- package/dist/tools/__tests__/constraint.test.d.ts +0 -1
- package/dist/tools/__tests__/constraint.test.js +0 -212
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
- package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
- package/dist/tools/__tests__/images.test.d.ts +0 -1
- package/dist/tools/__tests__/images.test.js +0 -59
- package/dist/tools/__tests__/relationship.test.d.ts +0 -1
- package/dist/tools/__tests__/relationship.test.js +0 -132
- package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
- package/dist/tools/__tests__/resource-constraints.test.js +0 -131
- package/dist/tools/__tests__/resource.test.d.ts +0 -1
- package/dist/tools/__tests__/resource.test.js +0 -190
- package/dist/tools/__tests__/time.test.d.ts +0 -1
- package/dist/tools/__tests__/time.test.js +0 -404
- package/dist/tools/__tests__/timers.test.d.ts +0 -1
- package/dist/tools/__tests__/timers.test.js +0 -426
- package/dist/tools/__tests__/world.test.d.ts +0 -1
- package/dist/tools/__tests__/world.test.js +0 -70
- package/dist/utils/__tests__/json.test.d.ts +0 -1
- package/dist/utils/__tests__/json.test.js +0 -55
- package/dist/utils/__tests__/validation.test.d.ts +0 -1
- package/dist/utils/__tests__/validation.test.js +0 -90
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { T } from "./t.js";
|
|
2
|
+
import type { EntityKind } from "./kinds.js";
|
|
3
|
+
/**
|
|
4
|
+
* Timeline export/import (GitHub issue #8, design §6): "a client must be
|
|
5
|
+
* able to freeze the entire timeline -- every entity, every fact interval,
|
|
6
|
+
* every event -- into a file it owns. Not a live query. Not a session
|
|
7
|
+
* handle. A file." This module is the library half of that; an MCP tool
|
|
8
|
+
* wrapper (if one is ever added) belongs elsewhere, over these two
|
|
9
|
+
* functions, per §6's "library functions first, MCP tools second."
|
|
10
|
+
*
|
|
11
|
+
* WHAT IS IN THE ARTIFACT: `entities`, `facts` (every column, including
|
|
12
|
+
* `irreversible`), `events`, and the game's `timeline_clock` row (its
|
|
13
|
+
* declared axis and `current_t`). Nothing else.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IS DELIBERATELY NOT IN IT -- and this is a requirement, not an
|
|
16
|
+
* omission:
|
|
17
|
+
*
|
|
18
|
+
* No media references of any kind. A `file_path` in a frozen artifact
|
|
19
|
+
* cannot satisfy the re-import-and-replay exit criterion: the path names a
|
|
20
|
+
* file on the exporting machine, the importing machine has no such file,
|
|
21
|
+
* and `replay(t)` would then differ between the two even though the export
|
|
22
|
+
* claimed they were identical. `file_path` lives only in `stored_images`
|
|
23
|
+
* and `stored_audio` (see `src/db/schema.ts`); neither table appears in
|
|
24
|
+
* `PROJECTED_TABLES` (`./projection.ts`), so no fact -- and therefore
|
|
25
|
+
* nothing this module reads -- can ever carry one. That is verified, not
|
|
26
|
+
* assumed: `src/timeline/__tests__/export.test.ts` creates a stored-image
|
|
27
|
+
* and stored-audio row for the game under test and asserts the exported
|
|
28
|
+
* artifact contains no `file_path` anywhere and that those rows
|
|
29
|
+
* contributed nothing (not even an extra entity).
|
|
30
|
+
*
|
|
31
|
+
* DECISION(#18): the frozen artifact carries no per-principal projection.
|
|
32
|
+
*
|
|
33
|
+
* No visibility filtering either, and the same "requirement, not omission"
|
|
34
|
+
* applies (issue #18): the artifact is the omniscient timeline. Whether a
|
|
35
|
+
* per-principal export should exist is not a small question deferred for
|
|
36
|
+
* tidiness -- it decides whether §6's "one file a deterministic consumer
|
|
37
|
+
* depends on" becomes N files plus a rule for choosing between them. That
|
|
38
|
+
* is a decision for the caller that first needs it to own, and it cannot be
|
|
39
|
+
* made well against no caller, so it is not made here.
|
|
40
|
+
*
|
|
41
|
+
* No live tables either. The live projected tables (`games`, `characters`,
|
|
42
|
+
* `resources`, ...) are a projection of the timeline, not a second source
|
|
43
|
+
* of truth (design §5.4's decided destination) -- a file carrying both
|
|
44
|
+
* would contain two answers to the same question. After `importTimeline`
|
|
45
|
+
* runs, the target database has a timeline and no live rows for the
|
|
46
|
+
* imported game, and that is correct: `replay(t)` reads only the timeline,
|
|
47
|
+
* which is exactly what the exit criterion measures. A caller that wants
|
|
48
|
+
* live rows back would need a separate, as-yet-unbuilt step that replays
|
|
49
|
+
* the timeline forward through the projection triggers -- not this
|
|
50
|
+
* module's job, and not attempted here.
|
|
51
|
+
*
|
|
52
|
+
* TYPES: `facts.irreversible` is stored in SQLite as the INTEGER 0/1 this
|
|
53
|
+
* codebase's triggers write (`projection.ts`). This module converts it to
|
|
54
|
+
* a real `boolean` on the way out (`Boolean(row.irreversible)`) and back to
|
|
55
|
+
* 0/1 on the way in, consistently in both directions -- the artifact itself
|
|
56
|
+
* never carries the raw integer.
|
|
57
|
+
*
|
|
58
|
+
* DETERMINISM: every array below is sorted by a total order that can never
|
|
59
|
+
* tie, because every one of them ends in the row's own primary key, which
|
|
60
|
+
* SQLite guarantees is unique: entities by `(created_at_t, id)`, facts by
|
|
61
|
+
* `(valid_from_t, entity_id, key, id)`, events by `(at_t, id)`. Exporting
|
|
62
|
+
* the same world twice therefore produces byte-identical
|
|
63
|
+
* `JSON.stringify` output -- see the determinism test in
|
|
64
|
+
* `export.test.ts`.
|
|
65
|
+
*/
|
|
66
|
+
export declare const TIMELINE_FORMAT_VERSION = 1;
|
|
67
|
+
export interface TimelineExportEntity {
|
|
68
|
+
id: string;
|
|
69
|
+
gameId: string;
|
|
70
|
+
kind: EntityKind;
|
|
71
|
+
name: string | null;
|
|
72
|
+
createdAtT: T;
|
|
73
|
+
destroyedAtT: T | null;
|
|
74
|
+
}
|
|
75
|
+
export interface TimelineExportFact {
|
|
76
|
+
id: string;
|
|
77
|
+
entityId: string;
|
|
78
|
+
key: string;
|
|
79
|
+
value: string;
|
|
80
|
+
validFromT: T;
|
|
81
|
+
validToT: T | null;
|
|
82
|
+
irreversible: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface TimelineExportEvent {
|
|
85
|
+
id: string;
|
|
86
|
+
gameId: string;
|
|
87
|
+
atT: T;
|
|
88
|
+
kind: string;
|
|
89
|
+
description: string | null;
|
|
90
|
+
causes: string | null;
|
|
91
|
+
}
|
|
92
|
+
export interface TimelineExportClock {
|
|
93
|
+
currentT: T;
|
|
94
|
+
axisKind: "sequence" | "elapsed" | "counter";
|
|
95
|
+
axisUnit: string;
|
|
96
|
+
}
|
|
97
|
+
/** The frozen artifact itself -- design §6's "a file it owns." */
|
|
98
|
+
export interface TimelineExport {
|
|
99
|
+
formatVersion: number;
|
|
100
|
+
gameId: string;
|
|
101
|
+
clock: TimelineExportClock | null;
|
|
102
|
+
entities: TimelineExportEntity[];
|
|
103
|
+
facts: TimelineExportFact[];
|
|
104
|
+
events: TimelineExportEvent[];
|
|
105
|
+
}
|
|
106
|
+
/** What `importTimeline` restored, for a caller that wants to report it. */
|
|
107
|
+
export interface TimelineImportResult {
|
|
108
|
+
gameId: string;
|
|
109
|
+
entities: number;
|
|
110
|
+
facts: number;
|
|
111
|
+
events: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Freezes one game's entire timeline into a plain, serializable object.
|
|
115
|
+
* Reads only -- see the module doc comment above for what is deliberately
|
|
116
|
+
* excluded and why.
|
|
117
|
+
*
|
|
118
|
+
* An unknown `gameId` (nothing has ever been declared or written for it)
|
|
119
|
+
* exports an empty-but-valid artifact -- `clock: null`, three empty arrays
|
|
120
|
+
* -- rather than throwing. This falls out of the queries below with no
|
|
121
|
+
* special-casing: each one simply returns zero rows for a `gameId` nothing
|
|
122
|
+
* matches.
|
|
123
|
+
*
|
|
124
|
+
* The four reads are wrapped in one transaction even though nothing here
|
|
125
|
+
* writes. A frozen artifact whose halves came from different moments is not
|
|
126
|
+
* frozen: without this, a write landing between the entity query and the
|
|
127
|
+
* fact query would export an entity with facts that postdate it, or facts
|
|
128
|
+
* whose entity is missing -- and `replay(t)` over the re-imported result
|
|
129
|
+
* would then disagree with the original at exactly the `t` nobody thought to
|
|
130
|
+
* check. SQLite holds one consistent read view for the life of a
|
|
131
|
+
* transaction, which is the cheapest way to make the artifact a snapshot of
|
|
132
|
+
* a single instant rather than of four consecutive ones.
|
|
133
|
+
*/
|
|
134
|
+
export declare function exportTimeline(gameId: string): TimelineExport;
|
|
135
|
+
/**
|
|
136
|
+
* Restores a frozen artifact into this database, verbatim. Everything runs
|
|
137
|
+
* inside one `withTransaction()` -- a partial import is worse than none
|
|
138
|
+
* (root CLAUDE.md's first inherited gotcha: `withTransaction()` was dead
|
|
139
|
+
* code until it was wired; this is exactly the kind of multi-table write it
|
|
140
|
+
* exists for).
|
|
141
|
+
*
|
|
142
|
+
* Nothing here is re-derived or re-numbered: ids, every `t` value, and the
|
|
143
|
+
* `irreversible` flag are carried through byte-for-byte. An import that
|
|
144
|
+
* renumbered `t` would be design §14's silent-attachment failure with the
|
|
145
|
+
* engine itself as the culprit.
|
|
146
|
+
*
|
|
147
|
+
* The `timeline_clock` row is restored by a direct INSERT, never through
|
|
148
|
+
* `declareTimeAxis` (`./clock.ts`) -- import is restoring recorded history,
|
|
149
|
+
* not declaring an axis. `declareTimeAxis` enforces "`t` never runs
|
|
150
|
+
* backwards" and refuses to change an already-declared axis, both of which
|
|
151
|
+
* are the right rules for a *live* game choosing its axis going forward,
|
|
152
|
+
* and the wrong rules for replacing an empty clock row with one that
|
|
153
|
+
* already has a history behind it -- `declareTimeAxis` has no path that
|
|
154
|
+
* starts a `counter` or `elapsed` axis already sitting at a nonzero
|
|
155
|
+
* `current_t` with a history of facts/events that predate it, which is
|
|
156
|
+
* exactly what a restored artifact is. `declared_at` (bookkeeping about
|
|
157
|
+
* when the axis was chosen, not part of the frozen timeline data per the
|
|
158
|
+
* artifact shape above) is stamped fresh at import time; it was never
|
|
159
|
+
* exported and never round-trips.
|
|
160
|
+
*
|
|
161
|
+
* `entities` are inserted before `facts` because `facts.entity_id` is a
|
|
162
|
+
* real foreign key and this database runs with `PRAGMA foreign_keys = ON`
|
|
163
|
+
* (`../db/connection.ts`) -- inserting out of order would fail loudly
|
|
164
|
+
* rather than silently, but there is no reason to invite the failure.
|
|
165
|
+
*/
|
|
166
|
+
export declare function importTimeline(artifact: TimelineExport): TimelineImportResult;
|
|
167
|
+
/**
|
|
168
|
+
* Thin file wrapper -- the whole point of design §6 is that the artifact is
|
|
169
|
+
* a FILE, not a live query or a session handle. Nothing clever: plain
|
|
170
|
+
* `JSON.stringify(artifact, null, 2)` via `node:fs`.
|
|
171
|
+
*/
|
|
172
|
+
export declare function exportTimelineToFile(params: {
|
|
173
|
+
gameId: string;
|
|
174
|
+
filePath: string;
|
|
175
|
+
}): TimelineExport;
|
|
176
|
+
/**
|
|
177
|
+
* Thin file wrapper for the other direction. Reads and parses are each
|
|
178
|
+
* wrapped so a caller gets a message naming the path, rather than a bare
|
|
179
|
+
* `ENOENT` or `SyntaxError` with no context about which import failed.
|
|
180
|
+
*/
|
|
181
|
+
export declare function importTimelineFromFile(filePath: string): TimelineImportResult;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { getDatabase, withTransaction } from "../db/connection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Timeline export/import (GitHub issue #8, design §6): "a client must be
|
|
5
|
+
* able to freeze the entire timeline -- every entity, every fact interval,
|
|
6
|
+
* every event -- into a file it owns. Not a live query. Not a session
|
|
7
|
+
* handle. A file." This module is the library half of that; an MCP tool
|
|
8
|
+
* wrapper (if one is ever added) belongs elsewhere, over these two
|
|
9
|
+
* functions, per §6's "library functions first, MCP tools second."
|
|
10
|
+
*
|
|
11
|
+
* WHAT IS IN THE ARTIFACT: `entities`, `facts` (every column, including
|
|
12
|
+
* `irreversible`), `events`, and the game's `timeline_clock` row (its
|
|
13
|
+
* declared axis and `current_t`). Nothing else.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IS DELIBERATELY NOT IN IT -- and this is a requirement, not an
|
|
16
|
+
* omission:
|
|
17
|
+
*
|
|
18
|
+
* No media references of any kind. A `file_path` in a frozen artifact
|
|
19
|
+
* cannot satisfy the re-import-and-replay exit criterion: the path names a
|
|
20
|
+
* file on the exporting machine, the importing machine has no such file,
|
|
21
|
+
* and `replay(t)` would then differ between the two even though the export
|
|
22
|
+
* claimed they were identical. `file_path` lives only in `stored_images`
|
|
23
|
+
* and `stored_audio` (see `src/db/schema.ts`); neither table appears in
|
|
24
|
+
* `PROJECTED_TABLES` (`./projection.ts`), so no fact -- and therefore
|
|
25
|
+
* nothing this module reads -- can ever carry one. That is verified, not
|
|
26
|
+
* assumed: `src/timeline/__tests__/export.test.ts` creates a stored-image
|
|
27
|
+
* and stored-audio row for the game under test and asserts the exported
|
|
28
|
+
* artifact contains no `file_path` anywhere and that those rows
|
|
29
|
+
* contributed nothing (not even an extra entity).
|
|
30
|
+
*
|
|
31
|
+
* DECISION(#18): the frozen artifact carries no per-principal projection.
|
|
32
|
+
*
|
|
33
|
+
* No visibility filtering either, and the same "requirement, not omission"
|
|
34
|
+
* applies (issue #18): the artifact is the omniscient timeline. Whether a
|
|
35
|
+
* per-principal export should exist is not a small question deferred for
|
|
36
|
+
* tidiness -- it decides whether §6's "one file a deterministic consumer
|
|
37
|
+
* depends on" becomes N files plus a rule for choosing between them. That
|
|
38
|
+
* is a decision for the caller that first needs it to own, and it cannot be
|
|
39
|
+
* made well against no caller, so it is not made here.
|
|
40
|
+
*
|
|
41
|
+
* No live tables either. The live projected tables (`games`, `characters`,
|
|
42
|
+
* `resources`, ...) are a projection of the timeline, not a second source
|
|
43
|
+
* of truth (design §5.4's decided destination) -- a file carrying both
|
|
44
|
+
* would contain two answers to the same question. After `importTimeline`
|
|
45
|
+
* runs, the target database has a timeline and no live rows for the
|
|
46
|
+
* imported game, and that is correct: `replay(t)` reads only the timeline,
|
|
47
|
+
* which is exactly what the exit criterion measures. A caller that wants
|
|
48
|
+
* live rows back would need a separate, as-yet-unbuilt step that replays
|
|
49
|
+
* the timeline forward through the projection triggers -- not this
|
|
50
|
+
* module's job, and not attempted here.
|
|
51
|
+
*
|
|
52
|
+
* TYPES: `facts.irreversible` is stored in SQLite as the INTEGER 0/1 this
|
|
53
|
+
* codebase's triggers write (`projection.ts`). This module converts it to
|
|
54
|
+
* a real `boolean` on the way out (`Boolean(row.irreversible)`) and back to
|
|
55
|
+
* 0/1 on the way in, consistently in both directions -- the artifact itself
|
|
56
|
+
* never carries the raw integer.
|
|
57
|
+
*
|
|
58
|
+
* DETERMINISM: every array below is sorted by a total order that can never
|
|
59
|
+
* tie, because every one of them ends in the row's own primary key, which
|
|
60
|
+
* SQLite guarantees is unique: entities by `(created_at_t, id)`, facts by
|
|
61
|
+
* `(valid_from_t, entity_id, key, id)`, events by `(at_t, id)`. Exporting
|
|
62
|
+
* the same world twice therefore produces byte-identical
|
|
63
|
+
* `JSON.stringify` output -- see the determinism test in
|
|
64
|
+
* `export.test.ts`.
|
|
65
|
+
*/
|
|
66
|
+
export const TIMELINE_FORMAT_VERSION = 1;
|
|
67
|
+
/**
|
|
68
|
+
* Freezes one game's entire timeline into a plain, serializable object.
|
|
69
|
+
* Reads only -- see the module doc comment above for what is deliberately
|
|
70
|
+
* excluded and why.
|
|
71
|
+
*
|
|
72
|
+
* An unknown `gameId` (nothing has ever been declared or written for it)
|
|
73
|
+
* exports an empty-but-valid artifact -- `clock: null`, three empty arrays
|
|
74
|
+
* -- rather than throwing. This falls out of the queries below with no
|
|
75
|
+
* special-casing: each one simply returns zero rows for a `gameId` nothing
|
|
76
|
+
* matches.
|
|
77
|
+
*
|
|
78
|
+
* The four reads are wrapped in one transaction even though nothing here
|
|
79
|
+
* writes. A frozen artifact whose halves came from different moments is not
|
|
80
|
+
* frozen: without this, a write landing between the entity query and the
|
|
81
|
+
* fact query would export an entity with facts that postdate it, or facts
|
|
82
|
+
* whose entity is missing -- and `replay(t)` over the re-imported result
|
|
83
|
+
* would then disagree with the original at exactly the `t` nobody thought to
|
|
84
|
+
* check. SQLite holds one consistent read view for the life of a
|
|
85
|
+
* transaction, which is the cheapest way to make the artifact a snapshot of
|
|
86
|
+
* a single instant rather than of four consecutive ones.
|
|
87
|
+
*/
|
|
88
|
+
export function exportTimeline(gameId) {
|
|
89
|
+
return withTransaction(() => readTimeline(gameId));
|
|
90
|
+
}
|
|
91
|
+
function readTimeline(gameId) {
|
|
92
|
+
const db = getDatabase();
|
|
93
|
+
const clockRow = db
|
|
94
|
+
.prepare(`SELECT current_t, axis_kind, axis_unit FROM timeline_clock WHERE game_id = ?`)
|
|
95
|
+
.get(gameId);
|
|
96
|
+
const entityRows = db
|
|
97
|
+
.prepare(`SELECT id, game_id, kind, name, created_at_t, destroyed_at_t
|
|
98
|
+
FROM entities
|
|
99
|
+
WHERE game_id = ?
|
|
100
|
+
ORDER BY created_at_t, id`)
|
|
101
|
+
.all(gameId);
|
|
102
|
+
// Facts have no game_id column of their own (design §5.1's schema) --
|
|
103
|
+
// scoped by joining back to entities, exactly the way replay.ts scopes
|
|
104
|
+
// "what was true of them" to "who was alive."
|
|
105
|
+
const factRows = db
|
|
106
|
+
.prepare(`SELECT f.id, f.entity_id, f.key, f.value, f.valid_from_t, f.valid_to_t, f.irreversible
|
|
107
|
+
FROM facts f
|
|
108
|
+
JOIN entities e ON e.id = f.entity_id
|
|
109
|
+
WHERE e.game_id = ?
|
|
110
|
+
ORDER BY f.valid_from_t, f.entity_id, f.key, f.id`)
|
|
111
|
+
.all(gameId);
|
|
112
|
+
const eventRows = db
|
|
113
|
+
.prepare(`SELECT id, game_id, at_t, kind, description, causes
|
|
114
|
+
FROM events
|
|
115
|
+
WHERE game_id = ?
|
|
116
|
+
ORDER BY at_t, id`)
|
|
117
|
+
.all(gameId);
|
|
118
|
+
return {
|
|
119
|
+
formatVersion: TIMELINE_FORMAT_VERSION,
|
|
120
|
+
gameId,
|
|
121
|
+
clock: clockRow
|
|
122
|
+
? { currentT: clockRow.current_t, axisKind: clockRow.axis_kind, axisUnit: clockRow.axis_unit }
|
|
123
|
+
: null,
|
|
124
|
+
entities: entityRows.map((row) => ({
|
|
125
|
+
id: row.id,
|
|
126
|
+
gameId: row.game_id,
|
|
127
|
+
kind: row.kind,
|
|
128
|
+
name: row.name,
|
|
129
|
+
createdAtT: row.created_at_t,
|
|
130
|
+
destroyedAtT: row.destroyed_at_t,
|
|
131
|
+
})),
|
|
132
|
+
facts: factRows.map((row) => ({
|
|
133
|
+
id: row.id,
|
|
134
|
+
entityId: row.entity_id,
|
|
135
|
+
key: row.key,
|
|
136
|
+
value: row.value,
|
|
137
|
+
validFromT: row.valid_from_t,
|
|
138
|
+
validToT: row.valid_to_t,
|
|
139
|
+
irreversible: Boolean(row.irreversible),
|
|
140
|
+
})),
|
|
141
|
+
events: eventRows.map((row) => ({
|
|
142
|
+
id: row.id,
|
|
143
|
+
gameId: row.game_id,
|
|
144
|
+
atT: row.at_t,
|
|
145
|
+
kind: row.kind,
|
|
146
|
+
description: row.description,
|
|
147
|
+
causes: row.causes,
|
|
148
|
+
})),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Throws with a clear, specific message unless `artifact` has the shape
|
|
153
|
+
* `TimelineExport` requires. Nothing here coerces -- a wrong type is a
|
|
154
|
+
* refusal, never a silent cast, per the issue's own instruction not to
|
|
155
|
+
* "silently coerce anything."
|
|
156
|
+
*/
|
|
157
|
+
function assertValidArtifactShape(artifact) {
|
|
158
|
+
if (typeof artifact !== "object" || artifact === null) {
|
|
159
|
+
throw new Error("timeline import: artifact must be an object, got " + typeof artifact);
|
|
160
|
+
}
|
|
161
|
+
const a = artifact;
|
|
162
|
+
if (typeof a.gameId !== "string" || a.gameId.length === 0) {
|
|
163
|
+
throw new Error("timeline import: artifact is missing a non-empty string gameId");
|
|
164
|
+
}
|
|
165
|
+
if (typeof a.formatVersion !== "number") {
|
|
166
|
+
throw new Error("timeline import: artifact is missing a numeric formatVersion");
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(a.entities)) {
|
|
169
|
+
throw new Error(`timeline import: artifact.entities must be an array for game '${a.gameId}'`);
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(a.facts)) {
|
|
172
|
+
throw new Error(`timeline import: artifact.facts must be an array for game '${a.gameId}'`);
|
|
173
|
+
}
|
|
174
|
+
if (!Array.isArray(a.events)) {
|
|
175
|
+
throw new Error(`timeline import: artifact.events must be an array for game '${a.gameId}'`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Refuses, naming both ids, if any row in the artifact belongs to a game
|
|
180
|
+
* other than `artifact.gameId`.
|
|
181
|
+
*
|
|
182
|
+
* This is what makes `assertTargetIsEmpty` below mean what it says. That
|
|
183
|
+
* check interrogates `artifact.gameId` and nothing else, so on its own it
|
|
184
|
+
* only guarantees "import into an empty game" for artifacts whose rows all
|
|
185
|
+
* belong to that game. A hand-edited or hand-assembled one naming a
|
|
186
|
+
* different game in its rows would pass the emptiness check for the innocent
|
|
187
|
+
* id and then land entities and events on top of a populated timeline that
|
|
188
|
+
* was never examined -- attaching one world's history to another, silently,
|
|
189
|
+
* which is design §14's failure moved off the time axis and onto the
|
|
190
|
+
* identity axis. `exportTimeline` can never produce such an artifact; a text
|
|
191
|
+
* file a human can open trivially can, which is the whole point of §6.
|
|
192
|
+
*
|
|
193
|
+
* `facts` are not checked here because they carry no `gameId` of their own
|
|
194
|
+
* (design §5.1) -- their game is whichever entity they point at, and
|
|
195
|
+
* `facts.entity_id` is a real foreign key, so a fact can only ever reach a
|
|
196
|
+
* game through an entity this function has already vouched for.
|
|
197
|
+
*/
|
|
198
|
+
function assertRowsBelongToGame(artifact) {
|
|
199
|
+
for (const entity of artifact.entities) {
|
|
200
|
+
if (entity.gameId !== artifact.gameId) {
|
|
201
|
+
throw new Error(`timeline import: artifact declares game '${artifact.gameId}' but entity '${entity.id}' belongs to ` +
|
|
202
|
+
`game '${entity.gameId}'. Refusing to import an artifact whose rows do not all belong to the game ` +
|
|
203
|
+
`it names -- importing it would attach one world's history to another.`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const event of artifact.events) {
|
|
207
|
+
if (event.gameId !== artifact.gameId) {
|
|
208
|
+
throw new Error(`timeline import: artifact declares game '${artifact.gameId}' but event '${event.id}' belongs to ` +
|
|
209
|
+
`game '${event.gameId}'. Refusing to import an artifact whose rows do not all belong to the game ` +
|
|
210
|
+
`it names -- importing it would attach one world's history to another.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Refuses, naming what was found, if `gameId` already has any recorded
|
|
216
|
+
* history in this database. Checked as three independent conditions
|
|
217
|
+
* (`timeline_clock`, `entities`, `events`) because any one of them alone is
|
|
218
|
+
* evidence this game's timeline is not empty, and merging two timelines
|
|
219
|
+
* would interleave two worlds' `t` with no way to tell them apart
|
|
220
|
+
* afterward -- the whole reason import refuses rather than merges.
|
|
221
|
+
*/
|
|
222
|
+
function assertTargetIsEmpty(gameId) {
|
|
223
|
+
const db = getDatabase();
|
|
224
|
+
const existingClock = db.prepare(`SELECT 1 FROM timeline_clock WHERE game_id = ?`).get(gameId);
|
|
225
|
+
if (existingClock) {
|
|
226
|
+
throw new Error(`timeline import: refusing to import into game '${gameId}' -- a timeline_clock row already exists for ` +
|
|
227
|
+
`it. Importing would merge two timelines' t with no way to tell them apart afterward; import into an ` +
|
|
228
|
+
`empty game, never an existing one.`);
|
|
229
|
+
}
|
|
230
|
+
const existingEntity = db.prepare(`SELECT 1 FROM entities WHERE game_id = ? LIMIT 1`).get(gameId);
|
|
231
|
+
if (existingEntity) {
|
|
232
|
+
throw new Error(`timeline import: refusing to import into game '${gameId}' -- entities already exist for it. Importing ` +
|
|
233
|
+
`would merge two timelines' t with no way to tell them apart afterward; import into an empty game, ` +
|
|
234
|
+
`never an existing one.`);
|
|
235
|
+
}
|
|
236
|
+
const existingEvent = db.prepare(`SELECT 1 FROM events WHERE game_id = ? LIMIT 1`).get(gameId);
|
|
237
|
+
if (existingEvent) {
|
|
238
|
+
throw new Error(`timeline import: refusing to import into game '${gameId}' -- events already exist for it. Importing ` +
|
|
239
|
+
`would merge two timelines' t with no way to tell them apart afterward; import into an empty game, ` +
|
|
240
|
+
`never an existing one.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Restores a frozen artifact into this database, verbatim. Everything runs
|
|
245
|
+
* inside one `withTransaction()` -- a partial import is worse than none
|
|
246
|
+
* (root CLAUDE.md's first inherited gotcha: `withTransaction()` was dead
|
|
247
|
+
* code until it was wired; this is exactly the kind of multi-table write it
|
|
248
|
+
* exists for).
|
|
249
|
+
*
|
|
250
|
+
* Nothing here is re-derived or re-numbered: ids, every `t` value, and the
|
|
251
|
+
* `irreversible` flag are carried through byte-for-byte. An import that
|
|
252
|
+
* renumbered `t` would be design §14's silent-attachment failure with the
|
|
253
|
+
* engine itself as the culprit.
|
|
254
|
+
*
|
|
255
|
+
* The `timeline_clock` row is restored by a direct INSERT, never through
|
|
256
|
+
* `declareTimeAxis` (`./clock.ts`) -- import is restoring recorded history,
|
|
257
|
+
* not declaring an axis. `declareTimeAxis` enforces "`t` never runs
|
|
258
|
+
* backwards" and refuses to change an already-declared axis, both of which
|
|
259
|
+
* are the right rules for a *live* game choosing its axis going forward,
|
|
260
|
+
* and the wrong rules for replacing an empty clock row with one that
|
|
261
|
+
* already has a history behind it -- `declareTimeAxis` has no path that
|
|
262
|
+
* starts a `counter` or `elapsed` axis already sitting at a nonzero
|
|
263
|
+
* `current_t` with a history of facts/events that predate it, which is
|
|
264
|
+
* exactly what a restored artifact is. `declared_at` (bookkeeping about
|
|
265
|
+
* when the axis was chosen, not part of the frozen timeline data per the
|
|
266
|
+
* artifact shape above) is stamped fresh at import time; it was never
|
|
267
|
+
* exported and never round-trips.
|
|
268
|
+
*
|
|
269
|
+
* `entities` are inserted before `facts` because `facts.entity_id` is a
|
|
270
|
+
* real foreign key and this database runs with `PRAGMA foreign_keys = ON`
|
|
271
|
+
* (`../db/connection.ts`) -- inserting out of order would fail loudly
|
|
272
|
+
* rather than silently, but there is no reason to invite the failure.
|
|
273
|
+
*/
|
|
274
|
+
export function importTimeline(artifact) {
|
|
275
|
+
assertValidArtifactShape(artifact);
|
|
276
|
+
if (artifact.formatVersion !== TIMELINE_FORMAT_VERSION) {
|
|
277
|
+
throw new Error(`timeline import: artifact has formatVersion ${artifact.formatVersion}, but this build of run-dmcp reads ` +
|
|
278
|
+
`and writes formatVersion ${TIMELINE_FORMAT_VERSION}. Refusing to import rather than guess at a ` +
|
|
279
|
+
`translation between versions.`);
|
|
280
|
+
}
|
|
281
|
+
assertRowsBelongToGame(artifact);
|
|
282
|
+
return withTransaction(() => {
|
|
283
|
+
const db = getDatabase();
|
|
284
|
+
assertTargetIsEmpty(artifact.gameId);
|
|
285
|
+
if (artifact.clock) {
|
|
286
|
+
db.prepare(`INSERT INTO timeline_clock (game_id, current_t, axis_kind, axis_unit, declared_at) VALUES (?, ?, ?, ?, ?)`).run(artifact.gameId, artifact.clock.currentT, artifact.clock.axisKind, artifact.clock.axisUnit, new Date().toISOString());
|
|
287
|
+
}
|
|
288
|
+
const insertEntity = db.prepare(`INSERT INTO entities (id, game_id, kind, name, created_at_t, destroyed_at_t) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
289
|
+
for (const entity of artifact.entities) {
|
|
290
|
+
insertEntity.run(entity.id, entity.gameId, entity.kind, entity.name, entity.createdAtT, entity.destroyedAtT);
|
|
291
|
+
}
|
|
292
|
+
const insertFact = db.prepare(`INSERT INTO facts (id, entity_id, key, value, valid_from_t, valid_to_t, irreversible) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
293
|
+
for (const fact of artifact.facts) {
|
|
294
|
+
insertFact.run(fact.id, fact.entityId, fact.key, fact.value, fact.validFromT, fact.validToT, fact.irreversible ? 1 : 0);
|
|
295
|
+
}
|
|
296
|
+
const insertEvent = db.prepare(`INSERT INTO events (id, game_id, at_t, kind, description, causes) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
297
|
+
for (const event of artifact.events) {
|
|
298
|
+
insertEvent.run(event.id, event.gameId, event.atT, event.kind, event.description, event.causes);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
gameId: artifact.gameId,
|
|
302
|
+
entities: artifact.entities.length,
|
|
303
|
+
facts: artifact.facts.length,
|
|
304
|
+
events: artifact.events.length,
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Thin file wrapper -- the whole point of design §6 is that the artifact is
|
|
310
|
+
* a FILE, not a live query or a session handle. Nothing clever: plain
|
|
311
|
+
* `JSON.stringify(artifact, null, 2)` via `node:fs`.
|
|
312
|
+
*/
|
|
313
|
+
export function exportTimelineToFile(params) {
|
|
314
|
+
const artifact = exportTimeline(params.gameId);
|
|
315
|
+
writeFileSync(params.filePath, JSON.stringify(artifact, null, 2));
|
|
316
|
+
return artifact;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Thin file wrapper for the other direction. Reads and parses are each
|
|
320
|
+
* wrapped so a caller gets a message naming the path, rather than a bare
|
|
321
|
+
* `ENOENT` or `SyntaxError` with no context about which import failed.
|
|
322
|
+
*/
|
|
323
|
+
export function importTimelineFromFile(filePath) {
|
|
324
|
+
let raw;
|
|
325
|
+
try {
|
|
326
|
+
raw = readFileSync(filePath, "utf8");
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
throw new Error(`timeline import: could not read export file at '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
330
|
+
}
|
|
331
|
+
let artifact;
|
|
332
|
+
try {
|
|
333
|
+
artifact = JSON.parse(raw);
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
throw new Error(`timeline import: export file at '${filePath}' is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
337
|
+
}
|
|
338
|
+
return importTimeline(artifact);
|
|
339
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type FactProvenance } from "./provenance.js";
|
|
2
|
+
/**
|
|
3
|
+
* DECISION(#21): contradiction is whole-value comparison under one key.
|
|
4
|
+
*
|
|
5
|
+
* `irreversible` -- the temporal member of the constraint family alongside
|
|
6
|
+
* `bounded`, `monotonic`, and conserved sets (design §5.3). Declared per
|
|
7
|
+
* fact, not per entity or per value: `facts.irreversible` is a column on the
|
|
8
|
+
* fact row itself, so any property a consumer wants irreversible has to live
|
|
9
|
+
* under its own fact `key` -- you cannot flag half a blob (see the schema
|
|
10
|
+
* comment on `facts` in schema.ts, and the "fact granularity" tests in
|
|
11
|
+
* irreversible.test.ts, which are what make that claim true rather than
|
|
12
|
+
* merely asserted).
|
|
13
|
+
*
|
|
14
|
+
* The actual enforcement -- refusing a contradicting assertion, and locking
|
|
15
|
+
* the `irreversible` flag itself to a one-way 0 -> 1 latch -- lives entirely
|
|
16
|
+
* in the two triggers on `facts` in schema.ts (`timeline_facts_irreversible`,
|
|
17
|
+
* `timeline_facts_immutable`). Every one of the 48 write sites in this
|
|
18
|
+
* codebase reaches `facts` through the generated projection triggers in
|
|
19
|
+
* projection.ts, so that is the only choke point a JS-level check could
|
|
20
|
+
* never be bypassed at. This module is a thin, typed API onto that trigger
|
|
21
|
+
* layer -- it never re-implements the rule, and it never returns a verdict
|
|
22
|
+
* (hard rule 2 / design §5.5): no `isClean`, no severity, just the fact
|
|
23
|
+
* that is or isn't there.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* §5.2c's one hop of causality, specialized to an irreversible fact. The
|
|
27
|
+
* shape itself now lives in `provenance.ts` as `FactProvenance` -- every
|
|
28
|
+
* carrier of "a fact plus the one event that opened it" extends the same
|
|
29
|
+
* interface rather than each re-declaring it and re-implementing the
|
|
30
|
+
* lookup, so this type and `narration.ts`'s `ConstraintFact` can never
|
|
31
|
+
* quietly disagree about what one hop means. `IrreversibleFact` adds
|
|
32
|
+
* nothing beyond the shared shape; the name is kept because every caller in
|
|
33
|
+
* this file, and `ConstraintViolationError.contradictedFact` (registry.ts),
|
|
34
|
+
* already reads it that way. A type alias rather than an empty `extends`
|
|
35
|
+
* interface -- ESLint's `no-empty-object-type` correctly rejects an
|
|
36
|
+
* interface that declares no members of its own, since it is indistinguishable
|
|
37
|
+
* from its supertype at every call site; a plain alias says the same thing
|
|
38
|
+
* without inventing a nominal type this codebase would then have to keep
|
|
39
|
+
* in sync with its supertype by hand.
|
|
40
|
+
*/
|
|
41
|
+
export type IrreversibleFact = FactProvenance;
|
|
42
|
+
/**
|
|
43
|
+
* Marks the currently-open fact for `(entityId, key)` irreversible.
|
|
44
|
+
*
|
|
45
|
+
* Throws, naming what's missing, rather than silently doing nothing, in
|
|
46
|
+
* exactly two cases:
|
|
47
|
+
* - the entity does not exist;
|
|
48
|
+
* - there is no open fact for that key. Irreversibility can only attach to
|
|
49
|
+
* a fact that currently holds -- "say what is, never what is absent"
|
|
50
|
+
* (hard rule 3) applies here too: there is no honest way to declare the
|
|
51
|
+
* irreversibility of an absence.
|
|
52
|
+
*
|
|
53
|
+
* The flip itself is a single `UPDATE facts SET irreversible = 1 WHERE id =
|
|
54
|
+
* ?`, which the amended `timeline_facts_immutable` latch (schema.ts)
|
|
55
|
+
* permits as the one legal transition. That single statement is also what
|
|
56
|
+
* makes this naturally idempotent: calling it again while the same fact is
|
|
57
|
+
* still open re-sends the identical 1 -> 1 update, which the latch treats as
|
|
58
|
+
* a no-op (not a change at all, so it never reaches the "reject anything but
|
|
59
|
+
* 0 -> 1" branch), and this function returns the same record either way.
|
|
60
|
+
*/
|
|
61
|
+
export declare function declareIrreversible(params: {
|
|
62
|
+
entityId: string;
|
|
63
|
+
key: string;
|
|
64
|
+
}): IrreversibleFact;
|
|
65
|
+
/**
|
|
66
|
+
* The irreversible fact currently governing `(entityId, key)`, or `null` if
|
|
67
|
+
* none has been declared. Looks at every row with `irreversible = 1` for
|
|
68
|
+
* that key -- open or closed -- the same set `timeline_facts_irreversible`
|
|
69
|
+
* (schema.ts) tests, so this can never report "none" for a key the guard
|
|
70
|
+
* would in fact refuse to contradict. When more than one such row exists
|
|
71
|
+
* (only possible after the key was closed and reopened at the SAME value,
|
|
72
|
+
* per rule 1 -- a reopen at a different value is refused outright), the
|
|
73
|
+
* most recently opened one is returned, ordered deterministically
|
|
74
|
+
* (`valid_from_t` then `id`) rather than left to query-plan order.
|
|
75
|
+
*/
|
|
76
|
+
export declare function irreversibleFactFor(entityId: string, key: string): IrreversibleFact | null;
|
|
77
|
+
/**
|
|
78
|
+
* Every irreversible fact belonging to `gameId`, optionally narrowed to one
|
|
79
|
+
* entity. A listing, not a verdict (hard rule 2) -- there is no summary
|
|
80
|
+
* count, no "is this game safe" flag, just the rows. Scoped to `gameId` via
|
|
81
|
+
* a join on `entities` rather than trusting a caller-supplied entity list,
|
|
82
|
+
* so one game's declarations can never leak into another's listing.
|
|
83
|
+
*/
|
|
84
|
+
export declare function listIrreversibleFacts(params: {
|
|
85
|
+
gameId: string;
|
|
86
|
+
entityId?: string;
|
|
87
|
+
}): IrreversibleFact[];
|