run-dmcp 0.8.0 → 0.9.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/dist/db/schema.js CHANGED
@@ -1270,6 +1270,67 @@ export function initializeSchema(options) {
1270
1270
  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');
1271
1271
  END;
1272
1272
  `);
1273
+ // Issue #42: a `create` leg of a resolution may declare `bounded`/
1274
+ // `resolve_only`/`monotonic` constraints on the entity it makes, applied
1275
+ // by resolve() inside the SAME transaction as the create -- so a later
1276
+ // leg of the same resolution is already held to them. Two additions to
1277
+ // `resource_constraints`:
1278
+ //
1279
+ // - min_value/max_value: 'bounded' only, carried on the constraint's own
1280
+ // row. declareBoundedConstraint() (src/tools/constraint.ts) has no
1281
+ // need for this -- it enforces whatever `bounds` a caller passes at
1282
+ // write time against an EXISTING resource's own min_value/max_value
1283
+ // columns -- but a constraint declared in the same breath as the
1284
+ // entity it governs is recorded here instead, opaquely: the engine
1285
+ // stores these bounds and does not interpret them (issue #42's own
1286
+ // scope note), the same "one column, several kinds, mostly null"
1287
+ // shape `direction` (monotonic) and `total` (conserved) already use.
1288
+ // - caused_by_event_id: which `resolution.recorded` event's resolve()
1289
+ // call declared this constraint -- design §5.2c's one hop of
1290
+ // causality, recorded rather than derived later. NULL for a
1291
+ // constraint declared the ordinary way, through
1292
+ // declareBoundedConstraint/declareMonotonicConstraint/
1293
+ // declareConservedConstraint/declareResolveOnlyConstraint, none of
1294
+ // which run inside a resolution and so have no event to point at.
1295
+ //
1296
+ // Idempotent ALTERs, the same idiom RESOURCE_CONSTRAINTS_ADD_FACT_KEY_DDL
1297
+ // uses above -- no CHECK constraint changes here, so no table rebuild is
1298
+ // needed. Placed here, after initializeTimelineSchema() (a few lines up)
1299
+ // rather than beside RESOURCE_CONSTRAINTS_DDL earlier in this function,
1300
+ // so a fresh database's `resource_constraints` picks these up right after
1301
+ // the tables issue #42's writer touches (`events`) already exist.
1302
+ //
1303
+ // `caused_by_event_id` carries NO `REFERENCES events(id)` -- deliberately,
1304
+ // unlike `facts.opened_by_event_id` a few lines up. That column is always
1305
+ // written NULL-then-UPDATEd once its target event already exists (see
1306
+ // this function's own comment on it and projection.ts's insert trigger);
1307
+ // resolve()'s create-leg constraints (src/timeline/resolve.ts, issue #42)
1308
+ // declare a constraint and stamp the resolution's event id in the SAME
1309
+ // statement, before that event row exists (the event is written only
1310
+ // after every change in the resolution has landed, so its own `t` can be
1311
+ // read post-write -- see resolve.ts step 7). An immediate FK here would
1312
+ // refuse every such INSERT. `events.causes` already carries this same
1313
+ // "one hop of provenance, unenforced by a foreign key" shape for every
1314
+ // other event-to-event reference in this codebase (resolution_id, row_id,
1315
+ // fact_id) -- this column follows it rather than being the one exception.
1316
+ try {
1317
+ db.exec(`ALTER TABLE resource_constraints ADD COLUMN min_value REAL`);
1318
+ }
1319
+ catch {
1320
+ // Column already exists.
1321
+ }
1322
+ try {
1323
+ db.exec(`ALTER TABLE resource_constraints ADD COLUMN max_value REAL`);
1324
+ }
1325
+ catch {
1326
+ // Column already exists.
1327
+ }
1328
+ try {
1329
+ db.exec(`ALTER TABLE resource_constraints ADD COLUMN caused_by_event_id TEXT`);
1330
+ }
1331
+ catch {
1332
+ // Column already exists.
1333
+ }
1273
1334
  }
1274
1335
  function runConsumerMigrations(db, migrations) {
1275
1336
  if (!migrations || migrations.length === 0) {
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export { writeConstrainedValue, transferConstrainedValue, valueHistory, } from "
23
23
  export type { ValueTransition } from "./timeline/constrained.js";
24
24
  export { ConstraintViolationError, constraintsFor, conservedConstraintFor } from "./timeline/registry.js";
25
25
  export { createResolver, ResolveProtocolError } from "./timeline/resolve.js";
26
- export type { Mechanic, Resolver, Proposal, Expectation, AdjudicationInput, Adjudication, IntendedChange, IntendedWrite, IntendedTransfer, IntendedSet, IntendedCreate, IntendedDestroy, EntityRef, Outcome, ResolveRefusalReason, } from "./timeline/resolve.js";
26
+ export type { Mechanic, Resolver, Proposal, Expectation, AdjudicationInput, Adjudication, IntendedChange, IntendedWrite, IntendedTransfer, IntendedSet, IntendedCreate, CreateConstraint, IntendedDestroy, EntityRef, Outcome, ResolveRefusalReason, } from "./timeline/resolve.js";
27
27
  export { createStateRenderer } from "./timeline/render.js";
28
28
  export type { RenderVocabulary, VocabularyEntry, StateRenderer, RenderedState, RenderedNoun, UnnamedFact, } from "./timeline/render.js";
29
29
  export { createTurnReader } from "./reader/turnReader.js";
@@ -6,7 +6,7 @@ export declare const SERVER_NAME = "dmcp";
6
6
  * the published package version by src/__tests__/serverVersion.test.ts --
7
7
  * this said "0.3.0" for the whole of 0.4.0, because a release bumps
8
8
  * package.json and nothing was watching this. */
9
- export declare const SERVER_VERSION = "0.8.0";
9
+ export declare const SERVER_VERSION = "0.9.0";
10
10
  /**
11
11
  * Build an MCP server with every CORE tool, resource and prompt this engine
12
12
  * serves -- entities, facts, events, the timeline, and the entity/property
@@ -45,7 +45,7 @@ export const SERVER_NAME = "dmcp";
45
45
  * the published package version by src/__tests__/serverVersion.test.ts --
46
46
  * this said "0.3.0" for the whole of 0.4.0, because a release bumps
47
47
  * package.json and nothing was watching this. */
48
- export const SERVER_VERSION = "0.8.0";
48
+ export const SERVER_VERSION = "0.9.0";
49
49
  /**
50
50
  * Build an MCP server with every CORE tool, resource and prompt this engine
51
51
  * serves -- entities, facts, events, the timeline, and the entity/property
@@ -19,10 +19,23 @@ import type { IrreversibleFact } from "./irreversible.js";
19
19
  * the read side here breaks that cycle before the choke point exists to hit
20
20
  * it.
21
21
  *
22
- * Everything that WRITES `resource_constraints` -- insertConstraint() and
23
- * the declare*() functions, and all the validation that goes with them --
24
- * stays in src/tools/constraint.ts, which imports the accessors below
25
- * rather than duplicating the query against resource_constraint_members.
22
+ * Everything that WRITES `resource_constraints` used to stay entirely in
23
+ * src/tools/constraint.ts -- insertConstraint() and the declare*()
24
+ * functions, and all the validation that goes with them. Issue #42 moved
25
+ * the one raw INSERT (insertConstraintRow() below) down here, because
26
+ * resolve() (src/timeline/resolve.ts) now also has to write this exact row,
27
+ * from INSIDE its own transaction, when a `create` leg declares
28
+ * `bounded`/`resolve_only`/`monotonic` on the entity it makes. Reaching
29
+ * from src/timeline/ back into src/tools/constraint.ts to get there would
30
+ * close the identical cycle this module's own doc comment (above) describes
31
+ * for the choke point: tools/resource.ts already imports tools/constraint.ts
32
+ * (indirectly, via getResource), and tools/constraint.ts would then import
33
+ * timeline/resolve.ts's writer, closing tools/* -> timeline/* -> tools/*.
34
+ * src/tools/constraint.ts's declare*() functions keep every piece of
35
+ * business validation they always had (game exists, resource exists, no
36
+ * duplicate, minValue/maxValue already set for 'bounded') and call
37
+ * insertConstraintRow() only once every check has passed -- they are not
38
+ * merged away, only pointed at the one place the row is actually written.
26
39
  */
27
40
  /** Absolute tolerance for floating-point sum comparisons on 'conserved'
28
41
  * constraints. IEEE 754 doubles cannot represent values like 0.1 exactly,
@@ -67,11 +80,36 @@ export interface ConstraintRow {
67
80
  total: number | null;
68
81
  fact_key: string;
69
82
  created_at: string;
83
+ min_value: number | null;
84
+ max_value: number | null;
85
+ caused_by_event_id: string | null;
70
86
  }
71
87
  /** The resource ids belonging to a constraint, in insertion order. Exported
72
88
  * alongside ConstraintRow/rowToConstraint for the same reason. */
73
89
  export declare function memberIdsFor(constraintId: string): string[];
74
90
  export declare function rowToConstraint(row: ConstraintRow): ResourceConstraint;
91
+ /**
92
+ * The one INSERT for `resource_constraints` (+ its members) -- see this
93
+ * module's own doc comment on why it lives here rather than in
94
+ * src/tools/constraint.ts. Every declare*Constraint() function there calls
95
+ * this only after its own business validation passes; resolve()'s create-leg
96
+ * declarations (src/timeline/resolve.ts, issue #42) call it directly, from
97
+ * inside their own transaction, after the leaner pre-transaction check that
98
+ * module runs (a declared key must be a live column -- see
99
+ * assertCreateConstraintKeysValid there). Either way this is the only
100
+ * `INSERT INTO resource_constraints` in the codebase.
101
+ */
102
+ export declare function insertConstraintRow(params: {
103
+ gameId: string;
104
+ kind: ConstraintKind;
105
+ resourceIds: readonly string[];
106
+ direction?: MonotonicDirection | null;
107
+ total?: number | null;
108
+ factKey?: string;
109
+ minValue?: number | null;
110
+ maxValue?: number | null;
111
+ causedByEventId?: string | null;
112
+ }): ResourceConstraint;
75
113
  /**
76
114
  * Every constraint governing `(entityId, factKey)`, ordered by
77
115
  * `created_at`. This is the whole point of Phase 3 step 1: a constraint
@@ -1,3 +1,4 @@
1
+ import { v4 as uuidv4 } from "uuid";
1
2
  import { getDatabase } from "../db/connection.js";
2
3
  /**
3
4
  * The read side of the resource-constraint registry (design §5.3 / §5.4
@@ -18,10 +19,23 @@ import { getDatabase } from "../db/connection.js";
18
19
  * the read side here breaks that cycle before the choke point exists to hit
19
20
  * it.
20
21
  *
21
- * Everything that WRITES `resource_constraints` -- insertConstraint() and
22
- * the declare*() functions, and all the validation that goes with them --
23
- * stays in src/tools/constraint.ts, which imports the accessors below
24
- * rather than duplicating the query against resource_constraint_members.
22
+ * Everything that WRITES `resource_constraints` used to stay entirely in
23
+ * src/tools/constraint.ts -- insertConstraint() and the declare*()
24
+ * functions, and all the validation that goes with them. Issue #42 moved
25
+ * the one raw INSERT (insertConstraintRow() below) down here, because
26
+ * resolve() (src/timeline/resolve.ts) now also has to write this exact row,
27
+ * from INSIDE its own transaction, when a `create` leg declares
28
+ * `bounded`/`resolve_only`/`monotonic` on the entity it makes. Reaching
29
+ * from src/timeline/ back into src/tools/constraint.ts to get there would
30
+ * close the identical cycle this module's own doc comment (above) describes
31
+ * for the choke point: tools/resource.ts already imports tools/constraint.ts
32
+ * (indirectly, via getResource), and tools/constraint.ts would then import
33
+ * timeline/resolve.ts's writer, closing tools/* -> timeline/* -> tools/*.
34
+ * src/tools/constraint.ts's declare*() functions keep every piece of
35
+ * business validation they always had (game exists, resource exists, no
36
+ * duplicate, minValue/maxValue already set for 'bounded') and call
37
+ * insertConstraintRow() only once every check has passed -- they are not
38
+ * merged away, only pointed at the one place the row is actually written.
25
39
  */
26
40
  /** Absolute tolerance for floating-point sum comparisons on 'conserved'
27
41
  * constraints. IEEE 754 doubles cannot represent values like 0.1 exactly,
@@ -70,6 +84,50 @@ export function rowToConstraint(row) {
70
84
  total: row.total,
71
85
  factKey: row.fact_key,
72
86
  createdAt: row.created_at,
87
+ minValue: row.min_value,
88
+ maxValue: row.max_value,
89
+ causedByEventId: row.caused_by_event_id,
90
+ };
91
+ }
92
+ /**
93
+ * The one INSERT for `resource_constraints` (+ its members) -- see this
94
+ * module's own doc comment on why it lives here rather than in
95
+ * src/tools/constraint.ts. Every declare*Constraint() function there calls
96
+ * this only after its own business validation passes; resolve()'s create-leg
97
+ * declarations (src/timeline/resolve.ts, issue #42) call it directly, from
98
+ * inside their own transaction, after the leaner pre-transaction check that
99
+ * module runs (a declared key must be a live column -- see
100
+ * assertCreateConstraintKeysValid there). Either way this is the only
101
+ * `INSERT INTO resource_constraints` in the codebase.
102
+ */
103
+ export function insertConstraintRow(params) {
104
+ const db = getDatabase();
105
+ const id = uuidv4();
106
+ const createdAt = new Date().toISOString();
107
+ const direction = params.direction ?? null;
108
+ const total = params.total ?? null;
109
+ const factKey = params.factKey ?? "value";
110
+ const minValue = params.minValue ?? null;
111
+ const maxValue = params.maxValue ?? null;
112
+ const causedByEventId = params.causedByEventId ?? null;
113
+ db.prepare(`INSERT INTO resource_constraints (id, game_id, kind, direction, total, fact_key, created_at, min_value, max_value, caused_by_event_id)
114
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.gameId, params.kind, direction, total, factKey, createdAt, minValue, maxValue, causedByEventId);
115
+ const memberStmt = db.prepare(`INSERT INTO resource_constraint_members (constraint_id, resource_id) VALUES (?, ?)`);
116
+ for (const resourceId of params.resourceIds) {
117
+ memberStmt.run(id, resourceId);
118
+ }
119
+ return {
120
+ id,
121
+ gameId: params.gameId,
122
+ kind: params.kind,
123
+ resourceIds: [...params.resourceIds],
124
+ direction,
125
+ total,
126
+ factKey,
127
+ createdAt,
128
+ minValue,
129
+ maxValue,
130
+ causedByEventId,
73
131
  };
