run-dmcp 0.6.0 → 0.7.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/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export { timelineDivergences } from "./timeline/checkpoint.js";
14
14
  export type { Divergence } from "./timeline/checkpoint.js";
15
15
  export { declareIrreversible, irreversibleFactFor, listIrreversibleFacts, } from "./timeline/irreversible.js";
16
16
  export type { IrreversibleFact } from "./timeline/irreversible.js";
17
+ export type { SetTransition } from "./timeline/constrained.js";
17
18
  export { openingEventId } from "./timeline/provenance.js";
18
19
  export type { FactProvenance } from "./timeline/provenance.js";
19
20
  export { narrationConstraintAt, contradictions, NARRATION_CONSTRAINT_FORMAT_VERSION, } from "./timeline/narration.js";
@@ -22,7 +23,7 @@ export { writeConstrainedValue, transferConstrainedValue, valueHistory, } from "
22
23
  export type { ValueTransition } from "./timeline/constrained.js";
23
24
  export { ConstraintViolationError, constraintsFor, conservedConstraintFor } from "./timeline/registry.js";
24
25
  export { createResolver, ResolveProtocolError } from "./timeline/resolve.js";
25
- export type { Mechanic, Resolver, Proposal, Expectation, AdjudicationInput, Adjudication, IntendedChange, IntendedWrite, IntendedTransfer, Outcome, ResolveRefusalReason, } from "./timeline/resolve.js";
26
+ export type { Mechanic, Resolver, Proposal, Expectation, AdjudicationInput, Adjudication, IntendedChange, IntendedWrite, IntendedTransfer, IntendedSet, Outcome, ResolveRefusalReason, } from "./timeline/resolve.js";
26
27
  export { createStateRenderer } from "./timeline/render.js";
27
28
  export type { RenderVocabulary, VocabularyEntry, StateRenderer, RenderedState, RenderedNoun, UnnamedFact, } from "./timeline/render.js";
