Joel Stevick

Appendix B · section 21 of 32

ask/sections.ts

From Once Found, Look It Up.

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")];
}