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.
- package/README.md +76 -10
- package/dist/bin/run-dmcp.d.ts +2 -0
- package/dist/bin/run-dmcp.js +55 -0
- package/dist/db/connection.d.ts +32 -0
- package/dist/db/connection.js +38 -16
- package/dist/db/schema.d.ts +29 -1
- package/dist/db/schema.js +439 -7
- package/dist/http/server.js +3 -3
- package/dist/index.d.ts +36 -2
- package/dist/index.js +184 -92
- package/dist/mcp-server.d.ts +49 -0
- package/dist/mcp-server.js +127 -0
- package/dist/reader/turnReader.d.ts +185 -0
- package/dist/reader/turnReader.js +288 -0
- package/dist/register/batch.js +5 -79
- package/dist/register/mcp-resources.d.ts +9 -0
- package/dist/register/mcp-resources.js +16 -62
- package/dist/register/render.d.ts +17 -0
- package/dist/register/render.js +50 -0
- package/dist/register/resolve.d.ts +14 -0
- package/dist/register/resolve.js +102 -0
- package/dist/register/resources.js +11 -4
- package/dist/register/timeline.d.ts +2 -0
- package/dist/register/timeline.js +311 -0
- package/dist/rpg/index.d.ts +29 -0
- package/dist/rpg/index.js +55 -0
- package/dist/rpg/register/abilities.d.ts +2 -0
- package/dist/rpg/register/abilities.js +165 -0
- package/dist/rpg/register/batch.d.ts +2 -0
- package/dist/rpg/register/batch.js +92 -0
- package/dist/rpg/register/combat.d.ts +2 -0
- package/dist/rpg/register/combat.js +207 -0
- package/dist/rpg/register/mcp-prompts.d.ts +2 -0
- package/dist/rpg/register/mcp-prompts.js +684 -0
- package/dist/rpg/register/mcp-resources.d.ts +2 -0
- package/dist/rpg/register/mcp-resources.js +61 -0
- package/dist/rpg/register/quests.d.ts +2 -0
- package/dist/rpg/register/quests.js +118 -0
- package/dist/rpg/register/status.d.ts +2 -0
- package/dist/rpg/register/status.js +130 -0
- package/dist/rpg/register/tables.d.ts +2 -0
- package/dist/rpg/register/tables.js +146 -0
- package/dist/rpg/tools/ability.d.ts +48 -0
- package/dist/rpg/tools/ability.js +238 -0
- package/dist/rpg/tools/combat.d.ts +13 -0
- package/dist/rpg/tools/combat.js +195 -0
- package/dist/rpg/tools/dice.d.ts +23 -0
- package/dist/rpg/tools/dice.js +111 -0
- package/dist/rpg/tools/quest.d.ts +34 -0
- package/dist/rpg/tools/quest.js +164 -0
- package/dist/rpg/tools/status.d.ts +36 -0
- package/dist/rpg/tools/status.js +218 -0
- package/dist/rpg/tools/tables.d.ts +33 -0
- package/dist/rpg/tools/tables.js +209 -0
- package/dist/schemas/index.d.ts +12 -12
- package/dist/timeline/adjudication.d.ts +150 -0
- package/dist/timeline/adjudication.js +174 -0
- package/dist/timeline/changes.d.ts +100 -0
- package/dist/timeline/changes.js +161 -0
- package/dist/timeline/checkpoint.d.ts +69 -0
- package/dist/timeline/checkpoint.js +131 -0
- package/dist/timeline/clock.d.ts +89 -0
- package/dist/timeline/clock.js +173 -0
- package/dist/timeline/constrained.d.ts +220 -0
- package/dist/timeline/constrained.js +671 -0
- package/dist/timeline/export.d.ts +171 -0
- package/dist/timeline/export.js +329 -0
- package/dist/timeline/irreversible.d.ts +85 -0
- package/dist/timeline/irreversible.js +108 -0
- package/dist/timeline/kinds.d.ts +14 -0
- package/dist/timeline/kinds.js +22 -0
- package/dist/timeline/narration.d.ts +175 -0
- package/dist/timeline/narration.js +259 -0
- package/dist/timeline/projection.d.ts +97 -0
- package/dist/timeline/projection.js +330 -0
- package/dist/timeline/provenance.d.ts +66 -0
- package/dist/timeline/provenance.js +45 -0
- package/dist/timeline/registry.d.ts +95 -0
- package/dist/timeline/registry.js +124 -0
- package/dist/timeline/render.d.ts +121 -0
- package/dist/timeline/render.js +187 -0
- package/dist/timeline/replay.d.ts +64 -0
- package/dist/timeline/replay.js +104 -0
- package/dist/timeline/resolve.d.ts +262 -0
- package/dist/timeline/resolve.js +226 -0
- package/dist/timeline/schema.d.ts +13 -0
- package/dist/timeline/schema.js +262 -0
- package/dist/timeline/t.d.ts +80 -0
- package/dist/timeline/t.js +37 -0
- package/dist/tools/constraint.d.ts +44 -80
- package/dist/tools/constraint.js +115 -124
- package/dist/tools/relationship.d.ts +83 -2
- package/dist/tools/relationship.js +139 -62
- package/dist/tools/resource.d.ts +31 -6
- package/dist/tools/resource.js +106 -153
- package/dist/types/index.d.ts +19 -1
- package/dist/utils/output-schemas.d.ts +593 -2
- package/dist/utils/output-schemas.js +3 -0
- package/dist/utils/webui.d.ts +32 -0
- package/dist/utils/webui.js +54 -1
- package/package.json +20 -4
- package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
- package/dist/__tests__/engineVocabulary.test.js +0 -147
- package/dist/db/__tests__/connection.test.d.ts +0 -1
- package/dist/db/__tests__/connection.test.js +0 -72
- package/dist/db/__tests__/testDb.d.ts +0 -33
- package/dist/db/__tests__/testDb.js +0 -41
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -13
- package/dist/tools/__tests__/audio.test.d.ts +0 -1
- package/dist/tools/__tests__/audio.test.js +0 -59
- package/dist/tools/__tests__/conserved.test.d.ts +0 -1
- package/dist/tools/__tests__/conserved.test.js +0 -488
- package/dist/tools/__tests__/constraint.test.d.ts +0 -1
- package/dist/tools/__tests__/constraint.test.js +0 -212
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
- package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
- package/dist/tools/__tests__/images.test.d.ts +0 -1
- package/dist/tools/__tests__/images.test.js +0 -59
- package/dist/tools/__tests__/relationship.test.d.ts +0 -1
- package/dist/tools/__tests__/relationship.test.js +0 -132
- package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
- package/dist/tools/__tests__/resource-constraints.test.js +0 -131
- package/dist/tools/__tests__/resource.test.d.ts +0 -1
- package/dist/tools/__tests__/resource.test.js +0 -190
- package/dist/tools/__tests__/time.test.d.ts +0 -1
- package/dist/tools/__tests__/time.test.js +0 -404
- package/dist/tools/__tests__/timers.test.d.ts +0 -1
- package/dist/tools/__tests__/timers.test.js +0 -426
- package/dist/tools/__tests__/world.test.d.ts +0 -1
- package/dist/tools/__tests__/world.test.js +0 -70
- package/dist/utils/__tests__/json.test.d.ts +0 -1
- package/dist/utils/__tests__/json.test.js +0 -55
- package/dist/utils/__tests__/validation.test.d.ts +0 -1
- package/dist/utils/__tests__/validation.test.js +0 -90
package/dist/tools/constraint.js
CHANGED
|
@@ -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
|
|
26
|
-
*
|
|
27
|
-
* exactly two members of the same
|
|
28
|
-
* transferResourceValue() for why an
|
|
29
|
-
* balanced multi-resource write, was
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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;
|