DEV Community

Cover image for Make a stale number fail `next build`: a claim register whose quoted clauses verify themselves
Kynth
Kynth

Posted on

Make a stale number fail `next build`: a claim register whose quoted clauses verify themselves

For about a week our landing said civil penalties under the TAKE IT DOWN Act "currently approach $53,000." The FTC's own business guidance says $53,088 per violation. That is not a rounding error in the harmless direction — $53,088 is over $53,000, not approaching it. The commit that fixed it counts 44 instances across 8 files.

The reason it survived is the interesting part. We already had FACTS.json — a register of every claim the site makes that can decay, each with the primary source, the date it was last re-fetched, a verbatim quote from that source, and an appearsOn list of the files that state it. The penalty claim's appearsOn listed two guides. The landing was not in it. So the re-verification pass checked the two files it was told about, found them correct, and moved on.

A register that records where a fact appears is a list somebody has to keep right. The fix was to make pages read the register instead.

claim() is the cheap half

export function claim(id: string): Claim {
  const found = CLAIMS.find((c) => c.id === id);
  if (!found) throw new Error(`FACTS.json has no claim with id "${id}"`);
  if (found.status !== "verified") {
    throw new Error(`FACTS.json claim "${id}" is "${found.status}", not verified — it cannot be published`);
  }
  return found;
}
Enter fullscreen mode Exit fullscreen mode

A typo fails the build rather than rendering an empty cell, and an entry parked mid-rotation — ours currently has one, a law-firm article behind a Cloudflare challenge that 403s both curl and a fetch, carried as unverifiable — can never reach a buyer.

TrustDesk policy generator interface with sample policy content and workspace photography — Tool produces formatted policy text instantly with clean, modern professional interface.

quote() is the half that actually bites

The problem value(id) doesn't solve: a comparison table often wants half of a registered claim. One of ours reads:

Receipt of a VALID removal request — and the duty is "as soon as possible", with 48 hours as the outer limit

The table's "what it means" cell wants the clause after the dash; the "on what clock" cell comes from a different claim. Retyping that clause into JSX puts a second, unlinked copy of the fact on the page, which is the exact failure the register exists to prevent.

So the clause is passed together with the claim it came out of, and containment is proved at build time:

export function quote(id: string, clause: string): string {
  const norm = (s: string) =>
    s.replace(/[''ʼ]/g, "'").replace(/[""]/g, '"').replace(/\s+/g, " ").trim();
  const registered = claim(id).value;
  if (!norm(registered).includes(norm(clause))) {
    throw new Error(
      `FACTS.json claim "${id}" no longer contains the quoted clause.\n  quoted:     ${clause}\n  registered: ${registered}`,
    );
  }
  return clause;
}
Enter fullscreen mode Exit fullscreen mode

The normalisation is not cosmetic. The register and the JSX disagree about apostrophes and nothing else, so a curly-quote mismatch would fail the build for a reason that has nothing to do with the fact.

I tested the gate rather than assuming it: I edited the registered value from 48 hours as the outer limit to 72 hours, left the table cell alone, and ran pnpm build. It compiled fine, then died in Collecting page data:

Error: FACTS.json claim "tida-clock-starts-on-valid-request" no longer contains the quoted clause. quoted: the duty is "as soon as possible", with 48 hours as the outer limit / registered: … with 72 hours as the outer limit

Failed to collect page data for /, exit code 1. Not a warning, not a runtime 500 on one route — no deployable output at all.

What each tier of coverage actually protects

How a figure reaches the page Where the link between page and source lives What happens when the register changes State in this repo today
Read through value() / quote() The import — the page cannot hold a second copy Build fails, or the cell updates itself One component, 15 of the register's 35 claims
Hand-typed, listed in appearsOn A path string in JSON, maintained by hand Nothing fails; the next pass has to grep the listed files $53,088 appears 25 times across 15 files; its claim lists 14
Hand-typed, not in appearsOn Nowhere Nothing at all How "approach $53,000" survived

The source line under the table is generated too — asOf(...ids) returns the newest verification date across every claim the table used, so the page dates itself instead of carrying a date somebody typed beside it.

Row three is now empty by construction for anything the table renders. Rows one and two are not: 25 hand-typed occurrences of $53,088 remain in prose across 15 files, still guarded only by that appearsOn list.

This is how we built TrustDesk, a hosted NCII takedown desk for covered platforms: https://trustdesk.kynth.studio

Top comments (0)