Build and deploy / build-and-deploy (push) Failing after 4s
Pouya's ruling, 2026-08-29. Treated as D3-class and swept accordingly. §4 Q.Arb becomes a HELD designation. Obtained July 2026 — recorded in the register, deliberately not published. Public copy carries "Q.Arb (ADRIC / ADRIO)" beside Q.Med and nothing more. Every stage form is struck: "commenced", "in progress", "pathway", "not yet". C.Med-Arb as a stated goal is struck. This DEVIATES FROM THE STRATEGY BRIEF, which made it "the explicit long-term professional narrative"; the brief is not in this repository, so the deviation is recorded in four places so nobody reinstates it from a document no reader can open. The designation stays in §11 as a definition — what is struck is its status as a goal of his. The paired-disclosure condition dissolves. It existed only because Q.Arb was in progress; there is no stage left to disclose, and a condition with no subject cannot be half-kept. Pages /about/'s credentialing arc is DELETED, not rewritten — two held designations are not a journey. /arbitration/ loses its stage section and its "Available now, and open about the stage" h1. /med-arb/'s endpoint section is rewritten. Deleting CREDENTIALS.goal/goalName rather than emptying them turned every consumer into a build error, which is how the last two surfaced. hasCredential now maps CREDENTIALS.designations instead of indexing [0]; _DesignationRowParity pins the visible credential row to the same constant. check:claims q-arb-as-a-stage (inverted from the predecessor, which barred Q.Arb reading as HELD) and c-med-arb-struck. Each page is now scanned twice — as published, and with inline tags collapsed — because [^<] cannot cross <strong>, which this site sets in prose. Two open questions for Pouya, neither blocking: Q55 — the acquisition date. §4 says obtained July 2026; the Change Log of 2026-08-26 records "commenced August 2026", both stamped [verified — Pouya]. They cannot both be true. Nothing published turns on it. Q56 — is the mediation offering scoped commercial? §4 leaves it unscoped; four surfaces say "commercial mediation"; /practice/insurance/ offers mediation in insured-versus-insurer SABS matters. Two review rounds, all findings applied or declined with a stated reason. Round 2 found ten lines in docs/03 still instructing the struck form — the 2026-08-29 sweep missed them because `git grep 'Q.Arb'` is line-anchored and the block never names it. Sweep the vocabulary, not only the subject. Gates, exit statuses read directly, never through a pipe: npm run check exit=0 (0 errors, 0 warnings, 0 hints) npm run lint exit=0 npm run build exit=0 (12 pages) npm run check:claims exit=0 (11 patterns, 26 approved strings) npm audit exit=0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
722 lines
34 KiB
JavaScript
722 lines
34 KiB
JavaScript
#!/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,
|
||
},
|
||
];
|
||
|
||
/**
|
||
* 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',
|
||
],
|
||
'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: [
|
||
'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 `can’t` 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(/&/gi, '&')
|
||
.replace(/ | /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.',
|
||
);
|