run-dmcp 0.1.0 → 0.2.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.
Files changed (135) hide show
  1. package/README.md +76 -10
  2. package/dist/bin/run-dmcp.d.ts +2 -0
  3. package/dist/bin/run-dmcp.js +55 -0
  4. package/dist/db/connection.d.ts +32 -0
  5. package/dist/db/connection.js +38 -16
  6. package/dist/db/schema.d.ts +29 -1
  7. package/dist/db/schema.js +439 -7
  8. package/dist/http/server.js +3 -3
  9. package/dist/index.d.ts +36 -2
  10. package/dist/index.js +184 -92
  11. package/dist/mcp-server.d.ts +49 -0
  12. package/dist/mcp-server.js +127 -0
  13. package/dist/reader/turnReader.d.ts +185 -0
  14. package/dist/reader/turnReader.js +288 -0
  15. package/dist/register/batch.js +5 -79
  16. package/dist/register/mcp-resources.d.ts +9 -0
  17. package/dist/register/mcp-resources.js +16 -62
  18. package/dist/register/render.d.ts +17 -0
  19. package/dist/register/render.js +50 -0
  20. package/dist/register/resolve.d.ts +14 -0
  21. package/dist/register/resolve.js +102 -0
  22. package/dist/register/resources.js +11 -4
  23. package/dist/register/timeline.d.ts +2 -0
  24. package/dist/register/timeline.js +311 -0
  25. package/dist/rpg/index.d.ts +29 -0
  26. package/dist/rpg/index.js +55 -0
  27. package/dist/rpg/register/abilities.d.ts +2 -0
  28. package/dist/rpg/register/abilities.js +165 -0
  29. package/dist/rpg/register/batch.d.ts +2 -0
  30. package/dist/rpg/register/batch.js +92 -0
  31. package/dist/rpg/register/combat.d.ts +2 -0
  32. package/dist/rpg/register/combat.js +207 -0
  33. package/dist/rpg/register/mcp-prompts.d.ts +2 -0
  34. package/dist/rpg/register/mcp-prompts.js +684 -0
  35. package/dist/rpg/register/mcp-resources.d.ts +2 -0
  36. package/dist/rpg/register/mcp-resources.js +61 -0
  37. package/dist/rpg/register/quests.d.ts +2 -0
  38. package/dist/rpg/register/quests.js +118 -0
  39. package/dist/rpg/register/status.d.ts +2 -0
  40. package/dist/rpg/register/status.js +130 -0
  41. package/dist/rpg/register/tables.d.ts +2 -0
  42. package/dist/rpg/register/tables.js +146 -0
  43. package/dist/rpg/tools/ability.d.ts +48 -0
  44. package/dist/rpg/tools/ability.js +238 -0
  45. package/dist/rpg/tools/combat.d.ts +13 -0
  46. package/dist/rpg/tools/combat.js +195 -0
  47. package/dist/rpg/tools/dice.d.ts +23 -0
  48. package/dist/rpg/tools/dice.js +111 -0
  49. package/dist/rpg/tools/quest.d.ts +34 -0
  50. package/dist/rpg/tools/quest.js +164 -0
  51. package/dist/rpg/tools/status.d.ts +36 -0
  52. package/dist/rpg/tools/status.js +218 -0
  53. package/dist/rpg/tools/tables.d.ts +33 -0
  54. package/dist/rpg/tools/tables.js +209 -0
  55. package/dist/schemas/index.d.ts +12 -12
  56. package/dist/timeline/adjudication.d.ts +150 -0
  57. package/dist/timeline/adjudication.js +174 -0
  58. package/dist/timeline/changes.d.ts +100 -0
  59. package/dist/timeline/changes.js +161 -0
  60. package/dist/timeline/checkpoint.d.ts +69 -0
  61. package/dist/timeline/checkpoint.js +131 -0
  62. package/dist/timeline/clock.d.ts +89 -0
  63. package/dist/timeline/clock.js +173 -0
  64. package/dist/timeline/constrained.d.ts +220 -0
  65. package/dist/timeline/constrained.js +671 -0
  66. package/dist/timeline/export.d.ts +171 -0
  67. package/dist/timeline/export.js +329 -0
  68. package/dist/timeline/irreversible.d.ts +85 -0
  69. package/dist/timeline/irreversible.js +108 -0
  70. package/dist/timeline/kinds.d.ts +14 -0
  71. package/dist/timeline/kinds.js +22 -0
  72. package/dist/timeline/narration.d.ts +175 -0
  73. package/dist/timeline/narration.js +259 -0
  74. package/dist/timeline/projection.d.ts +97 -0
  75. package/dist/timeline/projection.js +330 -0
  76. package/dist/timeline/provenance.d.ts +66 -0
  77. package/dist/timeline/provenance.js +45 -0
  78. package/dist/timeline/registry.d.ts +95 -0
  79. package/dist/timeline/registry.js +124 -0
  80. package/dist/timeline/render.d.ts +121 -0
  81. package/dist/timeline/render.js +187 -0
  82. package/dist/timeline/replay.d.ts +64 -0
  83. package/dist/timeline/replay.js +104 -0
  84. package/dist/timeline/resolve.d.ts +262 -0
  85. package/dist/timeline/resolve.js +226 -0
  86. package/dist/timeline/schema.d.ts +13 -0
  87. package/dist/timeline/schema.js +262 -0
  88. package/dist/timeline/t.d.ts +80 -0
  89. package/dist/timeline/t.js +37 -0
  90. package/dist/tools/constraint.d.ts +44 -80
  91. package/dist/tools/constraint.js +115 -124
  92. package/dist/tools/relationship.d.ts +83 -2
  93. package/dist/tools/relationship.js +139 -62
  94. package/dist/tools/resource.d.ts +31 -6
  95. package/dist/tools/resource.js +106 -153
  96. package/dist/types/index.d.ts +19 -1
  97. package/dist/utils/output-schemas.d.ts +593 -2
  98. package/dist/utils/output-schemas.js +3 -0
  99. package/dist/utils/webui.d.ts +32 -0
  100. package/dist/utils/webui.js +54 -1
  101. package/package.json +20 -4
  102. package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
  103. package/dist/__tests__/engineVocabulary.test.js +0 -147
  104. package/dist/db/__tests__/connection.test.d.ts +0 -1
  105. package/dist/db/__tests__/connection.test.js +0 -72
  106. package/dist/db/__tests__/testDb.d.ts +0 -33
  107. package/dist/db/__tests__/testDb.js +0 -41
  108. package/dist/test-setup.d.ts +0 -1
  109. package/dist/test-setup.js +0 -13
  110. package/dist/tools/__tests__/audio.test.d.ts +0 -1
  111. package/dist/tools/__tests__/audio.test.js +0 -59
  112. package/dist/tools/__tests__/conserved.test.d.ts +0 -1
  113. package/dist/tools/__tests__/conserved.test.js +0 -488
  114. package/dist/tools/__tests__/constraint.test.d.ts +0 -1
  115. package/dist/tools/__tests__/constraint.test.js +0 -212
  116. package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
  117. package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
  118. package/dist/tools/__tests__/images.test.d.ts +0 -1
  119. package/dist/tools/__tests__/images.test.js +0 -59
  120. package/dist/tools/__tests__/relationship.test.d.ts +0 -1
  121. package/dist/tools/__tests__/relationship.test.js +0 -132
  122. package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
  123. package/dist/tools/__tests__/resource-constraints.test.js +0 -131
  124. package/dist/tools/__tests__/resource.test.d.ts +0 -1
  125. package/dist/tools/__tests__/resource.test.js +0 -190
  126. package/dist/tools/__tests__/time.test.d.ts +0 -1
  127. package/dist/tools/__tests__/time.test.js +0 -404
  128. package/dist/tools/__tests__/timers.test.d.ts +0 -1
  129. package/dist/tools/__tests__/timers.test.js +0 -426
  130. package/dist/tools/__tests__/world.test.d.ts +0 -1
  131. package/dist/tools/__tests__/world.test.js +0 -70
  132. package/dist/utils/__tests__/json.test.d.ts +0 -1
  133. package/dist/utils/__tests__/json.test.js +0 -55
  134. package/dist/utils/__tests__/validation.test.d.ts +0 -1
  135. package/dist/utils/__tests__/validation.test.js +0 -90
