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.
@@ -10,6 +10,7 @@
10
10
  // The web UI runs by default, as it always has. DMCP_NO_HTTP turns it off, for
11
11
  // a host that spawns this as an MCP subprocess and has no use for an admin
12
12
  // page it cannot close.
13
+ import { readFileSync } from "node:fs";
13
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
15
  import { closeDatabase } from "../db/connection.js";
15
16
  import { initializeSchema } from "../db/schema.js";
@@ -26,7 +27,21 @@ async function main() {
26
27
  // An application owns its database, so this is where the schema is brought
27
28
  // up -- not at import time, and not in any module a consumer might load.
28
29
  initializeSchema();
29
- const server = createMcpServer();
30
+ // A game's rules as declared data (issue #41): a stock server loads its
31
+ // mechanics, gates and conditions from a JSON file. Unreadable or invalid
32
+ // rules stop startup here, loudly -- a server that came up without the
33
+ // rules it was pointed at would answer every resolve with unknown-mechanic.
34
+ const rulesPath = process.env.DMCP_RULES_FILE;
35
+ let rules;
36
+ if (rulesPath) {
37
+ try {
38
+ rules = JSON.parse(readFileSync(rulesPath, "utf8"));
39
+ }
40
+ catch (error) {
41
+ throw new Error(`DMCP_RULES_FILE '${rulesPath}' could not be read as JSON: ${error.message}`);
42
+ }
43
+ }
44
+ const server = createMcpServer(rules ? { rules } : undefined);
30
45
  if (webUiEnabled(process.env)) {
31
46
  const actualPort = await startHttpServer(httpPortFromEnv(process.env));
32
47
  setHttpPort(actualPort);
package/dist/db/schema.js CHANGED
@@ -1281,9 +1281,10 @@ export function initializeSchema(options) {
1281
1281
  // need for this -- it enforces whatever `bounds` a caller passes at
1282
1282
  // write time against an EXISTING resource's own min_value/max_value
1283
1283
  // columns -- but a constraint declared in the same breath as the
1284
- // entity it governs is recorded here instead, opaquely: the engine
1285
- // stores these bounds and does not interpret them (issue #42's own
1286
- // scope note), the same "one column, several kinds, mostly null"
1284
+ // entity it governs is recorded here instead, and enforced at the
1285
+ // choke point against every later write (assertConstraintsAllow,
1286
+ // src/timeline/constrained.ts) -- compared as numbers, never given a
1287
+ // meaning -- in the same "one column, several kinds, mostly null"
1287
1288
  // shape `direction` (monotonic) and `total` (conserved) already use.
1288
1289
  // - caused_by_event_id: which `resolution.recorded` event's resolve()
1289
1290
  // call declared this constraint -- design §5.2c's one hop of
package/dist/index.d.ts CHANGED
@@ -26,8 +26,12 @@ export { createResolver, ResolveProtocolError } from "./timeline/resolve.js";
26
26
  export type { Mechanic, Resolver, Proposal, Expectation, AdjudicationInput, Adjudication, IntendedChange, IntendedWrite, IntendedTransfer, IntendedSet, IntendedCreate, CreateConstraint, IntendedDestroy, EntityRef, Outcome, ResolveRefusalReason, } from "./timeline/resolve.js";
27
27
  export { createStateRenderer } from "./timeline/render.js";
28
28
  export type { RenderVocabulary, VocabularyEntry, StateRenderer, RenderedState, RenderedNoun, UnnamedFact, } from "./timeline/render.js";
29
- export { createTurnReader } from "./reader/turnReader.js";
30
- export type { TurnReader, ReaderQuestion, ReaderSource, ReaderTransport, ReadRequest, TransportAnswer, ReaderResult, AnsweredQuestion, RejectedOffer, RejectionReason, } from "./reader/turnReader.js";
29
+ export { createTurnReader, sourceWords, verifyAnswers } from "./reader/turnReader.js";
30
+ export type { TurnReader, ReaderQuestion, ReaderSource, ReaderTransport, ReadRequest, TransportAnswer, ReaderResult, AnsweredQuestion, RejectedOffer, RejectionReason, QuotedCitation, RangedCitation, AcceptedCitation, SourceWord, RungAttempt, RungReport, } from "./reader/turnReader.js";
31
+ export { checkGroundings } from "./reader/preflight.js";
32
+ export type { DeclaredGrounding, GroundingRow, TargetRow, GroundingReport } from "./reader/preflight.js";
33
+ export { declaredMechanics, evaluateConditions, validateDeclaredRules } from "./timeline/declared.js";
34
+ export type { DeclaredRules, DeclaredCondition, DeclaredMechanic, DeclaredLeg, ConditionClause, FactClause, ComparisonOp, EntityOperand, NumberOperand, ParamRef, ConditionRow, ClauseRow, } from "./timeline/declared.js";
31
35
  export { exportTimeline, importTimeline, exportTimelineToFile, importTimelineFromFile, TIMELINE_FORMAT_VERSION, } from "./timeline/export.js";
32
36
  export type { TimelineExport, TimelineExportEntity, TimelineExportFact, TimelineExportEvent, TimelineExportClock, TimelineImportResult, } from "./timeline/export.js";
33
37
  export { ENTITY_KINDS } from "./timeline/kinds.js";
package/dist/index.js CHANGED
@@ -174,7 +174,18 @@ export { createStateRenderer } from "./timeline/render.js";
174
174
  // scans for a consumer's language. The engine is provably ignorant of what is
175
175
  // on the other end of a rung, which is also why the ladder's ORDER is the
176
176
  // caller's: it never learns which rung is local and which is hosted.
177
- export { createTurnReader } from "./reader/turnReader.js";
177
+ export { createTurnReader, sourceWords, verifyAnswers } from "./reader/turnReader.js";
178
+ // A preflight over a declared world (GitHub issue #37): caller-declared
179
+ // spans checked by the turn reader's own citation rule before play -- which
180
+ // reachable targets nothing can cite, which spans are not unique, which
181
+ // spans name a target nothing can reach. Rows, never a verdict.
182
+ export { checkGroundings } from "./reader/preflight.js";
183
+ // Mechanics, gates and end conditions as declared data (GitHub issue #41): a
184
+ // small JSON declaration -- conditions over facts, and mechanics that adjust or
185
+ // set facts when their gate conditions hold -- turned into ordinary Mechanics
186
+ // for createResolver, and evaluated at any t for a principal's condition list.
187
+ // Anything the declaration cannot say stays a hand-written Mechanic.
188
+ export { declaredMechanics, evaluateConditions, validateDeclaredRules } from "./timeline/declared.js";
178
189
  // Timeline export (design §6) -- the boundary that keeps both halves honest:
179
190
  // conversational authoring upstream of a frozen artifact, deterministic
180
191
  // consumers downstream of it. These are exported as library functions first
@@ -1,4 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type DeclaredRules } from "./timeline/declared.js";
2
3
  import { type Mechanic } from "./timeline/resolve.js";
3
4
  import { type RenderVocabulary } from "./timeline/render.js";
4
5
  export declare const SERVER_NAME = "dmcp";
@@ -6,7 +7,7 @@ export declare const SERVER_NAME = "dmcp";
6
7
  * the published package version by src/__tests__/serverVersion.test.ts --
7
8
  * this said "0.3.0" for the whole of 0.4.0, because a release bumps
8
9
  * package.json and nothing was watching this. */
9
- export declare const SERVER_VERSION = "0.9.0";
10
+ export declare const SERVER_VERSION = "0.10.0";
10
11
  /**
11
12
  * Build an MCP server with every CORE tool, resource and prompt this engine
12
13
  * serves -- entities, facts, events, the timeline, and the entity/property
@@ -50,4 +51,8 @@ export declare const SERVER_VERSION = "0.9.0";
50
51
  export declare function createCoreMcpServer(options?: {
51
52
  mechanics?: readonly Mechanic[];
52
53
  vocabulary?: RenderVocabulary;
54
+ /** A game's mechanics, gates and conditions as declared data (issue #41):
55
+ * its mechanics register beside `mechanics`, and `conditions_at` is
56
+ * served. Validated here, so a server with broken rules never comes up. */
57
+ rules?: DeclaredRules;
53
58
  }): McpServer;
