@crewhaus/provability 0.0.0-stage → 0.8.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.
@@ -0,0 +1,326 @@
1
+ import { CrewhausError } from "@crewhaus/errors";
2
+ import { type PathRoots } from "./origin";
3
+ import type { EvidenceIndex, EvidenceRecord } from "./types";
4
+ export declare class ProvabilityError extends CrewhausError {
5
+ readonly name = "ProvabilityError";
6
+ }
7
+ /** One tool call, before its origins are read off it. */
8
+ export type ToolCall = {
9
+ readonly toolUseId: string;
10
+ readonly toolName: string;
11
+ readonly input: unknown;
12
+ /** The result as logged. */
13
+ readonly content: string;
14
+ /**
15
+ * The whole output, when `content` is tool-result-store's preview of it
16
+ * (`spilledOutput`): what the tool returned, which the evidence is read
17
+ * from. Absent when the log holds the result whole.
18
+ */
19
+ readonly body?: string;
20
+ readonly isError: boolean;
21
+ readonly sessionId?: string;
22
+ readonly turnNumber?: number;
23
+ };
24
+ /**
25
+ * The input schema a tool declared, by the name the log records it under
26
+ * (`RegisteredTool.jsonSchema`: for an MCP tool, what its server
27
+ * advertised), or `undefined` when the caller cannot say. Read for an MCP
28
+ * tool's origins (`inputOrigins`): only a field the schema declares names
29
+ * one.
30
+ */
31
+ export type InputSchemaLookup = (toolName: string) => unknown;
32
+ /** Where path-shaped inputs are measured from (default `process.cwd()`, the
33
+ * directory the tools themselves resolve relative paths against). */
34
+ export type EvidenceOptions = Pick<PathRoots, "workspaceRoot"> & {
35
+ /** Stamped onto records built from messages (they carry no session of their own). */
36
+ readonly sessionId?: string;
37
+ /**
38
+ * The input schema each tool declared (`InputSchemaLookup`) — the gate
39
+ * passes its catalog's. Default: the schema `defaultCatalog` holds for the
40
+ * tool, where a compiled harness registers its MCP tools. (0.8.0
41
+ * additive, convergence pass.)
42
+ */
43
+ readonly inputSchemaOf?: InputSchemaLookup;
44
+ /**
45
+ * Where the runtime spilled results over its threshold (its
46
+ * `toolResultRoot`; default `.crewhaus/tool-results` under the workspace
47
+ * root, tool-result-store's own default). A spilled output is read back
48
+ * only from under it (`spilledOutput`).
49
+ */
50
+ readonly toolResultsRoot?: string;
51
+ };
52
+ /** `sha256:<hex>` of a string's UTF-8 bytes. */
53
+ export declare function sha256Of(text: string): string;
54
+ /**
55
+ * Is `status` a page's own answer — 2xx? A 404 or a 503 body is the
56
+ * server's error page, not the page a citation names (design §2's
57
+ * `grounded` row: the cited source's BODY was fetched): one that prints
58
+ * the path it was asked for would otherwise ground any claim the model
59
+ * spelled into that path. A 3xx a fetcher hands back (it follows
60
+ * redirects) carries no page either.
61
+ */
62
+ export declare function servesPage(status: number | undefined): boolean;
63
+ /**
64
+ * The text a record is grounded against: the result — the whole output
65
+ * where the log holds tool-result-store's preview of it (`body`) — with its
66
+ * transport header taken off: WebFetch's `URL:`/`Status:` block and the `[user prompt:
67
+ * …]` line it prepends when the call carried a `prompt` (that is the model's
68
+ * OWN text: left in, a model could write the claim into `prompt` and find it
69
+ * "in the source"), and Fetch's status line and response headers. Read only
70
+ * off the tool that prints each; anything else is the result, verbatim.
71
+ */
72
+ export declare function groundingBody(record: Pick<EvidenceRecord, "content" | "input" | "toolName" | "body">): string;
73
+ /**
74
+ * The properties a tool's declared input schema lists at its top level, or
75
+ * `undefined` when no schema is known. A schema that lists none there (a
76
+ * `$ref`, an `anyOf`, a bare `{ type: "object" }`) declares no field this
77
+ * can read an address from: the empty set, so nothing its input carries
78
+ * names an origin (fail closed).
79
+ */
80
+ export declare function declaredFields(schema: unknown): ReadonlySet<string> | undefined;
81
+ /**
82
+ * Every place a call's input points at, whatever its schema declares — a
83
+ * builtin reader's one field, an MCP tool's URL and path fields and the
84
+ * entries of its `paths` / `files` lists — for the state-root test
85
+ * (verify.ts `provenanceOf`): a call whose input names the session log read
86
+ * it, whichever origin the body was then indexed under
87
+ * (`read_multiple_files {paths: [".crewhaus/sessions/…"], url}`).
88
+ */
89
+ export declare function inputPlaces(toolName: string, input: unknown, roots?: PathRoots): string[];
90
+ /** Is `toolName` one of the builtin readers (`WebFetch`, `Fetch`, `Read`: `READERS`)? */
91
+ export declare function isReader(toolName: string): boolean;
92
+ /** One place a call names: where it writes, or (`source`) where it copies, moves or unpacks from. */
93
+ export type WrittenPlace = {
94
+ readonly place: string;
95
+ readonly source: boolean;
96
+ };
97
+ /**
98
+ * The places a call may have WRITTEN, and the ones it copied, moved or
99
+ * unpacked them from, as its input names them, normalized like a
100
+ * citation's (verify.ts `provenanceOf` reads them: a source the run wrote
101
+ * itself and then read back, or a copy of the harness's own state). Any
102
+ * tool's input, builtin or not: the place it writes by
103
+ * {@link TARGET_FIELDS}; the place it copies from by {@link SOURCE_FIELDS};
104
+ * a list ({@link LIST_FIELDS}) as sources beside a target (`ConcatFiles`)
105
+ * and as written places otherwise; and each file a diff
106
+ * ({@link PATCH_FIELDS}) names, under the call's `cwd` (`GitApplyPatch`).
107
+ * None for a call that only reads — a builtin reader (`WebFetch`, `Read`, a
108
+ * GET or HEAD `Fetch`) and `ProvabilityPreflight`, which writes nowhere.
109
+ * Whether the tool can write at all (a pure tool cannot) is the caller's
110
+ * question.
111
+ */
112
+ export declare function writtenPlaces(toolName: string, input: unknown, roots?: PathRoots): WrittenPlace[];
113
+ /** What a call put where it wrote: the text it added, and the text it replaced. */
114
+ export type WrittenText = {
115
+ readonly added: readonly string[];
116
+ readonly removed: readonly string[];
117
+ };
118
+ /**
119
+ * The text a call put where it wrote ({@link writtenPlaces}), read off its
120
+ * input by field (verify.ts `selfWriter`): what a content field
121
+ * ({@link CONTENT_FIELDS}) carries, at any depth (`MultiEdit`'s `edits`, an
122
+ * MCP file server's `edits[].newText`), and a diff's `+` lines, as added;
123
+ * what an edit replaces ({@link OLD_TEXT_FIELDS}) and a diff's `-` lines, as
124
+ * removed — so an `Edit` of `14` to `30` added `30`, not `14`. A search's
125
+ * `pattern`, a commit `message`, an address: none of those is text put
126
+ * anywhere.
127
+ */
128
+ export declare function writtenText(input: unknown): WrittenText;
129
+ /**
130
+ * The input strings a call names its origins by, as written: the URL or
131
+ * path it was pointed at, not text it was handed — a builtin reader's one
132
+ * declared field, a non-builtin's URL and path fields whose origin the
133
+ * record carries (what `inputOrigins` read off an MCP tool, and the address
134
+ * a record built outside the log was fetched under — the research
135
+ * crawler's carry their own origins). A key the tool does not declare names
136
+ * no origin (`mcpOrigins`), so its value is text the call was handed like
137
+ * any other. `grounded`'s echo test (verify.ts `echoCheck`) reads these
138
+ * apart from the call's other strings, because a URL's slug can spell a
139
+ * claim the page itself states — a reader returns the page, not its
140
+ * address — and only a body that PRINTS the address (a 404 page repeating
141
+ * the path it was asked for, a page headed with its own URL) can be
142
+ * reflecting it.
143
+ */
144
+ export declare function originFieldValues(record: Pick<EvidenceRecord, "toolName" | "input" | "origins">, roots?: PathRoots): string[];
145
+ /** Every string a call's input carries, at any depth (the grounded echo test reads them too). */
146
+ export declare function inputStrings(value: unknown, out?: string[], depth?: number): string[];
147
+ /**
148
+ * The URL a record's body was served from, where the fetcher says so: the
149
+ * final URL a WebFetch header names on its first line, after any redirect
150
+ * (design §5.2), normalized. `undefined` for any other record — its body is
151
+ * what its origins name. A citation of the requested URL grounds in a body
152
+ * this URL served, so the host checks that hold a redirect hop (`sources.deny`,
153
+ * the private-address rule) and `grounded.independent` read the body by
154
+ * this (verify.ts `grounded`).
155
+ */
156
+ export declare function servedFrom(record: Pick<EvidenceRecord, "toolName" | "content" | "body">): string | undefined;
157
+ /**
158
+ * One call as an evidence record: its origins read off the input — a
159
+ * builtin reader's one field (WebFetch's and Fetch's `url`, Read's `path`),
160
+ * or an MCP tool's `url`, `uri`, `href`, `file_path`, `path`, `file`,
161
+ * `filePath` as far as its declared schema lets them (`mcpOrigins`), unless
162
+ * its result is an echo of its input (`inputOrigins`, `echoesInput`) — and
163
+ * the URL a WebFetch result's header names on its
164
+ * first line (indexed BESIDE the requested URL, so that once tool-web
165
+ * prints the final URL there a citation of either grounds — see
166
+ * `webFetchHeader`), and the hits a multi-source result carries (Retrieve;
167
+ * JSON results with `url`/`uri`/`href`/`link`/`source` from an MCP tool —
168
+ * see `resultHits` for which tools). A fetcher's HTTP status is carried
169
+ * (`status`): a non-2xx body is not the page (`servesPage`). Error results
170
+ * are recorded too: the verifiers reject them, by reason.
171
+ *
172
+ * Everything is read off the whole output (`body`) where the log holds a
173
+ * spill preview of it; `content` stays what the log holds, which a replay
174
+ * is compared against.
175
+ */
176
+ export declare function recordFromCall(call: ToolCall, opts?: EvidenceOptions): EvidenceRecord;
177
+ /**
178
+ * The whole output of a call whose logged result is tool-result-store's
179
+ * preview of it, or `undefined` (design §5.2: grounding needs the bodies
180
+ * the run read). runtime-core logs, and shows the model, only the first 100
181
+ * lines or 10 KB of a result over the threshold, then the path the whole
182
+ * was saved at; the claim the model reads further down a long page (the
183
+ * preview tells it to `Read` the parts) is in that file, not in the log.
184
+ *
185
+ * Read back only where it can be nothing but this call's own output:
186
+ *
187
+ * - the path is `<toolResultsRoot>/<runId>/<toolUseId>.txt` (or the
188
+ * `<toolUseId>.<n>.txt` tool-result-store moves on to when the name is
189
+ * taken) — THIS call's id, one run directory down, under the root the
190
+ * runtime spills to; a logged result is the tool's output verbatim, so a
191
+ * page that ends with a forged tail naming some other file reads nothing;
192
+ * - the file is a regular file opened inside that root, no link followed
193
+ * (`@crewhaus/tool-safety/fs`), at most {@link MAX_SPILL_BYTES};
194
+ * - it begins with the head the log holds.
195
+ *
196
+ * Anything else — a file gone, a tenant root not passed here — leaves the
197
+ * preview as the record's text: fewer words, a claim further down unmet,
198
+ * never more than the run read.
199
+ */
200
+ export declare function spilledOutput(content: string, toolUseId: string, opts?: Pick<EvidenceOptions, "workspaceRoot" | "toolResultsRoot">): string | undefined;
201
+ /**
202
+ * The structural slice of a transcript message this reads: Anthropic-shaped
203
+ * `MessageParam`s (assistant `tool_use` blocks `{ id, name, input }`, user
204
+ * `tool_result` blocks `{ tool_use_id, content, is_error }`). Declared here
205
+ * so the package imports no model SDK.
206
+ */
207
+ export type EvidenceMessage = {
208
+ readonly role: string;
209
+ readonly content: string | ReadonlyArray<unknown>;
210
+ };
211
+ /**
212
+ * Records for every completed tool call in a transcript, in call order. A
213
+ * `tool_use` with no `tool_result` yet is not a record (the call never
214
+ * finished); a result whose call is not in the transcript is skipped. An id
215
+ * that appears twice (a provider that reuses ids) pairs its LAST call with
216
+ * its LAST result, as the log walk does (`evidenceFromSessionLog`). A
217
+ * result the runtime spilled is read whole (`spilledOutput`).
218
+ */
219
+ export declare function evidenceFromMessages(messages: ReadonlyArray<EvidenceMessage>, opts?: EvidenceOptions): EvidenceRecord[];
220
+ export type SessionLogEvidenceOptions = {
221
+ /** The session whose log to read: `sess_<16 hex>`. */
222
+ readonly sessionId: string;
223
+ /** Where session `.jsonl` logs live (default `.crewhaus/sessions`). */
224
+ readonly rootDir?: string;
225
+ /** Walk sub-agent child sessions (`sub_agent_start` brackets). Default true. */
226
+ readonly includeChildren?: boolean;
227
+ /** How deep the child walk descends. Default 8. */
228
+ readonly maxDepth?: number;
229
+ /** Workspace root for path-shaped inputs (default `process.cwd()`). */
230
+ readonly workspaceRoot?: string;
231
+ /** Where the runtime spilled large results (see `EvidenceOptions.toolResultsRoot`). */
232
+ readonly toolResultsRoot?: string;
233
+ /** The input schema each tool declared (see `EvidenceOptions.inputSchemaOf`). */
234
+ readonly inputSchemaOf?: InputSchemaLookup;
235
+ /**
236
+ * Told when a session's log could not be read to its end — a torn last
237
+ * line from a crash mid-append, a malformed line, a link where the log
238
+ * should be. The records read before it are kept; the gate logs this.
239
+ */
240
+ readonly onUnreadable?: (info: {
241
+ readonly sessionId: string;
242
+ readonly message: string;
243
+ }) => void;
244
+ };
245
+ /**
246
+ * Records for every completed tool call in a session's event log, child
247
+ * sessions included, in call order: a child's calls are placed where its
248
+ * `sub_agent_start` bracket sits in the parent, because that is when they
249
+ * ran. The child walk follows the brackets `continuity-store`'s
250
+ * `resolveEvidence` follows — `sub_agent_start` payloads name a
251
+ * `childSessionId`, a session is read once, the depth is bounded — so a
252
+ * researcher child's fetches ground the parent's answer. (It is not that
253
+ * function: this reads every call of the tree once, in call order, where
254
+ * `resolveEvidence` looks one id up breadth-first.)
255
+ *
256
+ * One id logged more than once in a session pairs its LAST `tool_use` with
257
+ * its LAST `tool_result`, at the position it first appeared — the rule
258
+ * continuity-store's `scanSession` applies, so `PlanComplete` and the
259
+ * `checked` rung read one log the same way (design §6.3). An approval
260
+ * replay logs the `tool_use` twice (the second line records the input that
261
+ * RAN) and the result once; a provider that reuses ids (an
262
+ * OpenAI-compatible server's `functions.<name>:<n>`) logs both twice, and
263
+ * the last call is paired with its own result rather than with the first
264
+ * call's. A result the runtime spilled is read whole (`spilledOutput`).
265
+ * `turnNumber` is carried when a payload records one. A missing log
266
+ * — the session's or a child's — yields no records, never a throw. A log
267
+ * that cannot be read to its end (`@crewhaus/event-log` throws at a
268
+ * malformed line — a crash mid-append leaves one — or refuses a link at
269
+ * the log's name) keeps the records read before that point and reports it
270
+ * through `onUnreadable`: fewer records fails closed — a claim whose
271
+ * evidence was lost is unmet — where a throw would fail OPEN, every later
272
+ * turn of the session served `unverified` because one child's log is torn.
273
+ * Reading never creates the log directory.
274
+ */
275
+ export declare function evidenceFromSessionLog(opts: SessionLogEvidenceOptions): Promise<EvidenceRecord[]>;
276
+ export type EvidenceIndexOptions = {
277
+ /**
278
+ * The session being graded — the parent, whose sub-agents' logs the walk
279
+ * reads too (`evidenceFromSessionLog`). A record that names no session
280
+ * (the in-memory transcript's: `evidenceFromMessages` without
281
+ * `EvidenceOptions.sessionId`) is one of its calls, and a `tool:<id>` its
282
+ * calls and a child's share resolves to its own. Default: the session of
283
+ * the first record that names one — the log walk reads the parent first,
284
+ * and a parent's call (the `Task` that spawned a child) is logged before
285
+ * the child's bracket.
286
+ */
287
+ readonly sessionId?: string;
288
+ };
289
+ /**
290
+ * The index over `records`, one record per call: a call is its session and
291
+ * its `toolUseId` together (design §5.2: the index is keyed per call), so
292
+ * the log's record and the transcript's of ONE call merge into one, in the
293
+ * order each call first appears, and two sessions' calls never do. The
294
+ * log's record wins where the two disagree, except where the log holds
295
+ * only runtime-core's summary of block content and the transcript holds
296
+ * the text.
297
+ *
298
+ * An id is unique only within a conversation: an OpenAI-compatible server
299
+ * numbers its calls per conversation (`functions.<name>:<n>`, see
300
+ * `evidenceFromSessionLog`), and every sub-agent child is a fresh one, so a
301
+ * parent's first fetch and its child's share an id. Merged by id alone, the
302
+ * two became one record — one call's body, indexed under both calls' URLs:
303
+ * a claim the other page contradicts grounded, and the honest one failed.
304
+ * A record that names no session is the parent's (see
305
+ * {@link EvidenceIndexOptions.sessionId}); where the parent has no call by
306
+ * its id, it is the twin of the one call that has its tool and its input,
307
+ * and otherwise a call of its own.
308
+ *
309
+ * `byToolUseId` answers a `tool:<id>` the way continuity-store's
310
+ * `resolveEvidence` does — the parent's call first, then a child's — and
311
+ * an id only several children share (and the parent does not) is
312
+ * ambiguous: `undefined`, with every candidate on `callsById`, so the
313
+ * gate says so rather than grade one sub-agent's result for another's.
314
+ *
315
+ * `bySource` takes a normalized origin; a raw URL is normalized for the
316
+ * caller. Error records are indexed like any other — the verifiers reject
317
+ * them by reason, so "fetched, but it failed" reads differently from "never
318
+ * fetched".
319
+ */
320
+ export declare function buildEvidenceIndex(records: Iterable<EvidenceRecord>, opts?: EvidenceIndexOptions): EvidenceIndex;
321
+ /**
322
+ * The index restricted to calls made in `turnNumbers` (`scope: turn`). A
323
+ * record with no `turnNumber` is dropped: under a turn scope, evidence whose
324
+ * turn is unknown cannot be shown to be this turn's.
325
+ */
326
+ export declare function restrictToTurns(index: EvidenceIndex, turnNumbers: ReadonlySet<number>): EvidenceIndex;