Chapter 3 · section 2 of 6
What counts as a figure
The fourth question needs a definition of figure that a program can apply, and the deployed pipeline’s definition has grown one measured failure at a time. Yours starts with the cases that failed first.
Listing: ask/figures.ts
// Every figure a text states, as strings of digits: its digit runs and its
// spelled numbers. Statutes spell their figures where manuals print digits,
// and the prompt asks the model for digits, so "closer than five hundred feet"
// in a law has to ground "500 feet" in an answer.
const SMALL = [
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve",
];
const UNITS: Record<string, number> = {
one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8,
nine: 9, ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14,
fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18, nineteen: 19,
};
const TENS: Record<string, number> = {
twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70,
eighty: 80, ninety: 90,
};
export function figuresIn(text: string): Set<string> {
// "1,000" is one figure, not 1 and 000. "0.08" is read as ".08", the way
// the manuals write a blood alcohol level, so the leading zero is not a
// figure of its own.
const normalised = text
.replace(/(\d),(?=\d{3}(?!\d))/g, "$1")
.replace(/\b0(\.\d)/g, "$1");
const found = new Set<string>(normalised.match(/\d+/g) ?? []);
const lower = normalised.toLowerCase();
SMALL.forEach((word, value) => {
if (new RegExp(`\\b${word}\\b`).test(lower)) found.add(String(value));
});
for (const word of lower.match(/[a-z]+/g) ?? []) {
if (UNITS[word] !== undefined) found.add(String(UNITS[word]));
if (TENS[word] !== undefined) found.add(String(TENS[word]));
if (word === "hundred") found.add("100");
if (word === "thousand") found.add("1000");
}
for (const value of compounds(lower)) found.add(String(value));
// "One" is a pronoun far more often than a figure in a road-law answer --
// "within 15 feet of one", "a one-way street" -- and read as 1 it refused
// correct answers. A digit 1 standing on its own is still a figure.
if (!/(^|[^\d.])1(?![\d])/.test(normalised)) found.delete("1");
return found;
}
// Spelled compounds as the whole numbers they are: "five hundred" is 500,
// "twenty-five" is 25, "one thousand five hundred" is 1500. Added beside the
// word-by-word values, never instead of them, so nothing that was grounded
// before stops being grounded.
export function compounds(text: string): number[] {
const out: number[] = [];
let total = 0;
let chunk = 0;
let open = false;
const close = (): void => {
if (open) out.push(total + chunk);
total = 0;
chunk = 0;
open = false;
};
for (const word of text.toLowerCase().match(/[a-z]+/g) ?? []) {
if (UNITS[word] !== undefined) chunk += UNITS[word];
else if (TENS[word] !== undefined) chunk += TENS[word];
else if (word === "hundred" && open) chunk = (chunk === 0 ? 1 : chunk) * 100;
else if (word === "thousand" && open) {
total += (chunk === 0 ? 1 : chunk) * 1000;
chunk = 0;
} else if (word === "and" && open) continue;
else {
close();
continue;
}
open = true;
}
close();
return out;
} A figure is a string of digits, not a number, so that "08" and "8" stay different and the check never has to decide what a blood alcohol level is equal to. Every line of this file that is not the obvious regular expression is there because an answer was refused that should have been shown, or shown that should have been refused:
"1,000"is one figure. New York’s crash report law spells “one thousand dollars”; the correct$1,000in an answer was read as1and000and refused.- Spelled numbers count. Statutes spell their figures where manuals print digits, and the rules ask the model for digits, so Colorado’s “closer than five hundred feet” has to ground an answer’s “500 feet”. Read word by word it grounded 5 and 100, and the correct answer was refused.
- “One” is not a figure. In a road-law answer it is a pronoun far more often than a number, and read as
1it refused a correct hydrant answer half the time when the deployed check was measured on 5 October 2026. A digit1on its own still has to be grounded.
Each is a rule that widens what counts as grounded, and each was added beside the earlier rules rather than instead of them. The deployed version has several more, and the section at the end of this chapter shows three.