@@ -38,6 +38,9 @@ import { registerMcpResources } from "./register/mcp-resources.js";
38
38
  import { registerTimelineTools } from "./register/timeline.js";
39
39
  import { registerResolveTools } from "./register/resolve.js";
40
40
  import { registerRenderTools } from "./register/render.js";
41
+ import { registerReaderTools } from "./register/reader.js";
42
+ import { registerConditionTools } from "./register/conditions.js";
43
+ import { declaredMechanics, validateDeclaredRules } from "./timeline/declared.js";
41
44
  import { createResolver } from "./timeline/resolve.js";
42
45
  import { createStateRenderer } from "./timeline/render.js";
43
46
  export const SERVER_NAME = "dmcp";
@@ -45,7 +48,7 @@ export const SERVER_NAME = "dmcp";
45
48
  * the published package version by src/__tests__/serverVersion.test.ts --
46
49
  * this said "0.3.0" for the whole of 0.4.0, because a release bumps
47
50
  * package.json and nothing was watching this. */
48
- export const SERVER_VERSION = "0.9.0";
51
+ export const SERVER_VERSION = "0.10.0";
49
52
  /**
50
53
  * Build an MCP server with every CORE tool, resource and prompt this engine
51
54
  * serves -- entities, facts, events, the timeline, and the entity/property
@@ -87,6 +90,8 @@ export const SERVER_VERSION = "0.9.0";
87
90
  * shape (and the identical silence) as `mechanics` above.
88
91
  */