@@ -2,6 +2,11 @@ import { v4 as uuidv4 } from "uuid";
2
2
  import { getDatabase } from "../db/connection.js";
3
3
  import { validateGameExists } from "./game.js";
4
4
  import { getResource } from "./resource.js";
5
+ import { ConstraintViolationError, CONSERVED_SUM_EPSILON, constraintsFor, allConstraintsForEntity, rowToConstraint, } from "../timeline/registry.js";
6
+ // Re-exported so every existing importer (src/tools/resource.ts,
7
+ // src/register/resources.ts) keeps working unchanged -- these two now live
8
+ // in src/timeline/registry.js; see the paragraph below on why.
9
+ export { ConstraintViolationError, CONSERVED_SUM_EPSILON };
5
10
  /**
6
11
  * Declarative, opt-in, server-enforced invariants on `resources` rows.
7
12
  *
@@ -22,52 +27,56 @@ import { getResource } from "./resource.js";
22
27
  *
23
28
  * 'conserved' is fully enforced: a resource that is a member of a declared
24
29
  * 'conserved' constraint can no longer be written directly through
25
- * update_resource_value (see checkResourceConstraints() below) -- it must go
26
- * through transferResourceValue() in resource.ts, which moves value between
27
- * exactly two members of the same set atomically. See the comment on
28
- * transferResourceValue() for why an explicit transfer, rather than a
29
- * balanced multi-resource write, was chosen.
30
+ * update_resource_value (see assertConstraintsAllow() in
31
+ * src/timeline/constrained.ts) -- it must go through transferResourceValue()
32
+ * in resource.ts, which moves value between exactly two members of the same
33
+ * set atomically. See the comment on transferResourceValue() for why an
34
+ * explicit transfer, rather than a balanced multi-resource write, was
35
+ * chosen.
36
+ *
37
+ * `resolve_only` (design §5.3, §5.2a; issue #13) is this family's fourth
38
+ * ROW-based member, alongside `bounded`/`monotonic`/`conserved`: every
39
+ * direct write to the fact key it governs is refused, so the value can move
40
+ * only through the adjudicating call issue #10's resolver will open (the
41
+ * window mechanism, `src/timeline/adjudication.ts`, lands here first --
42
+ * "a value that may move only through adjudication needs the adjudicator to
43
+ * exist first," design §5.3). Unlike the other three, it takes an explicit
44
+ * `factKey` parameter rather than always governing `'value'` -- see
45
+ * declareResolveOnlyConstraint() below.
46
+ *
47
+ * `irreversible` (design §5.3) is this family's FIFTH member and its only
48
+ * temporal, non-row one -- `bounded`/`monotonic` constrain a value's range
49
+ * and direction, 'conserved' constrains a set's total, 'resolve_only'
50
+ * constrains WHO may write a value, and `irreversible` constrains what may
51
+ * be asserted about a value after a point in time. It is declared per-FACT
52
+ * (src/timeline/irreversible.ts's declareIrreversible()), not as a row in
53
+ * `resource_constraints` here, and enforced by triggers on the timeline's
54
+ * `facts` table (src/timeline/schema.ts) rather than by
55
+ * assertConstraintsAllow(). The two families still live on different
56
+ * substrates -- this one on `resources`/`resource_constraints`, that one on
57
+ * the timeline's interval-versioned `facts`.
58
+ *
59
+ * Design §5.4's option (C) merge (Phase 3) is complete for `bounded`/
60
+ * `monotonic`/`conserved`/`resolve_only`: this file declares constraints
61
+ * (insertConstraint and the declare*() functions below) and reads them back
62
+ * for display (listConstraints/getConstraintsForResource); EVALUATING a
63
+ * declared constraint against an intended value change happens in exactly
64
+ * one place, assertConstraintsAllow() (src/timeline/constrained.ts), which
65
+ * every constrained write (writeConstrainedValue/transferConstrainedValue,
66
+ * and through them updateResourceValue/transferResourceValue/updateResource
67
+ * in resource.ts) goes through. Nothing in this file evaluates a constraint
68
+ * against a value any more -- grep for `constraint.kind === "bounded"` or
69
+ * `constraint.direction ===` and constrained.ts is the only hit outside a
70
+ * test. 'resolve_only' has no value-shape to evaluate (no bounds, no
71
+ * direction, no total) -- what constrained.ts checks for it is WHETHER an
72
+ * adjudication window is open (src/timeline/adjudication.ts), not anything
73
+ * about the intended value itself.
30
74
  */
