run-dmcp 0.9.0 → 0.10.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.
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { ANNOTATIONS } from "../utils/tool-annotations.js";
3
3
  import { createLogger } from "../utils/logger.js";
4
4
  import { replay } from "../timeline/replay.js";
5
+ import { declareTurnOrder, dueAt, advanceTurn } from "../timeline/turns.js";
5
6
  import { declareTimeAxis, setStoryTime, currentStoryTime } from "../timeline/clock.js";
6
7
  import { declareIrreversible, listIrreversibleFacts } from "../timeline/irreversible.js";
7
8
  import { exportTimelineToFile, importTimelineFromFile } from "../timeline/export.js";
@@ -46,11 +47,16 @@ export function registerTimelineTools(server) {
46
47
  inputSchema: {
47
48
  gameId: z.string().max(100).describe("The game ID"),
48
49
  t: tSchema,
50
+ entityIds: z
51
+ .array(z.string().max(100))
52
+ .max(10000)
53
+ .optional()
54
+ .describe("Scope the read to these entities -- a selection you built, such as what one character perceives. Omitted: every entity. An empty list: none. An id not alive at t is simply absent."),
49
55
  },
50
56
  annotations: ANNOTATIONS.READ_ONLY,
51
- }, async ({ gameId, t }) => {
57
+ }, async ({ gameId, t, entityIds }) => {
52
58
  try {
53
- const snapshot = replay({ gameId, t });
59
+ const snapshot = replay({ gameId, t, entityIds });
54
60
  return { content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }] };
55
61
  }
56
62
  catch (error) {
@@ -308,4 +314,25 @@ export function registerTimelineTools(server) {
308
314
  };
309
315
  }
310
316
  });
317
+ // Issue #40: a principal is due to act. The engine records whose turn it
318
+ // is and refuses an out-of-turn `resolve`; it never runs a turn.
319
+ const turnTool = (name, description, inputSchema, annotations, run) => server.registerTool(name, { description, inputSchema, annotations }, async (args) => {
320
+ try {
321
+ return { content: [{ type: "text", text: JSON.stringify(run(args), null, 2) }] };
322
+ }
323
+ catch (error) {
324
+ log.error(`${name} failed`, { error: error.message });
325
+ return { content: [{ type: "text", text: JSON.stringify({ error: error.message }) }], isError: true };
326
+ }
327
+ });
328
+ turnTool("declare_turn_order", "Declare which entities act, in what cycle, one per t on this game's counter axis from fromT onward. Append-only: " +
329
+ "to change the order, declare again from a later t -- earlier turns keep answering as they did. Once declared, a resolve " +
330
+ "naming an actor who is not due at the current t is refused. The engine records whose turn it is; it never runs one.", {
331
+ gameId: z.string().max(100).describe("The game ID"),
332
+ principals: z.array(z.string().max(100)).max(1000).describe("Entity ids, in the order they act."),
333
+ fromT: tSchema.describe("The first t this order covers. An integer t on the counter axis, not before the current t."),
334
+ }, ANNOTATIONS.UPDATE, (a) => declareTurnOrder({ gameId: a.gameId, principals: a.principals, fromT: a.fromT }));
335
+ turnTool("due_at", "Who is due to act at t under this game's declared turn order -- any t, past included -- with the round and " +
336
+ "position in the cycle. {due: null} when no declaration covers t.", { gameId: z.string().max(100).describe("The game ID"), t: tSchema }, ANNOTATIONS.READ_ONLY, (a) => dueAt({ gameId: a.gameId, t: a.t }) ?? { due: null });
337
+ turnTool("advance_turn", "Move this game's clock to the next turn and say who is due there. Moves time only; runs nothing.", { gameId: z.string().max(100).describe("The game ID") }, ANNOTATIONS.UPDATE, (a) => advanceTurn({ gameId: a.gameId }));
311
338
  }
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { Mechanic } from "../timeline/resolve.js";
3
3
  import type { RenderVocabulary } from "../timeline/render.js";
