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,264 @@
|
|
|
1
|
+
import { getDatabase } from "../db/connection.js";
|
|
2
|
+
import { ENTITY_KINDS } from "./kinds.js";
|
|
3
|
+
import { installProjectionTriggers, reconcileTimeline } from "./projection.js";
|
|
4
|
+
/**
|
|
5
|
+
* The timeline substrate (design §5.1): `entities`, `facts`, `events`, and
|
|
6
|
+
* the per-game `timeline_clock` that tells a `sequence` axis what its next
|
|
7
|
+
* ordinal is. Strictly additive -- nothing here is read by any existing
|
|
8
|
+
* tool yet, and every statement is idempotent so calling this at every
|
|
9
|
+
* startup (the project's only migration mechanism; see root CLAUDE.md) is
|
|
10
|
+
* safe against both a fresh database and one that predates the timeline.
|
|
11
|
+
*
|
|
12
|
+
* What makes recorded `t` actually immutable is the trigger block at the
|
|
13
|
+
* bottom of this function, not application discipline -- see the comment
|
|
14
|
+
* there.
|
|
15
|
+
*/
|
|
16
|
+
export function initializeTimelineSchema() {
|
|
17
|
+
const db = getDatabase();
|
|
18
|
+
// entity_kinds -- the allowed values of entities.kind, enforced by FK
|
|
19
|
+
// rather than a CHECK constraint. SQLite cannot ALTER a CHECK, so a
|
|
20
|
+
// CHECK here would force a full table rebuild the day issue #2 (or
|
|
21
|
+
// anything later) adds a projected kind; a reference table just gets one
|
|
22
|
+
// more row via INSERT OR IGNORE, on every init, forever. This is the
|
|
23
|
+
// same shape as the inherited gotcha it exists to avoid: `stored_audio`
|
|
24
|
+
// and friends carry an unconstrained entity_type that can typo silently
|
|
25
|
+
// and read back never. `kinds.ts` is the single owner of the vocabulary
|
|
26
|
+
// seeded here.
|
|
27
|
+
db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS entity_kinds (
|
|
29
|
+
kind TEXT PRIMARY KEY
|
|
30
|
+
)
|
|
31
|
+
`);
|
|
32
|
+
const seedKind = db.prepare("INSERT OR IGNORE INTO entity_kinds (kind) VALUES (?)");
|
|
33
|
+
for (const kind of ENTITY_KINDS) {
|
|
34
|
+
seedKind.run(kind);
|
|
35
|
+
}
|
|
36
|
+
// entities -- one row per timeline-tracked thing. `name` is the name at
|
|
37
|
+
// creation and never changes; the current name (if the projection layer
|
|
38
|
+
// ever changes it) is a `name` fact instead, so nothing here can drift
|
|
39
|
+
// out from under a caller relying on it.
|
|
40
|
+
db.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
game_id TEXT NOT NULL,
|
|
44
|
+
kind TEXT NOT NULL REFERENCES entity_kinds(kind),
|
|
45
|
+
name TEXT,
|
|
46
|
+
created_at_t REAL NOT NULL,
|
|
47
|
+
destroyed_at_t REAL,
|
|
48
|
+
CHECK (destroyed_at_t IS NULL OR destroyed_at_t >= created_at_t)
|
|
49
|
+
)
|
|
50
|
+
`);
|
|
51
|
+
// facts -- interval-versioned key/value history for an entity. `value` is
|
|
52
|
+
// NOT NULL on purpose: a NULL column on the live row produces no fact at
|
|
53
|
+
// all (absence is the absence of a fact, never a fact of absence -- hard
|
|
54
|
+
// rule 3). No ON DELETE CASCADE and no FK to `games`: deleting a game
|
|
55
|
+
// deletes its live rows, but the timeline of that game survives.
|
|
56
|
+
// DECISION(#21): a property to be declared irreversible needs its own fact key.
|
|
57
|
+
//
|
|
58
|
+
// `irreversible` is a per-fact flag, not a per-entity or per-value one --
|
|
59
|
+
// so any property a future consumer wants to declare irreversible has to
|
|
60
|
+
// live under its own fact key; you cannot flag half a blob.
|
|
61
|
+
db.exec(`
|
|
62
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
entity_id TEXT NOT NULL REFERENCES entities(id),
|
|
65
|
+
key TEXT NOT NULL,
|
|
66
|
+
value TEXT NOT NULL,
|
|
67
|
+
valid_from_t REAL NOT NULL,
|
|
68
|
+
valid_to_t REAL,
|
|
69
|
+
irreversible INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
CHECK (valid_to_t IS NULL OR valid_to_t >= valid_from_t)
|
|
71
|
+
)
|
|
72
|
+
`);
|
|
73
|
+
// events -- the append-only log entry itself. `causes` carries one hop of
|
|
74
|
+
// provenance (design §5.2c) as a JSON string; nothing here is inferred
|
|
75
|
+
// from anything anyone wrote (hard rule 4) -- `kind`/`description` are
|
|
76
|
+
// tokens this codebase defines.
|
|
77
|
+
db.exec(`
|
|
78
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
game_id TEXT NOT NULL,
|
|
81
|
+
at_t REAL NOT NULL,
|
|
82
|
+
kind TEXT NOT NULL,
|
|
83
|
+
description TEXT,
|
|
84
|
+
causes TEXT
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
// timeline_clock -- one row per game, tracking the declared axis and its
|
|
88
|
+
// current position. A game that never declares an axis still needs one
|
|
89
|
+
// of these once its first entity/fact/event is written, with axis_kind
|
|
90
|
+
// 'sequence' (#6 owns declareTimeAxis/setStoryTime; this table just
|
|
91
|
+
// holds the row).
|
|
92
|
+
db.exec(`
|
|
93
|
+
CREATE TABLE IF NOT EXISTS timeline_clock (
|
|
94
|
+
game_id TEXT PRIMARY KEY,
|
|
95
|
+
current_t REAL NOT NULL,
|
|
96
|
+
axis_kind TEXT NOT NULL CHECK (axis_kind IN ('sequence', 'elapsed', 'counter')),
|
|
97
|
+
axis_unit TEXT NOT NULL,
|
|
98
|
+
declared_at TEXT NOT NULL
|
|
99
|
+
)
|
|
100
|
+
`);
|
|
101
|
+
db.exec(`
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_entities_game_kind ON entities(game_id, kind);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_entities_game_created ON entities(game_id, created_at_t);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_facts_entity_key ON facts(entity_id, key);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_facts_entity_key_valid_to ON facts(entity_id, key, valid_to_t);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_facts_valid_from ON facts(valid_from_t);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_events_game_at ON events(game_id, at_t);
|
|
108
|
+
`);
|
|
109
|
+
// Append-only guard triggers.
|
|
110
|
+
//
|
|
111
|
+
// Recorded `t` must be impossible to rewrite, not merely discouraged --
|
|
112
|
+
// that is the whole point of a substrate the checkpoint (#4) can trust.
|
|
113
|
+
// Every column comparison below uses IS NOT rather than <>, because <>
|
|
114
|
+
// with a NULL operand evaluates to NULL (neither true nor false) and the
|
|
115
|
+
// WHEN clause would silently fail to fire on exactly the rows -- open
|
|
116
|
+
// intervals -- where firing matters most.
|
|
117
|
+
//
|
|
118
|
+
// Each table gets exactly one permitted mutation: closing an open
|
|
119
|
+
// interval once (`facts.valid_to_t`, `entities.destroyed_at_t`, both
|
|
120
|
+
// NULL -> value). Every other UPDATE, and every DELETE, aborts.
|
|
121
|
+
//
|
|
122
|
+
// Every trigger below is DROP-then-CREATE, not CREATE TRIGGER IF NOT
|
|
123
|
+
// EXISTS. The column set being fixed is not the risk that decides this --
|
|
124
|
+
// it's that IF NOT EXISTS makes whatever guard shipped in the build that
|
|
125
|
+
// first created a given database permanent for that database. If this
|
|
126
|
+
// logic is ever tightened or a bug in it is fixed, that fix would
|
|
127
|
+
// silently never reach any database that already had the old trigger,
|
|
128
|
+
// which is exactly the "test against an existing database, not just a
|
|
129
|
+
// fresh one" trap this project has already been bitten by once. Dropping
|
|
130
|
+
// and recreating on every init means the guard a database has always
|
|
131
|
+
// matches the guard this build believes it deployed.
|
|
132
|
+
db.exec(`
|
|
133
|
+
DROP TRIGGER IF EXISTS timeline_entities_immutable;
|
|
134
|
+
CREATE TRIGGER timeline_entities_immutable
|
|
135
|
+
BEFORE UPDATE ON entities
|
|
136
|
+
WHEN NEW.id IS NOT OLD.id
|
|
137
|
+
OR NEW.game_id IS NOT OLD.game_id
|
|
138
|
+
OR NEW.kind IS NOT OLD.kind
|
|
139
|
+
OR NEW.name IS NOT OLD.name
|
|
140
|
+
OR NEW.created_at_t IS NOT OLD.created_at_t
|
|
141
|
+
OR (OLD.destroyed_at_t IS NOT NULL AND NEW.destroyed_at_t IS NOT OLD.destroyed_at_t)
|
|
142
|
+
BEGIN
|
|
143
|
+
SELECT RAISE(ABORT, 'timeline: entities are append-only; only destroyed_at_t may be set, once');
|
|
144
|
+
END;
|
|
145
|
+
`);
|
|
146
|
+
// `irreversible` is itself a one-way latch: 0 -> 1 exactly once, never
|
|
147
|
+
// 1 -> 0, never to any other value. The clause below aborts whenever the
|
|
148
|
+
// column changed at all (`NEW.irreversible IS NOT OLD.irreversible`)
|
|
149
|
+
// UNLESS that change is precisely OLD=0, NEW=1 -- so 0 -> 5 still aborts
|
|
150
|
+
// (NEW is not 1), 1 -> 0 still aborts (OLD is not 0), and 1 -> 1 never
|
|
151
|
+
// reaches this branch in the first place because the column didn't
|
|
152
|
+
// change. declareIrreversible() (irreversible.ts) relies on that last
|
|
153
|
+
// case to make re-declaring idempotent with a single UPDATE and no
|
|
154
|
+
// separate "already set" check.
|
|
155
|
+
db.exec(`
|
|
156
|
+
DROP TRIGGER IF EXISTS timeline_facts_immutable;
|
|
157
|
+
CREATE TRIGGER timeline_facts_immutable
|
|
158
|
+
BEFORE UPDATE ON facts
|
|
159
|
+
WHEN NEW.id IS NOT OLD.id
|
|
160
|
+
OR NEW.entity_id IS NOT OLD.entity_id
|
|
161
|
+
OR NEW.key IS NOT OLD.key
|
|
162
|
+
OR NEW.value IS NOT OLD.value
|
|
163
|
+
OR NEW.valid_from_t IS NOT OLD.valid_from_t
|
|
164
|
+
OR (NEW.irreversible IS NOT OLD.irreversible
|
|
165
|
+
AND (OLD.irreversible IS NOT 0 OR NEW.irreversible IS NOT 1))
|
|
166
|
+
OR (OLD.valid_to_t IS NOT NULL AND NEW.valid_to_t IS NOT OLD.valid_to_t)
|
|
167
|
+
BEGIN
|
|
168
|
+
SELECT RAISE(ABORT, 'timeline: facts are append-only; valid_from_t cannot be rewritten, valid_to_t may only be closed once, and irreversible may only move 0 -> 1');
|
|
169
|
+
END;
|
|
170
|
+
`);
|
|
171
|
+
db.exec(`
|
|
172
|
+
DROP TRIGGER IF EXISTS timeline_events_immutable;
|
|
173
|
+
CREATE TRIGGER timeline_events_immutable
|
|
174
|
+
BEFORE UPDATE ON events
|
|
175
|
+
BEGIN
|
|
176
|
+
SELECT RAISE(ABORT, 'timeline: an event never changes once recorded');
|
|
177
|
+
END;
|
|
178
|
+
`);
|
|
179
|
+
db.exec(`
|
|
180
|
+
DROP TRIGGER IF EXISTS timeline_entities_no_delete;
|
|
181
|
+
CREATE TRIGGER timeline_entities_no_delete
|
|
182
|
+
BEFORE DELETE ON entities
|
|
183
|
+
BEGIN
|
|
184
|
+
SELECT RAISE(ABORT, 'timeline: entities are append-only; rows are never deleted');
|
|
185
|
+
END;
|
|
186
|
+
`);
|
|
187
|
+
db.exec(`
|
|
188
|
+
DROP TRIGGER IF EXISTS timeline_facts_no_delete;
|
|
189
|
+
CREATE TRIGGER timeline_facts_no_delete
|
|
190
|
+
BEFORE DELETE ON facts
|
|
191
|
+
BEGIN
|
|
192
|
+
SELECT RAISE(ABORT, 'timeline: facts are append-only; rows are never deleted');
|
|
193
|
+
END;
|
|
194
|
+
`);
|
|
195
|
+
db.exec(`
|
|
196
|
+
DROP TRIGGER IF EXISTS timeline_events_no_delete;
|
|
197
|
+
CREATE TRIGGER timeline_events_no_delete
|
|
198
|
+
BEFORE DELETE ON events
|
|
199
|
+
BEGIN
|
|
200
|
+
SELECT RAISE(ABORT, 'timeline: events are append-only; rows are never deleted');
|
|
201
|
+
END;
|
|
202
|
+
`);
|
|
203
|
+
// Issue #7 / design §5.3: `irreversible` is the temporal member of the
|
|
204
|
+
// constraint family (bounded, monotonic, conserved sets). Rule: once a
|
|
205
|
+
// fact F = (entity_id, key, value) has irreversible = 1 at valid_from_t
|
|
206
|
+
// tF, no INSERT for the same entity_id/key with a DIFFERENT value and
|
|
207
|
+
// valid_from_t >= tF is permitted -- "for all t' > t", stated at the row
|
|
208
|
+
// level as `>=` because valid_from_t IS the instant the new value would
|
|
209
|
+
// take effect. `f.value IS NOT NEW.value` (not `<>`) for the same reason
|
|
210
|
+
// every other comparison in this file uses IS NOT: a NULL operand would
|
|
211
|
+
// otherwise make the WHEN clause silently not fire, and `value` can never
|
|
212
|
+
// legitimately be NULL here regardless (see the `facts` table comment)
|
|
213
|
+
// but this keeps the idiom uniform rather than relying on that.
|
|
214
|
+
//
|
|
215
|
+
// Deliberately an INSERT guard, not an UPDATE guard: facts are append-only
|
|
216
|
+
// (see timeline_facts_immutable above), so every new value for a key
|
|
217
|
+
// arrives as a fresh INSERT, never an UPDATE to an existing row's value.
|
|
218
|
+
//
|
|
219
|
+
// Closing an interval is NOT a contradiction and is NOT blocked here --
|
|
220
|
+
// `UPDATE facts SET valid_to_t = ...` never reaches this trigger at all,
|
|
221
|
+
// it is a different table event. This matters concretely: the projection
|
|
222
|
+
// layer's `_ad` delete trigger (projection.ts) closes every open fact for
|
|
223
|
+
// a destroyed entity via that same UPDATE path, and if closing were
|
|
224
|
+
// treated as a contradiction, any entity carrying an irreversible fact
|
|
225
|
+
// would become impossible to delete -- a policy this engine has no
|
|
226
|
+
// business imposing (see hard rule 2). Reopening the key afterward with a
|
|
227
|
+
// DIFFERENT value is still refused, because the WHEN clause below matches
|
|
228
|
+
// every irreversible row for the key, open or closed, not only the
|
|
229
|
+
// currently-open one.
|
|
230
|
+
//
|
|
231
|
+
// Because SQLite's RAISE(ABORT) backs out every change made by the firing
|
|
232
|
+
// statement -- including everything every trigger it invoked did, not
|
|
233
|
+
// just the INSERT this trigger body itself guards -- a refused reopen
|
|
234
|
+
// attempted from inside the projection layer's own AFTER UPDATE trigger
|
|
235
|
+
// (buildUpdateTrigger in projection.ts, which closes the old fact and
|
|
236
|
+
// then attempts to insert the new one in the same firing statement) rolls
|
|
237
|
+
// the close back too. The fact stays open, the live column stays
|
|
238
|
+
// unwritten, and the game's clock (also advanced earlier in that same
|
|
239
|
+
// trigger body) does not advance either. irreversible.test.ts's
|
|
240
|
+
// "real tool path" tests exercise this rollback directly, through
|
|
241
|
+
// updateResourceValue -> `UPDATE resources SET value = ...`.
|
|
242
|
+
db.exec(`
|
|
243
|
+
DROP TRIGGER IF EXISTS timeline_facts_irreversible;
|
|
244
|
+
CREATE TRIGGER timeline_facts_irreversible
|
|
245
|
+
BEFORE INSERT ON facts
|
|
246
|
+
WHEN EXISTS (
|
|
247
|
+
SELECT 1 FROM facts f
|
|
248
|
+
WHERE f.entity_id = NEW.entity_id
|
|
249
|
+
AND f.key = NEW.key
|
|
250
|
+
AND f.irreversible = 1
|
|
251
|
+
AND f.value IS NOT NEW.value
|
|
252
|
+
AND NEW.valid_from_t >= f.valid_from_t
|
|
253
|
+
)
|
|
254
|
+
BEGIN
|
|
255
|
+
SELECT RAISE(ABORT, 'timeline: an irreversible fact holds for key ''' || NEW.key || ''' on this entity as of its valid_from_t; a contradicting value is refused from that point onward');
|
|
256
|
+
END;
|
|
257
|
+
`);
|
|
258
|
+
// Issue #2: the projection layer. Triggers first, so every write from
|
|
259
|
+
// here on appends by construction; reconciliation second, so it backfills
|
|
260
|
+
// against triggers that are already live rather than a stale set. See
|
|
261
|
+
// projection.ts for what each does and why reconciliation must run last.
|
|
262
|
+
installProjectionTriggers();
|
|
263
|
+
reconcileTimeline();
|
|
264
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `t` -- story time, client-defined, chosen by one rule (design §14, hard
|
|
3
|
+
* rule 6):
|
|
4
|
+
*
|
|
5
|
+
* t is the axis that stays invariant when a caller re-segments its own
|
|
6
|
+
* units.
|
|
7
|
+
*
|
|
8
|
+
* A timestamp qualifies; so does a turn counter. An index into re-cuttable
|
|
9
|
+
* units does NOT -- re-cutting the units shifts what the index points at
|
|
10
|
+
* while the story underneath is unchanged. Choosing wrong raises no error:
|
|
11
|
+
* it silently attaches one unit's content to another's.
|
|
12
|
+
*
|
|
13
|
+
* `t` is an opaque, client-declared ordinal. It is never a datetime -- a
|
|
14
|
+
* `Date` carries a specific re-segmentation temptation (timezone, calendar,
|
|
15
|
+
* "which second did this actually happen") that a plain number does not
|
|
16
|
+
* invite. Callers that want elapsed real time declare an `elapsed` axis
|
|
17
|
+
* (see clock.ts, #6) and hand it a float; they do not hand the engine a
|
|
18
|
+
* `Date`.
|
|
19
|
+
*/
|
|
20
|
+
export type T = number;
|
|
21
|
+
/**
|
|
22
|
+
* The one comparator every place in the timeline that orders `t` goes
|
|
23
|
+
* through. Numeric ascending -- there is no other ordering `t` could have,
|
|
24
|
+
* since it is always a plain number, but centralizing this means a future
|
|
25
|
+
* axis kind can never introduce a second, inconsistent notion of "later."
|
|
26
|
+
*/
|
|
27
|
+
export declare function compareT(a: T, b: T): number;
|
|
28
|
+
/**
|
|
29
|
+
* Throws unless `value` is a finite number. Rejects everything else --
|
|
30
|
+
* `Date`, string (including a numeric string like `"12"`), `NaN`,
|
|
31
|
+
* `Infinity`, `null`, `undefined` -- naming what was actually supplied so
|
|
32
|
+
* the caller doesn't have to guess which of those it sent.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertT(value: unknown): asserts value is T;
|
|
35
|
+
/**
|
|
36
|
+
* The declared shape of a game's `t` -- which axis it moves along, chosen
|
|
37
|
+
* once per game and enforced by clock.ts (issue #6, `declareTimeAxis`).
|
|
38
|
+
* Every variant here is here because it satisfies §14's property (hard rule
|
|
39
|
+
* 6): it stays invariant when a caller re-segments its own units. There is a
|
|
40
|
+
* fourth, more obvious-looking kind that is deliberately absent, and the
|
|
41
|
+
* absence is the point -- see below.
|
|
42
|
+
*
|
|
43
|
+
* - `sequence` -- the engine's own append ordinal, one tick per write. The
|
|
44
|
+
* default for a game that declares nothing. Invariant because nothing
|
|
45
|
+
* re-cuts it: the engine is the only writer of the next value, so there is
|
|
46
|
+
* no "the caller's own units" for it to be an index into in the first
|
|
47
|
+
* place.
|
|
48
|
+
* - `elapsed` -- time since a fixed origin, in a caller-named `unit` (e.g.
|
|
49
|
+
* "seconds", in-game "minutes" -- whatever the caller's own story runs on).
|
|
50
|
+
* Invariant because re-segmenting is exactly the act of drawing new unit
|
|
51
|
+
* boundaries over a timeline that does not itself move; elapsed time from
|
|
52
|
+
* a fixed origin has nothing to re-cut.
|
|
53
|
+
* - `counter` -- a count of things that happened, in a caller-named `unit`
|
|
54
|
+
* (turns, ticks). Not a count of things authored. Invariant for the same
|
|
55
|
+
* reason `sequence` is: re-cutting authored material afterward does not
|
|
56
|
+
* change how many real events occurred before a given point.
|
|
57
|
+
*
|
|
58
|
+
* There is deliberately **no** variant for an index into units the caller
|
|
59
|
+
* may re-cut. Editing re-cuts those: the index shifts while the story
|
|
60
|
+
* underneath does not, and choosing one raises no error -- it silently
|
|
61
|
+
* attaches one unit's content to another's (§14). Because there is no
|
|
62
|
+
* variant for it, a caller who wants to hand the engine that kind of index
|
|
63
|
+
* anyway has to pick `elapsed` or `counter` and lie about the unit to do it
|
|
64
|
+
* -- there is no honest way to spell "index into units I might re-cut" in
|
|
65
|
+
* this type. That awkwardness is deliberate: it is the API making the wrong
|
|
66
|
+
* choice hard to make by accident, not merely documenting that it is wrong.
|
|
67
|
+
* `clock.ts`'s `declareTimeAxis` backs this up at runtime by refusing to let
|
|
68
|
+
* a game's axis change once declared -- swapping axes mid-timeline is the
|
|
69
|
+
* one runtime action that reproduces this exact failure, so that is where
|
|
70
|
+
* it gets refused.
|
|
71
|
+
*/
|
|
72
|
+
export type TimeAxis = {
|
|
73
|
+
kind: "sequence";
|
|
74
|
+
} | {
|
|
75
|
+
kind: "elapsed";
|
|
76
|
+
unit: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: "counter";
|
|
79
|
+
unit: string;
|
|
80
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one comparator every place in the timeline that orders `t` goes
|
|
3
|
+
* through. Numeric ascending -- there is no other ordering `t` could have,
|
|
4
|
+
* since it is always a plain number, but centralizing this means a future
|
|
5
|
+
* axis kind can never introduce a second, inconsistent notion of "later."
|
|
6
|
+
*/
|
|
7
|
+
export function compareT(a, b) {
|
|
8
|
+
return a - b;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Throws unless `value` is a finite number. Rejects everything else --
|
|
12
|
+
* `Date`, string (including a numeric string like `"12"`), `NaN`,
|
|
13
|
+
* `Infinity`, `null`, `undefined` -- naming what was actually supplied so
|
|
14
|
+
* the caller doesn't have to guess which of those it sent.
|
|
15
|
+
*/
|
|
16
|
+
export function assertT(value) {
|
|
17
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`timeline: t must be a finite number, got ${describeRejectedT(value)}`);
|
|
21
|
+
}
|
|
22
|
+
function describeRejectedT(value) {
|
|
23
|
+
if (value === null)
|
|
24
|
+
return "null";
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return "undefined";
|
|
27
|
+
if (value instanceof Date)
|
|
28
|
+
return `a Date (${value.toISOString()})`;
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
// Only NaN and +-Infinity reach here -- every other number returned
|
|
31
|
+
// above.
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "string")
|
|
35
|
+
return `a string (${JSON.stringify(value)})`;
|
|
36
|
+
return `a ${typeof value}`;
|
|
37
|
+
}
|
package/dist/tools/audio.js
CHANGED
|
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
2
2
|
import { getDatabase, getDataDir, withTransaction } from "../db/connection.js";
|
|
3
3
|
import { writeFileSync, readFileSync, mkdirSync, existsSync, unlinkSync, rmSync, } from "fs";
|
|
4
4
|
import { dirname, join, extname } from "path";
|
|
5
|
+
import { mediaDirPath, mediaFilePath, mediaPathWithin } from "../utils/media-path.js";
|
|
5
6
|
import { getCharacter } from "./character.js";
|
|
6
7
|
import { getLocation } from "./world.js";
|
|
7
8
|
import { getFaction } from "./faction.js";
|
|
@@ -128,10 +129,11 @@ export async function storeAudio(params) {
|
|
|
128
129
|
else {
|
|
129
130
|
throw new Error("Either url or filePath must be provided");
|
|
130
131
|
}
|
|
131
|
-
// Build file path
|
|
132
|
+
// Build file path. Every segment below arrives from the caller, so the
|
|
133
|
+
// composition goes through the media-path choke point, which rejects
|
|
134
|
+
// anything that would land outside the audio directory.
|
|
132
135
|
const ext = getExtension(mimeType);
|
|
133
|
-
const relativePath =
|
|
134
|
-
const fullPath = join(getAudioDir(), relativePath);
|
|
136
|
+
const { relativePath, fullPath } = mediaFilePath(getAudioDir(), [params.gameId, `${params.entityType}s`, params.entityId], `${id}.${ext}`);
|
|
135
137
|
// Ensure directory exists and write file
|
|
136
138
|
ensureDir(dirname(fullPath));
|
|
137
139
|
writeFileSync(fullPath, audioBuffer);
|
|
@@ -192,7 +194,7 @@ export function getAudioFilePath(audioId) {
|
|
|
192
194
|
const audio = getAudio(audioId);
|
|
193
195
|
if (!audio)
|
|
194
196
|
return null;
|
|
195
|
-
const fullPath =
|
|
197
|
+
const fullPath = mediaPathWithin(getAudioDir(), audio.filePath);
|
|
196
198
|
if (!existsSync(fullPath))
|
|
197
199
|
return null;
|
|
198
200
|
return fullPath;
|
|
@@ -201,7 +203,7 @@ export function getAudioData(audioId) {
|
|
|
201
203
|
const audio = getAudio(audioId);
|
|
202
204
|
if (!audio)
|
|
203
205
|
return null;
|
|
204
|
-
const fullPath =
|
|
206
|
+
const fullPath = mediaPathWithin(getAudioDir(), audio.filePath);
|
|
205
207
|
if (!existsSync(fullPath))
|
|
206
208
|
return null;
|
|
207
209
|
const buffer = readFileSync(fullPath);
|
|
@@ -264,7 +266,7 @@ export function getCharacterVoiceReferences(gameId, characterId) {
|
|
|
264
266
|
const audioDir = getAudioDir();
|
|
265
267
|
const filePaths = voiceRefs
|
|
266
268
|
.map((ref) => {
|
|
267
|
-
const fullPath =
|
|
269
|
+
const fullPath = mediaPathWithin(audioDir, ref.filePath);
|
|
268
270
|
return existsSync(fullPath) ? fullPath : null;
|
|
269
271
|
})
|
|
270
272
|
.filter((p) => p !== null);
|
|
@@ -283,7 +285,7 @@ export function deleteAudio(audioId) {
|
|
|
283
285
|
if (!audio)
|
|
284
286
|
return false;
|
|
285
287
|
// Delete file
|
|
286
|
-
const fullPath =
|
|
288
|
+
const fullPath = mediaPathWithin(getAudioDir(), audio.filePath);
|
|
287
289
|
if (existsSync(fullPath)) {
|
|
288
290
|
unlinkSync(fullPath);
|
|
289
291
|
}
|
|
@@ -339,8 +341,10 @@ export function updateAudioMetadata(audioId, updates) {
|
|
|
339
341
|
// Cleanup helper - delete all audio for a game
|
|
340
342
|
export function deleteGameAudio(gameId) {
|
|
341
343
|
const db = getDatabase();
|
|
342
|
-
// Delete files
|
|
343
|
-
|
|
344
|
+
// Delete files. mediaDirPath rejects before the rmSync below, which is
|
|
345
|
+
// recursive and forced: an unchecked `..` here would take the whole data
|
|
346
|
+
// directory with it.
|
|
347
|
+
const gameDir = mediaDirPath(getAudioDir(), [gameId]);
|
|
344
348
|
if (existsSync(gameDir)) {
|
|
345
349
|
rmSync(gameDir, { recursive: true, force: true });
|
|
346
350
|
}
|
|
@@ -1,47 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* A resource with no declared constraint behaves exactly as it always has --
|
|
6
|
-
* out-of-bounds writes are silently clamped by clampValue() in resource.ts.
|
|
7
|
-
* Declaring a constraint here changes that contract for that one resource
|
|
8
|
-
* (or set of resources, for 'conserved') only.
|
|
9
|
-
*
|
|
10
|
-
* SCOPE NOTE: this layer covers the `resources` table only. Three other
|
|
11
|
-
* numeric write paths bypass it entirely and are NOT covered:
|
|
12
|
-
* - factions.resources (JSON blob; see modifyFactionResource/setFactionResource
|
|
13
|
-
* in src/tools/faction.ts -- no bounds, no history, silently deletes an
|
|
14
|
-
* entry when it hits <= 0)
|
|
15
|
-
* - characters.attributes (arbitrary numeric JSON, unvalidated)
|
|
16
|
-
* - relationships.value (its own separate system)
|
|
17
|
-
* A game author who needs a server-enforced invariant on a quantity must
|
|
18
|
-
* model that quantity as a `resources` row, not one of the above.
|
|
19
|
-
*
|
|
20
|
-
* 'conserved' is fully enforced: a resource that is a member of a declared
|
|
21
|
-
* 'conserved' constraint can no longer be written directly through
|
|
22
|
-
* update_resource_value (see checkResourceConstraints() below) -- it must go
|
|
23
|
-
* through transferResourceValue() in resource.ts, which moves value between
|
|
24
|
-
* exactly two members of the same set atomically. See the comment on
|
|
25
|
-
* transferResourceValue() for why an explicit transfer, rather than a
|
|
26
|
-
* balanced multi-resource write, was chosen.
|
|
27
|
-
*/
|
|
28
|
-
/** Absolute tolerance for floating-point sum comparisons on 'conserved'
|
|
29
|
-
* constraints. IEEE 754 doubles cannot represent values like 0.1 exactly,
|
|
30
|
-
* so repeated addition/subtraction across many transfers can drift by a
|
|
31
|
-
* few ULPs. This is large enough to absorb that drift over realistic
|
|
32
|
-
* transfer volumes while still catching an actual logic bug (which would
|
|
33
|
-
* typically desync the sum by a whole `amount`, not a fraction of one). */
|
|
34
|
-
export declare const CONSERVED_SUM_EPSILON = 0.000001;
|
|
35
|
-
export declare class ConstraintViolationError extends Error {
|
|
36
|
-
readonly constraintKind: ConstraintKind;
|
|
37
|
-
readonly resourceId: string;
|
|
38
|
-
constructor(constraintKind: ConstraintKind, resourceId: string, message: string);
|
|
39
|
-
}
|
|
1
|
+
import type { MonotonicDirection, ResourceConstraint } from "../types/index.js";
|
|
2
|
+
import { ConstraintViolationError, CONSERVED_SUM_EPSILON } from "../timeline/registry.js";
|
|
3
|
+
export { ConstraintViolationError, CONSERVED_SUM_EPSILON };
|
|
40
4
|
/**
|
|
41
5
|
* Declare a 'bounded' constraint: the resource's value must stay within its
|
|
42
6
|
* existing minValue/maxValue (set via create_resource/update_resource).
|
|
43
7
|
* Once declared, out-of-bounds writes through updateResourceValue() are
|
|
44
|
-
* REJECTED instead of silently clamped -- see
|
|
8
|
+
* REJECTED instead of silently clamped -- see assertConstraintsAllow()
|
|
9
|
+
* (src/timeline/constrained.ts).
|
|
45
10
|
*/
|
|
46
11
|
export declare function declareBoundedConstraint(params: {
|
|
47
12
|
gameId: string;
|
|
@@ -58,6 +23,42 @@ export declare function declareMonotonicConstraint(params: {
|
|
|
58
23
|
resourceId: string;
|
|
59
24
|
direction: MonotonicDirection;
|
|
60
25
|
}): ResourceConstraint;
|
|
26
|
+
/**
|
|
27
|
+
* Declare a 'resolve_only' constraint (design §5.3, §5.2a; issue #13): every
|
|
28
|
+
* direct write to `factKey` on `resourceId` is refused --
|
|
29
|
+
* assertConstraintsAllow() (src/timeline/constrained.ts) rejects it whether
|
|
30
|
+
* it arrives through writeConstrainedValue OR transferConstrainedValue --
|
|
31
|
+
* unless made while an adjudication window is open
|
|
32
|
+
* (src/timeline/adjudication.ts's `withAdjudicationOpen`). The window is the
|
|
33
|
+
* mechanism this issue builds; the resolver that OPENS it (issue #10) is a
|
|
34
|
+
* separate, not-yet-built caller, and this function does not know or care
|
|
35
|
+
* what that caller will look like.
|
|
36
|
+
*
|
|
37
|
+
* `factKey` is optional and defaults to `'value'`, matching every other
|
|
38
|
+
* declare*Constraint() function's implicit scope -- but, unlike those,
|
|
39
|
+
* 'resolve_only' takes it as a REAL parameter rather than hardcoding it,
|
|
40
|
+
* because it is designed to guard a fact key a caller chooses, not only the
|
|
41
|
+
* one numeric column `resources` happens to expose today (design §5.4
|
|
42
|
+
* option (C)'s eventual generalization; see `factKey`'s doc comment on
|
|
43
|
+
* `ResourceConstraint`, src/types/index.ts).
|
|
44
|
+
*
|
|
45
|
+
* Validation mirrors declareBoundedConstraint()/declareMonotonicConstraint()
|
|
46
|
+
* exactly: the game and resource must exist, and re-declaring the same
|
|
47
|
+
* (resourceId, factKey) pair is rejected rather than silently accepted --
|
|
48
|
+
* scoped by factKey, not just resourceId, because that is the one thing
|
|
49
|
+
* that makes 'resolve_only' different from its two row-based siblings: two
|
|
50
|
+
* DIFFERENT fact keys on the same resource are two independent declarations,
|
|
51
|
+
* never a duplicate of each other.
|
|
52
|
+
*
|
|
53
|
+
* No shape prerequisite the way 'bounded' requires a min/max or 'monotonic'
|
|
54
|
+
* requires a direction -- 'resolve_only' constrains WHO may write, not what
|
|
55
|
+
* shape the value must take, so there is nothing else to validate.
|
|
56
|
+
*/
|
|
57
|
+
export declare function declareResolveOnlyConstraint(params: {
|
|
58
|
+
gameId: string;
|
|
59
|
+
resourceId: string;
|
|
60
|
+
factKey?: string;
|
|
61
|
+
}): ResourceConstraint;
|
|
61
62
|
/**
|
|
62
63
|
* Register a 'conserved' constraint: a set of resources that must always
|
|
63
64
|
* sum to a fixed total.
|
|
@@ -87,46 +88,9 @@ export declare function declareConservedConstraint(params: {
|
|
|
87
88
|
}): ResourceConstraint;
|
|
88
89
|
/** List constraints for a game, optionally filtered to ones governing a given resource. */
|
|
89
90
|
export declare function listConstraints(gameId: string, resourceId?: string): ResourceConstraint[];
|
|
90
|
-
/** All constraints (of any kind) that govern the given
|
|
91
|
+
/** All constraints (of any kind, any fact key) that govern the given
|
|
92
|
+
* resource. Delegates to the registry's allConstraintsForEntity() rather
|
|
93
|
+
* than writing its own JOIN -- see src/timeline/registry.ts. */
|
|
91
94
|
export declare function getConstraintsForResource(resourceId: string): ResourceConstraint[];
|
|
92
95
|
/** Remove a constraint by id. Returns true if a row was deleted. */
|
|
93
96
|
export declare function removeConstraint(id: string): boolean;
|
|
94
|
-
/**
|
|
95
|
-
* Check whether an intended value change for a single resource would
|
|
96
|
-
* violate any 'bounded' or 'monotonic' constraint declared on it. Throws
|
|
97
|
-
* ConstraintViolationError on violation; returns void otherwise. Shared by
|
|
98
|
-
* checkResourceConstraints() (the single-resource write path) and
|
|
99
|
-
* transferResourceValue() (resource.ts; the two-resource conserved-transfer
|
|
100
|
-
* path) -- both paths must respect 'bounded'/'monotonic' the same way.
|
|
101
|
-
* Deliberately does not look at 'conserved' constraints; callers decide
|
|
102
|
-
* separately what to do about those (see checkResourceConstraints() below
|
|
103
|
-
* and transferResourceValue()).
|
|
104
|
-
*/
|
|
105
|
-
export declare function checkBoundedAndMonotonicConstraints(resourceId: string, previousValue: number, intendedValue: number, bounds: {
|
|
106
|
-
minValue: number | null;
|
|
107
|
-
maxValue: number | null;
|
|
108
|
-
}): void;
|
|
109
|
-
/**
|
|
110
|
-
* Check whether an intended value change for a single resource would
|
|
111
|
-
* violate any constraint declared on it. Throws ConstraintViolationError on
|
|
112
|
-
* violation; returns void otherwise. Called from updateResourceValue() in
|
|
113
|
-
* resource.ts before the row is written.
|
|
114
|
-
*
|
|
115
|
-
* 'conserved' is handled specially here: ANY direct single-resource write to
|
|
116
|
-
* a conserved member is rejected, unconditionally, regardless of whether the
|
|
117
|
-
* particular delta would happen to preserve the total. The server cannot
|
|
118
|
-
* know where update_resource_value's counterpart delta should come from --
|
|
119
|
-
* writing one member without atomically adjusting another would silently
|
|
120
|
-
* break the set's invariant, which is exactly the failure this constraint
|
|
121
|
-
* exists to prevent. Use transfer_resource_value (transferResourceValue() in
|
|
122
|
-
* resource.ts) instead, which moves value between two members of the same
|
|
123
|
-
* set atomically.
|
|
124
|
-
*/
|
|
125
|
-
export declare function checkResourceConstraints(resourceId: string, previousValue: number, intendedValue: number, bounds: {
|
|
126
|
-
minValue: number | null;
|
|
127
|
-
maxValue: number | null;
|
|
128
|
-
}): void;
|
|
129
|
-
/** All conserved constraints (of kind 'conserved') governing a resource,
|
|
130
|
-
* i.e. zero or one (declareConservedConstraint() rejects overlapping
|
|
131
|
-
* conserved membership, so a resource can belong to at most one). */
|
|
132
|
-
export declare function getConservedConstraintFor(resourceId: string): ResourceConstraint | null;
|