feat: build step 5 — /practice/ and six area pages; check:claims gates §4 in dist
Build and deploy / build-and-deploy (push) Failing after 5s

Step 5 ships /practice/ and the six practice-area pages (construction,
technology, energy, insurance, shareholder, cross-border) from one route, and
adds the mechanical §4 gate Pouya ruled for.

check:claims — §4 Forbidden becomes a build error
  scripts/check-claims.mjs greps dist/**/*.html for 10 patterns, each carrying
  the incident that put it there. It strips <style> and non-JSON-LD <script>
  first (a bare sweep for "leading" returned 26 hits, 25 of them
  var(--leading-body)), self-tests every pattern against fixtures before
  sweeping, and refuses a missing, empty or stale dist/. Wired into /build
  Phase 5 and both deploy paths.

Q54 — six conduct undertakings publish, and §4 gains a third class
  Conduct undertakings sit apart from credentials and offerings: the gate is
  that Pouya said it in terms. The strings live in CONDUCT_UNDERTAKINGS so a
  softening is one visible diff. (e) and (f) replace the third-person sentences
  already on /arbitration/ rather than joining them.

Q49, Q50 recorded as rulings. §7 records the SES us-east-1 stray identity's
deletion. R11 holds typescript at its current major, with the peer-range
reason recorded.

Three facts corrected, two of them already shipped
  - The LAT gloss said mediation "before filing and continuing after filing";
    the Tribunal names mediation for "Before you apply" only and its second
    sentence is about negotiation. An ellipsis in docs/01 had deleted it.
  - "Connection allocation" is not an Ontario term.
  - "The 2026 privacy statute" does not exist — Bill C-27 died without royal
    assent. Struck from docs/03 rather than corrected in place.

ADR Chambers struck from /arbitration/ and from docs/01 item 3 (Pouya,
2026-08-30): the source establishes what the firm publishes, not that an
outside neutral can be appointed under its rules.

claims-auditor gains a second lens — for every quoted source, whether the
sentence beneath stays inside what the quotation establishes. Four shipped
defects had that shape and none of them is greppable.

CLAUDE.md gains a convention: never truncate the output of a check you intend
to believe. `npm run check | tail -3` returns warnings, hints and a blank line
and drops the errors line; it was reported as passing four times while
astro check was exiting 1 with 10 type errors.

Gates, exit status read directly, not through a pipe:
  npm run check        exit=0
  npm run lint         exit=0
  npm run build        exit=0
  npm run check:claims exit=0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-08-30 09:35:26 -04:00
co-authored by Claude Opus 5
parent f3138a0a79
commit 79b19a7bd0
32 changed files with 6715 additions and 116 deletions
+88
View File
@@ -0,0 +1,88 @@
---
/**
* The visible breadcrumb trail. `docs/04` requires `BreadcrumbList` markup on
* "all nested pages" AND that it **match the visible breadcrumbs** — so the
* markup and this component are built from one array at the call site, and a
* page cannot emit one without showing the other.
*
* WHICH PAGES GET ONE, AND WHY IT IS NOT EVERY PAGE. `/about/`, `/mediation/`,
* `/arbitration/`, `/med-arb/` and `/practice/` are all ONE HOP from the root
* and show no breadcrumb, so emitting the markup on them would assert a
* navigation structure the page does not have. The trail begins at the
* two-level pages: `/practice/<area>/`, and `/insights/<slug>/` at step 7.
*
* THE CURRENT PAGE IS NOT A LINK. A link to the page you are on is a target
* that does nothing, and `aria-current="page"` is the property that carries the
* meaning. `schema.org` still wants it as the last `ListItem`, which is why the
* caller passes the full trail and this component decides what to render.
*
* THE SEPARATOR IS `aria-hidden` AND LIVES IN CSS-adjacent markup rather than
* in the link text: a screen reader announcing "slash" between every crumb is
* noise, and `<nav aria-label="Breadcrumb">` already names the structure.
*/
interface Props {
/** Root-first, INCLUDING the current page as the last entry. */
trail: ReadonlyArray<{ name: string; href: string }>;
}
const { trail } = Astro.props;
---
<nav class="crumbs" aria-label="Breadcrumb">
<ol role="list">
{
trail.map((crumb, i) =>
i === trail.length - 1 ? (
<li aria-current="page">{crumb.name}</li>
) : (
<li>
<a href={crumb.href}>{crumb.name}</a>
<span aria-hidden="true">/</span>
</li>
),
)
}
</ol>
</nav>
<style>
/* `role="list"` on the <ol> and no local `list-style: none` — global.css does
both for `ol[role='list']`, and WebKit drops list semantics when the marker
goes, which is why the role is there. Same pairing as `.stage` on
`/arbitration/`. */
.crumbs ol {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--text-meta);
}
.crumbs li {
display: flex;
align-items: center;
gap: var(--space-2);
}
/* 44px is the touch-target floor in docs/02. These are small uppercase mono
links in a row, which is exactly the shape that lands under it — the same
defect measured on `/med-arb/`'s onward links at 390px (21px tall against a
44px floor). `inline-flex` + `min-block-size` is the fix the rest of the
site uses. */
.crumbs a {
display: inline-flex;
align-items: center;
min-block-size: 44px;
color: inherit;
text-decoration: none;
}
.crumbs a:hover {
color: var(--text);
text-decoration: underline;
text-underline-offset: 0.2em;
}
.crumbs [aria-current='page'] {
color: var(--text);
}
</style>