4
+ import type { DeclaredRules } from "../timeline/declared.js";
4
5
  /**
5
6
  * Build the FULL assembly: every core tool plus every RPG tool, resource and
6
7
  * prompt this engine has always served. Same name and same `{ mechanics?,
@@ -12,5 +13,6 @@ import type { RenderVocabulary } from "../timeline/render.js";
12
13
  export declare function createMcpServer(options?: {
13
14
  mechanics?: readonly Mechanic[];
14
15
  vocabulary?: RenderVocabulary;
16
+ rules?: DeclaredRules;
15
17
  }): McpServer;
16
18
  export { createHttpServer, startHttpServer } from "../http/server.js";
@@ -83,14 +83,14 @@ export declare const subjectDescriptionSchema: z.ZodObject<{
83
83
  condition: z.ZodOptional<z.ZodString>;
84
84
  glowOrEffects: z.ZodOptional<z.ZodString>;
85
85
  }, "strip", z.ZodTypeAny, {
86
+ condition?: string | undefined;
86
87
  size?: string | undefined;
87
88
  material?: string | undefined;
88
- condition?: string | undefined;
89
89
  glowOrEffects?: string | undefined;
90
90
  }, {
91
+ condition?: string | undefined;
91
92
  size?: string | undefined;
92
93
  material?: string | undefined;
93
- condition?: string | undefined;
94
94
  glowOrEffects?: string | undefined;
95
95
  }>>;
96
96
  pose: z.ZodOptional<z.ZodString>;
@@ -127,9 +127,9 @@ export declare const subjectDescriptionSchema: z.ZodObject<{
127
127
  vegetation?: string | undefined;
128
128
  } | undefined;
129
129
  objectDetails?: {
130
+ condition?: string | undefined;
130
131
  size?: string | undefined;
131
132
  material?: string | undefined;
132
- condition?: string | undefined;
133
133
  glowOrEffects?: string | undefined;
134
134
  } | undefined;
135
135
  pose?: string | undefined;
@@ -166,9 +166,9 @@ export declare const subjectDescriptionSchema: z.ZodObject<{
166
166
  vegetation?: string | undefined;
167
167
  } | undefined;
168
168
  objectDetails?: {
169
+ condition?: string | undefined;
169
170
  size?: string | undefined;
170
171
  material?: string | undefined;
171
- condition?: string | undefined;
172
172
  glowOrEffects?: string | undefined;
173
173
  } | undefined;
174
174
  pose?: string | undefined;
@@ -394,14 +394,14 @@ export declare const imageGenSchema: z.ZodObject<{
394
394
  condition: z.ZodOptional<z.ZodString>;
395
395
  glowOrEffects: z.ZodOptional<z.ZodString>;
396
396
  }, "strip", z.ZodTypeAny, {
397
+ condition?: string | undefined;
397
398
  size?: string | undefined;
398
399
  material?: string | undefined;
399
- condition?: string | undefined;
400
400
  glowOrEffects?: string | undefined;
401
401
  }, {
402
+ condition?: string | undefined;
402
403
  size?: string | undefined;
403
404
  material?: string | undefined;
404
- condition?: string | undefined;
405
405
  glowOrEffects?: string | undefined;
406
406
  }>>;
407
407
  pose: z.ZodOptional<z.ZodString>;
@@ -438,9 +438,9 @@ export declare const imageGenSchema: z.ZodObject<{
438
438
  vegetation?: string | undefined;
439
439
  } | undefined;
440
440
  objectDetails?: {
441
+ condition?: string | undefined;
441
442
  size?: string | undefined;
442
443
  material?: string | undefined;
443
- condition?: string | undefined;
444
444
  glowOrEffects?: string | undefined;
445
445
  } | undefined;
446
446
  pose?: string | undefined;
@@ -477,9 +477,9 @@ export declare const imageGenSchema: z.ZodObject<{
477
477
  vegetation?: string | undefined;
478
478
  } | undefined;
479
479
  objectDetails?: {
480
+ condition?: string | undefined;
480
481
  size?: string | undefined;
481
482
  material?: string | undefined;
482
- condition?: string | undefined;
483
483
  glowOrEffects?: string | undefined;
484
484
  } | undefined;
485
485
  pose?: string | undefined;
@@ -726,9 +726,9 @@ export declare const imageGenSchema: z.ZodObject<{
726
726
  vegetation?: string | undefined;
727
727
  } | undefined;
728
728
  objectDetails?: {
729
+ condition?: string | undefined;
729
730
  size?: string | undefined;
730
731
  material?: string | undefined;
731
- condition?: string | undefined;
732
732
  glowOrEffects?: string | undefined;
733
733
  } | undefined;
734
734
  pose?: string | undefined;
@@ -822,9 +822,9 @@ export declare const imageGenSchema: z.ZodObject<{
822
822
  vegetation?: string | undefined;
823
823
  } | undefined;
824
824
  objectDetails?: {
825
+ condition?: string | undefined;
825
826
  size?: string | undefined;
826
827
  material?: string | undefined;
827
- condition?: string | undefined;
828
828
  glowOrEffects?: string | undefined;
829
829
  } | undefined;
830
830
  pose?: string | undefined;
@@ -190,11 +190,10 @@ export declare function transferConstrainedValue(params: {
190
190
  * Fact transitions are joined to their annotation (if any) by
191
191
  * `json_extract(causes, '$.fact_id') = facts.id` -- an EXACT, unique link,
192
192
  * because `applyLiveWrite` recorded the fact id at write time. This is
193
- * deliberately stronger than irreversible.ts's `findOpenedByEventId`, which
194
- * has to approximate the same relationship via `(at_t, row_id)` because the
195
- * projection triggers that write `row_id` have no fact id to record at the
196
- * point they fire (issue #2 predates this module). Here, recording the real
197
- * id costs nothing extra and removes the approximation entirely.
193
+ * the same move issue #30 later made for a fact's opening event
194
+ * (`facts.opened_by_event_id`, stamped by the projection trigger in the
195
+ * firing that opens the fact): record the edge when it is true rather than
196
+ * approximate it afterward from `(at_t, row_id)`.
198
197
  *
199
198
  * `json_valid(causes)` guards every extraction, matching irreversible.ts's
200
199
  * `CASE WHEN json_valid(causes) THEN causes END` idiom for the same reason
@@ -122,12 +122,23 @@ export function assertConstraintsAllow(params) {
122
122
  throw new ConstraintViolationError("monotonic", entityId, `Resource '${entityId}' is constrained to never increase; rejected change from ${previousValue} to ${intendedValue}.`);
123
123
  }
124
124
  }
125
- if (constraint.kind === "bounded" && bounds) {
126
- if (bounds.minValue !== null && intendedValue < bounds.minValue) {
127
- throw new ConstraintViolationError("bounded", entityId, `Resource '${entityId}' is bounded-constrained (min ${bounds.minValue}); rejected value ${intendedValue} instead of clamping.`);
128
- }
129
- if (bounds.maxValue !== null && intendedValue > bounds.maxValue) {
130
- throw new ConstraintViolationError("bounded", entityId, `Resource '${entityId}' is bounded-constrained (max ${bounds.maxValue}); rejected value ${intendedValue} instead of clamping.`);
125
+ if (constraint.kind === "bounded") {
126
+ // Two sources of bounds, both checked, so the tighter side wins. The
127
+ // write's own `bounds` are how every 'bounded' constraint has always
128
+ // worked (a resource's min/max columns, supplied by its caller). A
129
+ // constraint DECLARED with bounds -- a `create` leg's `constraints`
130
+ // (issue #42) -- carries its own, and holds them against a write that
131
+ // supplies none: "held bounded from the moment it exists" means the
132
+ // declaration binds, not that every later writer must repeat it.
133
+ // Declared nulls leave that side to the write, as before.
134
+ const declared = { minValue: constraint.minValue ?? null, maxValue: constraint.maxValue ?? null };
135
+ for (const pair of bounds ? [declared, bounds] : [declared]) {
136
+ if (pair.minValue !== null && intendedValue < pair.minValue) {
137
+ throw new ConstraintViolationError("bounded", entityId, `Resource '${entityId}' is bounded-constrained (min ${pair.minValue}); rejected value ${intendedValue} instead of clamping.`);
138
+ }
139
+ if (pair.maxValue !== null && intendedValue > pair.maxValue) {
140
+ throw new ConstraintViolationError("bounded", entityId, `Resource '${entityId}' is bounded-constrained (max ${pair.maxValue}); rejected value ${intendedValue} instead of clamping.`);
141
+ }
131
142
  }
132
143
  }
133
144
  // 'resolve_only' (design §5.3, §5.2a; issue #13): every direct write to
@@ -270,20 +281,16 @@ function applyLiveWrite(params) {
270
281
  const at = new Date().toISOString();
271
282
  const delta = newValue - previousValue;
272
283
  const eventId = uuidv4();
273
- // `causes` deliberately does NOT carry a `row_id` key. irreversible.ts's
274
- // `findOpenedByEventId` matches `json_extract(causes, '$.row_id')` to
275
- // attach design §5.2c's one hop of provenance, picking the first event at
276
- // a given `t` by a random hex id when more than one matches. `row_id` is
277
- // the PROJECTION triggers' own token for "the live row this projection
278
- // event was generated from" (projection.ts) -- an annotation event this
279
- // choke point writes is not a projection event, and if it carried
280
- // `row_id` too, a projection event and an annotation event sharing one
281
- // `t` (which a constrained write's own UPDATE produces: the `_au`
282
- // trigger's `<kind>.updated` event and this `value.changed` event both
283
- // land at the same `t`) would make `findOpenedByEventId`'s pick
284
- // non-deterministic. `entity_id` is the accurate key for what this event
285
- // is about anyway, so using it instead of `row_id` is both the honest
286
- // name and the one that can never collide with that lookup.
284
+ // `causes` carries `entity_id`, not `row_id`. `row_id` is the PROJECTION
285
+ // triggers' own token for "the live row this projection event was
286
+ // generated from" (projection.ts), and an annotation event this choke
287
+ // point writes is not a projection event, so `entity_id` is the honest
288
+ // name for what it is about. (This once also kept a derived one-hop
289
+ // lookup deterministic when a projection event and this event shared a
290
+ // `t`; issue #30 replaced that lookup with the recorded
291
+ // `facts.opened_by_event_id`, so that reason is gone. `projectionEventId`
292
+ // below still matches `$.row_id`, but filters by event kind, so it could
293
+ // not collide either way.)
287
294
  const causes = JSON.stringify({
288
295
  source: "constrained_write",
289
296
  entity_id: entityId,
@@ -555,11 +562,10 @@ export function transferConstrainedValue(params) {
555
562
  * Fact transitions are joined to their annotation (if any) by
556
563
  * `json_extract(causes, '$.fact_id') = facts.id` -- an EXACT, unique link,
557
564
  * because `applyLiveWrite` recorded the fact id at write time. This is
558
- * deliberately stronger than irreversible.ts's `findOpenedByEventId`, which
559
- * has to approximate the same relationship via `(at_t, row_id)` because the
560
- * projection triggers that write `row_id` have no fact id to record at the
561
- * point they fire (issue #2 predates this module). Here, recording the real
562
- * id costs nothing extra and removes the approximation entirely.
565
+ * the same move issue #30 later made for a fact's opening event
566
+ * (`facts.opened_by_event_id`, stamped by the projection trigger in the
567
+ * firing that opens the fact): record the edge when it is true rather than
568
+ * approximate it afterward from `(at_t, row_id)`.
563
569
  *
564
570
  * `json_valid(causes)` guards every extraction, matching irreversible.ts's
565
571
  * `CASE WHEN json_valid(causes) THEN causes END` idiom for the same reason
@@ -0,0 +1,147 @@
1
+ import { type T } from "./t.js";
2
+ import type { Mechanic } from "./resolve.js";
3
+ /**
4
+ * Mechanics, gates and end conditions as DECLARED DATA (GitHub issue #41),
5
+ * so a stock server can load a game whose rules would otherwise be
6
+ * TypeScript handed to `createResolver` by whoever assembled it.
7
+ *
8
+ * DELIBERATELY SMALL. The issue's admission paragraph scopes the first cut to
9
+ * what its real caller's rules are made of -- bounded changes to numeric
10
+ * facts, and gates that open once a fact crosses a threshold -- "a small,
11
+ * generic subset to start from, not a general rules language". So:
12
+ *
13
+ * - a CONDITION is a named set of clauses (`all` or `any`), each comparing
14
+ * one stored fact to a declared value, or naming another condition;
15
+ * - a DECLARED MECHANIC applies its `legs` when every condition in `when`
16
+ * holds, and its `otherwise` legs (or nothing) when one does not;
17
+ * - a leg is `adjust` (current + sign * amount, clamped) or `set`.
18
+ *
19
+ * Anything else stays a TypeScript `Mechanic` -- that is the escape hatch, and
20
+ * both kinds register side by side on one resolver. A rule that needs an
21
+ * event ("after a search") rather than a fact, or arithmetic across
22
+ * entities, is the escape hatch's, not a reason to grow this.
23
+ *
24
+ * THE ENGINE READS NONE OF IT FOR MEANING (hard rule 4). `id`, `name`,
25
+ * `for`, `then` and `text` are opaque strings, compared for equality or
26
+ * carried through. A clause compares a stored value with a declared one:
27
+ * numerically when both are finite numbers, by equality otherwise -- a
28
+ * structural comparison of the same kind `contradictions()` makes, never an
29
+ * interpretation. `holds` is that comparison's result (hard rule 2): a row
30
+ * saying a stored value meets a declared threshold, never "the game is won".
31
+ *
32
+ * WHY `for`/`then` LIVE HERE. A principal's condition list -- "if these hold,
33
+ * this becomes available to you" -- is what measurably made model-driven
34
+ * principals act on their unlocks. Declared once, the list a caller renders
35
+ * and the gate a mechanic enforces are the SAME data, instead of prose kept
36
+ * in step with code by a test.
37
+ *
38
+ * All of it is plain JSON, so it round-trips through a file unchanged; the
39
+ * application loads one from `DMCP_RULES_FILE` (src/bin/run-dmcp.ts).
40
+ */
41
+ export type ComparisonOp = "<" | "<=" | "==" | "!=" | ">=" | ">";
42
+ /** A value from the proposal's own parameters, by name. */
43
+ export interface ParamRef {
44
+ param: string;
45
+ }
46
+ /** An entity: its id, a proposal parameter holding its id, or its name --
47
+ * resolved against the entities that have facts at `t`, and reported as
48
+ * unresolved when no entity or more than one carries that name. */
49
+ export type EntityOperand = string | ParamRef | {
50
+ named: string;
51
+ };
52
+ export type FactClause = {
53
+ entity: EntityOperand;
54
+ key: string;
55
+ op: ComparisonOp;
56
+ value: number | string | ParamRef;
57
+ /** Opaque, carried through for a caller's condition list. */
58
+ text?: string;
59
+ };
60
+ export type ConditionClause = FactClause | {
61
+ condition: string;
62
+ };
63
+ export interface DeclaredCondition {
64
+ id: string;
65
+ /** Opaque: whose condition this is, for rendering one principal's list. */
66
+ for?: string;
67
+ /** Opaque: what holding it makes available. */
68
+ then?: string;
69
+ all?: readonly ConditionClause[];
70
+ any?: readonly ConditionClause[];
71
+ }
72
+ export type NumberOperand = number | ParamRef;
73
+ export type DeclaredLeg = {
74
+ kind: "adjust";
75
+ entity: EntityOperand;
76
+ key: string;
77
+ amount: NumberOperand;
78
+ /** 1 adds, -1 subtracts. Default 1. */
79
+ sign?: 1 | -1;
80
+ min?: NumberOperand | null;
81
+ max?: NumberOperand | null;
82
+ } | {
83
+ kind: "set";
84
+ entity: EntityOperand;
85
+ key: string;
86
+ value: number | string | null | ParamRef;
87
+ min?: NumberOperand | null;
88
+ max?: NumberOperand | null;
89
+ };
90
+ export interface DeclaredMechanic {
91
+ name: string;
92
+ /** Condition ids that must ALL hold for `legs` to apply. */
93
+ when?: readonly string[];
94
+ legs: readonly DeclaredLeg[];
95
+ /** Applied instead when a `when` condition does not hold. */
96
+ otherwise?: readonly DeclaredLeg[];
97
+ }
98
+ export interface DeclaredRules {
99
+ conditions: readonly DeclaredCondition[];
100
+ mechanics: readonly DeclaredMechanic[];
101
+ }
102
+ export interface ClauseRow {
103
+ /** Present for a fact clause. */
104
+ entityId?: string | null;
105
+ resolution?: "resolved" | "none" | "ambiguous";
106
+ key?: string;
107
+ op?: ComparisonOp;
108
+ value?: number | string;
109
+ observed?: number | string | null;
110
+ /** Present for a clause naming another condition. */
111
+ condition?: string;
112
+ /** The proposal parameter this clause needed and was not given; the clause
113
+ * then does not hold. A gate that fails because of it is refused naming it. */
114
+ missingParameter?: string;
115
+ text?: string;
116
+ holds: boolean;
117
+ }
118
+ export interface ConditionRow {
119
+ id: string;
120
+ for?: string;
121
+ then?: string;
122
+ holds: boolean;
123
+ clauses: ClauseRow[];
124
+ }
125
+ export declare function validateDeclaredRules(rules: DeclaredRules): void;
126
+ type Params = Readonly<Record<string, unknown>>;
127
+ /**
128
+ * Every declared condition at `t`, in declared order, with whether it holds
129
+ * and each clause's observed value -- or only the conditions whose `for`
130
+ * equals the one given, which is a principal's condition list. Read-only.
131
+ */
132
+ export declare function evaluateConditions(params: {
133
+ gameId: string;
134
+ t: T;
135
+ rules: DeclaredRules;
136
+ for?: string;
137
+ parameters?: Params;
138
+ }): ConditionRow[];
139
+ /**
140
+ * One `Mechanic` per declared mechanic, for `createResolver` beside any
141
+ * hand-written ones. Each reads only the constraint `resolve()` hands it, like
142
+ * every mechanic, and returns ordinary changes that go through the one choke
143
+ * point. `result` records what it did: which gate conditions held, which leg
144
+ * set applied (`legs`, `otherwise`, or `none`), and each leg's before/after.
145
+ */
146
+ export declare function declaredMechanics(rules: DeclaredRules): Mechanic[];
147
+ export {};