31
- /** Absolute tolerance for floating-point sum comparisons on 'conserved'
32
- * constraints. IEEE 754 doubles cannot represent values like 0.1 exactly,
33
- * so repeated addition/subtraction across many transfers can drift by a
34
- * few ULPs. This is large enough to absorb that drift over realistic
35
- * transfer volumes while still catching an actual logic bug (which would
36
- * typically desync the sum by a whole `amount`, not a fraction of one). */
37
- export const CONSERVED_SUM_EPSILON = 1e-6;
38
- export class ConstraintViolationError extends Error {
39
- constraintKind;
40
- resourceId;
41
- constructor(constraintKind, resourceId, message) {
42
- super(message);
43
- this.constraintKind = constraintKind;
44
- this.resourceId = resourceId;
45
- this.name = "ConstraintViolationError";
46
- }
47
- }
48
- function memberIdsFor(constraintId) {
49
- const db = getDatabase();
50
- const rows = db
51
- .prepare(`SELECT resource_id FROM resource_constraint_members WHERE constraint_id = ? ORDER BY rowid`)
52
- .all(constraintId);
53
- return rows.map((r) => r.resource_id);
54
- }
55
- function rowToConstraint(row) {
56
- return {
57
- id: row.id,
58
- gameId: row.game_id,
59
- kind: row.kind,
60
- resourceIds: memberIdsFor(row.id),
61
- direction: row.direction,
62
- total: row.total,
63
- createdAt: row.created_at,
64
- };
65
- }
66
- function insertConstraint(gameId, kind, resourceIds, direction, total) {
75
+ function insertConstraint(gameId, kind, resourceIds, direction, total, factKey = "value") {
67
76
  const db = getDatabase();
68
77
  const id = uuidv4();
69
78
  const now = new Date().toISOString();
70
- db.prepare(`INSERT INTO resource_constraints (id, game_id, kind, direction, total, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(id, gameId, kind, direction, total, now);
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);
71
80
  const memberStmt = db.prepare(`INSERT INTO resource_constraint_members (constraint_id, resource_id) VALUES (?, ?)`);
72
81
  for (const resourceId of resourceIds) {
73
82
  memberStmt.run(id, resourceId);
@@ -79,6 +88,7 @@ function insertConstraint(gameId, kind, resourceIds, direction, total) {
79
88
  resourceIds,
80
89
  direction,
81
90
  total,
91
+ factKey,
82
92
  createdAt: now,
83
93
  };
84
94
  }
@@ -86,7 +96,8 @@ function insertConstraint(gameId, kind, resourceIds, direction, total) {
86
96
  * Declare a 'bounded' constraint: the resource's value must stay within its
87
97
  * existing minValue/maxValue (set via create_resource/update_resource).
88
98
  * Once declared, out-of-bounds writes through updateResourceValue() are
89
- * REJECTED instead of silently clamped -- see checkResourceConstraints().
99
+ * REJECTED instead of silently clamped -- see assertConstraintsAllow()
100
+ * (src/timeline/constrained.ts).
90
101
  */
91
102
  export function declareBoundedConstraint(params) {
92
103
  validateGameExists(params.gameId);
@@ -99,10 +110,10 @@ export function declareBoundedConstraint(params) {
99
110
  `A 'bounded' constraint enforces those bounds by rejecting instead of clamping -- ` +
100
111
  `set at least one bound with update_resource before declaring this constraint.`);
101
112
  }
102
- if (getConstraintsForResource(params.resourceId).some((c) => c.kind === "bounded")) {
113
+ if (constraintsFor(params.resourceId, "value").some((c) => c.kind === "bounded")) {
103
114
  throw new Error(`Resource '${params.resourceId}' already has a 'bounded' constraint.`);
104
115
  }
105
- return insertConstraint(params.gameId, "bounded", [params.resourceId], null, null);
116
+ return insertConstraint(params.gameId, "bounded", [params.resourceId], null, null, "value");
106
117
  }
107
118
  /**
108
119
  * Declare a 'monotonic' constraint: the resource's value may only move in
@@ -116,10 +127,53 @@ export function declareMonotonicConstraint(params) {
116
127
  if (!resource) {
117
128
  throw new Error(`Resource '${params.resourceId}' not found.`);
118
129
  }
119
- if (getConstraintsForResource(params.resourceId).some((c) => c.kind === "monotonic")) {
130
+ if (constraintsFor(params.resourceId, "value").some((c) => c.kind === "monotonic")) {
120
131
  throw new Error(`Resource '${params.resourceId}' already has a 'monotonic' constraint.`);
121
132
  }
122
- return insertConstraint(params.gameId, "monotonic", [params.resourceId], params.direction, null);
133
+ return insertConstraint(params.gameId, "monotonic", [params.resourceId], params.direction, null, "value");
134
+ }
135
+ /**
136
+ * Declare a 'resolve_only' constraint (design §5.3, §5.2a; issue #13): every
137
+ * direct write to `factKey` on `resourceId` is refused --
138
+ * assertConstraintsAllow() (src/timeline/constrained.ts) rejects it whether
139
+ * it arrives through writeConstrainedValue OR transferConstrainedValue --
140
+ * unless made while an adjudication window is open
141
+ * (src/timeline/adjudication.ts's `withAdjudicationOpen`). The window is the
142
+ * mechanism this issue builds; the resolver that OPENS it (issue #10) is a
143
+ * separate, not-yet-built caller, and this function does not know or care
144
+ * what that caller will look like.
145
+ *
146
+ * `factKey` is optional and defaults to `'value'`, matching every other
147
+ * declare*Constraint() function's implicit scope -- but, unlike those,
148
+ * 'resolve_only' takes it as a REAL parameter rather than hardcoding it,
149
+ * because it is designed to guard a fact key a caller chooses, not only the
150
+ * one numeric column `resources` happens to expose today (design §5.4
151
+ * option (C)'s eventual generalization; see `factKey`'s doc comment on
152
+ * `ResourceConstraint`, src/types/index.ts).
153
+ *
154
+ * Validation mirrors declareBoundedConstraint()/declareMonotonicConstraint()
155
+ * exactly: the game and resource must exist, and re-declaring the same
156
+ * (resourceId, factKey) pair is rejected rather than silently accepted --
157
+ * scoped by factKey, not just resourceId, because that is the one thing
158
+ * that makes 'resolve_only' different from its two row-based siblings: two
159
+ * DIFFERENT fact keys on the same resource are two independent declarations,
160
+ * never a duplicate of each other.
161
+ *
162
+ * No shape prerequisite the way 'bounded' requires a min/max or 'monotonic'
163
+ * requires a direction -- 'resolve_only' constrains WHO may write, not what
164
+ * shape the value must take, so there is nothing else to validate.
165
+ */
166
+ export function declareResolveOnlyConstraint(params) {
167
+ validateGameExists(params.gameId);
168
+ const resource = getResource(params.resourceId);
169
+ if (!resource) {
170
+ throw new Error(`Resource '${params.resourceId}' not found.`);
171
+ }
172
+ const factKey = params.factKey ?? "value";
173
+ if (constraintsFor(params.resourceId, factKey).some((c) => c.kind === "resolve_only")) {
174
+ throw new Error(`Resource '${params.resourceId}' already has a 'resolve_only' constraint on fact key '${factKey}'.`);
175
+ }
176
+ return insertConstraint(params.gameId, "resolve_only", [params.resourceId], null, null, factKey);
123
177
  }
124
178
  /**
125
179
  * Register a 'conserved' constraint: a set of resources that must always
@@ -158,7 +212,7 @@ export function declareConservedConstraint(params) {
158
212
  if (!resource) {
159
213
  throw new Error(`Resource '${resourceId}' not found.`);
160
214
  }
161
- if (getConstraintsForResource(resourceId).some((c) => c.kind === "conserved")) {
215
+ if (constraintsFor(resourceId, "value").some((c) => c.kind === "conserved")) {
162
216
  throw new Error(`Resource '${resourceId}' already belongs to a 'conserved' constraint. A resource may only be a ` +
163
217
  `member of one 'conserved' set at a time -- remove the existing constraint first (remove_resource_constraint) ` +
164
218
  `if you need to redefine the set it belongs to.`);
@@ -171,7 +225,7 @@ export function declareConservedConstraint(params) {
171
225
  `update_resource_value, before declaring the constraint) or the declared total so they already match -- ` +
172
226
  `declaring this constraint does not rewrite resource values to make a mismatched total true.`);
173
227
  }
174
- return insertConstraint(params.gameId, "conserved", uniqueIds, null, params.total);
228
+ return insertConstraint(params.gameId, "conserved", uniqueIds, null, params.total, "value");
175
229
  }
176
230
  /** List constraints for a game, optionally filtered to ones governing a given resource. */
177
231
  export function listConstraints(gameId, resourceId) {
@@ -184,16 +238,11 @@ export function listConstraints(gameId, resourceId) {
184
238
  return constraints;
185
239
  return constraints.filter((c) => c.resourceIds.includes(resourceId));
186
240
  }
187
- /** All constraints (of any kind) that govern the given resource. */
241
+ /** All constraints (of any kind, any fact key) that govern the given
242
+ * resource. Delegates to the registry's allConstraintsForEntity() rather
243
+ * than writing its own JOIN -- see src/timeline/registry.ts. */
188
244
  export function getConstraintsForResource(resourceId) {
189
- const db = getDatabase();
190
- const rows = db
191
- .prepare(`SELECT rc.* FROM resource_constraints rc
192
- JOIN resource_constraint_members rcm ON rcm.constraint_id = rc.id
193
- WHERE rcm.resource_id = ?
194
- ORDER BY rc.created_at`)
195
- .all(resourceId);
196
- return rows.map(rowToConstraint);
245
+ return allConstraintsForEntity(resourceId);
197
246
  }
198
247
  /** Remove a constraint by id. Returns true if a row was deleted. */
199
248
  export function removeConstraint(id) {
@@ -201,69 +250,11 @@ export function removeConstraint(id) {
201
250
  const result = db.prepare(`DELETE FROM resource_constraints WHERE id = ?`).run(id);
202
251
  return result.changes > 0;
203
252
  }
204
- /**
205
- * Check whether an intended value change for a single resource would
206
- * violate any 'bounded' or 'monotonic' constraint declared on it. Throws
207
- * ConstraintViolationError on violation; returns void otherwise. Shared by
208
- * checkResourceConstraints() (the single-resource write path) and
209
- * transferResourceValue() (resource.ts; the two-resource conserved-transfer
210
- * path) -- both paths must respect 'bounded'/'monotonic' the same way.
211
- * Deliberately does not look at 'conserved' constraints; callers decide
212
- * separately what to do about those (see checkResourceConstraints() below
213
- * and transferResourceValue()).
214
- */
215
- export function checkBoundedAndMonotonicConstraints(resourceId, previousValue, intendedValue, bounds) {
216
- const constraints = getConstraintsForResource(resourceId);
217
- for (const constraint of constraints) {
218
- if (constraint.kind === "monotonic") {
219
- if (constraint.direction === "increasing" && intendedValue < previousValue) {
220
- throw new ConstraintViolationError("monotonic", resourceId, `Resource '${resourceId}' is constrained to never decrease; rejected change from ${previousValue} to ${intendedValue}.`);
221
- }
222
- if (constraint.direction === "decreasing" && intendedValue > previousValue) {
223
- throw new ConstraintViolationError("monotonic", resourceId, `Resource '${resourceId}' is constrained to never increase; rejected change from ${previousValue} to ${intendedValue}.`);
224
- }
225
- }
226
- if (constraint.kind === "bounded") {
227
- if (bounds.minValue !== null && intendedValue < bounds.minValue) {
228
- throw new ConstraintViolationError("bounded", resourceId, `Resource '${resourceId}' is bounded-constrained (min ${bounds.minValue}); rejected value ${intendedValue} instead of clamping.`);
229
- }
230
- if (bounds.maxValue !== null && intendedValue > bounds.maxValue) {
231
- throw new ConstraintViolationError("bounded", resourceId, `Resource '${resourceId}' is bounded-constrained (max ${bounds.maxValue}); rejected value ${intendedValue} instead of clamping.`);
232
- }
233
- }
234
- // constraint.kind === "conserved": handled by callers, not here.
235
- }
236
- }
237
- /**
238
- * Check whether an intended value change for a single resource would
239
- * violate any constraint declared on it. Throws ConstraintViolationError on
240
- * violation; returns void otherwise. Called from updateResourceValue() in
241
- * resource.ts before the row is written.
242
- *
243
- * 'conserved' is handled specially here: ANY direct single-resource write to
244
- * a conserved member is rejected, unconditionally, regardless of whether the
245
- * particular delta would happen to preserve the total. The server cannot
246
- * know where update_resource_value's counterpart delta should come from --
247
- * writing one member without atomically adjusting another would silently
248
- * break the set's invariant, which is exactly the failure this constraint
249
- * exists to prevent. Use transfer_resource_value (transferResourceValue() in
250
- * resource.ts) instead, which moves value between two members of the same
251
- * set atomically.
252
- */
253
- export function checkResourceConstraints(resourceId, previousValue, intendedValue, bounds) {
254
- const constraints = getConstraintsForResource(resourceId);
255
- checkBoundedAndMonotonicConstraints(resourceId, previousValue, intendedValue, bounds);
256
- const conserved = constraints.find((c) => c.kind === "conserved");
257
- if (conserved) {
258
- throw new ConstraintViolationError("conserved", resourceId, `Resource '${resourceId}' is a member of a 'conserved' constraint (id '${conserved.id}', total ${conserved.total}) ` +
259
- `and cannot be written directly via update_resource_value -- a single-resource write is ambiguous about where ` +
260
- `the counterpart delta should come from, and could silently break the set's total. Use transfer_resource_value ` +
261
- `to move value between two members of this set atomically instead.`);
262
- }
263
- }
264
- /** All conserved constraints (of kind 'conserved') governing a resource,
265
- * i.e. zero or one (declareConservedConstraint() rejects overlapping
266
- * conserved membership, so a resource can belong to at most one). */
267
- export function getConservedConstraintFor(resourceId) {
268
- return getConstraintsForResource(resourceId).find((c) => c.kind === "conserved") ?? null;
269
- }
253
+ // `getConservedConstraintFor(resourceId)` used to live here, unscoped by fact
254
+ // key. Its callers (deleteResource/updateResource in src/tools/resource.ts)
255
+ // now use `conservedConstraintFor(entityId, factKey)` from
256
+ // src/timeline/registry.ts instead, and the unscoped version was left with no
257
+ // caller at all. Deleted rather than kept: a key-blind lookup surviving beside
258
+ // the key-scoped one is exactly the shape a future write reaches for by
259
+ // accident, and it would silently find a constraint governing a different
260
+ // fact key than the one being written.
@@ -12,12 +12,52 @@ export declare function createRelationship(params: {
12
12
  }): Relationship;
