Chapter 8 · section 2 of 5
The section as a key
Listing: ask/schedule.ts
import type { Law } from "./corpus.js";
// The court's fine schedule: what a driver actually pays, whether it can be
// paid without going to court, and the points. The code says what a driver
// must do and, at most, the range a fine may fall in; the figure a learner
// pays for not showing a license is $50 in the District Court's schedule and
// in no section of the Transportation Article.
export interface PenaltyRow {
id: string;
state: string;
/** The section the row joins to: the key the code's rows carry too. */
section: string;
/** The schedule's own reference, "16-112(c)", which the id is made from. */
code: string;
/** The offence in the schedule's own wording. */
offence: string;
fine_text: string;
fine_min_cents: number | null;
fine_max_cents: number | null;
points: number | null;
appear: "payable" | "must-appear" | "court-sets";
source_url: string;
source_title: string;
as_of: string;
/** The schedule's own line, verbatim: what the figure is grounded on. */
quote: string;
}
const SOURCE = {
state: "MD",
source_url: "https://www.mdcourts.gov/sites/default/files/court-forms/dccr090.pdf",
source_title: "District Court of Maryland, DC-CR-090 (Rev. 10/2026)",
as_of: "2026-10-11",
} as const;
export const ROWS: PenaltyRow[] = [
{
...SOURCE,
id: "MD-P-16-112-b",
section: "Md. Code, Transp. § 16-112",
code: "16-112(b)",
offence: "Failure of individual driving on hwy. to have lic. with them",
fine_text: "$50.00",
fine_min_cents: 5000,
fine_max_cents: 5000,
points: 0,
appear: "payable",
quote: "16 112 b Failure of individual driving on hwy. to have lic. with them $ 50.00 00",
},
{
...SOURCE,
id: "MD-P-16-112-c",
section: "Md. Code, Transp. § 16-112",
code: "16-112(c)",
offence: "Failure of individual driving on hwy. to display lic. to uniformed police on demand",
fine_text: "$50.00",
fine_min_cents: 5000,
fine_max_cents: 5000,
points: 0,
appear: "payable",
quote: "16 112 c Failure of individual driving on hwy. to display lic. to uniformed police on demand $ 50.00 00",
},
{
...SOURCE,
id: "MD-P-21-604-d",
section: "Md. Code, Transp. § 21-604",
code: "21-604(d)",
offence: "Driver failing to give turn signal continuously during last 100 ft. of travel before turning",
fine_text: "$90.00",
fine_min_cents: 9000,
fine_max_cents: 9000,
points: 1,
appear: "payable",
quote: "21 604 d Driver failing to give turn signal continuously during last 100 ft. of travel before turning $ 90.00 01",
},
{
...SOURCE,
id: "MD-P-21-801-1",
section: "Md. Code, Transp. § 21-801.1",
code: "21-801.1",
offence: "Driving veh. on hwy. at speed exceeding limit Exceeding posted speed limit by: 1 to 9 mph",
fine_text: "$80.00",
fine_min_cents: 8000,
fine_max_cents: 8000,
points: 1,
appear: "payable",
quote: "21 801.1 Driving veh. on hwy. at speed exceeding limit Exceeding posted speed limit by: 1 to 9 mph $ 80.00 01",
},
];
/** Cents as a learner reads a price: "$50", "$25.50", "$1,000". */
export function dollars(cents: number): string {
const whole = Math.floor(cents / 100);
const rest = cents % 100;
return `$${whole.toLocaleString("en-US")}${rest === 0 ? "" : `.${String(rest).padStart(2, "0")}`}`;
}
function pointsPhrase(points: number | null): string {
if (points === null) return "";
return points === 0 ? ", no points" : `, ${points} point${points === 1 ? "" : "s"}`;
}
// The law a schedule row is given to the model as: one sentence a learner can
// read. A row the court sets names the range and says so, never the low
// figure alone: "$25" for an offence a court may fine $200 is a promise the
// schedule does not make. Points only where the row has them: "no points"
// from a source that does not list points would be invented.
export function penaltySentence(row: PenaltyRow): string {
const points = pointsPhrase(row.points);
const { fine_min_cents: min, fine_max_cents: max } = row;
if (row.appear === "must-appear") {
return `${row.offence}: you must go to court, and the court sets the fine${points}.`;
}
if (row.appear === "court-sets") {
const range =
min !== null && max !== null
? `within ${dollars(min)} to ${dollars(max)}`
: max !== null
? `of up to ${dollars(max)}`
: min !== null
? `of at least ${dollars(min)}`
: "in an amount the law leaves to it";
return `${row.offence}: a fine set by the court ${range}${points}.`;
}
const fine = min === null ? row.fine_text : max === null || max === min ? dollars(min) : `${dollars(min)} to ${dollars(max)}`;
return `${row.offence}: ${fine}, can be paid without going to court${points}.`;
}
/** A schedule row as a law the model is given and the checker grounds, cited to the schedule's own line. */
export function penaltyLaw(row: PenaltyRow): Law {
return {
id: row.id,
law: penaltySentence(row),
question: "",
concept: row.offence,
citation: `${row.source_title} — '${row.quote}'`,
kind: "penalty",
asOf: row.as_of,
};
} The four rows are real rows of the District Court of Maryland’s schedule DC-CR-090, as a script read them from the PDF on 11 October 2026, and the quote on each is the schedule’s own line. penaltyLaw turns a row into a law of the shape every other stage already handles: a sentence the model reads, a citation whose quoted part is what the check grounds on. The $50 in the answer is grounded by the $ 50.00 in the quote, and the learner sees the schedule’s title and date under the answer, the way they see a section.
penaltySentence carries two rules that are about honesty rather than formatting. A row the court sets names the range and says the court decides, never a single figure: “$25” for an offence the court may fine $200 is a promise the schedule does not make. And points appear only where the row has them, because “no points” from a schedule that does not list points would be a fact from nowhere.
Listing: ask/sections.ts
import { referenceOf } from "./answer.js";
import type { Law } from "./corpus.js";
import { penaltyLaw, type PenaltyRow } from "./schedule.js";
// Once retrieval has found the section, the rest is a lookup, not a search.
// What *happens* is spread over three bodies, and all three are keyed by the
// section the code uses: the code itself holds the duty, the penalty range
// and any way out; the court's schedule holds the figure actually paid; the
// agency's own pages hold the process no section states.
/** At most this many laws of one section reach the model. */
export const SECTION_LAWS = 12;
/** At most this many laws reach the model on a question once its sections are whole. */
export const GIVEN_LAWS = 40;
/**
* The section a law belongs to, as its citation names it: the reference
* before " — " with its subsection removed. "§ 16-112(c)" and "§ 16-112(d)"
* are both "Md. Code, Transp. § 16-112". Null for a manual law, whose citation
* is a page.
*/
export function sectionOf(law: Pick<Law, "kind" | "citation">): string | null {
if (law.kind !== "statute") return null;
const section = referenceOf(law.citation)
.replace(/,\s*subd\..*$/, "")
.replace(/(?:\s*[–-]?\s*\([^()]*\))+[\s–-]*$/, "")
.trim();
return section === "" ? null : section;
}
/**
* The laws chosen, each statute with the rest of its section. The chosen
* laws stay first, in the order they were chosen, so the matched row is never
* pushed down by its neighbours; then each section's other rows in the order
* the file holds them. A manual law has no section and brings nothing.
*/
export function wholeSections(chosen: Law[], file: Law[]): Law[] {
const sections = [...new Set(chosen.map(sectionOf).filter((section): section is string => section !== null))];
if (sections.length === 0) return chosen;
const given = [...chosen];
const seen = new Set(chosen.map((law) => law.id));
for (const section of sections) {
let count = chosen.filter((law) => sectionOf(law) === section).length;
for (const law of file) {
if (given.length >= GIVEN_LAWS) return given;
if (count >= SECTION_LAWS) break;
if (seen.has(law.id) || sectionOf(law) !== section) continue;
given.push(law);
seen.add(law.id);
count += 1;
}
}
return given;
}
/** At most this many schedule rows join one section. */
export const SECTION_PENALTIES = 8;
/** At most this many schedule rows join a question's laws in all. */
export const GIVEN_PENALTIES = 16;
/**
* The laws given, each statute section followed by its rows of the court's
* fine schedule. Each row goes after the last law of its section, so it reads
* as the price of the law above it. The rows for the subsections the question
* matched come first; a row for a section that was not given is put last
* rather than hidden, so a lookup that matched too much is seen in what the
* model is given.
*/
export function withPenalties(given: Law[], rows: PenaltyRow[], chosen: Law[] = given): Law[] {
const sections = [...new Set(given.map(sectionOf).filter((section): section is string => section !== null))];
if (sections.length === 0) return given;
const found = rows.filter((row) => sections.includes(row.section));
const references = chosen
.filter((law) => sectionOf(law) !== null)
.map((law) => referenceOf(law.citation));
const named = (row: PenaltyRow): boolean => {
const own = `${row.section}${row.code.includes("(") ? row.code.slice(row.code.indexOf("(")) : ""}`;
return references.some((reference) => reference.startsWith(own) || own.startsWith(reference));
};
const ordered = [...found.filter(named), ...found.filter((row) => !named(row))];
const after = new Map<string, Law[]>();
const counted = new Map<string, number>();
let total = 0;
for (const row of ordered) {
if (total >= GIVEN_PENALTIES) break;
if ((counted.get(row.section) ?? 0) >= SECTION_PENALTIES) continue;
counted.set(row.section, (counted.get(row.section) ?? 0) + 1);
total += 1;
after.set(row.section, [...(after.get(row.section) ?? []), penaltyLaw(row)]);
}
const lastOf = new Map(given.map((law, index) => [sectionOf(law), index]));
const out: Law[] = [];
given.forEach((law, index) => {
out.push(law);
for (const [section, laws] of after) if (lastOf.get(section) === index) out.push(...laws);
});
return out;
}
/**
* The laws given, with the state's own pages after the manual and the code:
* the page is preferred for process and never for the rule of the road, and
* a page's paraphrase of the hydrant rule listed above the manual's own would
* be read first by a model told to answer from what it is given.
*/
export function pagesLast(laws: Law[]): Law[] {
return [...laws.filter((law) => law.kind !== "process"), ...laws.filter((law) => law.kind === "process")];
}
sectionOf is the key, and it is two regular expressions on the citation’s reference. Every trailing parenthesised group goes, not only the last, so that (c)(1) keys with (d); so does a range, Maryland’s (g)(1)–(5); so does Minnesota’s , subd. 1a, which is its word for a subsection. The deployed pipeline derives the key from the citation rather than from the id because the ids do not carry the section uniformly across states, and the citation, in each state’s own style, does.
Three orderings in those functions are each a measured failure.
The chosen laws stay first. A section’s neighbours come after the law retrieval found, in the order the code prints them, so the matched subsection is never pushed down by eleven others. The model reads top to bottom, and a subsection that answers the question in position twelve is one it may not reach.
The matched subsection’s schedule row comes first. Maryland’s § 21-1003 is thirty parking rows in the schedule, and in the schedule’s order the cap of eight stopped at (i), so the hydrant’s (m), the one the learner asked about, was never given. The rows for the subsections the question matched go ahead of the section’s others.
A page goes last. The agency’s page is preferred for process, how soon you can retake a test, and never for a rule of the road, and a page that paraphrases the hydrant rule listed above the manual’s own sentence would be read first by a model told to answer from what it is given.
And the caps are measurements: twelve rows a section, because Florida’s specialty-plate section, § 320.08058, is 400 rows and Wisconsin’s definitions section 235; forty laws in all; and in the deployed version a character budget as well, 24,000 characters of neighbours, because of the corpus’s 33,960 sections, 135 run past 20,000 characters and one to 173,000, and twelve rows of that one is a prompt several times the size of the answer’s worth.