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.
- package/dist/bin/run-dmcp.js +16 -1
- package/dist/db/schema.js +4 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.js +12 -1
- package/dist/mcp-server.d.ts +6 -1
- package/dist/mcp-server.js +11 -3
- package/dist/reader/preflight.d.ts +62 -0
- package/dist/reader/preflight.js +40 -0
- package/dist/reader/turnReader.d.ts +169 -13
- package/dist/reader/turnReader.js +232 -78
- package/dist/register/conditions.d.ts +16 -0
- package/dist/register/conditions.js +45 -0
- package/dist/register/reader.d.ts +2 -0
- package/dist/register/reader.js +132 -0
- package/dist/register/render.js +7 -2
- package/dist/register/resolve.js +8 -2
- package/dist/register/timeline.js +29 -2
- package/dist/rpg/server.d.ts +2 -0
- package/dist/schemas/index.d.ts +10 -10
- package/dist/timeline/constrained.d.ts +4 -5
- package/dist/timeline/constrained.js +31 -25
- package/dist/timeline/declared.d.ts +147 -0
- package/dist/timeline/declared.js +388 -0
- package/dist/timeline/render.d.ts +3 -0
- package/dist/timeline/render.js +1 -1
- package/dist/timeline/replay.d.ts +15 -0
- package/dist/timeline/replay.js +33 -2
- package/dist/timeline/resolve.d.ts +13 -3
- package/dist/timeline/resolve.js +34 -3
- package/dist/timeline/schema.js +28 -0
- package/dist/timeline/turns.d.ts +60 -0
- package/dist/timeline/turns.js +93 -0
- package/dist/utils/output-schemas.d.ts +6 -6
- package/package.json +1 -1
package/dist/bin/run-dmcp.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
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
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/dist/mcp-server.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
156
|
-
*
|
|
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;
|