Files
adr-sml/scripts/check-claims.mjs
T
Pouya LajevardiandClaude Opus 5 bd282aa47d
Build and deploy / build-and-deploy (push) Failing after 4s
feat: production run — Q61 ramp, /404/, CloudFront router, cutover runbook
Five items of Pouya's production run, 2026-09-01.

Q61 — scroll-padding-top becomes a max() ramp on `10lh - 83px`, with the
plain calc() first as the fallback for engines without `lh`. Hidden focus
stops under minimumFontSize=32: 290 of 1,455 -> 0, control build still
290. Default settings byte-identical (0 differences over 352 page-widths x
17 fields). The 12 residual cells at minimumFontSize=16/20 are pre-existing
and unchanged-or-better; reported, not widened, per instruction.

Intake backend + CloudFront — docs/09-cutover-runbook.md is the
copy-paste sequence for admin execution: every command followed by its
verification and expected output, rollback per part, and Part 10 is Q60's
TTL test. infra/cloudfront/router.js is the trailing-slash function
(30-case suite; 8 fail against the pre-review version, incl. a
protocol-relative open redirect). infra/cloudfront/configure.mjs is
dry-run-by-default and idempotent. scripts/intake-env.mjs emits the six
Lambda env vars from src/data/site.ts.

Four launch blockers found by reading the running system:
  - handler.mjs wrote pk/sk; the live table's key is submissionId with no
    sort key, so every submission would have failed validation silently
  - the Lambda invoke permission is scoped to the old route path
  - 22 of 23 pages 403 without the router function
  - there was no 404 page; src/pages/404.astro adds it

Claims audit (D20 cutover pass) — five gloss over-reaches corrected on
/practice/energy/, /practice/insurance/ (x2), /practice/technology/ and
/med-arb/. Three findings left open for Pouya: Q62, the /med-arb/ gloss,
and Q60.

Q62 — one frozen-tripwire pattern added under the freeze's own breach
exception, with a probe and four negative fixtures. check:claims exits 1
until the false /legal/privacy/ sentence is corrected, so both deploy
paths are blocked by a mechanism rather than by memory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
2026-09-02 06:52:20 -04:00

