Appendix B · section 11 of 32
ask/check.ts
From The Check That Is Not the Model.
import type { Reply } from "./answer.js";
import type { Law } from "./corpus.js";
import { figuresIn } from "./figures.js";
// Whether a road-law answer may be shown, decided here and nowhere else.
//
// The model may only have written from `given`, so that is the whole of what
// it may cite: an id outside it is a law nobody gave it, which means memory,
// and a page the answer names that no cited law is on is a page it made up.
// The figures are grounded against the cited laws' rule and the source's own
// quoted words, both -- never against the page or section reference in front
// of them, because "p.66" must not ground "66 feet".
/** Three sentences in the register the rules ask for run to about four hundred characters. */
export const MAX_ANSWER_CHARS = 600;
/** Why an answer was not shown. Written to the log by name. */
export type Refusal = "not-covered" | "uncited" | "ungrounded" | "overlong";
export type Checked =
| { ok: true; answer: string; laws: Law[] }
| { ok: false; reason: Refusal };
/** The pages a citation names: "p.66 (printed) — '…'" → ["66"]; "pp.12-13" → ["12", "13"]. */
export function citedPages(citation: string): string[] {
const reference = /^\s*pp?\.\s*([\d–,\s-]+)/i.exec(citation);
return reference === null ? [] : (reference[1].match(/\d+/g) ?? []);
}
/** The pages an answer names in prose: "p.66", "page 66", "pages 12 and 13". */
export function pagesNamedIn(answer: string): string[] {
const found: string[] = [];
for (const match of answer.matchAll(/\b(?:pp?|pages?)\.?\s*(\d+(?:\s*(?:[-–,]|and)\s*\d+)*)/gi)) {
found.push(...(match[1].match(/\d+/g) ?? []));
}
return found;
}
/** The source's own words: everything from the " — " on. A reference with no quote grounds nothing. */
export function quotedWords(citation: string): string {
const quoted = citation.indexOf(" — ");
return quoted < 0 ? "" : citation.slice(quoted);
}
export function checkAnswer(
reply: Extract<Reply, { kind: "reply" }>,
given: Law[],
// The learner's own words. A figure the learner gave may be said back --
// "a 45 mph road is over the 35 limit" -- without a law stating it.
question = "",
): Checked {
const answer = reply.answer.trim();
const ids = [...new Set(reply.sources)];
if (answer.length === 0 || ids.length === 0) return { ok: false, reason: "not-covered" };
const cited: Law[] = [];
for (const id of ids) {
const law = given.find((each) => each.id === id);
if (law === undefined) return { ok: false, reason: "uncited" };
cited.push(law);
}
const pages = new Set(cited.flatMap((law) => citedPages(law.citation)));
if (pagesNamedIn(answer).some((page) => !pages.has(page))) {
return { ok: false, reason: "uncited" };
}
const grounded = new Set<string>();
for (const law of cited) {
for (const figure of figuresIn(`${law.law}\n${quotedWords(law.citation)}`)) grounded.add(figure);
}
for (const figure of figuresIn(question)) grounded.add(figure);
for (const figure of figuresIn(answer)) {
if (!grounded.has(figure)) return { ok: false, reason: "ungrounded" };
}
if (answer.length > MAX_ANSWER_CHARS) return { ok: false, reason: "overlong" };
return { ok: true, answer, laws: cited };
}
// The lines the pipeline writes when nothing the model wrote is shown. They
// are ours, not the model's, and none contains a figure.
const POINTERS: Record<Refusal | "other" | "failed", string> = {
"not-covered":
"Your state's manual and code don't say, as far as I can find, so I won't guess about road law.",
uncited:
"I can't answer that from your state's manual or code in a way I can check, so I won't guess about road law.",
ungrounded:
"I can't answer that from your state's manual or code in a way I can check, so I won't guess about road law.",
overlong:
"I can't answer that from your state's manual or code in a way I can check, so I won't guess about road law.",
other:
"That is outside what I can answer here: I answer road law from your state's manual and code, and not much else.",
failed: "I couldn't reach the model that writes the answers. Try again in a minute.",
};
export function pointerFor(reason: keyof typeof POINTERS): string {
return POINTERS[reason];
}