The footer mailto was the last recorded overflow: `info@smlcompany.ca` demanded 310px of min-content in a 224-243px column. One declaration — `overflow-wrap: anywhere` on `.footer-contact a[href^='mailto:']`. `anywhere` and not `break-word`, established with a negative control rather than from the rule: only `anywhere` reduces min-content, and `break-word` injected in its place failed the same 88 rows as the unfixed baseline. Minimum-font-size 32 goes 88/352 -> 0/352. All four methods now read 0 of 352 (22 pages x 16 widths). Normal-settings identity: 0 differences across 8 metrics, with a positive control proving the comparison can detect one. It also closed 57 element-level cases the page-level table reports as clean, hidden inside `.wrap`'s 96px gutter. The item-2 ruling is NOT applied, and this is the deviation to read first. The acceptance rested on "no CSS mechanism can see minimum-font-size", which is false: the font-metric units — `ch`, `ex`, `cap`, `lh`, `rlh` — read the used font size and double, in property values, in `@media` and in `@container`. Only `rem`, `em`, `ic` and `px` are blind. And the cost is not a convenience loss: keyboard focus lands entirely behind the opaque header on 290 of 1,455 stops, 36 of them inside `#main`, which is WCAG 2.2 SC 2.4.11 at AA — the same level as the 1.4.10 failure it was traded against. A build of fce89d4~1 measures 0, so the header fix created it. Opened as Q61 with a verified candidate; docs/06 restored to unticked. R20's gate is now a build failure rather than three prose cross-references, which demonstrably did not gate it: with two articles published the build and all five checks passed while both header defects shipped. SiteHeader throws. R11's two majors (@astrojs/mdx 7->8, typescript 6->7) move to a new cutover-prep group in docs/06 with the 19-pin currency sweep. Two review rounds, eight findings, all resolved; four of round 2's five were defects in round 1's own fixes. Stopped at two per D19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
568 lines
34 KiB
Markdown
568 lines
34 KiB
Markdown
# CLAUDE.md — operating instructions for Claude Code
|
||
|
||
## Read this first
|
||
|
||
1. **`AGENTS.md` is the source of truth for this project.** Read it in full
|
||
before your first edit in any session. It carries the locked decisions, the
|
||
credential register, the open questions, and the full history.
|
||
2. **You are required to maintain `AGENTS.md`** under the constitution written at
|
||
the top of it. Update *Current Truth* in place; append to the *Change Log*,
|
||
newest first; never edit a past entry; never delete history. Record decisions
|
||
and plans, not only executed work. Stamp facts `[verified YYYY-MM-DD]` or
|
||
`[assumed]`.
|
||
3. Update it **at the end of every working session**, not only when something
|
||
ships. A session that produced a decision and no code still produces a Change
|
||
Log entry.
|
||
4. **Read `AGENTS.md` §12 Standing Reminders at the start of every substantial
|
||
session and surface anything live to Pouya.** These are decisions he parked
|
||
deliberately, not settled matters — R1 in particular is his explicit
|
||
instruction to keep raising the licensure wording. A parked decision that
|
||
stops being raised has quietly become permanent, which is the failure mode
|
||
§12 exists to prevent.
|
||
|
||
## The one rule that matters more than the code
|
||
|
||
This is Pouya's public marketing surface, and the site it replaces carried
|
||
fabricated credentials. **No factual claim about him, his credentials, his
|
||
experience, or his practice may appear on a public page unless it is in the
|
||
Verified table in `AGENTS.md` §4.**
|
||
|
||
(§4 does not verify licensure either way — so do not describe him as
|
||
"licensed", or as a "legal professional", anywhere, this file included. State
|
||
the reason for the rule, not a credential the register cannot vouch for.)
|
||
|
||
If a page needs a fact you do not have:
|
||
|
||
- Do not infer it from context.
|
||
- Do not soften it into something defensible ("extensive experience", "years of").
|
||
- Do not carry it over from the old site — the old site contained a fictitious
|
||
founder, invented matter values, and a fabricated testimonial.
|
||
- **Leave `TODO(pouya): <the exact question>` in the source, and add the question
|
||
to `AGENTS.md` §9.** A build that fails on an unanswered question is a correct
|
||
build.
|
||
|
||
Read the Forbidden table in §4 before writing any statistic, number, or
|
||
superlative.
|
||
|
||
## How work is executed here
|
||
|
||
Pouya is the architect. He makes the decisions and hands you the task. **You
|
||
implement, then you adversarially review your own work before calling it done.**
|
||
This is the standing agreement — it applies to every substantial change without
|
||
being restated in the prompt.
|
||
|
||
**Run `/build <task>` for any substantive change.** It encodes the loop:
|
||
|
||
1. **Plan** — read `AGENTS.md` (including §12 Standing Reminders, and surface
|
||
anything live), read the governing specs, name the decisions the task touches,
|
||
and **stop and ask on any conflict**. A blocked build is a correct build.
|
||
2. **Implement** — following the conventions below.
|
||
3. **Adversarial review** — invoke `adversarial-reviewer` on the diff. **D20:
|
||
`claims-auditor` does NOT run per step.** It runs once, at cutover, over the
|
||
whole finished site.
|
||
4. **Resolve** — fix each finding or decline it with a stated reason. Re-review
|
||
material fixes.
|
||
5. **Verify** — run the checks. Never report a check as passing that you did not
|
||
run.
|
||
6. **Record** — append the `AGENTS.md` Change Log entry.
|
||
|
||
`/review` runs phase 3 alone. `/wrap` runs phase 6 at session end.
|
||
|
||
**Agent definitions load at session start.** An edit to `.claude/agents/*.md`
|
||
does not reach the session you made it in — the brief in force is the one that
|
||
was on disk when the session began. After committing a change to one, **restart
|
||
before relying on it, and say in the report which version actually ran.** Found
|
||
2026-08-30: the gloss lens was added to `claims-auditor` and the agent then
|
||
reconstructed it from the `AGENTS.md` Change Log rather than having it in its
|
||
brief, which is luck, not process.
|
||
|
||
**Think deeply before acting.** Extended thinking is on by default for this
|
||
project (`.claude/settings.json`), and `/build` and `/review` request it
|
||
explicitly. The planning and review phases are where it earns its cost — a defect
|
||
reasoned out before implementation is far cheaper than one found after.
|
||
|
||
### Why the review is adversarial, and what would break it
|
||
|
||
Two rules make the difference between a review and a rubber stamp:
|
||
|
||
**Do not brief the reviewers on why your work is correct.** Give them the diff
|
||
and the specs, nothing else. Your rationale anchors them, and an anchored
|
||
reviewer produces agreement rather than review. They must form an independent
|
||
view from the artefact — that independence *is* the mechanism.
|
||
|
||
**The reviewers are instructed to treat uncertainty as a defect.** They will
|
||
sometimes be wrong, and that is the intended trade. Explaining why a finding is
|
||
mistaken costs minutes; a missed defect on this project's public marketing
|
||
pages costs considerably more — the site this replaces carried fabricated
|
||
credentials, and that is the standard being corrected. Do not read a finding as
|
||
an accusation, and do not argue a reviewer down — either fix it, or record the
|
||
reason you declined it so a later reader can see the judgement was made rather
|
||
than missed.
|
||
|
||
**Two reviewers, because they catch different things — but they no longer run at
|
||
the same time.** `adversarial-reviewer` reads the code. `claims-auditor` reads the
|
||
copy against the §4 register and knows nothing about whether the code is elegant.
|
||
A generic reviewer consistently under-weights the professional-conduct check,
|
||
which is the highest-stakes failure mode on this project — so it keeps its own
|
||
pass rather than being folded into the code review.
|
||
|
||
**D20, 2026-08-30 — the claims pass moved to cutover.** Per step it is
|
||
`adversarial-reviewer` alone. `claims-auditor` runs **once, over the whole
|
||
finished site**, as a blocking item on `docs/06`'s cutover checklist. Pouya's
|
||
reasoning, and it is a calibration and not an erosion: nothing has shipped, so
|
||
every claims finding so far has been about a page no visitor can reach — the risk
|
||
is deferred to cutover anyway, and one pass over twenty finished pages catches
|
||
**more** than nine passes over drafts, because it sees the site as a reader does.
|
||
The `/med-arb/` ADRIC gloss is the proof: no individual claim was false, the
|
||
defect was **adjacency**, and adjacency does not exist until the pages sit next to
|
||
each other. The code reviewer stays per step because what it catches **compounds**
|
||
— an accessibility or crawlability defect propagates into the next page built on
|
||
it, and a claims defect does not; it sits there until someone reads it.
|
||
|
||
**What it costs is recorded in `AGENTS.md` D20, not summarised away here.** Read
|
||
it before proposing any further relaxation: `claims-auditor` has caught defects
|
||
that would have been serious on a live page, and D20 accepts that such a defect
|
||
may now live in an unpublished draft for weeks. Two things carry that risk in the
|
||
meantime — **`npm run check:claims`, which is unchanged and runs on every build
|
||
and both deploy paths**, and **Pouya reading the copy as it is built**. Neither is
|
||
optional, and neither is a substitute for the cutover pass.
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
npm install
|
||
npm run dev # local dev server
|
||
npm run build # static build to ./dist
|
||
npm run preview # serve ./dist locally
|
||
npm run check # astro check — type and template errors
|
||
npm run check:claims # §4 Forbidden, enforced on dist/ — run it after a build
|
||
npm run check:intake # the form's field table vs the Lambda's — they are two on purpose
|
||
npm run og:proof # every og:image resolves; every card headline IS its page's <h1>
|
||
npm run lighthouse # the performance budget. LOCAL ONLY — needs Chrome, not in CI
|
||
npm run bio:pdf # re-renders the committed one-page PDF from /bio/. LOCAL ONLY
|
||
npm run lint # eslint + prettier check
|
||
npm run format # prettier — rewrite files in place
|
||
npm run deploy # build + deploy from this machine (see docs/06)
|
||
```
|
||
|
||
**Four of those are gates and two of them cannot run in CI.** `check`,
|
||
`check:claims`, `check:intake` and `og:proof` are pure Node and run anywhere.
|
||
`lighthouse` and `bio:pdf` drive an installed browser, and the Gitea runner has
|
||
none — so they are keyboard gates plus blocking items on `docs/06`'s cutover
|
||
checklist, and **they are deliberately not wired into `npm run build` or either
|
||
deploy path.** Do not describe either as gating a deploy: a check described as
|
||
running where it cannot is the defect `AGENTS.md` Q22 turned out to be.
|
||
|
||
**`og:proof` and `check:intake` exist because two facts in this repo are
|
||
deliberately duplicated**, and a duplicated fact needs a mechanism rather than a
|
||
comment. `og:proof` compares each generated OG card's headline against its page's
|
||
own `<h1>` — **text baked into a JPEG cannot be grepped by `check:claims`**, so
|
||
that comparison is the only thing keeping card copy inside the claim register.
|
||
`check:intake` compares the form's field table against the Lambda's, which are
|
||
independent because a server that validates against a list the client shipped it
|
||
is not validating.
|
||
|
||
## Where things live
|
||
|
||
```
|
||
AGENTS.md living project record — read first, maintain always
|
||
docs/ the specs you build from
|
||
01-architecture.md sitemap, URL map, per-page content outline
|
||
02-design-system.md tokens, type scale, motion, contrast constraints
|
||
03-content-spec.md voice, copy rules, per-page copy deck
|
||
04-seo-spec.md metadata, structured data, sitemap, crawlability
|
||
05-backend-spec.md intake form, Lambda/DynamoDB/SES, booking, PIPEDA
|
||
06-deployment.md S3/CloudFront, Gitea Actions, IAM, cutover checklist
|
||
src/
|
||
content.config.ts content collections — Content Layer API, NOT content/config.ts
|
||
styles/tokens.css design tokens — the single source of colour and scale
|
||
styles/global.css reset, base type, utilities
|
||
layouts/ page shells
|
||
components/ UI components
|
||
pages/ routes (file-based)
|
||
content/insights/ Insights MDX only; the config sits above, not in here
|
||
data/site.ts site-wide constants, nav, contact details
|
||
public/ static assets served as-is
|
||
```
|
||
|
||
## Conventions
|
||
|
||
**Framework.** Astro **7.x**, `output: 'static'` (D1 as amended). Never introduce
|
||
a server runtime without a Change Log entry recording why. The major is pinned
|
||
deliberately — check `npm view astro version` before changing it.
|
||
|
||
**JavaScript.** Default to zero. Reach for an Astro island only when a feature
|
||
genuinely cannot be CSS or progressive HTML. If you add a `client:*` directive,
|
||
say why in the Change Log. A `<details>` element beats a JS accordion.
|
||
|
||
**Styling.** Plain CSS with custom properties. No Tailwind, no CSS-in-JS, no
|
||
utility framework. Every colour, space, and font size comes from a token in
|
||
`tokens.css` — no raw hex values and no magic numbers in component styles.
|
||
|
||
**Accessibility is a build requirement, not a polish pass.** Semantic landmarks,
|
||
one `<h1>` per page, heading levels never skipped, visible focus states, all
|
||
interactive elements reachable by keyboard, `prefers-reduced-motion` honoured on
|
||
every animation. Gold `#c9a876` never sits on cream — it fails contrast at
|
||
2.10:1. See `docs/02-design-system.md`.
|
||
|
||
**Anything a spec makes a claim about must be reachable from the repository.**
|
||
If the artefact lives only in Drive, in a console, or on someone's laptop, no
|
||
reviewer can compare the claim against it and the claim is **unverifiable by
|
||
construction** — not merely unverified. Commit the artefact, or commit a faithful
|
||
extract with its provenance and the command that produced it.
|
||
|
||
This has cost twice. `AGENTS.md` Q24 was the AWS hosting guide, the only record
|
||
of how the infrastructure was hand-built, living outside the repo. Q32 was the
|
||
infinity mark: it was traced from the old site's *loading placeholder*, the
|
||
source comment said so in as many words — and **two adversarial review passes
|
||
still could not catch that the shape was wrong**, because the real artwork was
|
||
not in the repo to compare against. Stating a doubt is not enough when the thing
|
||
that would resolve it is unreachable. Tracked as R14.
|
||
|
||
**A command that did not run is not evidence of absence.** Check that a tool
|
||
exists before trusting its silence, and read exit status, not just stdout. This
|
||
project ran `timeout 60 ls "$DRIVE"` four times, got empty output each time, and
|
||
reported the brand assets unreachable — `timeout` is not installed on macOS, so
|
||
the command had never executed and the directory was fully readable all along.
|
||
Empty output from a command that failed to start looks exactly like empty output
|
||
from a command that found nothing. Same family as *a sweep is a command, not a
|
||
claim*: the claim must rest on output you actually read, from a command that
|
||
actually ran.
|
||
|
||
**And never suppress stderr in a verification script.** *Pouya's convention,
|
||
2026-08-28, from verifying the deploy credential (`AGENTS.md` Q22).* This is the
|
||
rule above from the other direction, and it is the more dangerous direction:
|
||
**`2>/dev/null` converts "it failed" into "it found nothing", and those are
|
||
opposite results.** His first pass at eight `simulate-principal-policy` checks
|
||
returned empty for all eight; the suppression was hiding an `InvalidInput` error,
|
||
and the empty output was then explained with a guess — *"probably lacks the
|
||
permission"* — which is the answer the check was supposed to produce, arrived at
|
||
without the check running. The actual cause was a **zsh parameter-expansion bug**:
|
||
`$ACCT:user/` parses `:u` as a history modifier and yields `327082975128ser/`.
|
||
Braces fixed it.
|
||
|
||
So: no `2>/dev/null` in anything whose output you intend to believe, read the
|
||
exit status, and when a result is empty **remove the suppression and look before
|
||
proposing a cause.** A guessed explanation for an empty result is worse than no
|
||
result, because it closes the question.
|
||
|
||
**And never TRUNCATE the output of a check you intend to believe.** *Added
|
||
2026-08-29, from build step 5.* This is the stderr rule's twin and it is easier
|
||
to commit, because the command runs and the pipe looks harmless. `npm run check`
|
||
prints its verdict as three lines — `- N errors`, `- N warnings`, `- N hints` —
|
||
followed by a blank line. **`npm run check 2>&1 | tail -3` therefore returns
|
||
warnings, hints and the blank line, and silently drops the errors line.** It was
|
||
run four times that way and reported as passing each time; `astro check` was
|
||
exiting **1 with 10 type errors**, and both deploy paths run it before the build,
|
||
so nothing could have shipped. `adversarial-reviewer` found it.
|
||
|
||
The fix is not a bigger `tail`. **Read the exit status** — `cmd; echo "exit=$?"`
|
||
or `cmd || echo FAILED` — because it is the one signal a pipe cannot silently
|
||
reshape. `head`, `tail`, `grep -c` and `| grep -i error` all have the same
|
||
failure mode: they turn a verdict you did not read into a verdict you assert.
|
||
Same family as *a sweep is a command, not a claim*, and note the asymmetry that
|
||
makes it dangerous — the truncation only ever hides the bad news, because the
|
||
error line comes first.
|
||
|
||
*Corroborated the same day, twice, in the same session and both in zsh:*
|
||
`grep -rn $EX 'Mediator-Arbitrator'` printed an option error and no matches —
|
||
which reads as clean — because zsh does not word-split unquoted variables; and a
|
||
digest-comparison loop using `set -- $pair` printed **`DIFFER` on all five rows**,
|
||
which reads as "the source changed under me", because the loop body received one
|
||
argument and the comparison never ran. Prefer `git grep`, quote or array-expand
|
||
anything you pass as flags, and re-check any result whose shape is "uniformly
|
||
bad".
|
||
|
||
**zsh does not word-split parameter expansions; a loop over `$VAR` runs ONCE —
|
||
use command substitution or arrays, and assert the iteration count.** *Pouya's
|
||
convention, 2026-09-01.* `for p in $PAGES` iterates one item, not twenty-two,
|
||
and `node probe.mjs 320,360 $P` measures one page — both of which then report
|
||
"max=0, nonzero=0" and read as a clean sweep. `$(cat file)` and `${=VAR}` do
|
||
split; `"${(@f)VAR}"` splits on newlines. **The fix is not remembering which:
|
||
assert the count before reading the result** — a probe that says how many rows it
|
||
measured cannot silently measure one.
|
||
|
||
**And re-check "uniformly GOOD" too — that is the dangerous half.** *Added
|
||
2026-08-30; sharpened on Pouya's instruction 2026-08-31, as "the sharpest
|
||
instrument finding yet".* The same `set -- $pair` loop recurred while confirming
|
||
nine restored files matched a saved copy, and this time it printed **`same` on
|
||
all nine**: `shasum` was handed both filenames as one argument, errored, and left
|
||
both variables empty, so `"" = ""` passed.
|
||
|
||
**The distinguishing property, and it is the whole rule: a broken verification
|
||
that fails loudly is safe; one that passes uniformly is not.** `DIFFER` on every
|
||
row announces itself — it is alarming, so it starts an investigation, and the
|
||
investigation finds the broken loop. A uniform pass is **the result you were
|
||
hoping for, so it ends the check** instead of starting one. The two failures come
|
||
from the identical bug and only one of them is survivable.
|
||
|
||
So a comparison must **assert that both things it compares exist** before
|
||
comparing them — that is the assertion the shell loop skipped, and it is what
|
||
turns this class of bug back into the loud kind. Note the same hole in `git grep`:
|
||
it silently misses untracked files, so a clean sweep across new work means
|
||
nothing until the files are staged.
|
||
|
||
**A parent cannot style a child component's root element.** Astro does not pass
|
||
a parent's scope attribute down, so `<Button class="header-cta" />` compiles the
|
||
parent's rule to `.header-cta[data-astro-cid-<parent>]` while the rendered `<a>`
|
||
carries only `<Button>`'s own cid. **The rule silently never matches** — no
|
||
error, no warning, and the CSS looks correct in the source. Wrap the child in an
|
||
element the parent owns (`<div class="header-cta"><Button …/></div>`), or reach
|
||
it deliberately with `:global()` from a parent-scoped ancestor. Inherited
|
||
properties (`white-space`, `color`, `font-*`) do cross the boundary and are the
|
||
exception. This cost a header CTA that was documented as hidden on mobile,
|
||
was not hidden, and sat 75 px short of the right edge on desktop — both found by
|
||
measurement, neither by reading. It will recur with `PracticeCard`,
|
||
`ArticleCard`, and `Pill`.
|
||
|
||
**Never write the `animation` shorthand beside `animation-timeline`.** Longhands
|
||
only — `animation-name`, `animation-duration`, `animation-timing-function`,
|
||
`animation-fill-mode`, then `animation-timeline` and `animation-range`.
|
||
`scroll()` and `view()` are not legal components of the shorthand, and Lightning
|
||
CSS folds the two declarations together on minify into something invalid, which
|
||
is then discarded whole. **It works in `npm run dev` and is dead in
|
||
`npm run build`** — the worst shape a defect can take. It happened twice in one
|
||
session, the second time inside the fix for the first. `/build` Phase 5 greps
|
||
`dist` for it; do not remove that check.
|
||
|
||
**Images.** Astro `<Image>` with explicit width and height. AVIF/WebP with
|
||
fallback. Never base64-inline an image into HTML — the old site did this with
|
||
~1 MB of logo PNGs — a figure `AGENTS.md` Q34 is now open against, so treat the
|
||
rule as standing on its own merits rather than on that number.
|
||
|
||
**Fonts.** Self-hosted, subset, `font-display: swap`, preloaded. No Google Fonts
|
||
request at runtime — it costs a round trip and adds a third-party call to a
|
||
page that collects legal inquiries.
|
||
|
||
**Every page ships with:** a unique `<title>` and meta description, a canonical
|
||
URL, Open Graph and Twitter card tags, and appropriate JSON-LD. See
|
||
`docs/04-seo-spec.md`. A page without these is not finished.
|
||
|
||
**A version pin is verified against the registry, never recalled.** Before you
|
||
write or change any dependency version, run `npm view <pkg> version` and pin
|
||
against what it returns. One second of checking; a stale pin costs a migration.
|
||
This rule exists because `astro: "^5.0.0"` was written from memory and was
|
||
**two majors stale on the day it was written** — which meant shipping a
|
||
framework carrying high-severity XSS advisories. The same check applies to
|
||
every pin in `package.json`, not just the framework.
|
||
|
||
Re-check currency at each phase boundary in the build order (`AGENTS.md` R11),
|
||
not only when something breaks.
|
||
|
||
**`AGENTS.md` §7 is the single source of truth for operational facts.** Resource
|
||
IDs, regions, DNS records, credential state, service status — these live in §7
|
||
and nowhere else. Specs in `docs/` **cite** §7; they do not restate it. Write
|
||
"the region `AGENTS.md` §7 records", not the region. Same for bucket names,
|
||
distribution IDs, DKIM tokens, endpoints, and account identifiers.
|
||
|
||
A duplicated fact is a fact that will eventually be wrong in one place, and the
|
||
copy that goes stale is the one nobody re-reads. This rule exists because
|
||
`docs/05-backend-spec.md` carried its own copy of the SES DKIM table, a
|
||
correction reached §7 and never reached it, and the stale copy ended up telling
|
||
an operator to delete the three records that authenticate outbound mail —
|
||
under the heading "Never delete".
|
||
|
||
**A measurement is a claim about your instrument until you check the
|
||
instrument.** This has now cost six times, and the shape is identical every
|
||
time: a number that looks like a finding, from a probe nobody validated.
|
||
|
||
- `timeout 60 ls "$DRIVE"` — **the command never ran.** `timeout` is not
|
||
installed on macOS. Empty output from a command that failed to start looks
|
||
exactly like empty output from a command that found nothing, and it produced a
|
||
report that the brand assets were unreachable when the directory was fully
|
||
readable.
|
||
- **`1.23:1` for the traced mark** — the bounding box of the path's *coordinate
|
||
hull*, not of the curve. A cubic's control points sit outside it, so the box
|
||
was 33% too tall while giving the *correct* width — which means the obvious
|
||
sanity check, "does the width look right?", passes.
|
||
- **"the mark renders at 24px"** — the harness reported the worst-deviating
|
||
instance on the page, not the instance under discussion, which was exact.
|
||
- **"0 overflow at every width"** — true, and it measured the *document*. A flex
|
||
child was absorbing the deficit by being crushed to aspect 0.891. **Measure
|
||
the elements, not only the page.**
|
||
- **`img.naturalWidth` = 64 at DPR 1, 2 and 3** — which reads as *the density
|
||
ladder is not being generated at all*, a shipped defect on every page. It is
|
||
**density-corrected by spec**: a 192px file selected at `3x` correctly reports
|
||
64. The files on disk were 64 / 128 / 192 all along.
|
||
- **"10 distinct contexts" from `grep -roh '.\{50\}X.\{50\}' dist/ | sort -u`**
|
||
— **`grep -o` takes NON-OVERLAPPING matches.** On minified HTML a page is a
|
||
handful of very long lines, so an early window eats the characters a later one
|
||
needs and occurrences vanish silently. A whole shipped sentence was missing
|
||
from the list. **A `grep -o` window count is not an enumeration** — to count
|
||
occurrences of a string, iterate every match position, or `grep -o` the bare
|
||
string with no context window.
|
||
|
||
So before acting on a number: say what it is a number *of*; confirm the command
|
||
actually ran and read its exit status; and check it against a second method that
|
||
cannot fail the same way — the bytes on disk, a screenshot, a hit test.
|
||
|
||
**Two simulations of 200% text are not equivalent: media-query `rem` resolves
|
||
against the browser DEFAULT font size, not the root element — measure under both
|
||
methods before declaring a reflow result.** *Pouya's convention, 2026-09-01.*
|
||
Raising the default moves the breakpoints along with the type, so the desktop
|
||
layout is never reached and nothing overflows; setting `documentElement.style
|
||
.fontSize` doubles the type and leaves the breakpoints where they were, which is
|
||
the layout the desktop nav was measured in. One of those reported **0** while the
|
||
other reported **944 px** on the same 22 pages, and the prose generalised the
|
||
zero.
|
||
|
||
⚠️ **AND THERE ARE THREE MECHANISMS, NOT TWO — the third defeats the `rem`/`em`
|
||
FAMILY, which is not the same as defeating CSS.** Measured 2026-09-01: Chrome's
|
||
**"Minimum font size"** *floors* computed font sizes instead of scaling them, so
|
||
text enlarges while `rem` keeps resolving at 16 px. A media query in `rem` does
|
||
not see it, and neither does a container query — `@container` `rem`/`em` DO track
|
||
the root element (that is the one real difference from `@media`, and it is
|
||
measured), but under a minimum-font-size setting they still resolve at 16 px while
|
||
`getComputedStyle` reports 32 px.
|
||
|
||
⚠️ **BUT "NO CSS CONDITIONAL CAN SEE IT" IS FALSE, AND ASSERTING IT COST A
|
||
CONFORMANCE FAILURE.** *Corrected 2026-09-01, same day, by `adversarial-reviewer`.*
|
||
The **font-metric** units read the *used* font size and therefore double:
|
||
**`ch`, `ex`, `cap`, `lh`, `rlh`** all respond — in property values, in `@media`
|
||
**and** in `@container` (`ch` 10.608 → 21.216 px; `@media (min-width: 100ch)`
|
||
flips). Only `rem`, `em`, `ic` and `px` are blind. The false generalisation was
|
||
written into `docs/02`, `docs/06`, `global.css` and `tokens.css`, and it was then
|
||
used as the premise for accepting a **WCAG 2.2 SC 2.4.11 (AA)** failure as
|
||
unfixable — *"the only fix is JS"*. **The lesson is the shape, not the units: "no
|
||
mechanism can X" is a claim about every mechanism, including the ones you did not
|
||
enumerate.** Test the family you did not think of before writing "none", and
|
||
prefer "every construct I measured is blind, and here is the list" — which is
|
||
falsifiable and was what the measurement actually supported.
|
||
|
||
For *reflow* the conclusion is unchanged: **used-value layout — wrapping — is
|
||
still the right mechanism**, because it needs no threshold and no fitted constant.
|
||
That path
|
||
was the worst of the three: on one grid of 22 pages × 16 widths, **219 of 352
|
||
page-widths overflowed** against root-style's **175**, and it was the only one
|
||
failing at 320 px and 1024 px. **Always state the grid with the count** — two
|
||
sweeps in that session quoted totals of 220, 330 and 352 for the same claim, and
|
||
side by side they read as contradictions rather than as different width lists.
|
||
Measure all three mechanisms; treat a clean result from one as evidence about
|
||
that one.
|
||
|
||
**And a grep that matches is not a finding until you read what it matched.**
|
||
A case-insensitive sweep for `LSO` hit `I aLSO practise`; a superlative sweep for
|
||
`leading` hit `the pLEADINGs`. Both on the same page on the same day. Print the
|
||
match with context before you believe it.
|
||
|
||
**Never name an Astro prop `as`.** `const { as = 'p' } = Astro.props` detaches
|
||
the `Props` interface from the component, and **every call site silently stops
|
||
being type-checked.** `astro check` reports it only as `ts(6196) 'Props' is
|
||
declared but never used`, which reads like lint noise. Measured: with the prop
|
||
named `as`, `<Eyebrow dot as="h9" bogusProp={1} />` compiled with **0 errors**;
|
||
renaming the one identifier to `tag` made the same probe fail correctly. **Do not
|
||
silence a `ts(6196)` with `Astro.props as Props`** — that hides the warning and
|
||
leaves the call sites unchecked, which is strictly worse. If that hint appears on
|
||
any component, pass it a bogus prop before believing its props are checked.
|
||
|
||
**A sweep is a command, not a claim.** Any statement that a change was applied
|
||
across files — a phrase removed everywhere, a path updated everywhere, a
|
||
decision swept through the docs — must cite the command that proves it, and be
|
||
written only after reading that command's output. Paste the `grep` into the
|
||
Change Log entry. Three consecutive entries on this project asserted a completed
|
||
sweep; instances survived all three, and one of them was inside
|
||
`.claude/agents/claims-auditor.md` — the definition of the agent whose job is to
|
||
catch exactly that. Recall is not evidence.
|
||
|
||
**And sweep the VOCABULARY, not only the subject.** *Added 2026-08-30, from the
|
||
Q.Arb amendment.* `git grep 'Q.Arb'` is line-anchored, so it could not find **ten
|
||
lines in `docs/03` that were entirely about Q.Arb and never named it** — an
|
||
unstruck, imperative block still instructing the struck form, eleven lines below
|
||
that change set's own strike notice on the same bullet. The sweep was a real
|
||
command and its output was read honestly. It was still the wrong command.
|
||
|
||
So after sweeping the term, sweep the words its claims are **made of** — here,
|
||
the stage vocabulary (`commenced`, `in progress`, `pathway`, `not yet`) with no
|
||
mention of the designation. This is R8's sharpest edge, and it is the one that
|
||
survives an honest reader: a sweep can pass every test in the rule above and
|
||
still miss everything, because the anchor you chose is not the anchor the text
|
||
uses. The same session also excluded `docs/reference/` as "sourced extracts" —
|
||
half right. The quotations there are evidence; **the commentary around them is
|
||
this repository's voice**, and three lines of it still asserted the struck row.
|
||
|
||
|
||
**`check:claims` IS FROZEN. It is a tripwire, not a program.** *Pouya's ruling,
|
||
2026-08-30.* Round 2 of the Q.Arb amendment found **five defects in round 1's own
|
||
fixes to that script, two of which made it worse than before the pattern
|
||
existed** — a dedup key that reported two breaches of the same string as one (the
|
||
check truncating its own output), and a collapsed-text view whose window leapt
|
||
paragraph boundaries onto approved copy while its comment claimed it could not.
|
||
At that point it was generating defects at roughly the rate it caught them.
|
||
|
||
The rule, and it has no exceptions:
|
||
|
||
- **A pattern is added only after a real breach has reached `dist/`.** Never
|
||
speculatively, never to close a gap you can imagine.
|
||
- **Each addition ships with a probe** — an injected page proving it catches the
|
||
actual breach — **and a negative fixture** proving it stays silent on the
|
||
approved copy nearest to it.
|
||
- **No refactors. No coverage improvements. No tidying.** If a pattern is wrong,
|
||
change that pattern deliberately, with a Change Log entry. Do not rewrite the
|
||
scanner around it.
|
||
|
||
Under D20 this script is the only per-step claims control, which is an argument
|
||
for keeping it **correct**, not for growing it. It catches the §4 breaches that
|
||
are greppable and makes no claim about the ones that are not.
|
||
|
||
**Comments record decisions, not history — D19.** *"X because D13"* stays.
|
||
*"This was Y, then flagged, then became X"* belongs in the `AGENTS.md` Change
|
||
Log, which is where a reader looks for how something got here. **A comment
|
||
longer than the code it explains must justify itself. Trim on sight.**
|
||
|
||
Pouya's ruling, 2026-08-28, on his own measurement: **342 lines added to `src/`
|
||
in one session for 8 functional lines**, and four of that session's review
|
||
findings were stale statements living inside those comments. A comment that
|
||
narrates its own revision history becomes a second record to keep true, and then
|
||
a source of defects about the record rather than about the site.
|
||
|
||
This does **not** license deleting a comment that carries a live constraint. The
|
||
parent-scope trap, the `animation-timeline` minifier defect and the `as`-prop
|
||
hazard above are load-bearing and stay. The test is whether a future reader needs
|
||
it **to avoid breaking something** — not whether it is interesting.
|
||
|
||
**Commits.** Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`).
|
||
One logical change per commit. Never commit secrets, `.env` files, or AWS
|
||
credentials. Gitea is not an AWS OIDC provider, so the deploy key is designed as
|
||
a static IAM access key to be held in Gitea Actions secrets — whether it has
|
||
actually been provisioned is `AGENTS.md` Q22. It must never reach the repo.
|
||
|
||
**Performance budget.** Lighthouse ≥ 95 on all four categories, on mobile, for
|
||
every page. Under 100 KB of JS on any route. LCP under 2.0 s on a simulated
|
||
Slow 4G connection. Treat a budget breach as a failing build.
|
||
|
||
**Lighthouse runs again as of 2026-08-31 — `npm run lighthouse`, and it is
|
||
`lighthouse` rather than `@lhci/cli`.** It enumerates every `index.html` in
|
||
`dist/`, so the page set cannot go stale; it asserts the four category scores and
|
||
**reports** LCP and CLS without asserting them, because simulated throttling on a
|
||
loopback server is not the Slow 4G field measurement `docs/04` describes.
|
||
|
||
**It is a LOCAL gate, not a CI check.** Standalone Lighthouse drives an installed
|
||
browser and the Gitea runner has none. So it is `npm run lighthouse` at a
|
||
keyboard plus a blocking item on `docs/06`'s cutover checklist, and it is
|
||
deliberately not wired into `npm run build` or either deploy path. Do not
|
||
describe it as gating a deploy.
|
||
|
||
**Two things about the numbers, and both have to travel with them.** The
|
||
accessibility category is measured with `prefers-reduced-motion` **forced** —
|
||
otherwise axe's `color-contrast` audit reads the scroll-driven reveal's
|
||
mid-animation opacity and reports 24 false nodes (measured; `#d0cbc4` on
|
||
`#f8f4ed`, neither of which is in this palette). And the reason it is
|
||
`lighthouse` and not `@lhci/cli` is that `AGENTS.md` §7's advisory attribution
|
||
was **wrong**: the carriers were `@lhci/cli`'s own `tmp` and `@puppeteer/browsers`'
|
||
`extract-zip`, not Lighthouse, and `lighthouse@13.4.1` audits clean. The budget
|
||
was unmeasurable for five days on a cause nobody re-derived — which is the same
|
||
lesson from the other side: **a documented control that no longer exists is
|
||
precisely the defect Q22 turned out to be**, and so is one recorded as impossible
|
||
on a reason that was never re-tested.
|
||
|
||
## What "done" means for a page
|
||
|
||
- [ ] Copy written from `docs/03-content-spec.md`, every claim traceable to `AGENTS.md` §4
|
||
- [ ] No `TODO(pouya)` left unlogged in §9
|
||
- [ ] Unique title, meta description, canonical, OG/Twitter tags, JSON-LD
|
||
- [ ] Semantic HTML; keyboard navigable; reduced-motion honoured
|
||
- [ ] Lighthouse ≥ 95 mobile, all four categories — `npm run lighthouse` after
|
||
`npm run build`. Read the exit status, not the table
|
||
- [ ] Renders correctly with JavaScript disabled
|
||
- [ ] `AGENTS.md` Change Log entry appended
|