74
132
  }
75
133
  /**
@@ -204,13 +204,54 @@ export interface IntendedSet {
204
204
  ref: string;
205
205
  };
206
206
  }
207
+ /**
208
+ * One constraint a `create` leg declares on the entity it makes (issue #42),
209
+ * applied by `resolve()` step 5 INSIDE the same transaction as the create --
210
+ * so a later leg of the same resolution is already held to it, and the
211
+ * whole resolution rolls back together if it is not (see
212
+ * assertCreateConstraintKeysValid and the `declareCreateConstraints` call in
213
+ * `resolveProposal` below). Vocabulary matches the existing constraint
214
+ * family exactly (declareBoundedConstraint/declareMonotonicConstraint/
215
+ * declareResolveOnlyConstraint, src/tools/constraint.ts) -- `conserved` is
216
+ * deliberately absent: it is a statement about several EXISTING entities
217
+ * summing to a total, not about one being created, so a single `create` leg
218
+ * has nothing to attach it to.
219
+ *
220
+ * The engine interprets neither `key` nor any bound below -- it only
221
+ * confirms, before any write, that `key` names a live column of the
222
+ * entity's own table (assertCreateConstraintKeysValid). `direction`'s
223
+ * vocabulary here ('up'/'down') is this field's own; declareCreateConstraints
224
+ * translates it to the registry's stored 'increasing'/'decreasing' the same
225
+ * way `entityKind` is translated to a table name -- a structural lookup, not
226
+ * an interpretation of what either word means (root CLAUDE.md hard rule 4).
227
+ */
228
+ export type CreateConstraint = {
229
+ kind: "bounded";
230
+ key: string;
231
+ minValue: number | null;
232
+ maxValue: number | null;
233
+ } | {
234
+ kind: "resolve_only";
235
+ key: string;
236
+ } | {
237
+ kind: "monotonic";
238
+ key: string;
239
+ direction: "up" | "down";
240
+ };
207
241
  /** One intended entity coming into existence (issue #34): a row in the
208
242
  * projected table for `entityKind`, with `columns` restricted to that
209
243
  * table's live columns and the game column filled by the engine from the
210
244
  * proposal. `ref` is the label later legs of this same resolution use for
211
245
  * it; the engine allocates the id and reports it in `Outcome.created`.
212
246
  * The engine never learns what the entity is for or what it was made
213
- * from -- "derived from" is the caller's to record. */
247
+ * from -- "derived from" is the caller's to record.
248
+ *
249
+ * `constraints` (issue #42) is optional and, unlike `columns`, is never a
250
+ * column write -- it declares `resource_constraints` rows governing the
251
+ * entity this leg creates, the same rows declareBoundedConstraint/
252
+ * declareMonotonicConstraint/declareResolveOnlyConstraint would declare
253
+ * after the fact, but inside the resolution's own transaction instead of a
254
+ * second write path after `resolve()` returns. */
214
255
  export interface IntendedCreate {
215
256
  kind: "create";
216
257
  ref: string;
@@ -218,6 +259,7 @@ export interface IntendedCreate {
218
259
  columns: Readonly<Record<string, string | number | null | {
219
260
  ref: string;
220
261
  }>>;
262
+ constraints?: readonly CreateConstraint[];
221
263
  }
222
264
  /** One intended entity ending (issue #34): its live row deleted, its facts
223
265
  * closed by the projection trigger. See `destroyProjectedEntity`
@@ -293,7 +335,10 @@ export type ResolveRefusalReason = "unknown-mechanic" | "no-clock" | "expectatio
293
335
  | "unresolved-ref"
294
336
  /** Two `create` legs of one resolution chose the same `ref` (issue #34).
295
337
  * Refused before any write. */
296
- | "duplicate-ref";
338
+ | "duplicate-ref"
339
+ /** A `create` leg's `constraints` names a `key` that is not a live column
340
+ * of the entity it creates (issue #42). Refused before any write. */
341
+ | "invalid-constraint-key";
297
342
  /**
298
343
  * Refused before dispatch, before any write, or (never, by construction --
299
344
  * see step 5 above) mid-apply. `reason` is the discriminant a caller
@@ -3,6 +3,8 @@ import { getDatabase, withTransaction } from "../db/connection.js";
3
3
  import { currentStoryTime } from "./clock.js";
4
4
  import { narrationConstraintAt, contradictions } from "./narration.js";
5
5
  import { withAdjudicationOpen } from "./adjudication.js";
6
+ import { PROJECTED_TABLES, liveColumns } from "./projection.js";
7
+ import { insertConstraintRow } from "./registry.js";
6
8
  import { writeConstrainedValue, transferConstrainedValue, setProjectedValue, createProjectedEntity, destroyProjectedEntity, } from "./constrained.js";
7
9
  /**
8
10
  * Refused before dispatch, before any write, or (never, by construction --
@@ -126,6 +128,85 @@ function assertRefsResolvable(mechanicName, changes) {
126
128
  }
127
129
  });
128
130
  }
131
+ /**
132
+ * Step 5's second precondition (issue #42), checked in the same voice and
133
+ * at the same point as assertRefsResolvable above: every `constraints`
134
+ * entry on every `create` leg names a `key` that is a live column of the
135
+ * projected table for that leg's `entityKind` -- decidable from the schema
136
+ * alone, with no database write and no dependency on the entity actually
137
+ * existing yet, because the live column set is a property of the KIND, not
138
+ * of any one row of it. A bad key refuses with no transaction opened and no
139
+ * write attempted, naming the key.
140
+ */
141
+ function assertCreateConstraintKeysValid(mechanicName, changes) {
142
+ const db = getDatabase();
143
+ changes.forEach((change, index) => {
144
+ if (change.kind !== "create" || !change.constraints || change.constraints.length === 0)
145
+ return;
146
+ const projected = PROJECTED_TABLES.find((p) => p.kind === change.entityKind);
147
+ // An entityKind with no projected table is createProjectedEntity's own
148
+ // refusal to make, inside the transaction -- nothing to validate a
149
+ // constraint key against here.
150
+ if (!projected)
151
+ return;
152
+ const cols = liveColumns(db, projected.table);
153
+ for (const constraint of change.constraints) {
154
+ if (!cols.includes(constraint.key)) {
155
+ throw new ResolveProtocolError("invalid-constraint-key", `resolve: leg ${index + 1} of mechanic '${mechanicName}' declares a '${constraint.kind}' constraint on key ` +
156
+ `'${constraint.key}' for the entity it creates under ref '${change.ref}', and '${constraint.key}' is not a ` +
157
+ `live column of '${projected.table}' -- refused before any write.`);
158
+ }
159
+ }
160
+ });
161
+ }
162
+ /**
163
+ * Translates every `CreateConstraint` (issue #42) a `create` leg declared
164
+ * into a `resource_constraints` row, through insertConstraintRow
165
+ * (registry.ts) -- the same primitive every declare*Constraint()
166
+ * (src/tools/constraint.ts) call now shares, so this is never a second
167
+ * write path for that table. Called from INSIDE the resolution's own
168
+ * transaction (resolveProposal below), immediately after the entity it
169
+ * governs is created, so `entityId` is real and `eventId` is the id the
170
+ * resolution's own `resolution.recorded` event will carry.
171
+ */
172
+ function declareCreateConstraints(params) {
173
+ for (const constraint of params.constraints) {
174
+ if (constraint.kind === "bounded") {
175
+ insertConstraintRow({
176
+ gameId: params.gameId,
177
+ kind: "bounded",
178
+ resourceIds: [params.entityId],
179
+ factKey: constraint.key,
180
+ minValue: constraint.minValue,
181
+ maxValue: constraint.maxValue,
182
+ causedByEventId: params.eventId,
183
+ });
184
+ }
185
+ else if (constraint.kind === "resolve_only") {
186
+ insertConstraintRow({
187
+ gameId: params.gameId,
188
+ kind: "resolve_only",
189
+ resourceIds: [params.entityId],
190
+ factKey: constraint.key,
191
+ causedByEventId: params.eventId,
192
+ });
193
+ }
194
+ else {
195
+ insertConstraintRow({
196
+ gameId: params.gameId,
197
+ kind: "monotonic",
198
+ resourceIds: [params.entityId],
199
+ factKey: constraint.key,
200
+ // This field's own vocabulary ('up'/'down') is translated to the
201
+ // registry's stored 'increasing'/'decreasing' -- a structural
202
+ // lookup, not an interpretation of what either word means (see
203
+ // CreateConstraint's own doc comment).
204
+ direction: constraint.direction === "up" ? "increasing" : "decreasing",
205
+ causedByEventId: params.eventId,
206
+ });
207
+ }
208
+ }
209
+ }
129
210
  function deref(value, refs) {
130
211
  if (typeof value === "string")
131
212
  return value;
@@ -246,8 +327,10 @@ function resolveProposal(mechanicsByName, proposal) {
246
327
  });
247
328
  const resolutionId = uuidv4();
248
329
  const changes = adjudication.changes ?? [];
249
- // 5's precondition: every ref resolves, before the transaction opens.
330
+ // 5's preconditions: every ref resolves, and every create leg's declared
331
+ // constraint keys are live columns -- both before the transaction opens.
250
332
  assertRefsResolvable(mechanicName, changes);
333
+ assertCreateConstraintKeysValid(mechanicName, changes);
251
334
  // 5 & 6. Apply every intended change through the one choke point, and
252
335
  // record one event -- both inside ONE transaction with the adjudication
253
336
  // window nested inside it (adjudication.ts's own doc comment asks for
@@ -262,14 +345,31 @@ function resolveProposal(mechanicsByName, proposal) {
262
345
  const created = [];
263
346
  const destroyed = [];
264
347
  const refs = new Map();
348
+ // Generated here, before any change is applied, rather than after the
349
+ // loop below (as it used to be) -- issue #42's create-leg constraint
350
+ // declarations (below) need to stamp the SAME event id this
351
+ // resolution's own `resolution.recorded` event is about to carry, and
352
+ // that event cannot be written until every change has landed (step 7's
353
+ // t comes from AFTER the writes). uuidv4() needs nothing from the
354
+ // database, so generating it early costs nothing and lets both sides
355
+ // agree on one id.
356
+ const eventId = uuidv4();
265
357
  for (const change of changes) {
266
358
  const applied = applyChange(change, gameId, refs);
267
359
  if ("transitions" in applied)
268
360
  transitions.push(...applied.transitions);
269
361
  else if ("set" in applied)
270
362
  sets.push(applied.set);
271
- else if ("created" in applied)
363
+ else if ("created" in applied) {
272
364
  created.push(applied.created);
365
+ // Issue #42: a create leg's declared constraints are applied
366
+ // immediately, inside this same transaction, once the entity they
367
+ // govern has a real id -- so a later leg of this same resolution
368
+ // (still to come in this loop) is already held to them.
369
+ if (change.kind === "create" && change.constraints && change.constraints.length > 0) {
370
+ declareCreateConstraints({ gameId, entityId: applied.created.entityId, constraints: change.constraints, eventId });
371
+ }
372
+ }
273
373
  else
274
374
  destroyed.push(applied.destroyed);
275
375
  }
@@ -286,7 +386,6 @@ function resolveProposal(mechanicsByName, proposal) {
286
386
  // function has no business assuming that silently forever.
287
387
  throw new Error(`resolve: game '${gameId}' lost its timeline clock mid-resolution -- cannot record the outcome event`);
288
388
  }
289
- const eventId = uuidv4();
290
389
  const causes = {
291
390
  source: "resolve",
292
391
  resolution_id: resolutionId,
@@ -1,8 +1,7 @@
1
- import { v4 as uuidv4 } from "uuid";
2
1
  import { getDatabase } from "../db/connection.js";
3
2
  import { validateGameExists } from "./game.js";
4
3
  import { getResource } from "./resource.js";
5
- import { ConstraintViolationError, CONSERVED_SUM_EPSILON, constraintsFor, allConstraintsForEntity, rowToConstraint, } from "../timeline/registry.js";
4
+ import { ConstraintViolationError, CONSERVED_SUM_EPSILON, constraintsFor, allConstraintsForEntity, insertConstraintRow, rowToConstraint, } from "../timeline/registry.js";
6
5
  // Re-exported so every existing importer (src/tools/resource.ts,
7
6
  // src/register/resources.ts) keeps working unchanged -- these two now live
8
7
  // in src/timeline/registry.js; see the paragraph below on why.
@@ -72,25 +71,16 @@ export { ConstraintViolationError, CONSERVED_SUM_EPSILON };
72
71
  * adjudication window is open (src/timeline/adjudication.ts), not anything
73
72
  * about the intended value itself.
74
73
  */
74
+ // The raw INSERT this used to perform directly now lives in
75
+ // insertConstraintRow (src/timeline/registry.js) -- see that module's own
76
+ // doc comment for why (issue #42: resolve() needs to write the identical
77
+ // row from inside its own transaction, and src/timeline/ cannot reach back
78
+ // into src/tools/ to get here without closing a cycle). This wrapper keeps
79
+ // every call site below unchanged; declared constraints from this file
80
+ // always carry a null causedByEventId, because none of these run inside a
81
+ // resolution.
75
82
  function insertConstraint(gameId, kind, resourceIds, direction, total, factKey = "value") {
76
- const db = getDatabase();
77
- const id = uuidv4();
78
- const now = new Date().toISOString();
79
- db.prepare(`INSERT INTO resource_constraints (id, game_id, kind, direction, total, fact_key, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, gameId, kind, direction, total, factKey, now);
80
- const memberStmt = db.prepare(`INSERT INTO resource_constraint_members (constraint_id, resource_id) VALUES (?, ?)`);
81
- for (const resourceId of resourceIds) {
82
- memberStmt.run(id, resourceId);
83
- }
84
- return {
85
- id,
86
- gameId,
87
- kind,
88
- resourceIds,
89
- direction,
90
- total,
91
- factKey,
92
- createdAt: now,
93
- };
83
+ return insertConstraintRow({ gameId, kind, resourceIds, direction, total, factKey });
94
84
  }
95
85
  /**
96
86
  * Declare a 'bounded' constraint: the resource's value must stay within its
@@ -512,6 +512,9 @@ export interface ResourceConstraint {
512
512
  total: number | null;
513
513
  factKey: string;
514
514
  createdAt: string;
515
+ minValue: number | null;
516
+ maxValue: number | null;
517
+ causedByEventId: string | null;
515
518
  }
516
519
  export interface GameDateTime {
517
520
  year: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run-dmcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "An MCP server for LLM-run interactive fiction where the server owns what is true - including when it was true. A continuation of DMCP.",
5
5
  "license": "MIT",
6
6
  "author": "Derek Ferguson",