@crewhaus/provability 0.0.0-stage → 0.8.1
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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +300 -2
- package/dist/artifact.d.ts +9 -0
- package/dist/artifact.js +38 -0
- package/dist/builtins.d.ts +32 -0
- package/dist/builtins.js +46 -0
- package/dist/evidence.d.ts +326 -0
- package/dist/evidence.js +1391 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/levels.d.ts +19 -0
- package/dist/levels.js +42 -0
- package/dist/origin.d.ts +83 -0
- package/dist/origin.js +296 -0
- package/dist/private-address.d.ts +34 -0
- package/dist/private-address.js +209 -0
- package/dist/purity.d.ts +18 -0
- package/dist/purity.js +15 -0
- package/dist/render.d.ts +47 -0
- package/dist/render.js +315 -0
- package/dist/store.d.ts +59 -0
- package/dist/store.js +319 -0
- package/dist/summary.d.ts +8 -0
- package/dist/summary.js +22 -0
- package/dist/types.d.ts +459 -0
- package/dist/types.js +2 -0
- package/dist/vacuous.d.ts +50 -0
- package/dist/vacuous.js +423 -0
- package/dist/verify.d.ts +176 -0
- package/dist/verify.js +3608 -0
- package/package.json +47 -4
package/dist/vacuous.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vacuous-pass backstop (design §2, §0 A and B).
|
|
3
|
+
*
|
|
4
|
+
* An answer with no claims passes every floor with nothing to prove — the
|
|
5
|
+
* right verdict for a greeting, a clarifying question or a code block, and
|
|
6
|
+
* the wrong one for an answer whose claim the extractor failed to see.
|
|
7
|
+
* Review rounds one and two each found new shapes that merged a claim into
|
|
8
|
+
* a question or a heading — "30 days [1] — anything else?", "1,284.60; want
|
|
9
|
+
* the PDF?", a no-break space, a `<br>`, U+2028, a table row, a heading over
|
|
10
|
+
* a question — and claim-ledger's `extractClaims` saw none of them, so the
|
|
11
|
+
* gate served scenario A or B with a ✓. Each was fixed shape by shape, and
|
|
12
|
+
* each fix left the next shape open. So the verdict does not rest on the
|
|
13
|
+
* extractor alone: an answer may be vacuous only when what a reader sees
|
|
14
|
+
* of it, code and definition lines aside, carries
|
|
15
|
+
*
|
|
16
|
+
* - no citation marker at all, and
|
|
17
|
+
* - no figure outside a question,
|
|
18
|
+
*
|
|
19
|
+
* read by a segmenter far blunter than the extractor, which cuts at every
|
|
20
|
+
* line break (U+0085, U+2028, U+2029, `\f`, `\v` and `<br>` included),
|
|
21
|
+
* every sentence terminator whether a space follows it or not, every
|
|
22
|
+
* semicolon, em or en dash and table pipe — never at a comma. A heading
|
|
23
|
+
* is a title to the figure rule but for what it states after a label
|
|
24
|
+
* (claim-ledger's `headingClaim` reads the headings that state one); a
|
|
25
|
+
* marker in it still counts. Otherwise
|
|
26
|
+
* each segment that carries a marker, and each that states a figure and is
|
|
27
|
+
* no question, is a claim, verified like any other: "30 days" cut off a
|
|
28
|
+
* trailing "anything else?" is checked against the page, and fails where
|
|
29
|
+
* the page says 14.
|
|
30
|
+
*
|
|
31
|
+
* A question stays a question. A piece of text that ends in `?` is a
|
|
32
|
+
* question from the first segment in it that opens as one ("Which of the 4
|
|
33
|
+
* plans do you mean — Basic or Pro?", "Should I book 2 rooms\nfor Monday?")
|
|
34
|
+
* or ends as one; what comes before that segment is said, not asked ("The
|
|
35
|
+
* refund window is 30 days — anything else?"). A clarifying question with a
|
|
36
|
+
* figure in it ("Do you want 2 or 3 copies?"), a greeting, and an answer
|
|
37
|
+
* that is only code stay vacuous.
|
|
38
|
+
*/
|
|
39
|
+
import { citationMarkers, codeSpans, extractNumbers, isAssumptionMarker, lineIndex, plainClaim, } from "@crewhaus/claim-ledger";
|
|
40
|
+
/** A definition line (`[1]: …`, `[^a]: …`), up to three spaces in. */
|
|
41
|
+
const DEFINITION_LINE = /^[ \t]{0,3}\[\^?[^\]\s]+\]:[^\n]*/gm;
|
|
42
|
+
/** A URL: its dots and slashes are not sentence ends. */
|
|
43
|
+
const URL_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`]+/gi;
|
|
44
|
+
/** An HTML line break. */
|
|
45
|
+
const BREAK_TAG = /<br\s*\/?>/gi;
|
|
46
|
+
/**
|
|
47
|
+
* A list item's marker at the start of a line — `1.`, `2)`, `-`, `*`, `+`,
|
|
48
|
+
* a bold `**1.**` — inside block quotes too: a reader sees an item's number,
|
|
49
|
+
* not a figure the answer states ("1. Open Settings").
|
|
50
|
+
*/
|
|
51
|
+
const LIST_MARKER = /^([ \t]*(?:>[ \t]?)*[ \t]*)((?:\*\*|__)?(?:\d{1,9}[.)]|[-*+])(?:\*\*|__)?)(?=[ \t])/gm;
|
|
52
|
+
/** Every character that breaks a line. */
|
|
53
|
+
const LINE_BREAK = /[\n\r\v\f\u0085\u2028\u2029]/;
|
|
54
|
+
/** What may close a sentence after its terminator: quotes, brackets, emphasis. */
|
|
55
|
+
const CLOSERS = /["'”’»)\]*_~`]/;
|
|
56
|
+
/**
|
|
57
|
+
* Words a question opens with — the interrogatives, the auxiliaries an
|
|
58
|
+
* inverted question starts on, and the openers of an elliptical one ("Want
|
|
59
|
+
* the PDF?", "Any of these work?"). A conjunction is not one: "And the
|
|
60
|
+
* window is 30 days; want details?" says something before it asks.
|
|
61
|
+
*/
|
|
62
|
+
const QUESTION_OPENERS = new Set([
|
|
63
|
+
"what",
|
|
64
|
+
"which",
|
|
65
|
+
"who",
|
|
66
|
+
"whom",
|
|
67
|
+
"whose",
|
|
68
|
+
"when",
|
|
69
|
+
"where",
|
|
70
|
+
"why",
|
|
71
|
+
"how",
|
|
72
|
+
"whether",
|
|
73
|
+
"do",
|
|
74
|
+
"does",
|
|
75
|
+
"did",
|
|
76
|
+
"don't",
|
|
77
|
+
"doesn't",
|
|
78
|
+
"didn't",
|
|
79
|
+
"is",
|
|
80
|
+
"isn't",
|
|
81
|
+
"are",
|
|
82
|
+
"aren't",
|
|
83
|
+
"was",
|
|
84
|
+
"wasn't",
|
|
85
|
+
"were",
|
|
86
|
+
"weren't",
|
|
87
|
+
"am",
|
|
88
|
+
"can",
|
|
89
|
+
"can't",
|
|
90
|
+
"could",
|
|
91
|
+
"couldn't",
|
|
92
|
+
"will",
|
|
93
|
+
"won't",
|
|
94
|
+
"would",
|
|
95
|
+
"wouldn't",
|
|
96
|
+
"shall",
|
|
97
|
+
"should",
|
|
98
|
+
"shouldn't",
|
|
99
|
+
"may",
|
|
100
|
+
"might",
|
|
101
|
+
"must",
|
|
102
|
+
"have",
|
|
103
|
+
"haven't",
|
|
104
|
+
"has",
|
|
105
|
+
"hasn't",
|
|
106
|
+
"had",
|
|
107
|
+
"want",
|
|
108
|
+
"wanna",
|
|
109
|
+
"need",
|
|
110
|
+
"any",
|
|
111
|
+
"anything",
|
|
112
|
+
"anyone",
|
|
113
|
+
"anybody",
|
|
114
|
+
"else",
|
|
115
|
+
"ok",
|
|
116
|
+
"okay",
|
|
117
|
+
"please",
|
|
118
|
+
]);
|
|
119
|
+
/** A sentence terminator. */
|
|
120
|
+
const TERMINATOR = /[.!?…]/;
|
|
121
|
+
/** What may open the segment glued to a terminator: a quote, a bracket, emphasis. */
|
|
122
|
+
const OPENERS = /["'“‘(\[*_]/;
|
|
123
|
+
/**
|
|
124
|
+
* The answer as the segmenter reads it, offsets kept (one character per
|
|
125
|
+
* UTF-16 unit): code (fenced and inline), definition lines, URLs and list
|
|
126
|
+
* item markers blanked, `<br>` turned into a line break.
|
|
127
|
+
*/
|
|
128
|
+
function visibleText(text) {
|
|
129
|
+
const out = text.split("");
|
|
130
|
+
const blank = (from, to) => {
|
|
131
|
+
for (let i = from; i < to; i++)
|
|
132
|
+
if (out[i] !== "\n")
|
|
133
|
+
out[i] = " ";
|
|
134
|
+
};
|
|
135
|
+
for (const [from, to] of codeSpans(text))
|
|
136
|
+
blank(from, to);
|
|
137
|
+
for (const m of text.matchAll(DEFINITION_LINE)) {
|
|
138
|
+
if (m.index !== undefined)
|
|
139
|
+
blank(m.index, m.index + m[0].length);
|
|
140
|
+
}
|
|
141
|
+
for (const m of text.matchAll(URL_TEXT)) {
|
|
142
|
+
if (m.index !== undefined)
|
|
143
|
+
blank(m.index, m.index + m[0].length);
|
|
144
|
+
}
|
|
145
|
+
for (const m of text.matchAll(LIST_MARKER)) {
|
|
146
|
+
if (m.index === undefined)
|
|
147
|
+
continue;
|
|
148
|
+
const from = m.index + m[1].length;
|
|
149
|
+
blank(from, from + m[2].length);
|
|
150
|
+
}
|
|
151
|
+
for (const m of text.matchAll(BREAK_TAG)) {
|
|
152
|
+
if (m.index === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
blank(m.index, m.index + m[0].length);
|
|
155
|
+
out[m.index] = "\n";
|
|
156
|
+
}
|
|
157
|
+
return out.join("");
|
|
158
|
+
}
|
|
159
|
+
const isSpace = (c) => c !== undefined && /\s/u.test(c);
|
|
160
|
+
const isLetter = (c) => c !== undefined && /\p{L}/u.test(c);
|
|
161
|
+
const isDigit = (c) => c !== undefined && /\p{N}/u.test(c);
|
|
162
|
+
const isBreak = (c) => c !== undefined && LINE_BREAK.test(c);
|
|
163
|
+
/** Where the closers after a terminator that ends at `at` end. */
|
|
164
|
+
function pastClose(text, at) {
|
|
165
|
+
let i = at;
|
|
166
|
+
while (i < text.length && CLOSERS.test(text[i]))
|
|
167
|
+
i++;
|
|
168
|
+
return i;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Past the markers that follow a sentence end on its line (`[1]` after
|
|
172
|
+
* "rose 12%." is that sentence's), by where each marker the citation
|
|
173
|
+
* grammar reads (`citationMarkers`) ends, keyed by where it starts.
|
|
174
|
+
*/
|
|
175
|
+
function pastMarkers(text, at, markers) {
|
|
176
|
+
let i = at;
|
|
177
|
+
for (;;) {
|
|
178
|
+
let j = i;
|
|
179
|
+
while (j < text.length && isSpace(text[j]) && !isBreak(text[j]))
|
|
180
|
+
j++;
|
|
181
|
+
const end = markers.get(j);
|
|
182
|
+
if (end === undefined)
|
|
183
|
+
return i;
|
|
184
|
+
i = end;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Is the `.` at `at` glued to a word that starts a new segment —
|
|
189
|
+
* "days.Anything", "1,284.50.want" — rather than inside a name ("Node.js",
|
|
190
|
+
* "e.g.", "policy.md")? A capital starts one; so does a lowercase word of
|
|
191
|
+
* four letters or more. A decimal point never does.
|
|
192
|
+
*/
|
|
193
|
+
function gluedFullStop(text, at) {
|
|
194
|
+
if (isDigit(text[at - 1]) && isDigit(text[at + 1]))
|
|
195
|
+
return false;
|
|
196
|
+
if (!isLetter(text[at + 1]))
|
|
197
|
+
return false;
|
|
198
|
+
let end = at + 1;
|
|
199
|
+
while (end < text.length && isLetter(text[end]))
|
|
200
|
+
end++;
|
|
201
|
+
const word = text.slice(at + 1, end);
|
|
202
|
+
return /^\p{Lu}/u.test(word) || word.length >= 4;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The answer cut into pieces — at a terminator a space or a line break
|
|
206
|
+
* follows, and at a blank line — and each piece into segments: at every
|
|
207
|
+
* other line break, at a terminator glued to the next word, and at every
|
|
208
|
+
* semicolon, em dash, en dash (not one between figures, a range) and pipe.
|
|
209
|
+
* Over {@link visibleText}.
|
|
210
|
+
*/
|
|
211
|
+
function segments(text) {
|
|
212
|
+
const out = [];
|
|
213
|
+
const markers = new Map(citationMarkers(text).map((m) => [m.at, m.end]));
|
|
214
|
+
let pieceStart = 0;
|
|
215
|
+
let partStart = 0;
|
|
216
|
+
let parts = [];
|
|
217
|
+
const cutPart = (end, next) => {
|
|
218
|
+
parts.push([partStart, end]);
|
|
219
|
+
partStart = next;
|
|
220
|
+
};
|
|
221
|
+
const cutPiece = (end, next) => {
|
|
222
|
+
cutPart(end, next);
|
|
223
|
+
out.push({ piece: [pieceStart, end], parts });
|
|
224
|
+
parts = [];
|
|
225
|
+
pieceStart = next;
|
|
226
|
+
};
|
|
227
|
+
let i = 0;
|
|
228
|
+
while (i < text.length) {
|
|
229
|
+
const c = text[i];
|
|
230
|
+
if (TERMINATOR.test(c)) {
|
|
231
|
+
let end = i + 1;
|
|
232
|
+
while (end < text.length && TERMINATOR.test(text[end]))
|
|
233
|
+
end++;
|
|
234
|
+
const closed = pastClose(text, end);
|
|
235
|
+
if (closed >= text.length || isSpace(text[closed])) {
|
|
236
|
+
const after = pastMarkers(text, closed, markers);
|
|
237
|
+
cutPiece(after, after);
|
|
238
|
+
i = after;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const next = text[closed];
|
|
242
|
+
const glued = c === "." && end === i + 1
|
|
243
|
+
? gluedFullStop(text, i)
|
|
244
|
+
: isLetter(next) || isDigit(next) || OPENERS.test(next);
|
|
245
|
+
if (glued)
|
|
246
|
+
cutPart(closed, closed);
|
|
247
|
+
i = glued ? closed : end;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (isBreak(c)) {
|
|
251
|
+
let j = c === "\r" && text[i + 1] === "\n" ? i + 2 : i + 1;
|
|
252
|
+
while (j < text.length && /[ \t]/.test(text[j]))
|
|
253
|
+
j++;
|
|
254
|
+
if (j >= text.length || isBreak(text[j])) {
|
|
255
|
+
// A blank line: the piece ends.
|
|
256
|
+
let next = j;
|
|
257
|
+
while (next < text.length && isSpace(text[next]))
|
|
258
|
+
next++;
|
|
259
|
+
cutPiece(i, next);
|
|
260
|
+
i = next;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
cutPart(i, i + 1);
|
|
264
|
+
i += 1;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const range = c === "–" && isDigit(text[i - 1]) && isDigit(text[i + 1]);
|
|
269
|
+
if (c === ";" || c === "|" || c === "—" || (c === "–" && !range))
|
|
270
|
+
cutPart(i, i + 1);
|
|
271
|
+
i += 1;
|
|
272
|
+
}
|
|
273
|
+
if (pieceStart < text.length)
|
|
274
|
+
cutPiece(text.length, text.length);
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
/** An ATX heading line, and a setext underline. */
|
|
278
|
+
const ATX_HEADING = /^[ \t]{0,3}#{1,6}(?:[ \t]|$)/;
|
|
279
|
+
const SETEXT_UNDERLINE = /^[ \t]{0,3}(?:=+|-{2,})[ \t]*$/;
|
|
280
|
+
/** A line a list item or a block quote opens: its `---` is a break, not an underline. */
|
|
281
|
+
const BLOCK_OPENER = /^[ \t]*(?:[-*+>]|\d{1,9}[.)])[ \t]/;
|
|
282
|
+
/**
|
|
283
|
+
* The heading lines of `text`, as `[from, to)` spans: a line `#` opens, and
|
|
284
|
+
* the paragraph line a setext underline (`===`, `---`) sits under.
|
|
285
|
+
*/
|
|
286
|
+
function headingLines(text) {
|
|
287
|
+
const out = [];
|
|
288
|
+
let previous;
|
|
289
|
+
let at = 0;
|
|
290
|
+
for (const line of text.split("\n")) {
|
|
291
|
+
const span = [at, at + line.length];
|
|
292
|
+
if (ATX_HEADING.test(line))
|
|
293
|
+
out.push(span);
|
|
294
|
+
else if (SETEXT_UNDERLINE.test(line) &&
|
|
295
|
+
previous !== undefined &&
|
|
296
|
+
text.slice(...previous).trim() !== "" &&
|
|
297
|
+
!BLOCK_OPENER.test(text.slice(...previous)) &&
|
|
298
|
+
!SETEXT_UNDERLINE.test(text.slice(...previous))) {
|
|
299
|
+
out.push(previous);
|
|
300
|
+
}
|
|
301
|
+
previous = span;
|
|
302
|
+
at += line.length + 1;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
/** What a heading states after a label ("Invoice total: 1,284.50"), or nothing. */
|
|
307
|
+
function labelled(heading) {
|
|
308
|
+
const colon = heading.indexOf(":");
|
|
309
|
+
return colon === -1 ? "" : heading.slice(colon + 1);
|
|
310
|
+
}
|
|
311
|
+
/** `text[from, to)` with its edges trimmed, as a span. */
|
|
312
|
+
function trimmed(text, [from, to]) {
|
|
313
|
+
let a = from;
|
|
314
|
+
let b = to;
|
|
315
|
+
while (a < b && isSpace(text[a]))
|
|
316
|
+
a++;
|
|
317
|
+
while (b > a && isSpace(text[b - 1]))
|
|
318
|
+
b--;
|
|
319
|
+
return [a, b];
|
|
320
|
+
}
|
|
321
|
+
/** Does `text` end as a question once its closers and the markers after it are set aside? */
|
|
322
|
+
function endsAsQuestion(text) {
|
|
323
|
+
const ends = new Map(citationMarkers(text).map((m) => [m.end, m.at]));
|
|
324
|
+
let end = text.length;
|
|
325
|
+
while (end > 0) {
|
|
326
|
+
const mark = ends.get(end);
|
|
327
|
+
if (mark !== undefined)
|
|
328
|
+
end = mark;
|
|
329
|
+
else if (isSpace(text[end - 1]) || CLOSERS.test(text[end - 1]))
|
|
330
|
+
end -= 1;
|
|
331
|
+
else
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
return text[end - 1] === "?";
|
|
335
|
+
}
|
|
336
|
+
/** Does `text` open as a question: its first word, past bullets, figures and marks, an opener? */
|
|
337
|
+
function opensAsQuestion(text) {
|
|
338
|
+
const word = /[\p{L}][\p{L}'’]*/u.exec(text)?.[0];
|
|
339
|
+
return word !== undefined && QUESTION_OPENERS.has(word.toLowerCase().replace(/’/g, "'"));
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Claims the backstop reads in an answer the extractor found nothing to
|
|
343
|
+
* count in (see the module doc), numbered from `firstIndex`. Each is the
|
|
344
|
+
* segment as written (`raw`, markers in it), read as `plainClaim` reads a
|
|
345
|
+
* sentence, citing the markers it carries — a marker the document defines,
|
|
346
|
+
* a numeral, a footnote or an assumption id; a segment that is only
|
|
347
|
+
* markers gives them to the segment before it. A segment the extractor
|
|
348
|
+
* already returned (an assumption it read) is not read twice. Empty when
|
|
349
|
+
* the answer may be vacuous.
|
|
350
|
+
*/
|
|
351
|
+
export function backstopClaims(text, definitions, extracted, firstIndex) {
|
|
352
|
+
const visible = visibleText(text);
|
|
353
|
+
const lineOf = lineIndex(text);
|
|
354
|
+
// A heading is a title to the figure rule — "## Refund options for 2024"
|
|
355
|
+
// over "Which option do you want?" has nothing to prove; claim-ledger's
|
|
356
|
+
// `headingClaim` decides which heading states one. What one states after
|
|
357
|
+
// a label ("## Invoice total: 1,284.50") is read, and a marker in one
|
|
358
|
+
// still makes it a claim.
|
|
359
|
+
const headings = headingLines(visible);
|
|
360
|
+
const inHeading = ([from]) => headings.some(([a, b]) => from >= a && from < b);
|
|
361
|
+
const known = extracted.map((claim) => claim.text.replace(/\s+/g, " ").trim().toLowerCase());
|
|
362
|
+
const citing = (id, caret) => definitions.has(id) || caret || /^\d+$/.test(id) || isAssumptionMarker(id);
|
|
363
|
+
const out = [];
|
|
364
|
+
for (const { piece, parts: spans } of segments(visible)) {
|
|
365
|
+
const pieceText = visible.slice(...trimmed(visible, piece));
|
|
366
|
+
const asked = endsAsQuestion(pieceText);
|
|
367
|
+
const parts = [];
|
|
368
|
+
// In a piece that ends as a question: asked from the segment that
|
|
369
|
+
// opens one to the segment that ends one; said before it.
|
|
370
|
+
let questioning = false;
|
|
371
|
+
for (const raw of spans) {
|
|
372
|
+
const span = trimmed(visible, raw);
|
|
373
|
+
const shown = visible.slice(...span);
|
|
374
|
+
if (shown === "")
|
|
375
|
+
continue;
|
|
376
|
+
const markers = [];
|
|
377
|
+
for (const m of citationMarkers(shown)) {
|
|
378
|
+
if (citing(m.marker, m.caret) && !markers.includes(m.marker))
|
|
379
|
+
markers.push(m.marker);
|
|
380
|
+
}
|
|
381
|
+
const words = plainClaim(shown);
|
|
382
|
+
if (words === "" && markers.length > 0 && parts.length > 0) {
|
|
383
|
+
// Markers on their own: the segment before them is what they cite.
|
|
384
|
+
const before = parts[parts.length - 1];
|
|
385
|
+
for (const marker of markers)
|
|
386
|
+
if (!before.markers.includes(marker))
|
|
387
|
+
before.markers.push(marker);
|
|
388
|
+
before.span = [before.span[0], span[1]];
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const ends = endsAsQuestion(shown);
|
|
392
|
+
if (asked && opensAsQuestion(words))
|
|
393
|
+
questioning = true;
|
|
394
|
+
parts.push({
|
|
395
|
+
span,
|
|
396
|
+
markers,
|
|
397
|
+
figure: extractNumbers(inHeading(span) ? labelled(words) : words).length > 0,
|
|
398
|
+
question: questioning || ends,
|
|
399
|
+
});
|
|
400
|
+
if (ends)
|
|
401
|
+
questioning = false;
|
|
402
|
+
}
|
|
403
|
+
for (const part of parts) {
|
|
404
|
+
if (part.markers.length === 0 && (!part.figure || part.question))
|
|
405
|
+
continue;
|
|
406
|
+
const raw = text.slice(...part.span).trim();
|
|
407
|
+
const claimText = plainClaim(raw);
|
|
408
|
+
if (claimText === "")
|
|
409
|
+
continue;
|
|
410
|
+
const folded = claimText.replace(/\s+/g, " ").trim().toLowerCase();
|
|
411
|
+
if (known.some((seen) => seen.includes(folded) || folded.includes(seen)))
|
|
412
|
+
continue;
|
|
413
|
+
out.push({
|
|
414
|
+
index: firstIndex + out.length,
|
|
415
|
+
text: claimText,
|
|
416
|
+
raw,
|
|
417
|
+
line: lineOf(part.span[0]),
|
|
418
|
+
markers: part.markers,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { type ProofBundle, type ProofClaim, type ProvabilityConfig, type UnmetReason, type VerifyOptions } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Distinct URLs one verification sends a HEAD to under `sources.resolve:
|
|
4
|
+
* head` (design §6.1). The answer is model-written, so without a ceiling it
|
|
5
|
+
* decides how many outbound requests the gate makes and how long it waits
|
|
6
|
+
* on them; a URL past the ceiling is `unresolved` — not checked is not
|
|
7
|
+
* reachable — and the detail names the cap.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_HEAD_REQUESTS = 50;
|
|
10
|
+
/**
|
|
11
|
+
* How many of those HEADs are in flight at once, and how long one
|
|
12
|
+
* verification waits on them in all (design §6.1, §18: the gate's cost is
|
|
13
|
+
* bounded). Sent one at a time under a per-request timeout alone, fifty
|
|
14
|
+
* URLs on hosts that never answer held one pass for minutes, and again on
|
|
15
|
+
* every retry; past the deadline a URL is `unresolved` — not checked is not
|
|
16
|
+
* reachable — whatever is still pending.
|
|
17
|
+
*/
|
|
18
|
+
export declare const HEAD_CONCURRENCY = 6;
|
|
19
|
+
export declare const HEAD_DEADLINE_MS = 15000;
|
|
20
|
+
/**
|
|
21
|
+
* Fetched text one verification reads while grounding, in tokens: each
|
|
22
|
+
* claim reads every body it cites end to end, so a long answer citing a
|
|
23
|
+
* large page costs claims × body — 200 claims over a 4 MB page held the
|
|
24
|
+
* harness thread for twelve seconds per pass. Past this a source binding
|
|
25
|
+
* is not read for grounding: it reaches `cited` and, at a higher floor,
|
|
26
|
+
* reads `below_floor` with a detail naming the cap (§18: bounded gate
|
|
27
|
+
* cost). The verifier also yields to the event loop between claims, so
|
|
28
|
+
* the work it does do never holds the process for long at a stretch.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MAX_GROUNDING_TOKENS = 40000000;
|
|
31
|
+
/** Test seam: a smaller grounding budget, or (no argument) {@link MAX_GROUNDING_TOKENS} again. */
|
|
32
|
+
export declare function _setGroundingBudgetForTest(tokens?: number): void;
|
|
33
|
+
/**
|
|
34
|
+
* Whitespace collapsed, cut to `max` with an ellipsis. Only a prefix of a
|
|
35
|
+
* long text is collapsed — a 2 MB tool result clipped to 240 characters
|
|
36
|
+
* must not be rewritten whole — unless that prefix is so full of
|
|
37
|
+
* whitespace that it collapses to fewer than `max` characters.
|
|
38
|
+
*/
|
|
39
|
+
export declare function clip(text: string, max: number): string;
|
|
40
|
+
/**
|
|
41
|
+
* At most `max` characters of `text` around `[from, to)`, whitespace
|
|
42
|
+
* collapsed, an ellipsis marking each cut: the frozen excerpt of a long
|
|
43
|
+
* body or result, centred on what met the floor (design §5.3: a bundle
|
|
44
|
+
* stays human-checkable after the body is gone, so its excerpt must show
|
|
45
|
+
* the figure or the words that grounded the claim, not the text's first
|
|
46
|
+
* 240 characters). When `[from, to)` itself is longer, its start is kept.
|
|
47
|
+
*/
|
|
48
|
+
export declare function clipAround(text: string, from: number, to: number, max: number): string;
|
|
49
|
+
/**
|
|
50
|
+
* The reasons a claim is unmet: `no_citation` for a claim with no marker at
|
|
51
|
+
* all (it has zero bindings — the representation design §5.1's nudge reads),
|
|
52
|
+
* else the reason of every binding that is not `ok`, in order, each once. An
|
|
53
|
+
* empty list for a met or assumption claim.
|
|
54
|
+
*/
|
|
55
|
+
export declare function claimReasons(claim: ProofClaim): UnmetReason[];
|
|
56
|
+
/**
|
|
57
|
+
* Is a recorded tool name on `execution.allow`? The IR carries CANONICAL
|
|
58
|
+
* entries (ir `IrProvability.execution.allow`): a builtin is its spec key
|
|
59
|
+
* (`moneyAdd`), anything else its exact name. A recorded builtin name
|
|
60
|
+
* (`MoneyAdd`) is mapped to its key case-insensitively, the way the compiler
|
|
61
|
+
* maps `tools:`; a non-builtin name (`mcp__calc__add`) must match exactly,
|
|
62
|
+
* because MCP tool names are case-sensitive.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isToolAllowed(toolName: string, allow: ReadonlyArray<string>): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Result tokens the figure polarity rule has read into a statement's table
|
|
67
|
+
* of words: for a test to count with, as claim-ledger's `scanMeter` is.
|
|
68
|
+
* Only ever added to.
|
|
69
|
+
*/
|
|
70
|
+
export declare const polarityMeter: {
|
|
71
|
+
tokens: number;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Why the answer's sentence does not state what a proof's recorded claim
|
|
75
|
+
* says, or `undefined` when it does (design §6.4): "the grounded coverage
|
|
76
|
+
* test", read the way `grounded` reads it — the words, AND the figures and
|
|
77
|
+
* the negations that coverage alone cannot see.
|
|
78
|
+
*
|
|
79
|
+
* - Words: the artifact claim's content tokens covered by the sentence's
|
|
80
|
+
* at the grounded bar, so an accepted proof of something else cannot be
|
|
81
|
+
* pinned to this sentence.
|
|
82
|
+
* - Figures: the same figures on both sides (`two` and `2` are one). A
|
|
83
|
+
* proof about meetings 1 and 2 does not prove a sentence about all six,
|
|
84
|
+
* and `contentTokens` drops one- and two-digit numbers.
|
|
85
|
+
* - Polarity, word by word: each word of the proof's claim (a figure by its
|
|
86
|
+
* value) is denied on both sides or on neither, as claim-ledger's
|
|
87
|
+
* `governingNegation` scopes a negation, wherever the sentence uses it.
|
|
88
|
+
* `contentTokens` drops "no" (two letters) and "not" (a stopword), so
|
|
89
|
+
* without this an accepted proof that two of the six meetings overlap
|
|
90
|
+
* would prove "no two of the six meetings overlap" — acceptance scenario
|
|
91
|
+
* C served false. Read per word, not per sentence, because the sentence
|
|
92
|
+
* may say more than the proof (design §6.4): "no discount was applied, so
|
|
93
|
+
* the invoice total is 1,284.50 EUR" states the proved total, and its
|
|
94
|
+
* "no" denies a discount the proof never mentions. A word one side both
|
|
95
|
+
* states and denies is not compared, and a negation that moves from one
|
|
96
|
+
* word to another ("no two of the meetings overlap", "two of the
|
|
97
|
+
* meetings didn't overlap") is a different statement. A partitive denial
|
|
98
|
+
* of the whole statement ("none of the 3 invoices is overdue") is read
|
|
99
|
+
* too ({@link partitiveDenial}), and so is a frame that negates it ("it
|
|
100
|
+
* is not true that …", {@link negationFrames}): one side framed and the
|
|
101
|
+
* other not states the opposite (acceptance C).
|
|
102
|
+
*
|
|
103
|
+
* What it cannot see is a sentence that adds a clause of its own to the
|
|
104
|
+
* proved statement ("…, and the CEO approved it"): the coverage runs from
|
|
105
|
+
* the proof's claim into the sentence, as the design specifies, so the
|
|
106
|
+
* sentence may carry more words. That residual, like the formalization gap
|
|
107
|
+
* itself, is shown beside the claim (the statement), not closed here.
|
|
108
|
+
*/
|
|
109
|
+
export declare function statementMismatch(proved: string, sentence: string, bar: number): string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* An evidence id as the model may have dressed it — `` tool:`toolu_01` ``,
|
|
112
|
+
* `proof:prf_….` at the end of a sentence — down to the id. Ids are
|
|
113
|
+
* `[A-Za-z0-9_-]` runs, so wrapping quotes, backticks and angle brackets and
|
|
114
|
+
* trailing sentence punctuation are never part of one. Trimmed by walking
|
|
115
|
+
* in from each end: an end-anchored pattern (`/[…]+$/`) restarts at every
|
|
116
|
+
* character of a long run of them that does not end the id — quadratic in
|
|
117
|
+
* text the model writes, six seconds for 80 000 dots.
|
|
118
|
+
*/
|
|
119
|
+
export declare function bareId(id: string): string;
|
|
120
|
+
/**
|
|
121
|
+
* What `grounded.independent` counts as one origin: the registrable domain
|
|
122
|
+
* of a URL (`docs.acme.example` and `www.acme.example` are one publisher), a
|
|
123
|
+
* DOI's registrant prefix (`10.1000` — one publisher's papers are one
|
|
124
|
+
* origin, like one site's pages), and a workspace file's own path.
|
|
125
|
+
*/
|
|
126
|
+
export declare function independenceKey(origin: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* `sha256:<hex>` over the canonical JSON (keys sorted recursively) of every
|
|
129
|
+
* field of the bundle except `bundleHash` itself — what the audit record
|
|
130
|
+
* and the trace line carry, so a stored bundle can be shown to be the one
|
|
131
|
+
* that was graded.
|
|
132
|
+
*/
|
|
133
|
+
export declare function bundleHash(bundle: Omit<ProofBundle, "bundleHash"> | ProofBundle): string;
|
|
134
|
+
/**
|
|
135
|
+
* The bundle for a verifier that THREW (design §5.1): grading
|
|
136
|
+
* infrastructure failed, the gate fails open, and the bundle says so rather
|
|
137
|
+
* than printing a ✓ — `verdict: "unverified"`, no claims, the error clipped.
|
|
138
|
+
* Only the floor and the threshold are read, so the runtime gate — which
|
|
139
|
+
* holds `RunProvability`, not the lowered IR block — can build one too.
|
|
140
|
+
*/
|
|
141
|
+
export declare function unverifiedBundle(config: Pick<ProvabilityConfig, "level" | "threshold">, error: unknown, retryIndex?: number, now?: Date | (() => Date)): ProofBundle;
|
|
142
|
+
/**
|
|
143
|
+
* Citations the up-front HEAD collection in {@link verifyDocument} has
|
|
144
|
+
* examined: for a test to count with, as claim-ledger's `scanMeter` is.
|
|
145
|
+
* Only ever added to.
|
|
146
|
+
*/
|
|
147
|
+
export declare const headCollectMeter: {
|
|
148
|
+
markers: number;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Verify every claim of `text` against `opts.evidence` at `opts.config`'s
|
|
152
|
+
* floor, and return the proof bundle (design §5.3).
|
|
153
|
+
*
|
|
154
|
+
* - Claims are claim-ledger's `extractClaims` — plus the text any
|
|
155
|
+
* definition line shows beyond what it names, read as claims citing that
|
|
156
|
+
* definition (see `definitionClaims`); a claim with no marker is unmet
|
|
157
|
+
* with zero bindings (`no_citation`), one with a dangling marker is unmet
|
|
158
|
+
* (`no_definition`).
|
|
159
|
+
* - Each marker binds to its definition, classified by claim-ledger's
|
|
160
|
+
* evidence-ref grammar: a source (cited → grounded), `tool:` (checked),
|
|
161
|
+
* `proof:` (proven) or `assume:` (an assumption).
|
|
162
|
+
* - A binding records the highest rung it reached and, when that is below
|
|
163
|
+
* the floor, the FIRST check it failed. Rungs above the floor are still
|
|
164
|
+
* computed — `achieved` is "the highest floor this answer would have
|
|
165
|
+
* passed" — but the three outside calls are made only where the floor
|
|
166
|
+
* needs them: `resolveHead` at `cited`/`grounded` (no source can meet a
|
|
167
|
+
* higher floor), `judgeWaiver` at `grounded` (for a coverage-only
|
|
168
|
+
* paraphrase miss), `replay` at `checked`. So `achieved` above the floor
|
|
169
|
+
* is computed without them.
|
|
170
|
+
* - `scope: turn` (`turnNumbers`) restricts the evidence to those turns.
|
|
171
|
+
*
|
|
172
|
+
* Never throws for anything a document or its evidence can contain — an
|
|
173
|
+
* unreadable artifact, a failing judge or replay, a refused HEAD are each a
|
|
174
|
+
* binding's reason. Throws only for a malformed config (a programmer error).
|
|
175
|
+
*/
|
|
176
|
+
export declare function verifyDocument(text: string, opts: VerifyOptions): Promise<ProofBundle>;
|