89
92
  export function createCoreMcpServer(options) {
93
+ if (options?.rules)
94
+ validateDeclaredRules(options.rules);
90
95
  const server = new McpServer({
91
96
  name: SERVER_NAME,
92
97
  version: SERVER_VERSION,
@@ -110,11 +115,14 @@ export function createCoreMcpServer(options) {
110
115
  registerDisplayTools(server); // Display/Theme Configuration
111
116
  registerBatchTools(server); // Batch Operations (multi-entity, workflows -- entity/property only)
112
117
  registerTimelineTools(server); // replay(t), story-time axis declaration
113
- const mechanics = options?.mechanics;
114
- if (mechanics && mechanics.length > 0) {
118
+ registerReaderTools(server); // prepare_reading, verify_reading -- the turn reader with the model on the caller's side
119
+ const mechanics = [...(options?.mechanics ?? []), ...(options?.rules ? declaredMechanics(options.rules) : [])];
120
+ if (mechanics.length > 0) {
115
121
  const resolver = createResolver({ mechanics });
116
122
  registerResolveTools(server, resolver); // resolve(), list_mechanics -- only when mechanics are registered
117
123
  }
124
+ if (options?.rules)
125
+ registerConditionTools(server, options.rules); // conditions_at -- only when rules are declared
118
126
  const vocabulary = options?.vocabulary;
119
127
  if (vocabulary) {
120
128
  // Built here rather than accepted pre-built so the vocabulary is
@@ -0,0 +1,62 @@
1
+ import { type AcceptedCitation, type ReaderSource, type RejectionReason } from "./turnReader.js";
2
+ /**
3
+ * A preflight over a declared world (GitHub issue #37), run before play.
4
+ *
5
+ * Two failures that are otherwise silent until a transcript is read: a
6
+ * reachable property whose object's text holds nothing a citation could
7
+ * quote for it (every attempt on it is refused, forever), and prose that an
8
+ * author meant to matter with nothing modelled behind it (an actor plans
9
+ * around it and gets nowhere). The engine cannot find either by reading a
10
+ * description -- "does this text ground `integrity`" is a judgement about
11
+ * meaning (hard rule 4). So the CALLER declares, as data it already owns,
12
+ * which span of which source grounds which target, and this checks those
13
+ * declarations with the turn reader's own citation rule (`resolveCitation`),
14
+ * so a span that passes here is exactly a span that would pass live.
15
+ *
16
+ * `target` is opaque: the engine never parses it, compares it only for
17
+ * equality, and has no idea it is usually "<object>.<property>".
18
+ *
19
+ * Rows, never a verdict (hard rule 2). The counts are the report: a target
20
+ * with `grounded: 0` can never be cited, a span with `occurrences` above 1
21
+ * does not pick out one place, a span with `reachable: false` names a target
22
+ * nothing can act on. What any of that means for a given world is the
23
+ * caller's to decide. Quantity claims ("one of five") are not checked here:
24
+ * only the caller knows what it modelled, and once it has declared the span,
25
+ * comparing two of its own numbers needs no engine.
26
+ */
27
+ export interface DeclaredGrounding {
28
+ target: string;
29
+ sourceId: string;
30
+ quote?: string;
31
+ from?: number;
32
+ to?: number;
33
+ }
34
+ export interface GroundingRow {
35
+ target: string;
36
+ sourceId: string;
37
+ /** The span as it would be cited: the quote given, or a range rebuilt. Absent when it does not cite. */
38
+ quote?: string;
39
+ range?: AcceptedCitation["range"];
40
+ /** The citation rule's own reason, when the span does not cite. */
41
+ reason?: RejectionReason;
42
+ /** Byte-exact positions the span starts at in its source, overlapping included; 0 when it does not cite. */
43
+ occurrences: number;
44
+ /** Whether `target` is among the targets the caller listed as reachable. */
45
+ reachable: boolean;
46
+ }
47
+ export interface TargetRow {
48
+ target: string;
49
+ /** Declared spans for this target that cite. */
50
+ grounded: number;
51
+ /** Of those, the ones that occur exactly once in their source. */
52
+ uniquelyGrounded: number;
53
+ }
54
+ export interface GroundingReport {
55
+ groundings: GroundingRow[];
56
+ targets: TargetRow[];
57
+ }
58
+ export declare function checkGroundings(params: {
59
+ sources: readonly ReaderSource[];
60
+ targets: readonly string[];
61
+ groundings: readonly DeclaredGrounding[];
62
+ }): GroundingReport;
@@ -0,0 +1,40 @@
1
+ import { resolveCitation, validateSourceIds } from "./turnReader.js";
2
+ /** Every position the span starts at, overlapping included: "ab ab" starts
3
+ * twice in "ab ab ab", and a live quote of it would pick out neither. */
4
+ function occurrencesOf(text, span) {
5
+ let count = 0;
6
+ for (let at = text.indexOf(span); at !== -1; at = text.indexOf(span, at + 1))
7
+ count++;
8
+ return count;
9
+ }
10
+ export function checkGroundings(params) {
11
+ validateSourceIds(params.sources);
12
+ const reachable = new Set();
13
+ for (const target of params.targets) {
14
+ if (reachable.has(target))
15
+ throw new Error(`duplicate target '${target}'`);
16
+ reachable.add(target);
17
+ }
18
+ const sourcesById = new Map(params.sources.map((s) => [s.id, s]));
19
+ const groundings = params.groundings.map((g) => {
20
+ const { target, sourceId, quote, from, to } = g;
21
+ const cited = resolveCitation({ sourceId, quote, from, to }, sourcesById);
22
+ const base = { target, sourceId, reachable: reachable.has(target) };
23
+ if ("reason" in cited)
24
+ return { ...base, reason: cited.reason, occurrences: 0 };
25
+ const text = sourcesById.get(sourceId)?.text ?? "";
26
+ return {
27
+ target,
28
+ sourceId,
29
+ quote: cited.citation.quote,
30
+ ...(cited.citation.range ? { range: cited.citation.range } : {}),
31
+ occurrences: occurrencesOf(text, cited.citation.quote),
32
+ reachable: base.reachable,
33
+ };
34
+ });
35
+ const targets = params.targets.map((target) => {
36
+ const citing = groundings.filter((g) => g.target === target && g.reason === undefined);
37
+ return { target, grounded: citing.length, uniquelyGrounded: citing.filter((g) => g.occurrences === 1).length };
38
+ });
39
+ return { groundings, targets };
40
+ }
@@ -96,14 +96,30 @@ export interface ReadRequest {
96
96
  questions: readonly ReaderQuestion[];
97
97
  sources: readonly ReaderSource[];
98
98
  }
99
- /** What a transport returns: KEYS and citations, never prose. */
99
+ /** A citation by quote: `quote` must occur byte-exact in the source named. */
100
+ export interface QuotedCitation {
101
+ sourceId: string;
102
+ quote: string;
103
+ }
104
+ /** A citation by word range (GitHub issue #35): words `from` through `to` of
105
+ * the source named, numbered from 1 exactly as `sourceWords()` numbers them.
106
+ * The engine rebuilds it into a quote -- the source sliced from the first
107
+ * character of word `from` to the last of word `to` -- and holds that quote
108
+ * to the same rule as a typed one. A range can only name a substring of its
109
+ * source, so it makes misquoting unconstructable rather than instructing
110
+ * against it. */
111
+ export interface RangedCitation {
112
+ sourceId: string;
113
+ from: number;
114
+ to: number;
115
+ }
116
+ /** What a transport returns: KEYS and citations, never prose. A citation
117
+ * that carries `from` or `to` is read as a range and any `quote` beside it
118
+ * is not read; otherwise it is read as a quote. */
100
119
  export interface TransportAnswer {
101
120
  questionId: string;
102
121
  answerKey: string;
103
- citation: {
104
- sourceId: string;
105
- quote: string;
106
- };
122
+ citation: QuotedCitation | RangedCitation;
107
123
  }
108
124
  /** An injected capability, not a call this module makes itself -- see the
109
125
  * module doc comment's "hard constraint" section. A transport may throw,
@@ -114,18 +130,32 @@ export type ReaderTransport = (request: ReadRequest) => Promise<readonly Transpo
114
130
  /** The literal, engine-defined reasons an offered answer is discarded.
115
131
  * Never a severity, never a score -- a row names exactly which mechanical
116
132
  * check failed (hard rule 2). */
117
- export type RejectionReason = "unknown-question" | "unknown-answer-key" | "unknown-source-id" | "quote-not-in-source" | "empty-quote" | "duplicate-answer";
133
+ export type RejectionReason = "malformed-offer" | "missing-citation" | "unknown-question" | "unknown-answer-key" | "unknown-source-id" | "quote-not-in-source" | "empty-quote" | "invalid-range" | "range-start-past-end" | "duplicate-answer";
118
134
  /** One discarded offer, carried verbatim so a reviewer can see exactly what
119
135
  * was offered and why it did not count -- never summarised, never
120
136
  * scored. `rung` is the index into the caller's own `transports` array
121
137
  * that produced this offer; the engine reports the position, it does not
122
- * interpret what that position means. */
138
+ * interpret what that position means. `offer` is typed `unknown` in
139
+ * spirit: for `malformed-offer` it is whatever the transport put in its
140
+ * list, which may not be an object at all. */
123
141
  export interface RejectedOffer {
124
142
  reason: RejectionReason;
125
143
  rung: number;
126
144
  /** The offer exactly as the transport returned it. */
127
145
  offer: TransportAnswer;
128
146
  }
147
+ /** An accepted citation, always as a quote. `range` is present when the
148
+ * offer cited by range: the span actually quoted, with `to` clamped to the
149
+ * source's last word (issue #35's overshoot). The offer as given is in
150
+ * `AnsweredQuestion.acceptedOffer`, so a clamp is visible, never silent. */
151
+ export interface AcceptedCitation {
152
+ sourceId: string;
153
+ quote: string;
154
+ range?: {
155
+ from: number;
156
+ to: number;
157
+ };
158
+ }
129
159
  /** One row of the result: a question's final answer, whether it came from
130
160
  * a transport or from the caller's own safe default, and every offer for
131
161
  * that question this read discarded along the way. */
@@ -140,21 +170,49 @@ export interface AnsweredQuestion {
140
170
  answeredByRung: number | null;
141
171
  /** `null` when `fromSafeDefault` is true -- a default was never cited
142
172
  * against anything, because nothing was accepted for it to cite. */
143
- citation: {
144
- sourceId: string;
145
- quote: string;
146
- } | null;
173
+ citation: AcceptedCitation | null;
174
+ /** The accepted offer exactly as the transport gave it (issue #36);
175
+ * `null` when nothing was accepted. */
176
+ acceptedOffer: TransportAnswer | null;
177
+ /** Every rung this question was put to, in order (issue #36). Empty means
178
+ * it was never asked; non-empty with no `rejected` rows and no
179
+ * acceptance means the rungs asked offered nothing for it -- and
180
+ * `ReaderResult.rungs` says whether each of those rungs answered at all. */
181
+ askedOfRungs: readonly number[];
147
182
  rejected: readonly RejectedOffer[];
148
183
  }
184
+ /** What one call to one rung came back as (issue #36). A record of what
185
+ * happened, never a judgement of the transport: `threw` carries the thrown
186
+ * value as a string, exactly as the transport raised it. */
187
+ export type RungAttempt = {
188
+ outcome: "answered";
189
+ offers: number;
190
+ } | {
191
+ outcome: "threw";
192
+ error: string;
193
+ } | {
194
+ outcome: "not-a-list";
195
+ };
196
+ /** Every rung that was called, in order, with the questions it was asked and
197
+ * each attempt within its `attemptsPerTransport` budget. A rung never
198
+ * called -- because every question was already answered -- is not listed. */
199
+ export interface RungReport {
200
+ rung: number;
201
+ asked: readonly string[];
202
+ attempts: readonly RungAttempt[];
203
+ }
149
204
  export interface ReaderResult {
150
205
  /** One row per question this read declared, in the caller's own declared
151
206
  * order -- deterministic regardless of which rung answered what or in
152
207
  * what order a transport's own response array happened to list them. */
153
208
  answers: readonly AnsweredQuestion[];
154
209
  /** Offers naming a `questionId` that is not any question this read
155
- * declared -- cannot be attached to a row in `answers` because there is
156
- * no question for it to belong to. Always `reason: "unknown-question"`. */
210
+ * declared, or that are not offers at all (`malformed-offer`) -- cannot
211
+ * be attached to a row in `answers` because there is no question for
212
+ * them to belong to. */
157
213
  unmatched: readonly RejectedOffer[];
214
+ /** What each rung that was called came back as (issue #36). */
215
+ rungs: readonly RungReport[];
158
216
  }
159
217
  export interface TurnReader {
160
218
  /** Runs the ladder for this reader's declared questions against `sources`
@@ -165,6 +223,40 @@ export interface TurnReader {
165
223
  * changes turn to turn. */
166
224
  read(sources: readonly ReaderSource[]): Promise<ReaderResult>;
167
225
  }
226
+ /** One word of a source, as a range citation numbers it (issue #35). */
227
+ export interface SourceWord {
228
+ /** 1-based, the number a range names. */
229
+ index: number;
230
+ word: string;
231
+ /** Offsets into the source text: `text.slice(start, end) === word`. */
232
+ start: number;
233
+ end: number;
234
+ }
235
+ /**
236
+ * The words of a source as a range citation numbers them: each maximal run
237
+ * of non-whitespace, numbered from 1. Exported so a caller that shows its
238
+ * model numbered words builds that numbering from the same function the
239
+ * rebuild uses -- two numberings that disagree by one word would cite the
240
+ * wrong span without any error. Lexical only: punctuation stays on its word
241
+ * and nothing is split by what it means (hard rule 4).
242
+ */
243
+ export declare function sourceWords(text: string): SourceWord[];
244
+ /**
245
+ * Construction-time validation, in the same voice as `validateMechanics`
246
+ * (resolve.ts) and `validateMigrations` (src/db/schema.ts): every check
247
+ * that could make an invalid reader constructable runs ONCE, here, so a bad
248
+ * declaration fails loudly before a single `read()` call rather than
249
+ * surfacing as an answer silently outside the declared set three calls
250
+ * later. In particular: a reader that could ever return a key outside its
251
+ * own declared `answerKeys` must be unconstructable, which is why
252
+ * `safeDefault` membership is checked here rather than trusted.
253
+ */
254
+ export declare function validateQuestions(questions: readonly ReaderQuestion[]): void;
255
+ /** A citation names its source by id, so two sources sharing one make a
256
+ * citation ambiguous. Checked everywhere sources are handed over: `read()`
257
+ * (which rejects), `verifyAnswers`, and the MCP verbs over it. Until
258
+ * 2026-09-27 `read()` let the last duplicate win silently. */
259
+ export declare function validateSourceIds(sources: readonly ReaderSource[]): void;
168
260
  /**
169
261
  * Builds the reader a caller uses for the lifetime of a session -- the
170
262
  * `createResolver({ mechanics })` idiom (resolve.ts) copied directly:
@@ -183,3 +275,67 @@ export declare function createTurnReader(params: {
183
275
  transports: readonly ReaderTransport[];
184
276
  attemptsPerTransport?: number;
185
277
  }): TurnReader;
278
+ /**
279
+ * Whether `citation` cites a real source, non-empty, verbatim -- the
280
+ * citation rule's three conjuncts. All three must hold; the order below is
281
+ * only the order a REASON is reported in when more than one fails, and it
282
+ * runs cheapest-first: the quote must be a non-empty string, the source must
283
+ * be one that is actually in the request, and the quote must occur EXACTLY
284
+ * (`String.prototype.includes`, no case folding, no trimming, no fuzzy
285
+ * matching) inside that source's text. An offer that fails two conjuncts is
286
+ * rejected either way -- which reason it carries is a reporting detail, not
287
+ * a difference in whether it counts. Returns the accepted citation, or the
288
+ * first failing reason -- see the module doc comment's rule-4 discussion for
289
+ * why this literal presence test does not become the pattern-matching-meaning
290
+ * it is built beside.
291
+ *
292
+ * A RANGE (issue #35) is rebuilt into a quote first, then held to the same
293
+ * rule -- which a rebuilt quote always passes, being a slice of the source.
294
+ * Its own conjuncts: both ends integers with 1 <= from <= to
295
+ * (`invalid-range`), and `from` naming a real word (`range-start-past-end`).
296
+ * A `to` past the last word is CLAMPED to it, not rejected: the span cites
297
+ * what is there plus nothing. Dropping those instead was the recorded
298
+ * failure -- short sources overshoot most, and in the consumer that hit it
299
+ * a whole class of four-word intents was never once ruled.
300
+ *
301
+ * A citation that names no span at all -- absent, `{}`, or a source alone
302
+ * -- is `missing-citation`, reported before any of the above. A null field
303
+ * counts as absent throughout.
304
+ *
305
+ * Defensive against a citation that is missing entirely or missing a field
306
+ * -- a transport is caller-supplied code this module does not control at
307
+ * runtime, and a malformed citation must still be rejected mechanically
308
+ * rather than throwing out of this function and aborting the whole read.
309
+ */
310
+ export declare function resolveCitation(citation: unknown, sourcesById: ReadonlyMap<string, ReaderSource>): {
311
+ citation: AcceptedCitation;
312
+ } | {
313
+ reason: "missing-citation" | "unknown-source-id" | "empty-quote" | "quote-not-in-source" | "invalid-range" | "range-start-past-end";
314
+ };
315
+ /**
316
+ * Verifies one list of answers a caller's own model produced, by the same
317
+ * rule a `read()` applies to a rung's offers (GitHub issue #39: intent in,
318
+ * ruling out, with the ruling model outside the engine). For a caller that
319
+ * cannot hand the engine a transport -- a client reaching it over MCP, whose
320
+ * model runs on its own side -- this is the second of the verb's two steps:
321
+ * the caller builds the request and gets it answered; the engine verifies the
322
+ * answers and returns the ruling. It never infers anything itself.
323
+ *
324
+ * The result is exactly what `createTurnReader({ questions, transports: [t]
325
+ * }).read(sources)` returns when `t` resolves to `offers`, and both refuse
326
+ * duplicate source ids: the offers are rung
327
+ * 0, asked every question, and go through the one tally every rung's offers
328
+ * go through, so the verb and the library cannot disagree about what counts.
329
+ * Synchronous, because there is nothing to wait for.
330
+ *
331
+ * The question set is validated as `createTurnReader` validates it. A
332
+ * non-list `offers` throws rather than reading as "no offers": at this
333
+ * boundary the CALLER parsed its model's reply, so a non-list is the caller's
334
+ * bug, not a rung failure to record. Individual malformed entries inside the
335
+ * list are still rows (`malformed-offer`), as on the ladder.
336
+ */
337
+ export declare function verifyAnswers(params: {
338
+ questions: readonly ReaderQuestion[];
339
+ sources: readonly ReaderSource[];
340
+ offers: readonly TransportAnswer[];
341
+ }): ReaderResult;