28
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.6.0";
9
+ export declare const SERVER_VERSION = "0.7.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.6.0";
48
+ export const SERVER_VERSION = "0.7.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
@@ -218,3 +218,42 @@ export declare function transferConstrainedValue(params: {
218
218
  * discipline. `limit` applies after ordering, never before.
219
219
  */
220
220
  export declare function valueHistory(entityId: string, key: string, limit?: number): ValueTransition[];
221
+ /**
222
+ * One non-numeric column set, as `resolve()`'s `set` intent applies it (issue
223
+ * #32). A row, never a verdict: what the column held, what it holds now, and
224
+ * the fact the write opened -- `null` when the value did not move, because an
225
+ * unchanged value opens no new interval (projection.ts's update trigger).
226
+ */
227
+ export interface SetTransition {
228
+ entityId: string;
229
+ key: string;
230
+ previousValue: string | number | null;
231
+ newValue: string | number | null;
232
+ t: T;
233
+ factId: string | null;
234
+ }
235
+ /**
236
+ * Sets one live column of an entity's projected table to `value` (issue #32).
237
+ * The engine stores what it is handed and never learns what the column means;
238
+ * the projection triggers version it exactly as they version every other
239
+ * column write, so nothing here touches `facts` directly.
240
+ *
241
+ * The same choke point as numeric writes, and the same checks in the same
242
+ * voice: the entity and column are resolved against `PROJECTED_TABLES` /
243
+ * `liveColumns` (never a caller-supplied table name); the column that places
244
+ * the entity in its game is refused; a key carrying a numeric constraint is
245
+ * refused, because that value changes by a write where the constraint is
246
+ * evaluated; `resolve_only` asks the one question it always asks, whether an
247
+ * adjudication window is open; and a contradicted irreversible fact is
248
+ * translated into a typed `ConstraintViolationError` with the one hop
249
+ * attached, by `translateIrreversibleFailure`, never by reading the trigger's
250
+ * message.
251
+ *
252
+ * Not exported from the library: `resolve()` is its only caller, so a
253
+ * non-numeric consequence reaches storage through a resolution or not at all.
254
+ */
255
+ export declare function setProjectedValue(params: {
256
+ entityId: string;
257
+ key: string;
258
+ value: string | number | null;
259
+ }): SetTransition;
@@ -669,3 +669,81 @@ export function valueHistory(entityId, key, limit) {
669
669
  const limited = limit !== undefined ? ranked.slice(0, limit) : ranked;
670
670
  return limited.map((r) => r.transition);
671
671
  }
672
+ /** The numeric members of the constraint family. A key carrying one of them
673
+ * changes by a write (`writeConstrainedValue`), where the constraint is
674
+ * evaluated; a `set` would step round it. */
675
+ const NUMERIC_CONSTRAINT_KINDS = new Set(["monotonic", "bounded", "conserved"]);
676
+ function openFactId(db, entityId, key) {
677
+ const row = db
678
+ .prepare(`SELECT id FROM facts WHERE entity_id = ? AND key = ? AND valid_to_t IS NULL ORDER BY valid_from_t DESC, id DESC LIMIT 1`)
679
+ .get(entityId, key);
680
+ return row?.id ?? null;
681
+ }
682
+ /**
683
+ * Sets one live column of an entity's projected table to `value` (issue #32).
684
+ * The engine stores what it is handed and never learns what the column means;
685
+ * the projection triggers version it exactly as they version every other
686
+ * column write, so nothing here touches `facts` directly.
687
+ *
688
+ * The same choke point as numeric writes, and the same checks in the same
689
+ * voice: the entity and column are resolved against `PROJECTED_TABLES` /
690
+ * `liveColumns` (never a caller-supplied table name); the column that places
691
+ * the entity in its game is refused; a key carrying a numeric constraint is
692
+ * refused, because that value changes by a write where the constraint is
693
+ * evaluated; `resolve_only` asks the one question it always asks, whether an
694
+ * adjudication window is open; and a contradicted irreversible fact is
695
+ * translated into a typed `ConstraintViolationError` with the one hop
696
+ * attached, by `translateIrreversibleFailure`, never by reading the trigger's
697
+ * message.
698
+ *
699
+ * Not exported from the library: `resolve()` is its only caller, so a
700
+ * non-numeric consequence reaches storage through a resolution or not at all.
701
+ */
702
+ export function setProjectedValue(params) {
703
+ const resolved = resolveProjection(params.entityId, params.key);
704
+ const db = getDatabase();
705
+ const projected = PROJECTED_TABLES.find((p) => p.table === resolved.table);
706
+ if (projected && projected.gameIdColumn === params.key) {
707
+ throw new Error(`timeline: '${params.key}' places entity '${params.entityId}' in its game and cannot be set -- ` +
708
+ `an entity does not move between games`);
709
+ }
710
+ for (const constraint of constraintsFor(params.entityId, params.key)) {
711
+ if (NUMERIC_CONSTRAINT_KINDS.has(constraint.kind)) {
712
+ throw new ConstraintViolationError(constraint.kind, params.entityId, `Entity '${params.entityId}' is ${constraint.kind}-constrained for key '${params.key}', which a set does not ` +
713
+ `evaluate; this value changes by a write, where the constraint is checked.`);
714
+ }
715
+ if (constraint.kind === "resolve_only" && !adjudicationOpen()) {
716
+ throw new ConstraintViolationError("resolve_only", params.entityId, `Entity '${params.entityId}' is resolve_only-constrained for key '${params.key}'; direct writes are refused. ` +
717
+ `This value can only change through the adjudicating call that opens the resolution window.`);
718
+ }
719
+ }
720
+ const row = db.prepare(`SELECT ${resolved.key} AS value FROM ${resolved.table} WHERE id = ?`).get(resolved.entityId);
721
+ if (!row) {
722
+ throw new Error(`timeline: no live row in '${resolved.table}' for entity '${resolved.entityId}' -- ` +
723
+ `it may have been destroyed since it was last confirmed to exist`);
724
+ }
725
+ try {
726
+ return withTransaction(() => {
727
+ const before = openFactId(db, resolved.entityId, resolved.key);
728
+ db.prepare(`UPDATE ${resolved.table} SET ${resolved.key} = ? WHERE id = ?`).run(params.value, resolved.entityId);
729
+ const after = openFactId(db, resolved.entityId, resolved.key);
730
+ const story = currentStoryTime(resolved.gameId);
731
+ if (!story) {
732
+ throw new Error(`timeline: game '${resolved.gameId}' has no timeline clock -- a set has no t to attach to`);
733
+ }
734
+ return {
735
+ entityId: resolved.entityId,
736
+ key: resolved.key,
737
+ previousValue: row.value,
738
+ newValue: params.value,
739
+ t: story.t,
740
+ factId: after !== before ? after : null,
741
+ };
742
+ });
743
+ }
744
+ catch (err) {
745
+ if (params.value === null)
746
+ throw err;
747
+ translateIrreversibleFailure(db, [{ entityId: params.entityId, key: params.key, table: resolved.table, attemptedValue: params.value }], err);
748
+ }
749
+ }
@@ -1,6 +1,6 @@
1
1
  import { type T } from "./t.js";
2
2
  import { type NarrationConstraint, type Contradiction } from "./narration.js";
3
- import { type ValueTransition } from "./constrained.js";
3
+ import { type ValueTransition, type SetTransition } from "./constrained.js";
4
4
  /**
5
5
  * The inbound half of authority (design §5.2a, GitHub issue #10): propose ->
6
6
  * adjudicate -> outcome. The engine enforces the PROTOCOL -- resolution
@@ -71,8 +71,9 @@ import { type ValueTransition } from "./constrained.js";
71
71
  * INSIDE it (adjudication.ts's own doc comment asks for exactly this
72
72
  * nesting, so the window row rolls back with the writes it
73
73
  * authorized). Every change goes through `writeConstrainedValue` /
74
- * `transferConstrainedValue` -- the one choke point (root CLAUDE.md
75
- * hard rule 7) -- never a direct write. A constraint violation
74
+ * `transferConstrainedValue` / `setProjectedValue` (issue #32, a
75
+ * non-numeric column) -- the one choke point (root CLAUDE.md hard
76
+ * rule 7) -- never a direct write. A constraint violation
76
77
  * anywhere in the list propagates out of the transaction untouched
77
78
  * (never caught and re-labelled here) and rolls back EVERY change the
78
79
  * transaction made, including ones that individually would have
@@ -169,7 +170,17 @@ export interface IntendedTransfer {
169
170
  maxValue: number | null;
170
171
  };
171
172
  }
172
- export type IntendedChange = IntendedWrite | IntendedTransfer;
173
+ /** One intended set of a non-numeric column on an entity's projected row
174
+ * (issue #32) -- a thing changing owner, a character changing place. The
175
+ * engine stores `value` and never learns what `key` means; see
176
+ * `setProjectedValue` (constrained.ts) for what it refuses. */
177
+ export interface IntendedSet {
178
+ kind: "set";
179
+ entityId: string;
180
+ key: string;
181
+ value: string | number | null;
182
+ }
183
+ export type IntendedChange = IntendedWrite | IntendedTransfer | IntendedSet;
173
184
  /**
174
185
  * What a mechanic returns. `changes` are intents, not writes -- `resolve()`
175
186
  * applies every one of them through the one choke point (step 5); the
@@ -201,6 +212,9 @@ export interface Outcome {
201
212
  t: T;
202
213
  result: Record<string, unknown>;
203
214
  transitions: ValueTransition[];
215
+ /** Every `set` this resolution applied, in order (issue #32). Kept apart
216
+ * from `transitions`, whose values are numbers. */
217
+ sets: SetTransition[];
204
218
  constraint: NarrationConstraint;
205
219
  eventId: string;
206
220
  }
@@ -3,7 +3,7 @@ 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 { writeConstrainedValue, transferConstrainedValue } from "./constrained.js";
6
+ import { writeConstrainedValue, transferConstrainedValue, setProjectedValue } from "./constrained.js";
7
7
  /**
8
8
  * Refused before dispatch, before any write, or (never, by construction --
9
9
  * see step 5 above) mid-apply. `reason` is the discriminant a caller
@@ -81,6 +81,9 @@ export function createResolver(params) {
81
81
  };
82
82
  }
83
83
  function applyChange(change) {
84
+ if (change.kind === "set") {
85
+ return setProjectedValue({ entityId: change.entityId, key: change.key, value: change.value });
86
+ }
84
87
  if (change.kind === "write") {
85
88
  return [
86
89
  writeConstrainedValue({
@@ -180,8 +183,13 @@ function resolveProposal(mechanicsByName, proposal) {
180
183
  // is never relabelled as a ResolveProtocolError).
181
184
  const applied = withTransaction(() => withAdjudicationOpen(gameId, () => {
182
185
  const transitions = [];
186
+ const sets = [];
183
187
  for (const change of changes) {
184
- transitions.push(...applyChange(change));
188
+ const applied = applyChange(change);
189
+ if (Array.isArray(applied))
190
+ transitions.push(...applied);
191
+ else
192
+ sets.push(applied);
185
193
  }
186
194
  // Re-read the clock AFTER every write has landed, inside this same
187
195
  // transaction -- a sequence-axis game advances its own t once per
@@ -206,7 +214,7 @@ function resolveProposal(mechanicsByName, proposal) {
206
214
  getDatabase()
207
215
  .prepare(`INSERT INTO events (id, game_id, at_t, kind, description, causes) VALUES (?, ?, ?, 'resolution.recorded', ?, ?)`)
208
216
  .run(eventId, gameId, postStory.t, adjudication.description ?? null, JSON.stringify(causes));
209
- return { transitions, eventId, postT: postStory.t };
217
+ return { transitions, sets, eventId, postT: postStory.t };
210
218
  }));
211
219
  // 7. The outcome's constraint, built AFTER the writes landed and the
212
220
  // transaction holding them has already committed -- reachable only from a
@@ -220,6 +228,7 @@ function resolveProposal(mechanicsByName, proposal) {
220
228
  t,
221
229
  result: adjudication.result ?? {},
222
230
  transitions: applied.transitions,
231
+ sets: applied.sets,
223
232
  constraint: postConstraint,
224
233
  eventId: applied.eventId,
225
234
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run-dmcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",