762 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* `npm run check:claims` — a mechanical gate on the SHIPPED OUTPUT.
*
* Pouya's ruling, 2026-08-29, and the reasoning is the point:
*
* "Your recurring failure this project is a specific shape: you write a rule
* into a header comment, then breach it in the file below, in the same
* change set. Q53's sweep, Q54's gate four times, the D13 'cannot' phrasing
* fifty lines under your own warning about it. That is not a discipline
* problem — prose in a comment does not govern the writing that follows it.
* The pattern that actually worked was mechanical: deleting the `class` prop
* so passing one is a build error. Nobody has breached that since."
*
* So this is not documentation. It is a build error.
*
* SCOPE — `dist/**\/*.html` AND NOTHING ELSE. Not `AGENTS.md`, not `docs/`, not
* `src/`. D19 bounds review to what ships, and §4's Forbidden table says in as
* many words that it "governs published pages… It is not a word filter over the
* repository's own documentation." A register that records a forbidden phrase in
* order to forbid it must not be failed by its own quotation of it.
*
* `<style>` BLOCKS AND NON-JSON-LD `<script>` BLOCKS ARE STRIPPED FIRST, and
* that is load-bearing rather than tidy. Measured against the step-4 build
* before this file existed: a bare case-insensitive sweep for `leading` returned
* **26 hits, 25 of them `var(--leading-body)`** inside Astro's inlined critical
* CSS. A check that cries wolf on every page on its first run is a check
* somebody deletes in week two. JSON-LD is deliberately NOT stripped — a claim
* in a `<script type="application/ld+json">` is still a claim, and docs/04 says
* so: "Marking an unheld credential as held in structured data is a
* misrepresentation that happens to be machine-readable."
*
* EVERY PATTERN CARRIES ITS `incident`, PRINTED ON FAILURE. Pouya's
* instruction: "so nobody deletes one for being noisy." A pattern whose cost is
* visible and whose reason is not gets deleted; this prints the reason at the
* moment the cost is felt.
*
* AND IT SELF-TESTS BEFORE IT SWEEPS. See `FIXTURES` below. A regex that has
* quietly stopped matching passes a clean sweep forever, which is the exact
* shape of AGENTS.md Q22 — a documented control that no longer existed.
*/
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
const ROOT = resolve(import.meta.dirname, '..');
const DIST = resolve(ROOT, process.argv[2] ?? 'dist');
/* Sources whose mtime, if newer than the newest built page, means this sweep
would be reading yesterday's output. `docs/` is absent on purpose: it directs
copy but does not produce it, so a spec edit does not stale the build. */
const SOURCE_DIRS = ['src', 'public'];
const SOURCE_FILES = ['astro.config.mjs', 'package.json'];
/**
* The patterns.
*
* `re` is matched against the page's text with `<style>` and non-JSON-LD
* `<script>` removed. All are global and case-insensitive.
*
* WORD BOUNDARIES ARE NOT DECORATION ON ANY OF THESE. `\bleading\b` does not
* match "pleadings" and `\blicensed\b` does not match "licensing" — both are
* words this site publishes, and both were live false positives when the
* patterns were first drafted. Loosening a boundary here re-creates the noise
* this file was built to avoid.
*/
const PATTERNS = [
{
id: 'licensure-of-pouya',
rule: 'D13 / §4 Forbidden — the site asserts the JD and nothing further.',
incident:
'§4 opened with "Pouya is a licensed legal professional", then "a legal ' +
'professional", then an assertion that LSO marketing rules apply — three ' +
'progressively weaker forms of the same unverified claim, each surviving a ' +
'sweep meant to remove it, in the section written to stop exactly that. ' +
'§4 does not establish licence status either way.',
re: /\blawyer\b|\bcalled to the bar\b|\blicensed\b|\bpost-nominal\b/gi,
note: '"licensing" and "licence" are NOT matched — /practice/technology/ ships "IP and licensing".',
},
{
id: 'acting-for-a-party',
rule: '§4 Forbidden — implication is the risk, not just the word.',
incident:
'§4 bars "acts for clients", "represents parties", "my clients", ' +
'"my litigation practice". The site sells neutrality; a partisan verb ' +
'undercuts the central claim before it reaches the licensure question. ' +
'Settlement counsel was struck from three specs for the same reason (Q42).',
re: /\bmy litigation practice\b|\bmy law practice\b|\bmy clients?\b|\bacts for\b|\bI act for\b|\brepresents?\b/gi,
note:
'"I do not act for a party" is the APPROVED sentence (/mediation/) and does ' +
'not match. THE SINGULAR WAS ADDED 2026-08-29: the pattern read ' +
'`my clients` and `/med-arb/` was shipping "The neutral who heard my ' +
'client in caucus" in its FAQ — the possessive belonged to the counsel ' +
'voicing the objection, not to Pouya, but the gate cannot tell and the ' +
'phrase is forbidden in his voice, so the copy changed to "our client" ' +
'and the pattern widened. Same shape as the capacity-phrasing hole: a ' +
'pattern narrow enough to miss the real form of the breach.',
},
{
id: 'capacity-phrasing',
rule: 'docs/03 — when a fact is [unestablished], write around the capacity question.',
incident:
'Attempt 1 "I do not give legal advice" was flagged as an ELECTION ' +
'(entitlement withheld by choice). Attempt 2 "I cannot give legal advice" ' +
'was flagged as a DENIAL of capacity. Both audits were right: they are ' +
'opposite answers to a question §4 holds neither side of. The shipped ' +
'sentence makes no capacity claim at all.',
re: /\b(cannot|can ?not|can't|do(es)? not|don't|will not|won't|unable to|not (qualified|permitted|able) to)\s+(give|provide|offer)\s+legal advice\b|\bI\s+(?:cannot|can ?not|can't|do not|don't|will not|won't)\b[^.<]{0,25}\badvis(?:e|ing)\b/gi,
note:
'THE SECOND ALTERNATIVE WAS ADDED 2026-08-29 BECAUSE THE FIRST MISSED A REAL ' +
'BREACH. `/practice/cross-cultural/` shipped "What I do not do is advise on the ' +
'law of another country" — the election form, narrowed by jurisdiction, which ' +
'makes it worse rather than better: naming only foreign law invites the reader ' +
'to supply the domestic half. `claims-auditor` caught it and noted, correctly, ' +
'that this pattern could not, because it required the literal object "legal ' +
'advice". The verb list is deliberately tight — give / provide / offer / advise ' +
'/ render — and the window is 25 characters, so the six approved first-person ' +
'negations this site ships ("I do not act for a party", "I do not run a case", ' +
'"I will not run a process", "I do not carry a number across the hall", "I will ' +
'not convert a mediation", "If I cannot decide a remaining issue") all stay ' +
'silent. Every one of them is in the mustNotMatch fixtures below.',
},
{
id: 'designation-name',
rule: "ADRIO's own form is Chartered Med-Arbitrator.",
incident:
'"Chartered Mediator-Arbitrator" is not the name of anything ADRIC or ' +
'ADRIO confers. docs/03 §About names the correct form and the source is ' +
'docs/reference/adrio-designations.md. A designation stated in a form its ' +
'own institution does not use reads as a designation not held.',
re: /Chartered Mediator[- ]Arbitrator/gi,
},
{
id: 'rule-set-name',
rule: 'ADRIC publishes the ADRIC National Mediation Rules.',
incident:
'`docs/01` §/mediation/ said "Model Mediation Rules", which is not the ' +
'name of anything ADRIC publishes — "Model" belongs to the Model Dispute ' +
'Resolution Clause INSIDE the rules. Caught at build step 4 only because ' +
'the rules were fetched rather than named from recall (R14).',
re: /Model Mediation Rules/gi,
},
{
id: 'counts-and-tenure',
rule: '§4 Forbidden — no count of matters, hours mediated, or years in practice.',
incident:
'The site this replaces carried "Since 2009", "sixteen years", London and ' +
'New York offices and a company number, all artefacts of a purchased ' +
'template and all false. §4: the practice is new, and small true numbers ' +
'do not persuade a sophisticated GC — they invite scrutiny.',
re: /\b\d[\d,]*\s*\+?\s*(matters|mediations|arbitrations|appointments|awards)\b|\b\d[\d,]*\s+years\s+in\b|\bsince\s+(19|20)\d{2}\b|\b(ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty)[\s-]years\b/gi,
},
{
id: 'superlatives',
rule: '§4 Forbidden — unverifiable as written, and they read as insecure.',
incident:
'The reason §4 gives is editorial before it is regulatory: this audience ' +
'discounts everything after a superlative. AND THE FALSE POSITIVES ARE ' +
'REAL AND KNOWN — "pleadings" contains "leading" and every inlined ' +
'stylesheet contains var(--leading-body). Both are handled, by the word ' +
'boundary and by the <style> strip. Do not loosen the boundary and do not ' +
'delete this pattern because it once matched a token name.',
re: /\bleading\b|\bpremier\b|\btop[- ]rated\b|\bbest\b|\bworld[- ]class\b|\bunrivalled\b|\bunparalleled\b/gi,
},
/* --- Beyond Pouya's minimum list, 2026-08-29. Each names a real incident in
this repository and each has near-zero false-positive surface on this
site's vocabulary. Strike any of the three and the rest still stand. --- */
{
id: 'fabricated-founder',
rule: '§4 Forbidden — the fictitious founder from the template.',
incident:
'The site this replaces carried a founder who does not exist, a ' +
'testimonial attributed to a person who does not exist, invented matter ' +
'values and fabricated office locations. This register exists because of ' +
'that, and this is the one pattern whose match would be unambiguous.',
re: /\bS\.?\s?M\.?\s+Lawrence\b|\b07452218\b/gi,
},
{
id: 'q-arb-as-a-stage',
rule: '§4 — Q.Arb is HELD. It must never read as a stage, a pathway, or pending.',
incident:
'THIS PATTERN WAS INVERTED ON 2026-08-29 AND ITS PREDECESSOR IS GONE. It ' +
'used to bar Q.Arb reading as HELD, because §4 recorded it as "commenced ' +
'August 2026"; Pouya ruled it held (obtained July 2026, date NOT ' +
'published), so every stage word now understates a real credential. The ' +
'site carried the struck forms on six surfaces — an h1 reading "Available ' +
'now, and open about the stage", a whole /arbitration/ section headed ' +
'"Where I am in the arc", /about/\'s arc section, the footer strip, and ' +
'both JSON-LD nodes. Publish "Q.Arb (ADRIC / ADRIO)" and nothing more.',
/* ~50 characters either side, per Pouya's specification. The window stops at
the next TAG boundary so it cannot leap a paragraph. It stopped at a
sentence boundary too (`[^.<]`) until the fixture below proved that too
tight: "Qualified Arbitrator. Newly commenced" splits the anchor from the
stage word across a full stop, 22 characters apart, and that is precisely
the copy this pattern exists for. The self-test caught the regression.
⚠️ THE ANCHOR MATCHES THE EXPANSION TOO, and it did not on the first
draft. Tested against the nine strings this change set removed: it caught
seven and MISSED "Qualified Arbitrator. Newly commenced — not held, and
not nearing completion." — /about/'s own arc body, which never wrote the
abbreviation. An abbreviation-only anchor cannot see copy that spells the
designation out, and prose is exactly where it gets spelled out.
KNOWN LIMIT, stated rather than papered over: this cannot catch a stage
expressed without naming the designation at all. The h1 it replaced —
"Available now, and open about the stage" — matches nothing here and no
regex over shipped HTML would catch it without firing on ordinary copy.
That one is `claims-auditor`'s. */
/* WHAT EACH ALTERNATIVE IS FOR, because none is obvious from the regex:
(1) stage word after the designation, (2) before it, (3) an acquisition
DATE near it — §4 records July 2026 and bars publishing it, and docs/03
cited this script as enforcing that before it did (the Q22 shape) —
(4) "once/when I hold it", which must take the designation as its object
or it fires on "when I hold a preliminary conference".
THE ANCHOR MATCHES THE EXPANSION because `docs/03` authorises "Qualified
Arbitrator" as publishable prose, and prose is where it gets spelled out.
KNOWN LIMIT, stated rather than papered over: this cannot catch a stage
expressed without naming the designation. The h1 it replaced — "Available
now, and open about the stage" — matches nothing here, and no regex over
shipped HTML would catch it without firing on ordinary copy. That one is
`claims-auditor`'s. */
re: /(?:Q\.?\s?Arb|Qualified Arbitrator)\b[^<\uE000]{0,50}?\b(commenc\w*|in progress|pathway|not yet|not held|nearing completion|pending|working toward|under ?way)\b|\b(commenc\w*|in progress|pathway|not yet|not held|nearing completion|pending|working toward|under ?way)\b[^<\uE000]{0,50}?\b(?:Q\.?\s?Arb|Qualified Arbitrator)\b|(?:Q\.?\s?Arb|Qualified Arbitrator)\b[^<\uE000]{0,50}?\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+20\d\d\b|\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+20\d\d\b[^<\uE000]{0,50}?\b(?:Q\.?\s?Arb|Qualified Arbitrator)\b|\b(?:once|when)\s+(?:I|he)\s+(?:holds?|obtains?|receives?|completes?)\s+(?:it\b|the\s+)?(?:Q\.?\s?Arb|designation)\b/gi,
},
{
id: 'c-med-arb-struck',
rule: 'C.Med-Arb is out entirely — it appears on no page (Pouya, 2026-08-29).',
incident:
'It was the strategy brief\'s "explicit long-term professional narrative" ' +
'and it shipped on /, /about/, /arbitration/ and /med-arb/ as the endpoint ' +
'of a credentialing arc. Pouya dispensed with it: he holds Q.Med and ' +
'Q.Arb, which is what med-arb requires. THE DESIGNATION IS STILL REAL and ' +
'stays in AGENTS.md §11 as a definition — this bars naming it in shipped ' +
'output, not knowing what it is. Barred in every spelling both ' +
'institutions use, because the reinstatement risk is a well-meaning ' +
'implementer reading the brief, not a typo.',
/* ⚠️ THE LEADING \b AND THE MANDATORY DOT ARE BOTH LOAD-BEARING, and this
pattern shipped without them for one run. `C\.?\s?Med-Arb` matched
"ADRIC Med-Arb Rules" as "C Med-Arb" — the C of ADRIC, a space, then the
real rule-set name — and failed the build on approved copy on /med-arb/.
The abbreviation always carries the dot; "C Med-Arb" is not a form either
institution uses. The fixture below pins it. */
re: /\bC\.\s?Med-Arb(?:itrat(?:or|ion))?s?\b|\bChartered Med-Arb(?:itrat(?:or|ion))?s?\b/gi,
},
{
id: 'struck-universal-q39',
rule: 'Q39 — the class statement about arbitral gating was FALSE and was published once.',
incident:
'§4 carried "Anyone may be appointed an arbitrator in Ontario. Nothing in ' +
'law gates the role behind a designation." Pouya checked the ' +
'counter-example and confirmed it: "My assertion was FALSE as a ' +
'universal." Ontario family arbitrators must complete prescribed ' +
'training. The scoped replacement is COMMERCIAL arbitration and it is ' +
"Pouya's attributed position, not a verified fact. Swept four times; " +
'reached a public page once.',
re: /anyone (may|can) be appointed an arbitrator|nothing in law gates|no (licence|license|designation) is (required|needed) to (be appointed|arbitrate|act as an arbitrator)/gi,
},
{
id: 'sole-administrator-q62',
rule: 'Q62 — /legal/privacy/ claims sole administrative access to the intake table, and that is FALSE.',
incident:
'The page shipped "nobody else has access to the table. There is no team, ' +
'no assistant and no external administrator." The AWS account has an ' +
'`admins` IAM group carrying AdministratorAccess with TWO members, and ' +
'`simulate-principal-policy` returns allowed for dynamodb:GetItem, Query ' +
'and Scan on the table for both ' +
'[verified 2026-09-01 — docs/reference/intake-table-access-verification.md]. ' +
'THE REACHED-DIST CONDITION IS WHY THIS PATTERN EXISTS AT ALL: the ' +
'sentence was in dist/legal/privacy/index.html, `build`, `check` and this ' +
'script all exited 0 over it, and the only thing between it and a live ' +
'privacy policy was a TODO(pouya) in a JSX comment — which Astro strips, ' +
'so `grep -rn "TODO(pouya)" dist/` returned nothing. The gate was human ' +
'memory. Found by `adversarial-reviewer`, 2026-09-01. ' +
'DELETE THIS PATTERN when Q62 is ruled: either the access is removed and ' +
'the sentence becomes true, or the sentence is rewritten. It is a tripwire ' +
'on one specific published falsehood, not a rule about a class.',
/* `\s+`, NOT LITERAL SPACES: `compressHTML` collapses whitespace between
tags and leaves it alone inside a text node, so the published bytes read
"nobody else has access to\n the table". A literal-space version
runs, prints `ok`, and exits 0 with the breach in `dist/`. */
re: /nobody\s+else\s+has\s+access\s+to\s+the\s+table|there\s+is\s+no\s+team,\s+no\s+assistant\s+and\s+no\s+external\s+administrator/gi,
},
];
/**
* THE INSTRUMENT CHECK, and it runs before every sweep.
*
* `mustMatch` — a pattern that has stopped firing passes a clean sweep forever
* and looks identical to a clean site. `mustNotMatch` — the known-legitimate
* strings this site actually publishes; if one of them starts failing, the
* pattern got looser, not the copy worse.
*
* CLAUDE.md, five times over: "a measurement is a claim about your instrument
* until you check the instrument."
*/
const FIXTURES = {
mustMatch: {
'licensure-of-pouya': [
'He is a lawyer.',
'called to the bar',
'a licensed neutral',
],
'acting-for-a-party': [
'my litigation practice',
'acts for clients',
'I act for the claimant',
'represents parties',
'my clients',
/* The singular, which the pattern missed until 2026-08-29. */
'the neutral who heard my client in caucus',
],
'capacity-phrasing': [
'I cannot give legal advice',
'I do not give legal advice',
"I can't provide legal advice",
/* The real breach the first form of this pattern could not catch. */
'What I do not do is advise on the law of another country',
'I do not advise on foreign law',
],
'designation-name': ['Chartered Mediator-Arbitrator'],
'rule-set-name': ['the Model Mediation Rules'],
'counts-and-tenure': [
'40 matters',
'120 mediations',
'16 years in ADR',
'Since 2009',
'sixteen years',
],
superlatives: [
'a leading neutral',
'premier',
'top-rated',
'the best mediator',
],
'fabricated-founder': ['S. M. Lawrence', 'Co. № 07452218'],
'q-arb-as-a-stage': [
'The Q.Arb pathway commenced in August 2026',
'Q.Arb is in progress',
'Q.Arb — not yet held',
'once I hold Q.Arb',
'my Q.Arb pathway',
/* The expansion, which the first draft of this pattern missed. */
'Qualified Arbitrator. Newly commenced — not held, and not nearing completion.',
/* The acquisition date, which no stage word caught. */
'Q.Arb, obtained July 2026',
'Q.Arb (ADRIC / ADRIO), held since July 2026',
/* The `rule` line above named these and the regex never matched them —
a documented control not covering what it is cited for (Q22's shape). */
'Q.Arb is pending',
'Q.Arb — the designation I am working toward',
'the Q.Arb process is under way',
],
'c-med-arb-struck': [
'C.Med-Arb is the endpoint',
'Chartered Med-Arbitrator',
'the ADRIC Chartered Med-Arb designation',
/* The blend §11's own expansion invites, which the trailing \b in the
first draft made invisible: `C\.Med-Arb\b` cannot match when the next
character is a word char. */
'the C.Med-Arbitrator designation',
'Chartered Med-Arbitration',
/* Plurals: the trailing \b survived the first repair and blocked these. */
'Chartered Med-Arbitrators',
'C.Med-Arbitrators',
'C.Med-Arbitration',
],
'sole-administrator-q62': [
/* The two published clauses, verbatim from dist/legal/privacy/. */
'nobody else has access to the table',
'There is no team, no assistant and no external administrator.',
],
'struck-universal-q39': [
'Anyone may be appointed an arbitrator in Ontario',
'nothing in law gates the role',
],
},
/* Every one of these is real published or spec-approved copy on this site. */
mustNotMatch: [
/* NEGATIVE FIXTURES FOR `sole-administrator-q62` — the approved copy nearest
to the two clauses it catches. The pattern is deliberately anchored on the
two published sentences rather than on the ideas in them, because the
replacement wording is not yet decided and a looser pattern would fire on
whatever Q62's ruling produces. These four are what "nearest" means: the
same page's true sentences about the same subject. */
'The table is reachable by the function that writes to it.',
'Two accounts hold administrative access to the AWS account, and the function that writes to the table cannot read it.',
'Nobody else has access to my mailbox.',
'There is no team. Every inquiry is read by me.',
'I act as a neutral. I do not act for a party in a matter I take, and each party should have their own legal advice.',
'I run a process, I do not run a case for anybody in it.',
'I will not run a process whose shape nobody agreed to in advance.',
'I do not carry a number across the hall that I was not given to carry.',
'I will not convert a mediation into an arbitration on the day.',
'Commercial matters. I do not accept family arbitration.',
/* An OFFERING statement, not a capacity claim. The capacity pattern carried
`offer` for one pass and fired on this. */
'I do not offer family arbitration.',
/* Counsel's voice in /med-arb/'s FAQ, after the possessive was changed. */
'The neutral who heard our client in caucus then decides the case.',
/* Undertaking (c) — the nearest miss on the widened capacity pattern. */
'If I cannot decide a remaining issue without relying on something said to me in confidence, I say so and step out of the arbitral phase rather than decide on it.',
'Pleadings, disclosure, witnesses, experts, oral argument.',
/* Wrapped, because the wrapper IS what is being tested. The first draft of
this fixture was the bare declaration and the self-test failed on it —
correctly: an unwrapped token name is not something the strip can reach,
so the fixture was asserting the wrong thing. 25 of the 26 real hits were
inside <style>. */
'<style>.hero-lede{line-height:var(--leading-body);color:var(--text-secondary)}</style>',
'Software contracts, SLA and MSA breakdowns, data residency and processing, AI vendor diligence, IP and licensing.',
/* The approved replacements for the two negatives struck on 2026-08-29,
which were 'The Q.Arb pathway commenced in August 2026; C.Med-Arb is
the endpoint.' and a bare 'Chartered Med-Arbitrator'. Both are now
breaches, and both moved to mustMatch. */
'Q.Med (ADRIC / ADRIO) · Q.Arb (ADRIC / ADRIO)',
/* The expansion in approved use — §11 definitional, no stage word. */
'Q.Arb stands for Qualified Arbitrator, an ADRIC and ADRIO designation.',
/* `once` and `when` were BARE stage words for one run, and all four of these
compliant sentences failed the build. They now require a hold-verb. */
'I hold Q.Med and Q.Arb, and I say so when a party asks',
'Q.Arb (ADRIC / ADRIO), which is what parties look at when they appoint',
'Once appointed, I hold Q.Arb and act as sole arbitrator',
'Q.Arb, and when the parties agree the process runs on that basis',
/* `hold` is a common verb on an arbitration page with a different object;
the repair narrowed `once|when` rather than closing it, and this shipped
past it for one run. The hold-verb must now take the designation. */
'I hold Q.Arb, and when I hold a preliminary conference the parties attend',
'I hold the Q.Med and Q.Arb designations through the ADR Institute of Canada and the ADR Institute of Ontario.',
/* The false positive the first form of `c-med-arb-struck` produced. */
'The ADR Institute of Canada publishes ADRIC Med-Arb Rules, developed by a task force.',
'The ADRIC National Mediation Rules.',
'adopted a new edition effective 1 March 2025',
'Commercial arbitration in Ontario. I do not accept family arbitration.',
'Published as typical, not as a guarantee.',
'Liens, delay and change-order claims, scheduling, subcontract and deficiency disputes.',
],
};
/* --- machinery ----------------------------------------------------------- */
/* `relative()` escapes the root with `../../..` when the optional path argument
points outside the repo (which only a test run does). Show the plain absolute
path in that case — an unreadable path in a failure report is one more thing
between a reader and the match. */
function rel(p) {
const r = relative(ROOT, p);
return r && !r.startsWith('..') ? r : p;
}
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...walk(full));
else if (entry.name.endsWith('.html')) out.push(full);
}
return out;
}
function newestMtime(paths) {
let newest = 0;
let which = null;
const visit = (p) => {
const s = statSync(p);
if (s.isDirectory()) {
for (const e of readdirSync(p)) visit(join(p, e));
} else if (s.mtimeMs > newest) {
newest = s.mtimeMs;
which = p;
}
};
for (const p of paths) visit(p);
return { newest, which };
}
/**
* Strip what is not published prose or published data.
*
* `<style>` goes because Astro inlines critical CSS into every page and the
* token names collide with the superlative pattern. Non-JSON-LD `<script>` goes
* because a third-party analytics snippet is somebody else's vocabulary — there
* is none today (D15's Plausible is one line and not yet added) and there will
* be. `ld+json` STAYS: it is claim-bearing.
*
* Attributes are NOT stripped, deliberately. `<title>`, `<meta name=
* "description">`, `og:*` and image `alt` text are published copy and are among
* the places a claim is least likely to be re-read.
*/
function publishedText(html) {
return (
html
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
.replace(
/<script\b(?![^>]*application\/ld\+json)[^>]*>[\s\S]*?<\/script>/gi,
' ',
)
/* Smart punctuation, so `cant` is matched by a pattern written `can't`.
Astro emits whatever the source used and the source uses curly quotes. */
.replace(/[‘’]/g, "'")
.replace(/[“”]/g, '"')
.replace(/&#(?:39|x27);/gi, "'")
.replace(/&amp;/gi, '&')
.replace(/&nbsp;|&#160;/gi, ' ')
);
}
/**
* THE SECOND VIEW, AND IT EXISTS BECAUSE `<strong>` DEFEATED THE GATE.
*
* `publishedText` leaves tags in place, and several patterns use `[^<]` as a
* window boundary so they cannot leap a paragraph. That boundary also stops
* them crossing INLINE markup: `<strong>Q.Arb</strong> — the pathway commenced
* in August 2026` matched nothing, and this site sets `<strong>` in prose on
* `/arbitration/` and `/`. Found by `adversarial-reviewer` 2026-08-30.
*
* So every page is scanned TWICE — once as published, once with inline tags
* collapsed to a single space. Block-level tags collapse to a full stop
* instead, which preserves the "do not leap a paragraph" property that made the
* `[^.<]` window worth having in the first place. Matches are deduplicated by
* the matched text, so a hit visible in both views is reported once.
*/
const BLOCK_TAGS =
/<\/?(?:p|div|section|article|header|footer|nav|main|aside|h[1-6]|li|ul|ol|dl|dt|dd|table|tr|td|th|blockquote|figure|figcaption|br|hr)\b[^>]*>/gi;
/**
* ⚠️ THE BLOCK BOUNDARY IS A SENTINEL (`\uE000`), NOT A FULL STOP, AND THAT IS
* WHAT LETS ONE REGEX SERVE BOTH VIEWS.
*
* First attempt substituted `' . '` and the windows were written `[^<]` — but
* collapsed text contains no `<` at all, so the window was bounded by nothing
* and leapt three block boundaries to join an `<h2>Q.Arb</h2>` to a paragraph
* reading "took effect on 1 March 2025" — approved copy that ships. Measured by
* `adversarial-reviewer`.
*
* Writing the windows `[^.<]` instead would fix that and reintroduce the defect
* the collapsed view exists for: a real sentence period inside one paragraph
* ("Qualified Arbitrator. Newly commenced") must still be crossable.
*
* A sentinel separates the two jobs. `[^<\uE000]` is bounded by `<` in the
* published view and by `\uE000` in the collapsed one; neither character occurs
* in the other view, and a full stop bounds neither. Every window in PATTERNS
* uses that class.
*/
const BLOCK_BOUNDARY = '\uE000'; // Private Use Area: never occurs in real content,
// and unlike \u0000 it is not a control character, which `no-control-regex` bars.
function collapsedText(html) {
return publishedText(html)
.replace(BLOCK_TAGS, BLOCK_BOUNDARY)
.replace(/<[^>]+>/g, ' ')
.replace(/[ \t]+/g, ' ');
}
function contextAt(text, index, length) {
const from = Math.max(0, index - 55);
const to = Math.min(text.length, index + length + 55);
const snip = text.slice(from, to).replace(/\s+/g, ' ');
return `${from > 0 ? '…' : ''}${snip}${to < text.length ? '…' : ''}`;
}
function lineOf(text, index) {
let line = 1;
for (let i = 0; i < index; i++) if (text.charCodeAt(i) === 10) line++;
return line;
}
function scan(text, re) {
const hits = [];
re.lastIndex = 0;
let m;
while ((m = re.exec(text)) !== null) {
hits.push({ match: m[0], index: m.index });
if (m[0].length === 0) re.lastIndex++;
}
return hits;
}
function selfTest() {
const failures = [];
for (const p of PATTERNS) {
for (const positive of FIXTURES.mustMatch[p.id] ?? []) {
if (scan(positive, p.re).length === 0) {
failures.push(
`${p.id}: STOPPED MATCHING its own fixture — ${JSON.stringify(positive)}`,
);
}
}
if (!(p.id in FIXTURES.mustMatch)) {
failures.push(
`${p.id}: has no fixture. Every pattern proves it still fires.`,
);
}
for (const negative of FIXTURES.mustNotMatch) {
/* ⚠️ BOTH VIEWS. This read `publishedText` only, which left the entire
false-positive surface of `collapsedText` untested — and that is
exactly where the loosening happened: the collapsed view leapt three
block boundaries onto approved copy and the instrument check could not
see it. Found by `adversarial-reviewer`. */
const hits = [
...scan(publishedText(negative), p.re),
...scan(collapsedText(negative), p.re),
];
if (hits.length > 0) {
failures.push(
`${p.id}: matched APPROVED copy — ${JSON.stringify(hits[0].match)} in ${JSON.stringify(negative)}`,
);
}
}
}
return failures;
}
/* --- run ----------------------------------------------------------------- */
let failed = false;
console.log(
'check:claims — AGENTS.md §4 Forbidden, enforced on shipped output',
);
console.log(` target: ${rel(DIST)}/**/*.html\n`);
// 1. The instrument, before the measurement.
const selfTestFailures = selfTest();
if (selfTestFailures.length > 0) {
console.error(
'SELF-TEST FAILED — the patterns cannot be trusted, so nothing was swept.\n',
);
for (const f of selfTestFailures) console.error(` ${f}`);
console.error(
'\nA pattern that no longer fires passes a clean sweep forever.',
);
process.exit(2);
}
console.log(
` self-test: ${PATTERNS.length} patterns fire on their fixtures and stay silent on ${FIXTURES.mustNotMatch.length} approved strings`,
);
// 2. The target has to exist and have content in it. An empty sweep is not a
// pass — CLAUDE.md: "a command that did not run is not evidence of absence."
let pages;
try {
pages = walk(DIST);
} catch (err) {
console.error(`\nFAILED: cannot read ${DIST}${err.message}`);
console.error(
'Run `npm run build` first. This checks the built output, not the source.',
);
process.exit(2);
}
if (pages.length === 0) {
console.error(`\nFAILED: no HTML under ${DIST}. Nothing was checked.`);
console.error(
'An empty sweep reads exactly like a clean one. Run `npm run build`.',
);
process.exit(2);
}
// 3. Staleness. A pass against yesterday's dist is the same false negative in
// slower motion.
const newestPage = newestMtime(pages);
const newestSource = newestMtime([
...SOURCE_DIRS.map((d) => join(ROOT, d)),
...SOURCE_FILES.map((f) => join(ROOT, f)),
]);
if (newestSource.newest > newestPage.newest) {
console.error(`\nFAILED: ${rel(DIST)} is older than the source.`);
console.error(` newest source: ${rel(newestSource.which)}`);
console.error(` newest page: ${rel(newestPage.which)}`);
console.error(
'\nThis would have checked a build that does not include your change.',
);
console.error('Run `npm run build`, then this again.');
process.exit(2);
}
console.log(` pages: ${pages.length}\n`);
for (const p of PATTERNS) {
const hits = [];
for (const page of pages) {
const raw = readFileSync(page, 'utf8');
/* ⚠️ DEDUPE BY OCCURRENCE COUNT, NEVER BY TEXT. The first version keyed a
Set on the matched string, so `He is a lawyer.` twice on one page was
reported ONCE and the count said 1 — the check truncating its own
output, which is the one thing CLAUDE.md says a check must never do.
Measured by `adversarial-reviewer` on a two-line fixture.
The published view is authoritative; the collapsed view only ever ADDS
occurrences the tags hid, so a collapsed hit counts only where it exceeds
what the published view already found for that same text. */
const counted = new Map();
const bump = (key) => {
const n = (counted.get(key) ?? 0) + 1;
counted.set(key, n);
return n;
};
const publishedCounts = new Map();
for (const [viewIndex, text] of [
publishedText(raw),
collapsedText(raw),
].entries()) {
for (const hit of scan(text, p.re)) {
const key = hit.match.replace(/\s+/g, ' ').trim().toLowerCase();
if (viewIndex === 0) {
publishedCounts.set(key, (publishedCounts.get(key) ?? 0) + 1);
} else if (bump(key) <= (publishedCounts.get(key) ?? 0)) {
continue;
}
hits.push({
page: rel(page),
line: lineOf(text, hit.index),
match: hit.match,
context: contextAt(text, hit.index, hit.match.length),
});
}
}
}
if (hits.length === 0) {
console.log(` ok ${p.id}`);
continue;
}
failed = true;
console.error(
`\n FAIL ${p.id}${hits.length} match${hits.length === 1 ? '' : 'es'}`,
);
console.error(` rule: ${p.rule}`);
console.error(` incident: ${p.incident}`);
if (p.note) console.error(` note: ${p.note}`);
console.error('');
for (const hit of hits) {
console.error(
` ${hit.page}:${hit.line} ${JSON.stringify(hit.match)}`,
);
console.error(` ${hit.context}`);
}
}
if (failed) {
console.error('\n' + '-'.repeat(72));
console.error(
'A match is not yet a finding — read the context printed above before acting.',
);
console.error(
'This site has had a superlative sweep hit "pleadings" and a case-insensitive',
);
console.error(
'sweep for LSO hit "I aLSO practise". Read what matched, then either fix the',
);
console.error(
'copy or change this file DELIBERATELY, with an AGENTS.md Change Log entry.',
);
console.error('Do not delete a pattern to make a build pass.');
process.exit(1);
}
console.log(
'\nClean. Every pattern ran and every pattern is still firing on its fixture.',
);