13
13
  export declare function getRelationship(id: string): Relationship | null;
14
14
  export declare function getRelationshipBetween(gameId: string, sourceId: string, targetId: string, relationshipType?: string): Relationship | null;
15
+ /**
16
+ * Metadata updater that also accepts a new value, written by a single
17
+ * direct UPDATE -- deliberately NOT routed through writeConstrainedValue()
18
+ * (src/timeline/constrained.ts). Its value write therefore lands as an
19
+ * UNANNOTATED transition in valueHistory()/getRelationshipHistory() below
20
+ * (reason: null, eventId: null) rather than one a constrained write
21
+ * stamped -- which is still strictly MORE history than the old
22
+ * relationship_history table ever held for this path, since nothing wrote
23
+ * there for a plain updateRelationship() call either. Mirrors
24
+ * updateResource() in src/tools/resource.ts, whose own value-adjacent write
25
+ * (re-clamping on a bounds change) is likewise a direct column write, not a
26
+ * choke-point one.
27
+ */
15
28
  export declare function updateRelationship(id: string, updates: {
16
29
  relationshipType?: string;
17
30
  value?: number;
18
31
  label?: string | null;
19
32
  notes?: string;
20
33
  }): Relationship | null;
34
+ /**
35
+ * Modify a relationship's value by a delta, with optional bounds. Delegates
36
+ * the value write entirely to writeConstrainedValue() (src/timeline/
37
+ * constrained.ts) -- the one choke point every constrained numeric fact key
38
+ * writes through, mirroring updateResourceValue() in src/tools/resource.ts.
39
+ * The resolve/check/clamp/write/annotate sequence, and the atomicity of the
40
+ * write and its audit trail, all live there now.
41
+ *
42
+ * Logs unconditionally, including for a zero delta -- exactly as this
43
+ * function always has. The choke point itself already handles a no-op
44
+ * write: it opens no new fact (there is nothing to version when the value
45
+ * doesn't move), but it still writes the annotation event, and
46
+ * valueHistory()/getRelationshipHistory() below surface that event as a
47
+ * history row regardless. So a zero-delta call still produces a row with no
48
+ * extra code here -- see conserved.test.ts's "logged even though nothing
49
+ * moved" assertions on the resource side of the same mechanism.
50
+ *
51
+ * `updated_at` gets its own UPDATE, in the same withTransaction() as the
52
+ * value write, for the same reason updateRelationshipValue() below writes
53
+ * its metadata separately: the choke point writes ONE column -- the
54
+ * constrained fact key -- and nothing else, because a writer that also
55
+ * touched neighbouring columns would be making policy about them. This
56
+ * function's `updated_at` bump is that policy, so it stays here, where it
57
+ * always was. Dropping it would be a silent regression: listRelationships()
58
+ * orders by `updated_at DESC`, so a relationship modified through this path
59
+ * would stop sorting as recently touched.
60
+ */
21
61
  export declare function modifyRelationship(params: {
22
62
  relationshipId: string;
23
63
  delta: number;
@@ -36,10 +76,51 @@ export declare function listRelationships(gameId: string, filter?: {
36
76
  relationshipType?: string;
37
77
  entityType?: string;
38
78
  }): Relationship[];
79
+ /**
80
+ * Every recorded change to a relationship's value, newest first. Built
81
+ * entirely from the timeline (valueHistory() in src/timeline/
82
+ * constrained.ts) -- there is no `relationship_history` table backing this
83
+ * any more (design §5.4 option (C); see the freeze trigger in
84
+ * src/db/schema.ts). Mirrors getResourceHistory() in src/tools/resource.ts
85
+ * exactly, and is a strict superset of what `relationship_history` ever
86
+ * held for the same reason: it also surfaces transitions no constrained
87
+ * write annotated (e.g. updateRelationship()'s direct column write above),
88
+ * which the old table simply never recorded.
89
+ */
39
90
  export declare function getRelationshipHistory(relationshipId: string, limit?: number): RelationshipChange[];
40
91
  /**
41
- * Update a relationship value with optional metadata changes. Supports both direct set and delta modes.
42
- * Logs to history when value changes.
92
+ * Update a relationship value with optional metadata changes. Supports both
93
+ * direct set and delta modes. Logs to history when the value changes; no
94
+ * history row (and `change: null`) when it doesn't -- that "no-op means no
95
+ * row" contract is this function's own, distinct from modifyRelationship()
96
+ * above, which always logs (see its doc comment). The choke point itself
97
+ * ALWAYS leaves a trace of a write, including a no-op one (an annotation
98
+ * event with no fact behind it -- see applyLiveWrite() in
99
+ * src/timeline/constrained.ts), so writeConstrainedValue() below is called
100
+ * ONLY when newValue !== previousValue -- routing a genuine no-op through it
101
+ * would put a row in getRelationshipHistory() that this contract says must
102
+ * not appear.
103
+ *
104
+ * `newValue` is computed and clamped here, BEFORE the choke-point call, for
105
+ * two reasons: first, to make that changed/unchanged decision at all;
106
+ * second, so the value handed to writeConstrainedValue() (mode: "set") is
107
+ * already the caller's real intent -- the choke point's own clamp against
108
+ * the same `bounds` is then a no-op on an already-clamped number, and its
109
+ * constraint check (assertConstraintsAllow(), src/timeline/constrained.ts)
110
+ * still sees a real, meaningful intended value rather than a raw delta.
111
+ *
112
+ * The metadata columns (relationship_type, label, notes, updated_at) are
113
+ * written by their own UPDATE, separate from the value write below, but
114
+ * both run inside one withTransaction() so the whole call is still one
115
+ * atomic unit -- exactly as before, just as two statements against
116
+ * `relationships` instead of one. That is a real, visible consequence of
117
+ * routing the value column through the one choke point every constrained
118
+ * write goes through: each statement fires the table's own projection
119
+ * trigger (projection.ts) independently, so a value-changing call now
120
+ * advances the timeline's `t` twice and logs two `relationship.updated`
121
+ * events instead of one. One write path for "what did this value used to
122
+ * be" is worth that -- see constrained.ts's own header comment on why a
123
+ * second write path is the failure this project keeps rediscovering.
43
124
  */
44
125
  export declare function updateRelationshipValue(params: {
45
126
  relationshipId: string;