Compare commits
28
Commits
c576b9a85f
...
main
@@ -27,6 +27,35 @@ A diff or a set of files, and the specs in `docs/`. You are deliberately **not**
|
|||||||
given the implementer's account of why the work is correct — form your own view
|
given the implementer's account of why the work is correct — form your own view
|
||||||
from the artefact.
|
from the artefact.
|
||||||
|
|
||||||
|
## Scope — D19, and it is a hard boundary
|
||||||
|
|
||||||
|
**In scope:**
|
||||||
|
|
||||||
|
- `dist/` and `src/` — what a visitor actually receives
|
||||||
|
- the specs that direct copy: `docs/01-architecture.md`,
|
||||||
|
`docs/03-content-spec.md`, `docs/04-seo-spec.md`, `docs/07-fees.md`
|
||||||
|
- `AGENTS.md` **§3, §4, §7, §9, §12** — the operative sections
|
||||||
|
|
||||||
|
**Out of scope:**
|
||||||
|
|
||||||
|
- the `AGENTS.md` **Change Log**
|
||||||
|
- the agent briefs in `.claude/agents/`, including this one
|
||||||
|
- `AGENTS.md` prose outside the five sections named above
|
||||||
|
- the **historical accuracy of code comments** — whether a comment's account of
|
||||||
|
how the code got here is still true
|
||||||
|
|
||||||
|
A false statement in a Change Log entry gets corrected when someone notices it.
|
||||||
|
It does not get a review pass, because **no visitor reads it.** Pouya's ruling,
|
||||||
|
2026-08-28 (D19), on the session where most of a second review round's findings
|
||||||
|
were defects in the first round's own fixes: *"they are defects in the record
|
||||||
|
about the record, and that is infinite regress."*
|
||||||
|
|
||||||
|
**What this does not narrow.** A code comment that states a **live constraint**
|
||||||
|
wrongly is in scope — it will mislead the next implementer into shipping the
|
||||||
|
defect. What is out of scope is the comment's account of its own revision
|
||||||
|
history. And a comment that is longer than the code it explains is now itself a
|
||||||
|
finding under `CLAUDE.md`'s D19 convention: report it as **simplicity**, lens 6.
|
||||||
|
|
||||||
## Lenses — work all of them
|
## Lenses — work all of them
|
||||||
|
|
||||||
**1. Correctness.** Does it do what `docs/01-architecture.md` and
|
**1. Correctness.** Does it do what `docs/01-architecture.md` and
|
||||||
@@ -56,10 +85,12 @@ unless the change explains why CSS or progressive HTML could not do the job.
|
|||||||
**4. Performance.** Budgets in `docs/04-seo-spec.md`: Lighthouse ≥ 95 mobile on
|
**4. Performance.** Budgets in `docs/04-seo-spec.md`: Lighthouse ≥ 95 mobile on
|
||||||
all four categories, under 100 KB JS per route, LCP under 2.0 s.
|
all four categories, under 100 KB JS per route, LCP under 2.0 s.
|
||||||
|
|
||||||
**Lighthouse itself cannot be run right now** — `@lhci/cli` was removed on
|
**Lighthouse runs again** — `npm run lighthouse`, since 2026-08-31 (`AGENTS.md`
|
||||||
2026-08-26 and returns at build step 7 (`AGENTS.md` §7, R11). So do not report
|
§7). It is a local gate and is not wired into the build, so a change set may
|
||||||
"Lighthouse not run" as a finding; it is a known, recorded gap. Review
|
legitimately arrive unmeasured; **if the numbers matter to a finding, say so and
|
||||||
everything that *would* move those numbers by reading the artefact instead:
|
say they were not run**, rather than either assuming them or treating the absence
|
||||||
|
as the finding. Review everything that *would* move those numbers by reading the
|
||||||
|
artefact as well:
|
||||||
base64-inlined images, images without explicit dimensions, runtime font
|
base64-inlined images, images without explicit dimensions, runtime font
|
||||||
requests, and third-party scripts. The old build is *said* to have inlined ~1 MB
|
requests, and third-party scripts. The old build is *said* to have inlined ~1 MB
|
||||||
of logo PNGs — `AGENTS.md` Q34 is open against that figure, so watch for
|
of logo PNGs — `AGENTS.md` Q34 is open against that figure, so watch for
|
||||||
@@ -84,5 +115,20 @@ For each finding:
|
|||||||
rather than dressing up a preference as a bug.
|
rather than dressing up a preference as a bug.
|
||||||
- **The fix**, specifically
|
- **The fix**, specifically
|
||||||
|
|
||||||
|
### Severity filter — D19
|
||||||
|
|
||||||
|
**Report blocking and should-fix. Raise a `consider` only if acting on it would
|
||||||
|
change what ships.**
|
||||||
|
|
||||||
|
The standing bias above says an approving review that misses a real defect is a
|
||||||
|
failure. This is its twin, and Pouya added it 2026-08-28: **a review that reports
|
||||||
|
twenty things in order to produce twenty small edits has spent more than it
|
||||||
|
saved.** Both failures are real and they pull in opposite directions. The
|
||||||
|
resolution is *not* to soften a genuine defect — it is to stop promoting
|
||||||
|
preferences into findings.
|
||||||
|
|
||||||
|
If you are uncertain whether something is a defect, the standing bias still
|
||||||
|
applies: report it, at the severity your confidence supports.
|
||||||
|
|
||||||
If you genuinely find nothing at a given severity, say which lenses you applied
|
If you genuinely find nothing at a given severity, say which lenses you applied
|
||||||
and what you checked, so the gap is auditable. **"Looks good" is not a review.**
|
and what you checked, so the gap is auditable. **"Looks good" is not a review.**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: claims-auditor
|
name: claims-auditor
|
||||||
description: Audits every factual assertion in site copy against the verified claim register in AGENTS.md section 4. Invoked before any page or article is considered complete. This is the professional-conduct guard, not a proofreading pass.
|
description: Audits every factual assertion in site copy against the verified claim register in AGENTS.md section 4. Under D20 this runs ONCE, at cutover, over the whole finished site — not per build step. This is the professional-conduct guard, not a proofreading pass.
|
||||||
tools: Read, Grep, Glob
|
tools: Read, Grep, Glob
|
||||||
model: opus
|
model: opus
|
||||||
---
|
---
|
||||||
@@ -13,6 +13,66 @@ The site this replaces contained a fictitious founder, invented matter values
|
|||||||
and a testimonial attributed to a person who does not exist. Your existence is
|
and a testimonial attributed to a person who does not exist. Your existence is
|
||||||
the control that stops that recurring.
|
the control that stops that recurring.
|
||||||
|
|
||||||
|
## When you run — D20, and read this before anything else
|
||||||
|
|
||||||
|
**You run ONCE, at cutover, over the whole finished site.** Not per build step.
|
||||||
|
Pouya's ruling, 2026-08-30, recorded in full in `AGENTS.md` D20.
|
||||||
|
|
||||||
|
Three consequences, and they change how you work rather than only when:
|
||||||
|
|
||||||
|
1. **You are the only claims pass this project gets.** There is no second run
|
||||||
|
behind you and no round 3 to catch what you miss. `npm run check:claims` is a
|
||||||
|
greppable tripwire, not a reader. Treat every page as final, because it is.
|
||||||
|
2. **Read the site as a visitor does, not as a diff.** The reason the pass moved
|
||||||
|
here is that the defects worth catching late are the ones that only exist once
|
||||||
|
the pages sit next to each other. The `/med-arb/` ADRIC gloss is the case:
|
||||||
|
every individual claim was true, the quote was verbatim and correctly cited,
|
||||||
|
and the defect was **adjacency** — the sentence beneath the quote answered
|
||||||
|
ADRIC's question with a different designation than ADRIC's own answer. A
|
||||||
|
per-page audit cannot see that. Follow the reading order: `/`, then the nav,
|
||||||
|
then a practice page, then `/about/`. Ask what a visitor now believes.
|
||||||
|
3. **A finding here blocks the cutover.** `docs/06`'s checklist carries it as a
|
||||||
|
blocking item: nothing publishes until your findings are resolved. So the
|
||||||
|
uncertainty-is-a-defect rule still stands, and D19's two-round cap does **not**
|
||||||
|
apply to you — it governs the per-step code review. There is no cap on
|
||||||
|
resolving what this pass finds.
|
||||||
|
|
||||||
|
If you are invoked mid-build anyway, say so in your report: you are auditing a
|
||||||
|
draft rather than the finished site, which is the exact condition D20 says makes
|
||||||
|
this pass weaker.
|
||||||
|
|
||||||
|
## Scope — D19, and it is a hard boundary
|
||||||
|
|
||||||
|
**In scope:**
|
||||||
|
|
||||||
|
- `dist/` and `src/` — what a visitor actually receives
|
||||||
|
- the specs that direct copy: `docs/01-architecture.md`,
|
||||||
|
`docs/03-content-spec.md`, `docs/04-seo-spec.md`, `docs/07-fees.md`
|
||||||
|
- `AGENTS.md` **§3, §4, §7, §9, §12** — the operative sections
|
||||||
|
|
||||||
|
**Out of scope:**
|
||||||
|
|
||||||
|
- the `AGENTS.md` **Change Log**
|
||||||
|
- the agent briefs in `.claude/agents/`, including this one
|
||||||
|
- `AGENTS.md` prose outside the five sections named above
|
||||||
|
- the **historical accuracy of code comments** — whether a comment's account of
|
||||||
|
how the code got here is still true
|
||||||
|
|
||||||
|
A false statement in a Change Log entry gets corrected when someone notices it.
|
||||||
|
It does not get a review pass, because **no visitor reads it.** Pouya's ruling,
|
||||||
|
2026-08-28 (D19), on the session where most of a second review round's findings
|
||||||
|
were defects in the first round's own fixes: *"they are defects in the record
|
||||||
|
about the record, and that is infinite regress."*
|
||||||
|
|
||||||
|
**What this does not narrow, and read this carefully.** Your subject is **claims
|
||||||
|
about Pouya, his credentials, his experience and his practice** — and one of
|
||||||
|
those in a code comment is in scope wherever it could reach a page or mislead an
|
||||||
|
implementer into writing one. `src/data/site.ts` and `src/data/schema.ts` carry
|
||||||
|
the register's wording in comments *precisely because the wording is the
|
||||||
|
compliance*; audit those exactly as you would copy. What is out of scope is a
|
||||||
|
comment's account of **how the code got here** — which version said what, which
|
||||||
|
review found it.
|
||||||
|
|
||||||
## Method
|
## Method
|
||||||
|
|
||||||
1. Read `AGENTS.md` §4 in full — the Verified table, the Forbidden table, and
|
1. Read `AGENTS.md` §4 in full — the Verified table, the Forbidden table, and
|
||||||
@@ -22,6 +82,9 @@ the control that stops that recurring.
|
|||||||
role, an institution, a language, a number, a date, a location, a capability,
|
role, an institution, a language, a number, a date, a location, a capability,
|
||||||
a comparison.
|
a comparison.
|
||||||
3. For each one, find its line in the Verified table.
|
3. For each one, find its line in the Verified table.
|
||||||
|
4. **Then run the second lens below over every quoted or cited source** —
|
||||||
|
claims about the world are audited against `docs/reference/`, not §4, and
|
||||||
|
they are the ones that have shipped wrong most often.
|
||||||
|
|
||||||
## The rule
|
## The rule
|
||||||
|
|
||||||
@@ -29,13 +92,79 @@ the control that stops that recurring.
|
|||||||
no "defensible", no "everyone says this". Report it and require it be removed or
|
no "defensible", no "everyone says this". Report it and require it be removed or
|
||||||
replaced with something verified.
|
replaced with something verified.
|
||||||
|
|
||||||
|
## The second lens — does the gloss stay inside the quote?
|
||||||
|
|
||||||
|
**This is a separate pass over a separate class of claim, and you must run it.**
|
||||||
|
Everything above audits claims **about Pouya** against §4. This lens audits
|
||||||
|
claims **about the world** — a statute, a tribunal's process, a regulator's
|
||||||
|
name, a bill's status — against the `docs/reference/` extract they are sourced
|
||||||
|
to. §4 cannot help you here; the extract is the register.
|
||||||
|
|
||||||
|
**The failure shape, which is now four-for-four on this project: a correct
|
||||||
|
verbatim quotation with an overreaching sentence beneath it.** The quotation
|
||||||
|
passes audit because it is accurate. The gloss fails because it asserts more
|
||||||
|
than the quotation establishes — and it is the gloss that ships as the page's
|
||||||
|
own voice, so it is the half a reader relies on.
|
||||||
|
|
||||||
|
**So for every quoted or cited source in the material under review:**
|
||||||
|
|
||||||
|
1. Read the quotation.
|
||||||
|
2. Read the sentence beneath it — the page's own words.
|
||||||
|
3. Ask **what a reader would take the second to mean**, and then whether the
|
||||||
|
first actually establishes that. Not "is it consistent with", not "is it
|
||||||
|
plausible given" — **does the quoted text establish it.**
|
||||||
|
4. Where it does not, the finding is on the gloss, not the quote. Say which
|
||||||
|
words of the quotation would have to be different for the gloss to hold.
|
||||||
|
|
||||||
|
**Four specific ways it has gone wrong here, so you know the shapes:**
|
||||||
|
|
||||||
|
- **A subject swapped between sentences.** The LAT extract quoted the Tribunal
|
||||||
|
correctly; the gloss said it *"points parties at private mediation, before
|
||||||
|
filing and continuing after filing."* The Tribunal's first sentence names
|
||||||
|
mediation for *"Before you apply"* only; its second is about **negotiation**,
|
||||||
|
and said so twice. Shipped on three pages. An ellipsis in `docs/01` had
|
||||||
|
deleted the second *"negotiation"*.
|
||||||
|
- **A term the source never uses.** *"Connection allocation"* is not an Ontario
|
||||||
|
term — the IESO pages contain zero occurrences of *"allocation"*. Shipped on
|
||||||
|
three pages. **A term of art that appears nowhere in the extract is a term
|
||||||
|
somebody wrote from recall.**
|
||||||
|
- **A status assumed to have held.** *"The 2026 privacy statute"* does not
|
||||||
|
exist; Bill C-27 died without royal assent. It came from a brief's
|
||||||
|
*"replacement privacy legislation in 2026"*, which was a forecast read as an
|
||||||
|
event. **A bill is not an Act until something says it received assent.**
|
||||||
|
- **A class asserted from one instance.** ADR Chambers' self-amendment clause
|
||||||
|
is quoted from its *arbitration* rules; the page wrote *"amends them"* of all
|
||||||
|
three rule sets. You caught that one. The related over-read — that publishing
|
||||||
|
rules establishes an outside neutral can be **appointed** under them — is why
|
||||||
|
the firm is no longer named on any page (2026-08-30).
|
||||||
|
|
||||||
|
**None of this is greppable, which is why it is yours.** `npm run check:claims`
|
||||||
|
catches a fixed set of forbidden strings in `dist/`; it cannot compare a
|
||||||
|
sentence against a source. If a page states a fact about the world and you
|
||||||
|
cannot find the extract that establishes it, that is a finding — an unsourced
|
||||||
|
world-fact is the same defect as an unregistered claim about Pouya, and R14
|
||||||
|
says a claim nobody can check against a committed artefact is unverifiable by
|
||||||
|
construction rather than merely unverified.
|
||||||
|
|
||||||
## Specific things to catch
|
## Specific things to catch
|
||||||
|
|
||||||
**Licensure (D13).** The site asserts the JD and nothing further. Flag: "lawyer",
|
**Licensure (D13).** The site asserts the JD and nothing further. Flag: "lawyer",
|
||||||
"called to the bar", "licensed", "my law practice", "my litigation practice",
|
"called to the bar", "licensed", "my law practice", "my litigation practice",
|
||||||
"my clients", "acts for", "represents", "legal advice", or any post-nominal
|
"my clients", "acts for", "represents", or any post-nominal implying a licence.
|
||||||
implying a licence. **Flag implication as hard as assertion** — "my litigation
|
**Flag implication as hard as assertion** — "my litigation practice" claims
|
||||||
practice" claims licensure without the word.
|
licensure without the word.
|
||||||
|
|
||||||
|
**"legal advice" — FLAG IT ONLY WHERE IT ATTACHES A CAPACITY TO POUYA.** This
|
||||||
|
list carried a bare *"legal advice"* until 2026-08-28, which applied literally
|
||||||
|
flags the **ratified** sentence *"each party should have their own legal
|
||||||
|
advice"*. Flag *giving* legal advice, *practising law*, or *holding a licence*
|
||||||
|
predicated of him — not the phrase wherever it appears. `docs/03`'s
|
||||||
|
*"When a fact is `[unestablished]`"* section carries the pattern and the three
|
||||||
|
tests; the shipped sentence is its worked example of a **pass**.
|
||||||
|
|
||||||
|
*This is the fourth stale claim found inside this brief. The same correction
|
||||||
|
reached `docs/03`'s compliance checklist and did not reach here — the shape this
|
||||||
|
paragraph's own memberships note records two sections down.*
|
||||||
|
|
||||||
The approved phrasing for the boutique role is **"active litigation exposure"**
|
The approved phrasing for the boutique role is **"active litigation exposure"**
|
||||||
or **"involvement in litigation and ADR matters"**. The word **"practice"** in
|
or **"involvement in litigation and ADR matters"**. The word **"practice"** in
|
||||||
@@ -49,9 +178,25 @@ years in ADR practice, or time-to-award statistic is forbidden outright. The
|
|||||||
approved stat set is `Q.Med` / `JD + ML` / `EN · FA`, plus `Q.Arb` in a fourth
|
approved stat set is `Q.Med` / `JD + ML` / `EN · FA`, plus `Q.Arb` in a fourth
|
||||||
slot.
|
slot.
|
||||||
|
|
||||||
**Q.Arb.** Commenced August 2026. Flag anything reading as held, imminent, or
|
**Q.Arb — DO NOT HOLD ITS STATE HERE EITHER. Read §4's row at audit time.**
|
||||||
nearly complete. The Arbitration page must state plainly what is available now
|
This paragraph said *"Commenced August 2026. Flag anything reading as held,
|
||||||
versus what follows designation.
|
imminent, or nearly complete. The Arbitration page must state plainly what is
|
||||||
|
available now versus what follows designation."* §4 recorded Q.Arb as **HELD** on
|
||||||
|
2026-08-29, struck every stage form — `commenced`, `in progress`, `pathway`,
|
||||||
|
`not yet` — struck the Forbidden row against *"held, imminent, nearly complete"*
|
||||||
|
with it, and dissolved the paired-disclosure condition with an explicit
|
||||||
|
instruction to leave no residue. **Applied literally, the struck text would have
|
||||||
|
flagged correct copy and demanded the struck form**, and an imperative sentence
|
||||||
|
about what a page "must state plainly" is the kind an agent obeys.
|
||||||
|
|
||||||
|
Found by this agent in the D20 cutover pass, 2026-09-01, which read §4 instead —
|
||||||
|
**the fifth stale claim found inside this file**, after the memberships list
|
||||||
|
below, and the shape is identical every time: a fact copied here, corrected in
|
||||||
|
§4, never swept. The rule that follows from five instances is the one the
|
||||||
|
memberships paragraph already states, generalised: **this file holds the
|
||||||
|
questions to ask, not the answers.** Any state that can change — a designation, a
|
||||||
|
membership, a date, a rate — is read from §4 at audit time. If you find yourself
|
||||||
|
about to write a value here, write the §4 pointer instead.
|
||||||
|
|
||||||
**Memberships.** **Do not hold a list here. Read the memberships row in
|
**Memberships.** **Do not hold a list here. Read the memberships row in
|
||||||
`AGENTS.md` §4 at audit time and use what it says.** This paragraph used to
|
`AGENTS.md` §4 at audit time and use what it says.** This paragraph used to
|
||||||
@@ -93,6 +238,17 @@ machine-readable misrepresentation is still a misrepresentation.
|
|||||||
A table: **claim quoted verbatim · location · verdict (VERIFIED / NOT IN
|
A table: **claim quoted verbatim · location · verdict (VERIFIED / NOT IN
|
||||||
REGISTER / FORBIDDEN) · the register line it matches, or what to do instead.**
|
REGISTER / FORBIDDEN) · the register line it matches, or what to do instead.**
|
||||||
|
|
||||||
|
### Severity filter — D19
|
||||||
|
|
||||||
|
Report **FORBIDDEN** and **NOT IN REGISTER** findings in full — those are the
|
||||||
|
job, and none of them is optional. Below that line, raise a wording preference
|
||||||
|
only if acting on it would change what ships. Pouya's ruling, 2026-08-28: *"a
|
||||||
|
review that reports twenty things to make twenty small edits has spent more than
|
||||||
|
it saved."*
|
||||||
|
|
||||||
|
This does not soften the rule above it. **A claim not in the Verified table does
|
||||||
|
not ship**, at any count.
|
||||||
|
|
||||||
Then a single line: **PASS** — every assertion traced — or **FAIL**, with the
|
Then a single line: **PASS** — every assertion traced — or **FAIL**, with the
|
||||||
count of untraceable claims.
|
count of untraceable claims.
|
||||||
|
|
||||||
|
|||||||
+74
-13
@@ -19,7 +19,7 @@ does not apply, say which and why before moving on.
|
|||||||
Reminders** and surface anything live to Pouya before you start.
|
Reminders** and surface anything live to Pouya before you start.
|
||||||
2. Read the specs in `docs/` that bear on this task.
|
2. Read the specs in `docs/` that bear on this task.
|
||||||
3. Restate the task in your own words, and name:
|
3. Restate the task in your own words, and name:
|
||||||
- which locked decisions (D1–D18) it touches
|
- which locked decisions (D1–D20) it touches
|
||||||
- which specs govern it
|
- which specs govern it
|
||||||
- which facts it needs from the §4 Verified register
|
- which facts it needs from the §4 Verified register
|
||||||
4. **Stop and ask if you find a conflict** — between the task and a locked
|
4. **Stop and ask if you find a conflict** — between the task and a locked
|
||||||
@@ -36,21 +36,57 @@ hex, no magic numbers. Semantic HTML. Every page gets its metadata.
|
|||||||
Where you need a fact you do not have: `TODO(pouya): <the exact question>` in the
|
Where you need a fact you do not have: `TODO(pouya): <the exact question>` in the
|
||||||
source **and** a new numbered question in `AGENTS.md` §9. Do not invent it.
|
source **and** a new numbered question in `AGENTS.md` §9. Do not invent it.
|
||||||
|
|
||||||
|
**But do not open a numbered question for something that gates nothing — D19.**
|
||||||
|
The Q-series is past 50 and most recent additions gate nothing. If an observation
|
||||||
|
does not block a page, a deploy or a claim, it is a **Change Log note**, not a
|
||||||
|
numbered question — and retire the non-gating ones you find. A `TODO(pouya)` in
|
||||||
|
source is not this case: it blocks the page it sits on, so it still gets a
|
||||||
|
number.
|
||||||
|
|
||||||
## Phase 3 — Adversarial review (this is not optional)
|
## Phase 3 — Adversarial review (this is not optional)
|
||||||
|
|
||||||
Invoke **both** review agents on the change, in parallel:
|
Invoke **one** review agent on the change:
|
||||||
|
|
||||||
- `adversarial-reviewer` — correctness, accessibility, crawlability,
|
- `adversarial-reviewer` — correctness, accessibility, crawlability,
|
||||||
performance, security, simplicity
|
performance, security, simplicity
|
||||||
- `claims-auditor` — every factual assertion traced to `AGENTS.md` §4
|
|
||||||
|
|
||||||
**Give them the diff and the specs. Do not give them your reasoning for why the
|
**`claims-auditor` does NOT run here — D20, Pouya, 2026-08-30.** It runs **once,
|
||||||
work is correct.** Your rationale anchors the reviewer and produces agreement
|
at cutover**, over the whole finished site, as a blocking item on `docs/06`'s
|
||||||
instead of review. They form their own view from the artefact; that independence
|
checklist. Do not invoke it per step, and do not reinstate it because a change
|
||||||
is the whole point of the phase.
|
set looks claim-heavy: that judgement was already made against the measured cost,
|
||||||
|
which `AGENTS.md` D20 records in full. If a claim genuinely worries you, the
|
||||||
|
answer is a `TODO(pouya)` and a §9 question — the thing that blocks — not an
|
||||||
|
unscheduled audit.
|
||||||
|
|
||||||
If the change touches no user-facing copy, `claims-auditor` may be skipped — say
|
Two things carry the claims risk between now and cutover, and neither is
|
||||||
so explicitly.
|
optional: **`npm run check:claims` in Phase 5**, and **Pouya reading the copy as
|
||||||
|
it is built**.
|
||||||
|
|
||||||
|
**Give it the diff and the specs. Do not give it your reasoning for why the work
|
||||||
|
is correct.** Your rationale anchors the reviewer and produces agreement instead
|
||||||
|
of review. It forms its own view from the artefact; that independence is the
|
||||||
|
whole point of the phase.
|
||||||
|
|
||||||
|
### Scope — D19
|
||||||
|
|
||||||
|
Review is bounded. **In scope:** `dist/`, `src/`, the specs that direct copy
|
||||||
|
(`docs/01`, `03`, `04`, `07`), and `AGENTS.md` §3, §4, §7, §9, §12. **Out of
|
||||||
|
scope:** the Change Log, the agents' own briefs, `AGENTS.md` prose outside those
|
||||||
|
sections, and the historical accuracy of code comments. Both agent briefs carry
|
||||||
|
the same boundary — do not widen it in the prompt. It binds the cutover claims
|
||||||
|
pass too, which is the one place `claims-auditor` still runs.
|
||||||
|
|
||||||
|
### The stop signal
|
||||||
|
|
||||||
|
**If a round's findings are predominantly about the record rather than the
|
||||||
|
output, stop the round and say so.** That is the symptom of the loop feeding on
|
||||||
|
itself, and noticing it is part of the job — not a licence to skip the phase.
|
||||||
|
The shape to watch for, from the session that produced D19: a Change Log sweep
|
||||||
|
that could not reproduce *because writing it changed the file it counted*.
|
||||||
|
|
||||||
|
**The claims pass is deferred, not deleted.** Say which review ran in your
|
||||||
|
report. "Reviewed" without naming the agent reads as both, and under D20 it is
|
||||||
|
one.
|
||||||
|
|
||||||
## Phase 4 — Resolve
|
## Phase 4 — Resolve
|
||||||
|
|
||||||
@@ -69,22 +105,47 @@ under review pressure is exactly where the second defect lives.
|
|||||||
> launch articles. None of the four existed before the review started. Skip the
|
> launch articles. None of the four existed before the review started. Skip the
|
||||||
> re-review and you ship the repair, not the bug.
|
> re-review and you ship the repair, not the bug.
|
||||||
|
|
||||||
|
### Two rounds is the cap — D19
|
||||||
|
|
||||||
|
Round 1 reviews the change. Round 2 reviews the material fixes. **There is no
|
||||||
|
round 3: fix what round 2 finds, and ship.**
|
||||||
|
|
||||||
|
The measurement above is why round 2 exists. The measurement below is why round 3
|
||||||
|
does not. On 2026-08-28 two rounds produced **48 findings, and most of round
|
||||||
|
two's were defects in round one's own fixes** — so each further round both finds
|
||||||
|
and manufactures defects, and past two the manufacture rate exceeds the value of
|
||||||
|
the marginal finding. Pouya's ruling. **State this reasoning when you stop at
|
||||||
|
two**, so a later reader does not read the cap as laxness.
|
||||||
|
|
||||||
## Phase 5 — Verify — run it, do not assert it
|
## Phase 5 — Verify — run it, do not assert it
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run check
|
npm run check
|
||||||
npm run build
|
npm run build
|
||||||
|
npm run check:claims
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`check:claims` greps the built pages for the §4 Forbidden set — Pouya's ruling,
|
||||||
|
2026-08-29. **Under D20 it is the only per-step claims control there is**, so
|
||||||
|
never skip it and never let a build ship on a stale `dist/`. It is a tripwire,
|
||||||
|
not a program: it catches the §4 breaches that are greppable and it makes no
|
||||||
|
claim about the ones that are not. It self-tests its own patterns
|
||||||
|
before it sweeps and refuses to run against an empty or stale `dist/`, so a pass
|
||||||
|
is a pass on the bytes that would ship. **A match is not yet a finding** — read
|
||||||
|
the context it prints. If a pattern is genuinely wrong, change it deliberately
|
||||||
|
with a Change Log entry; do not delete one to make a build pass.
|
||||||
|
|
||||||
Then, as applicable to what changed:
|
Then, as applicable to what changed:
|
||||||
|
|
||||||
- Serve `dist/` and confirm the page **renders its full content with JavaScript
|
- Serve `dist/` and confirm the page **renders its full content with JavaScript
|
||||||
disabled** — the failure this whole project exists to fix
|
disabled** — the failure this whole project exists to fix
|
||||||
- `curl` the built HTML and confirm real content, not a shell
|
- `curl` the built HTML and confirm real content, not a shell
|
||||||
- ~~Lighthouse mobile ≥ 95 on all four categories~~ — **UNAVAILABLE.**
|
- **Lighthouse mobile ≥ 95 on all four categories** — `npm run lighthouse`,
|
||||||
`@lhci/cli` was removed on 2026-08-26 and is not re-added until build step 7
|
after `npm run build`, on the pages the change touches or on all of them.
|
||||||
(`AGENTS.md` R11, §7). Report it as *not run, tool unavailable*. Do not
|
Available again since 2026-08-31 (`AGENTS.md` §7). It exits non-zero on a
|
||||||
substitute a manual DevTools run and describe it as the same check
|
breach, so **read the exit status** rather than the table. Do not substitute a
|
||||||
|
manual DevTools run and describe it as the same check. Note when reporting that
|
||||||
|
the accessibility figure is measured with `prefers-reduced-motion` forced
|
||||||
- Every internal link resolves
|
- Every internal link resolves
|
||||||
- Metadata present: unique title, description, canonical, OG, JSON-LD
|
- Metadata present: unique title, description, canonical, OG, JSON-LD
|
||||||
- **No scroll-driven animation was eaten by the minifier.** This must return
|
- **No scroll-driven animation was eaten by the minifier.** This must return
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ Scope: $ARGUMENTS
|
|||||||
If no scope is given, review the uncommitted working tree (`git status`,
|
If no scope is given, review the uncommitted working tree (`git status`,
|
||||||
`git diff`).
|
`git diff`).
|
||||||
|
|
||||||
Invoke **both** agents in parallel on that scope:
|
Invoke **`adversarial-reviewer`** on that scope.
|
||||||
|
|
||||||
- `adversarial-reviewer`
|
**`claims-auditor` is not part of this command — D20, Pouya, 2026-08-30.** The
|
||||||
- `claims-auditor` — unless nothing user-facing changed, in which case say so
|
claims pass runs **once, at cutover**, over the whole finished site; `AGENTS.md`
|
||||||
|
D20 records the reasoning and the measured cost of deferring it. Run it here only
|
||||||
|
if Pouya asks for it by name in `$ARGUMENTS` — in which case say in the report
|
||||||
|
that it was run out of band, and against a draft rather than the finished site,
|
||||||
|
because that is the condition D20 says weakens it.
|
||||||
|
|
||||||
Give them the diff and the relevant specs from `docs/`. **Do not brief them on
|
Give it the diff and the relevant specs from `docs/`. **Do not brief it on why
|
||||||
why the code is correct** — that anchors the review and turns it into agreement.
|
the code is correct** — that anchors the review and turns it into agreement.
|
||||||
|
|
||||||
Report findings grouped by severity, most severe first. For each: the defect, the
|
Report findings grouped by severity, most severe first. For each: the defect, the
|
||||||
concrete failure it produces, and the fix. Do not fix anything yet — Pouya
|
concrete failure it produces, and the fix. Do not fix anything yet — Pouya
|
||||||
|
|||||||
@@ -30,12 +30,18 @@ reasoning.
|
|||||||
instances survived all three, one of them inside the definition of the agent
|
instances survived all three, one of them inside the definition of the agent
|
||||||
whose job is to catch it.
|
whose job is to catch it.
|
||||||
|
|
||||||
4. **Check §12 Standing Reminders.** Is anything now due? Should something new
|
4. **Do not open a numbered question for something that gates nothing — D19.**
|
||||||
|
The Q-series is past 50 and most recent additions gate nothing. If it does
|
||||||
|
not block a page, a deploy or a claim, it belongs in this entry as a note, not
|
||||||
|
in §9 as a number — and retire the non-gating ones you find while you are in
|
||||||
|
there. Pouya's ruling, 2026-08-28.
|
||||||
|
|
||||||
|
5. **Check §12 Standing Reminders.** Is anything now due? Should something new
|
||||||
be added — a decision Pouya parked, or one you made on his behalf that he has
|
be added — a decision Pouya parked, or one you made on his behalf that he has
|
||||||
not yet ratified?
|
not yet ratified?
|
||||||
|
|
||||||
5. **Leave the tree clean.** `git status` should show only intended changes. No
|
6. **Leave the tree clean.** `git status` should show only intended changes. No
|
||||||
stray build output, no `.env`, no credentials, no `aws-inventory.txt`.
|
stray build output, no `.env`, no credentials, no `aws-inventory.txt`.
|
||||||
|
|
||||||
6. Report: what changed in `AGENTS.md`, what is open, and the single most useful
|
7. Report: what changed in `AGENTS.md`, what is open, and the single most useful
|
||||||
next action.
|
next action.
|
||||||
|
|||||||
@@ -37,9 +37,16 @@ jobs:
|
|||||||
AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }}
|
AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }}
|
||||||
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
||||||
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
|
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
|
||||||
# Job-level so the guard can see it. An empty INTAKE_ENDPOINT does not
|
# NO INTAKE_ENDPOINT. Build step 8 moved the intake form to the
|
||||||
# fail the build - it ships a live contact form posting to nothing.
|
# same-origin path /api/intake, after which nothing in src/ read this
|
||||||
INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
# value - `git grep PUBLIC_INTAKE_ENDPOINT -- src/` returned nothing - and
|
||||||
|
# the guard below was blocking a deploy on it. The comment that stood here
|
||||||
|
# said an empty value "ships a live contact form posting to nothing",
|
||||||
|
# which became false in both directions: the form posts to /api/intake
|
||||||
|
# regardless, and what decides whether it works is the CloudFront /api/*
|
||||||
|
# behaviour, which nothing guarded. See scripts/deploy-local.sh, which
|
||||||
|
# carries the post-deploy route check that replaced it.
|
||||||
|
# Found by `adversarial-reviewer`, 2026-08-31.
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Runs first, before checkout and before any AWS call, so a
|
# Runs first, before checkout and before any AWS call, so a
|
||||||
@@ -66,7 +73,6 @@ jobs:
|
|||||||
[ -n "$AWS_DEFAULT_REGION" ] || missing="$missing AWS_REGION(var)"
|
[ -n "$AWS_DEFAULT_REGION" ] || missing="$missing AWS_REGION(var)"
|
||||||
[ -n "$S3_BUCKET" ] || missing="$missing S3_BUCKET(var)"
|
[ -n "$S3_BUCKET" ] || missing="$missing S3_BUCKET(var)"
|
||||||
[ -n "$CLOUDFRONT_DISTRIBUTION_ID" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID(var)"
|
[ -n "$CLOUDFRONT_DISTRIBUTION_ID" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID(var)"
|
||||||
[ -n "$INTAKE_ENDPOINT" ] || missing="$missing INTAKE_ENDPOINT(var)"
|
|
||||||
[ -n "$AWS_ACCESS_KEY_ID" ] || missing="$missing AWS_ACCESS_KEY_ID(secret)"
|
[ -n "$AWS_ACCESS_KEY_ID" ] || missing="$missing AWS_ACCESS_KEY_ID(secret)"
|
||||||
[ -n "$AWS_SECRET_ACCESS_KEY" ] || missing="$missing AWS_SECRET_ACCESS_KEY(secret)"
|
[ -n "$AWS_SECRET_ACCESS_KEY" ] || missing="$missing AWS_SECRET_ACCESS_KEY(secret)"
|
||||||
if [ -n "$missing" ]; then
|
if [ -n "$missing" ]; then
|
||||||
@@ -96,11 +102,19 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
env:
|
env:
|
||||||
|
# PUBLIC_SITE_URL only, because it is the one variable
|
||||||
|
# astro.config.mjs reads. PUBLIC_INTAKE_ENDPOINT and
|
||||||
|
# PUBLIC_BOOKING_URL were set here and consumed by nothing;
|
||||||
|
# `CONTACT.bookingUrl` is null in source while R6 keeps booking parked.
|
||||||
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
||||||
# vars, not env — Gitea expression-context support is the very thing
|
|
||||||
# the guard above exists to not depend on.
|
# AGENTS.md §4 Forbidden, enforced on the built output before a single
|
||||||
PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
# byte is uploaded. Runs here rather than in `npm run check` because it
|
||||||
PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
# reads dist/, and it refuses a stale or empty dist for the same reason
|
||||||
|
# this workflow guards its variables: an empty sweep reads exactly like a
|
||||||
|
# clean one. Mirrored in scripts/deploy-local.sh.
|
||||||
|
- name: Claim check
|
||||||
|
run: npm run check:claims
|
||||||
|
|
||||||
# Some Gitea runner images ship without the AWS CLI. Install if missing.
|
# Some Gitea runner images ship without the AWS CLI. Install if missing.
|
||||||
- name: Ensure AWS CLI
|
- name: Ensure AWS CLI
|
||||||
@@ -147,5 +161,69 @@ jobs:
|
|||||||
--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
|
--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
|
||||||
--paths "/*"
|
--paths "/*"
|
||||||
|
|
||||||
|
# Mirrors the same step in scripts/deploy-local.sh, because that script's
|
||||||
|
# header requires the two paths to match on everything that determines
|
||||||
|
# what gets published - and this replaced the INTAKE_ENDPOINT guard.
|
||||||
|
#
|
||||||
|
# It ASSERTS A POSITIVE. The first version excluded one status code and
|
||||||
|
# passed on everything else; `adversarial-reviewer` round 2 measured it
|
||||||
|
# passing on a refused connection (curl -w already prints 000, so the
|
||||||
|
# `|| echo 000` double-appended and made $code "000000") and on a real 501.
|
||||||
|
# It would also have passed the case that matters most: with the /api/*
|
||||||
|
# behaviour MISSING, the POST falls to the S3 default behaviour and
|
||||||
|
# CloudFront answers 403 for a disallowed method - indistinguishable from
|
||||||
|
# the handler's Origin refusal, which is the one distinction this check
|
||||||
|
# exists to draw.
|
||||||
|
#
|
||||||
|
# With the correct Origin and an empty submission the handler validates,
|
||||||
|
# rejects, and redirects 303 to /contact/could-not-send/ - BEFORE any
|
||||||
|
# DynamoDB write and before any email, which is what makes it safe against
|
||||||
|
# production. Probed on four cases: refused, 501, 403, and the real 303.
|
||||||
|
#
|
||||||
|
# It warns rather than failing: the site is already deployed by this point,
|
||||||
|
# and failing the job would not un-deploy it.
|
||||||
|
- name: Intake route check
|
||||||
|
run: |
|
||||||
|
url="https://adr.smlcompany.ca/api/intake"
|
||||||
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
--max-time 15 \
|
||||||
|
-H "Origin: https://adr.smlcompany.ca" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data 'deploy-route-probe=1' "$url")
|
||||||
|
rc=$?
|
||||||
|
location=$(curl -sS -o /dev/null -w '%{redirect_url}' -X POST \
|
||||||
|
--max-time 15 \
|
||||||
|
-H "Origin: https://adr.smlcompany.ca" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data 'deploy-route-probe=1' "$url" || true)
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
echo "WARNING: the POST to /api/intake did not complete (curl exit $rc)."
|
||||||
|
echo "The site is deployed and the contact form is unverified."
|
||||||
|
echo "See docs/06-deployment.md's cutover checklist."
|
||||||
|
elif [ "$code" = "303" ] && case "$location" in *"/contact/could-not-send/") true;; *) false;; esac; then
|
||||||
|
echo "POST /api/intake -> 303 -> $location (routed, validating)"
|
||||||
|
else
|
||||||
|
echo "WARNING: POST /api/intake returned $code, expected 303 to"
|
||||||
|
echo "/contact/could-not-send/; redirect was '${location:-none}'."
|
||||||
|
# Kept in step with scripts/deploy-local.sh — the two are one
|
||||||
|
# artefact in two places. 404 is ambiguous between three causes and
|
||||||
|
# the distribution's 404 mapping hides API Gateway's own body.
|
||||||
|
echo "404: /api/* behaviour missing (docs/09 Part 3), OR the POST"
|
||||||
|
echo "/api/intake route missing (Part 6.2), OR the route exists and"
|
||||||
|
echo "the 404 mapping replaced the API's body. Separate them with"
|
||||||
|
echo "aws apigatewayv2 get-routes --api-id <id> --query"
|
||||||
|
echo "'Items[].RouteKey' — the --api-id is required; without it the"
|
||||||
|
echo "CLI exits 252 on ParamValidation."
|
||||||
|
echo "403: method rejected, or the handler refused the Origin —"
|
||||||
|
echo "read which origin request policy /api/* carries. Since"
|
||||||
|
echo "2026-09-04 it may be the custom whitelist"
|
||||||
|
echo "adr-sml-api-viewer-address rather than the managed"
|
||||||
|
echo "AllViewerExceptHostHeader; a policy that does not forward"
|
||||||
|
echo "Origin 403s every real submission. Rollback id:"
|
||||||
|
echo "b689b0a8-53d0-40ab-baf2-68738e2966ac."
|
||||||
|
echo "500: the invoke permission for this route is missing (6.1)."
|
||||||
|
echo "See docs/09-cutover-runbook.md Part 7.1."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
run: echo "Deployed to https://adr.smlcompany.ca — commit ${GITHUB_SHA:0:7}"
|
run: echo "Deployed to https://adr.smlcompany.ca — commit ${GITHUB_SHA:0:7}"
|
||||||
|
|||||||
@@ -46,3 +46,9 @@ test-results/
|
|||||||
|
|
||||||
# generated inventory — safe to share, but not tracked
|
# generated inventory — safe to share, but not tracked
|
||||||
aws-inventory.txt
|
aws-inventory.txt
|
||||||
|
|
||||||
|
# The pre-cutover archive of the old single-file build (docs/09 Part 8.1).
|
||||||
|
# NOT committed: it is ~3.3 MB of the page that carried the fabricated founder
|
||||||
|
# and the invented matter values, and Part 8.3 runs `git tag` eleven lines later.
|
||||||
|
# `docs/06` says to keep it, not to version it.
|
||||||
|
_archive/
|
||||||
|
|||||||
@@ -15,3 +15,18 @@ docs/reference/
|
|||||||
# measured contrast ratios can be scanned down the page — see docs/02. Prettier
|
# measured contrast ratios can be scanned down the page — see docs/02. Prettier
|
||||||
# collapses that alignment, which is the one thing the file is for.
|
# collapses that alignment, which is the one thing the file is for.
|
||||||
src/styles/tokens.css
|
src/styles/tokens.css
|
||||||
|
|
||||||
|
# ⚠️ MDX IS IGNORED, AND THE FIRST REASON IS THAT PRETTIER BREAKS IT.
|
||||||
|
# Measured 2026-08-31: `npm run format` rewrote an MDX JSX comment from
|
||||||
|
# `{/* … */}` to `{/_ … _/}` — it read the asterisks as markdown emphasis — and
|
||||||
|
# the build then failed with `Could not parse expression with oxc: Unterminated
|
||||||
|
# regular expression`, because MDX parses `{/_ …` as a regex literal. It works
|
||||||
|
# in the source and dies at build, which is the worst shape a defect can take.
|
||||||
|
#
|
||||||
|
# The second reason is the one that would matter even if that were fixed
|
||||||
|
# upstream: **these files are hand-audited prose.** Each of the five launch
|
||||||
|
# articles was read line by line against `AGENTS.md` §4 and against the sourced
|
||||||
|
# extracts in `docs/reference/`, and 76 findings were applied to them. Machine
|
||||||
|
# reflowing audited copy means the committed bytes are no longer the bytes that
|
||||||
|
# were audited. Prose wrapping here is checked by eye, exactly as `*.md` above.
|
||||||
|
*.mdx
|
||||||
|
|||||||
@@ -57,8 +57,9 @@ being restated in the prompt.
|
|||||||
anything live), read the governing specs, name the decisions the task touches,
|
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.
|
and **stop and ask on any conflict**. A blocked build is a correct build.
|
||||||
2. **Implement** — following the conventions below.
|
2. **Implement** — following the conventions below.
|
||||||
3. **Adversarial review** — invoke `adversarial-reviewer` and `claims-auditor` in
|
3. **Adversarial review** — invoke `adversarial-reviewer` on the diff. **D20:
|
||||||
parallel on the diff.
|
`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
|
4. **Resolve** — fix each finding or decline it with a stated reason. Re-review
|
||||||
material fixes.
|
material fixes.
|
||||||
5. **Verify** — run the checks. Never report a check as passing that you did not
|
5. **Verify** — run the checks. Never report a check as passing that you did not
|
||||||
@@ -67,6 +68,14 @@ being restated in the prompt.
|
|||||||
|
|
||||||
`/review` runs phase 3 alone. `/wrap` runs phase 6 at session end.
|
`/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
|
**Think deeply before acting.** Extended thinking is on by default for this
|
||||||
project (`.claude/settings.json`), and `/build` and `/review` request it
|
project (`.claude/settings.json`), and `/build` and `/review` request it
|
||||||
explicitly. The planning and review phases are where it earns its cost — a defect
|
explicitly. The planning and review phases are where it earns its cost — a defect
|
||||||
@@ -90,11 +99,33 @@ 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
|
reason you declined it so a later reader can see the judgement was made rather
|
||||||
than missed.
|
than missed.
|
||||||
|
|
||||||
**Two reviewers, because they catch different things.** `adversarial-reviewer`
|
**Two reviewers, because they catch different things — but they no longer run at
|
||||||
reads the code. `claims-auditor` reads the copy against the §4 register and knows
|
the same time.** `adversarial-reviewer` reads the code. `claims-auditor` reads the
|
||||||
nothing about whether the code is elegant. A generic reviewer consistently
|
copy against the §4 register and knows nothing about whether the code is elegant.
|
||||||
under-weights the professional-conduct check, which is the highest-stakes failure
|
A generic reviewer consistently under-weights the professional-conduct check,
|
||||||
mode on this project — so it gets its own pass.
|
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
|
## Commands
|
||||||
|
|
||||||
@@ -104,11 +135,45 @@ npm run dev # local dev server
|
|||||||
npm run build # static build to ./dist
|
npm run build # static build to ./dist
|
||||||
npm run preview # serve ./dist locally
|
npm run preview # serve ./dist locally
|
||||||
npm run check # astro check — type and template errors
|
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 icons # re-derives public/favicon.ico from the brand master. REGENERATOR
|
||||||
npm run lint # eslint + prettier check
|
npm run lint # eslint + prettier check
|
||||||
npm run format # prettier — rewrite files in place
|
npm run format # prettier — rewrite files in place
|
||||||
npm run deploy # build + deploy from this machine (see docs/06)
|
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.
|
||||||
|
|
||||||
|
**`bio:pdf` and `icons` are REGENERATORS, not gates** — they rewrite committed
|
||||||
|
artefacts (`public/pouya-lajevardi-bio.pdf`, `public/favicon.ico`) rather than
|
||||||
|
checking anything, so they are run deliberately and their output is committed.
|
||||||
|
⚠️ **`LOCAL ONLY` above means a different thing for each, so do not read the two
|
||||||
|
labels as one.** `bio:pdf` **cannot** run in CI — it drives a browser. `icons`
|
||||||
|
is pure Node and **could**; it is out of the build because a build should not
|
||||||
|
silently rewrite an artefact a human approved, which is why it is labelled
|
||||||
|
`REGENERATOR` rather than `LOCAL ONLY`. It regenerates the favicon **only** — the touch icon
|
||||||
|
is hand-made and must stay opaque cream (`docs/reference/brand-assets.md`
|
||||||
|
§The icon set).
|
||||||
|
|
||||||
|
**`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
|
## Where things live
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -176,6 +241,79 @@ 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
|
claim*: the claim must rest on output you actually read, from a command that
|
||||||
actually ran.
|
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 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
|
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>`
|
parent's rule to `.header-cta[data-astro-cid-<parent>]` while the rendered `<a>`
|
||||||
@@ -237,7 +375,7 @@ an operator to delete the three records that authenticate outbound mail —
|
|||||||
under the heading "Never delete".
|
under the heading "Never delete".
|
||||||
|
|
||||||
**A measurement is a claim about your instrument until you check the
|
**A measurement is a claim about your instrument until you check the
|
||||||
instrument.** This has now cost five times, and the shape is identical every
|
instrument.** This has now cost nine times, and the shape is identical every
|
||||||
time: a number that looks like a finding, from a probe nobody validated.
|
time: a number that looks like a finding, from a probe nobody validated.
|
||||||
|
|
||||||
- `timeout 60 ls "$DRIVE"` — **the command never ran.** `timeout` is not
|
- `timeout 60 ls "$DRIVE"` — **the command never ran.** `timeout` is not
|
||||||
@@ -258,11 +396,92 @@ time: a number that looks like a finding, from a probe nobody validated.
|
|||||||
ladder is not being generated at all*, a shipped defect on every page. It is
|
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
|
**density-corrected by spec**: a 192px file selected at `3x` correctly reports
|
||||||
64. The files on disk were 64 / 128 / 192 all along.
|
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.
|
||||||
|
- **An ink bounding box of `48x48` filling the whole 48 px favicon** — which
|
||||||
|
reads as *the regeneration blew the mark up to the full square*. The probe
|
||||||
|
identified ink as "differs from cream", which is **correct on a matted icon and
|
||||||
|
meaningless on a transparent one**, because `alpha = 0` pixels still carry RGB
|
||||||
|
`(0,0,0)`. **Nothing about the probe changed — the class of input did**, and a
|
||||||
|
probe cannot tell you that. Composite over a known ground first, so both things
|
||||||
|
compared are the kind of thing the instrument was built for.
|
||||||
|
- **A contrast ratio of `14.02:1` for the mark's darkest ink** — computed from a
|
||||||
|
raw channel value **without asking what alpha it is painted at**. Exactly one
|
||||||
|
pixel carried that colour and its alpha was 251; a `>= 250` filter let it
|
||||||
|
through and the ratio was then taken as if it were opaque. **It is a number
|
||||||
|
about a colour that is never painted.** Composite against the actual ground
|
||||||
|
before measuring contrast, and take "the ink colour" only from fully opaque
|
||||||
|
pixels.
|
||||||
|
- **`POST /api/intake` returning 403 — read as "the route does not exist", on a
|
||||||
|
LIVE site.** ⚠️ **A bare POST to `/api/intake` returns 403 BY DESIGN.** The
|
||||||
|
handler rejects a request with no `Origin` header, and **`docs/09` §7.1 says so
|
||||||
|
in as many words** — *"403 means the `Origin` header did not arrive"* — three
|
||||||
|
lines below the probe it prescribes. **The only valid route probe is `docs/09`
|
||||||
|
§7.1 verbatim, `Origin` header included; a 403 without that header is not
|
||||||
|
evidence about the route.** Run correctly it returns **303** to
|
||||||
|
`/contact/could-not-send/`, which is the handler answering as designed. This
|
||||||
|
fired **twice on this project in two days** — Pouya's own probe tripped it
|
||||||
|
2026-09-02, and it then reached a Change Log entry, a `docs/06` blocker and a
|
||||||
|
report to him as *"the intake form is live and broken"*. **The status code was
|
||||||
|
read without reading the document that defines what that status code means on
|
||||||
|
that route**, and the document was in the repo the whole time. Generalised:
|
||||||
|
**before interpreting a response, check whether the endpoint documents its own
|
||||||
|
failure modes** — an API that rejects by design looks exactly like an API that
|
||||||
|
is missing.
|
||||||
|
|
||||||
So before acting on a number: say what it is a number *of*; confirm the command
|
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
|
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.
|
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.**
|
**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
|
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
|
`leading` hit `the pLEADINGs`. Both on the same page on the same day. Print the
|
||||||
@@ -287,6 +506,62 @@ 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
|
`.claude/agents/claims-auditor.md` — the definition of the agent whose job is to
|
||||||
catch exactly that. Recall is not evidence.
|
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:`).
|
**Commits.** Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`).
|
||||||
One logical change per commit. Never commit secrets, `.env` files, or AWS
|
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
|
credentials. Gitea is not an AWS OIDC provider, so the deploy key is designed as
|
||||||
@@ -297,13 +572,30 @@ actually been provisioned is `AGENTS.md` Q22. It must never reach the repo.
|
|||||||
every page. Under 100 KB of JS on any route. LCP under 2.0 s on a simulated
|
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.
|
Slow 4G connection. Treat a budget breach as a failing build.
|
||||||
|
|
||||||
**Lighthouse cannot currently be run.** `@lhci/cli` was removed on 2026-08-26
|
**Lighthouse runs again as of 2026-08-31 — `npm run lighthouse`, and it is
|
||||||
(it carried 7 high-severity advisories, `0.15.1` is `latest`, and it had no
|
`lighthouse` rather than `@lhci/cli`.** It enumerates every `index.html` in
|
||||||
pages and no `lighthouserc` to work with). The budget stands; the instrument is
|
`dist/`, so the page set cannot go stale; it asserts the four category scores and
|
||||||
missing. It is re-added at build step 7 under `AGENTS.md` R11 — with a freshly
|
**reports** LCP and CLS without asserting them, because simulated throttling on a
|
||||||
verified pin, not on the assumption that `0.15.1` is still the ceiling. **Say
|
loopback server is not the Slow 4G field measurement `docs/04` describes.
|
||||||
"not run — tool unavailable" rather than silently omitting it.** A documented
|
|
||||||
control that no longer exists is precisely the defect Q22 turned out to be.
|
**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
|
## What "done" means for a page
|
||||||
|
|
||||||
@@ -311,7 +603,7 @@ control that no longer exists is precisely the defect Q22 turned out to be.
|
|||||||
- [ ] No `TODO(pouya)` left unlogged in §9
|
- [ ] No `TODO(pouya)` left unlogged in §9
|
||||||
- [ ] Unique title, meta description, canonical, OG/Twitter tags, JSON-LD
|
- [ ] Unique title, meta description, canonical, OG/Twitter tags, JSON-LD
|
||||||
- [ ] Semantic HTML; keyboard navigable; reduced-motion honoured
|
- [ ] Semantic HTML; keyboard navigable; reduced-motion honoured
|
||||||
- [ ] Lighthouse ≥ 95 mobile, all four categories — **UNAVAILABLE until step 7**
|
- [ ] Lighthouse ≥ 95 mobile, all four categories — `npm run lighthouse` after
|
||||||
(see the performance budget above). Report it as not run; do not tick it
|
`npm run build`. Read the exit status, not the table
|
||||||
- [ ] Renders correctly with JavaScript disabled
|
- [ ] Renders correctly with JavaScript disabled
|
||||||
- [ ] `AGENTS.md` Change Log entry appended
|
- [ ] `AGENTS.md` Change Log entry appended
|
||||||
|
|||||||
@@ -19,13 +19,37 @@ npm run dev # http://localhost:4321
|
|||||||
| Command | Does |
|
| Command | Does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `npm run dev` | Development server with hot reload |
|
| `npm run dev` | Development server with hot reload |
|
||||||
| `npm run build` | Static build to `./dist` |
|
| `npm run build` | Static build to `./dist` — 22 pages |
|
||||||
| `npm run preview` | Serve the built site locally |
|
| `npm run preview` | Serve the built site locally |
|
||||||
| `npm run check` | `astro check` — type and template errors |
|
| `npm run check` | `astro check` — type and template errors |
|
||||||
|
| `npm run check:claims` | `AGENTS.md` §4 Forbidden, enforced on `dist/`. **Runs on every deploy** |
|
||||||
|
| `npm run check:intake` | The intake form's field table against the Lambda's — two on purpose |
|
||||||
|
| `npm run og:proof` | Every `og:image` resolves; every card headline **is** its page's `<h1>` |
|
||||||
| `npm run lint` | ESLint + Prettier check |
|
| `npm run lint` | ESLint + Prettier check |
|
||||||
| `npm run format` | Prettier — rewrite files in place |
|
| `npm run format` | Prettier — rewrite files in place |
|
||||||
|
| `npm run lighthouse` | The performance budget, all four categories. **Local only** |
|
||||||
|
| `npm run bio:pdf` | Re-renders the committed one-page PDF from `/bio/`. **Local only** |
|
||||||
| `npm run deploy` | Build and deploy from this machine — see Deployment |
|
| `npm run deploy` | Build and deploy from this machine — see Deployment |
|
||||||
|
|
||||||
|
**Two of those cannot run in CI, and that is stated rather than left to be
|
||||||
|
discovered.** `lighthouse` and `bio:pdf` drive an installed browser; the Gitea
|
||||||
|
runner has none. 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 — 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 here are deliberately
|
||||||
|
duplicated**, and a duplicated fact needs a mechanism rather than a comment.
|
||||||
|
Text baked into an OG card cannot be grepped by `check:claims`, so `og:proof`
|
||||||
|
comparing each card's headline to its page's `<h1>` is the only thing keeping
|
||||||
|
card copy inside the claim register. And the Lambda validates against its own
|
||||||
|
field table, because a server that validates against a list the client shipped
|
||||||
|
it is not validating.
|
||||||
|
|
||||||
|
*(`npm run check:claims` was missing from this table before 2026-08-31, along
|
||||||
|
with the four added that day. A table of the project's controls that omits a
|
||||||
|
control is the shape those controls exist to catch.)*
|
||||||
|
|
||||||
## Before you contribute
|
## Before you contribute
|
||||||
|
|
||||||
Read **`AGENTS.md`** first, and maintain it as you work — it is the living
|
Read **`AGENTS.md`** first, and maintain it as you work — it is the living
|
||||||
@@ -59,11 +83,13 @@ both.
|
|||||||
`.gitea/workflows/deploy.yml` is the CI pipeline — **Gitea Actions**, not GitHub
|
`.gitea/workflows/deploy.yml` is the CI pipeline — **Gitea Actions**, not GitHub
|
||||||
Actions. **It has never run**, for two reasons that are not oversights:
|
Actions. **It has never run**, for two reasons that are not oversights:
|
||||||
|
|
||||||
- The scoped IAM user does not exist. `aws iam get-user --user-name
|
|
||||||
adr-sml-deploy` returns `NoSuchEntity` (`AGENTS.md` Q22).
|
|
||||||
- Actions are not enabled and no runner is registered. The Gitea instance is
|
- Actions are not enabled and no runner is registered. The Gitea instance is
|
||||||
jointly administered, so both need its second administrator (Q23).
|
jointly administered, so both need its second administrator (Q23).
|
||||||
|
|
||||||
|
*(It previously listed a second reason — that the scoped IAM user did not exist.
|
||||||
|
**It exists**: `adr-sml-deploy`, created 2026-08-26, Q22 closed 2026-08-28. See
|
||||||
|
`AGENTS.md` §7 for the inventory and the least-privilege verification.)*
|
||||||
|
|
||||||
Its **first** step is a guard: the run aborts, naming what is missing, if any
|
Its **first** step is a guard: the run aborts, naming what is missing, if any
|
||||||
required variable or either AWS secret is empty. Only emptiness is tested and no
|
required variable or either AWS secret is empty. Only emptiness is tested and no
|
||||||
value is echoed.
|
value is echoed.
|
||||||
@@ -75,11 +101,14 @@ Gitea falls back to that directory when `.gitea/workflows` is absent, so a
|
|||||||
workflow file left there with a `push` trigger would be only conditionally
|
workflow file left there with a `push` trigger would be only conditionally
|
||||||
inert. As an `.example` under `docs/` it cannot be picked up at all.
|
inert. As an `.example` under `docs/` it cannot be picked up at all.
|
||||||
|
|
||||||
**The pipeline is designed around a long-lived AWS credential, and it does not
|
**The pipeline is designed around a long-lived AWS credential, and that
|
||||||
exist yet.** Gitea is not an AWS OIDC provider, so there is no role to assume:
|
credential now exists.** Gitea is not an AWS OIDC provider, so there is no role
|
||||||
deploys are *to* authenticate as a scoped IAM user, `adr-sml-deploy`, with its
|
to assume: deploys authenticate as a scoped IAM user, `adr-sml-deploy`, with its
|
||||||
access key in the repository's Gitea Actions secrets. `aws iam get-user`
|
access key in the repository's Gitea Actions secrets. **The user was created
|
||||||
confirms that user has not been created (Q22). In the meantime the local script
|
2026-08-26 and verified least-privilege by execution — `AGENTS.md` §7, Q22 closed
|
||||||
|
2026-08-28.** Its access key has **never been used** (`LastUsed` null), because
|
||||||
|
deploys still run locally. *(This paragraph asserted the user "has not been
|
||||||
|
created" until 2026-08-28.)* In the meantime the local script
|
||||||
**refuses to run as `user/pouya`**, the broadly-permissioned personal user —
|
**refuses to run as `user/pouya`**, the broadly-permissioned personal user —
|
||||||
see `AGENTS.md` §10. Two things are meant to bound the risk, and neither is in
|
see `AGENTS.md` §10. Two things are meant to bound the risk, and neither is in
|
||||||
place yet:
|
place yet:
|
||||||
@@ -93,11 +122,14 @@ place yet:
|
|||||||
narrowness is what keeps a compromised runner away from it, and it is
|
narrowness is what keeps a compromised runner away from it, and it is
|
||||||
load-bearing rather than hygiene. See `AGENTS.md` §10. If a deploy step needs
|
load-bearing rather than hygiene. See `AGENTS.md` §10. If a deploy step needs
|
||||||
a permission the policy lacks, question the step; do not widen the policy.
|
a permission the policy lacks, question the step; do not widen the policy.
|
||||||
- **The key must be rotated quarterly, and nobody owns that yet.** Create a
|
- **The key must be rotated quarterly. It now has an owner and a date: first
|
||||||
second access key, update the Gitea secrets, confirm a deploy succeeds, then
|
rotation due 2026-11-26** (key created 2026-08-26) — `AGENTS.md` §12 **R17**.
|
||||||
delete the old one — rotation that leaves the old key active is not
|
Create a second access key, update the Gitea secrets, confirm a deploy
|
||||||
rotation. OIDC would have removed the obligation entirely; it is
|
succeeds, **then** delete the old one — rotation that leaves the old key active
|
||||||
unavailable, so this is a standing calendar task still waiting on an owner.
|
is not rotation, and deleting before verifying leaves no way to authenticate
|
||||||
|
the fix. OIDC would have removed the obligation entirely; it is unavailable, so
|
||||||
|
this is a standing calendar task. *(It read "nobody owns that yet" until
|
||||||
|
2026-08-28; R17 is the owner, and the date is the whole point of the row.)*
|
||||||
|
|
||||||
Full procedure, IAM policy, runner setup, and cutover checklist:
|
Full procedure, IAM policy, runner setup, and cutover checklist:
|
||||||
`docs/06-deployment.md`.
|
`docs/06-deployment.md`.
|
||||||
|
|||||||
+27
-2
@@ -32,12 +32,37 @@ export default defineConfig({
|
|||||||
integrations: [
|
integrations: [
|
||||||
mdx(),
|
mdx(),
|
||||||
sitemap({
|
sitemap({
|
||||||
// /legal/* is noindex by spec (docs/04) and nothing else is excluded.
|
// NOINDEX PAGES ARE EXCLUDED, and the list is now three shapes rather
|
||||||
|
// than one. `/legal/*` is noindex by spec (docs/04). The two added at
|
||||||
|
// build step 8 are the intake form's POST-redirect-GET landing pages:
|
||||||
|
// both are transactional, neither has standalone value, and a search
|
||||||
|
// result reading "your inquiry has been received" for someone who has not
|
||||||
|
// sent one is worse than no result at all.
|
||||||
|
//
|
||||||
|
// ⚠️ `/insights/` IS DELIBERATELY NOT HERE even though it emits
|
||||||
|
// `noindex` while no article is published. This filter cannot see
|
||||||
|
// collection data — it runs from build config, with no access to
|
||||||
|
// `getCollection` — so the exclusion could only be a guess at the
|
||||||
|
// collection's state, and it would then be wrong in the direction that
|
||||||
|
// matters the moment an article publishes. The page derives its own
|
||||||
|
// `noindex` from the collection on every build, so the mismatch is
|
||||||
|
// temporary, self-clearing, and reported accurately by Search Console as
|
||||||
|
// excluded-by-noindex. Recorded rather than fixed with a frontmatter
|
||||||
|
// parser in build config.
|
||||||
|
//
|
||||||
// The `/type-scale/` half of this condition is gone with the page it
|
// The `/type-scale/` half of this condition is gone with the page it
|
||||||
// named: the step-1 proof sheet was deleted at step 2, as its own comment
|
// named: the step-1 proof sheet was deleted at step 2, as its own comment
|
||||||
// and InfinityMark's both said it would be. Recoverable from git if the
|
// and InfinityMark's both said it would be. Recoverable from git if the
|
||||||
// specimen is ever wanted again; it is not a route the site ships.
|
// specimen is ever wanted again; it is not a route the site ships.
|
||||||
filter: (page) => !page.includes('/legal/'),
|
filter: (page) =>
|
||||||
|
!page.includes('/legal/') &&
|
||||||
|
!page.includes('/contact/received/') &&
|
||||||
|
!page.includes('/contact/could-not-send/') &&
|
||||||
|
// `/bio/` is a condensed duplicate of `/about/` and `/fees/`, and it
|
||||||
|
// exists to be rendered to a PDF (R16). Two URLs competing on the same
|
||||||
|
// content is the thing `docs/04` is most concerned with, so it is
|
||||||
|
// `noindex` and out of the sitemap.
|
||||||
|
!page.includes('/bio/'),
|
||||||
changefreq: 'monthly',
|
changefreq: 'monthly',
|
||||||
// No `lastmod`. It was `new Date()`, which stamped every URL with the
|
// No `lastmod`. It was `new Date()`, which stamped every URL with the
|
||||||
// build time — telling crawlers all 17 pages changed whenever one did.
|
// build time — telling crawlers all 17 pages changed whenever one did.
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* The intake handler's OWN field table. Spec: docs/05-backend-spec.md §Form fields.
|
||||||
|
*
|
||||||
|
* ⚠️ THIS IS A SECOND, INDEPENDENT COPY OF THE FORM'S FIELD LIST, AND THE
|
||||||
|
* DUPLICATION IS ARCHITECTURAL RATHER THAN AN OVERSIGHT.
|
||||||
|
*
|
||||||
|
* docs/05: "Client-side validation is a convenience. **The Lambda re-validates
|
||||||
|
* everything.**" A server that validates against a list the client shipped it is
|
||||||
|
* not validating — it is asking the caller what the rules are. And this file is
|
||||||
|
* deployed inside the Lambda zip, which cannot import from `src/` at all.
|
||||||
|
*
|
||||||
|
* WHAT KEEPS THE TWO HONEST IS A CHECK, NOT A SHARED IMPORT.
|
||||||
|
* `npm run check:intake` imports this module and `src/data/intake.ts` and
|
||||||
|
* asserts they agree on every field name, on which are required, on every length
|
||||||
|
* cap, and on every closed option set. A disagreement means either the form
|
||||||
|
* offers something the handler rejects — a lost inquiry that looks like a
|
||||||
|
* browser bug — or the handler accepts something no form ever shows.
|
||||||
|
*
|
||||||
|
* It lives in its own file rather than inside `handler.mjs` so the check can
|
||||||
|
* import it. `handler.mjs` calls `requireEnv()` at module scope and throws
|
||||||
|
* without a configured environment, so importing THAT would mean inventing
|
||||||
|
* fixture credentials to run a check that has nothing to do with them.
|
||||||
|
* (The first version of the check scraped this table out of the handler as text
|
||||||
|
* and evaluated it. Its "refuse anything executable" guard then rejected the
|
||||||
|
* table on the word `process` — which is a FIELD NAME. A guard that fires on the
|
||||||
|
* data it exists to protect is worse than no guard, and the fix was to stop
|
||||||
|
* scraping.)
|
||||||
|
*
|
||||||
|
* `select` and `radio` fields carry their option list, and a value outside it is
|
||||||
|
* REJECTED rather than coerced — a select is a closed set, and a request that
|
||||||
|
* sends something else is not a browser.
|
||||||
|
*
|
||||||
|
* ⚠️ **`label` IS HERE BECAUSE THE CONFIRMATION EMAIL PRINTED FIELD NAMES.**
|
||||||
|
* `summaryLines` was `${f.name}: ${value}`, so the inquirer's receipt read
|
||||||
|
* `practiceArea: Construction`, `otherParties: …`, `opposingCounsel: …`. That
|
||||||
|
* email is the one artefact an inquirer keeps from this practice, and it is also
|
||||||
|
* the artefact that quotes third-party names back at them, so its legibility is
|
||||||
|
* not cosmetic. Found by `adversarial-reviewer`, 2026-08-31.
|
||||||
|
* `npm run check:intake` compares labels as well as names, requiredness, caps
|
||||||
|
* and option sets — so the receipt cannot drift from the form's own wording.
|
||||||
|
*/
|
||||||
|
export const FIELDS = [
|
||||||
|
{ name: 'name', label: 'Your name', required: true, max: 120 },
|
||||||
|
{ name: 'email', label: 'Email', required: true, max: 254 },
|
||||||
|
{ name: 'phone', label: 'Phone', required: false, max: 40 },
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
label: 'Your role',
|
||||||
|
required: true,
|
||||||
|
options: ['Counsel', 'In-house', 'Party', 'Institution', 'Other'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'organisation',
|
||||||
|
label: 'Firm or organisation',
|
||||||
|
required: false,
|
||||||
|
max: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'process',
|
||||||
|
label: 'Process sought',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
'Mediation',
|
||||||
|
'Arbitration',
|
||||||
|
'Med-Arb',
|
||||||
|
'Early neutral evaluation',
|
||||||
|
'Not sure',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'practiceArea',
|
||||||
|
label: 'Subject matter',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
'Construction',
|
||||||
|
'Technology',
|
||||||
|
'Energy',
|
||||||
|
'Insurance',
|
||||||
|
'Shareholder',
|
||||||
|
'Cross-border',
|
||||||
|
'Other',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'otherParties', label: 'Other parties', required: false, max: 300 },
|
||||||
|
{
|
||||||
|
name: 'opposingCounsel',
|
||||||
|
label: 'Opposing counsel',
|
||||||
|
required: false,
|
||||||
|
max: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'summary',
|
||||||
|
label: 'What the dispute is about',
|
||||||
|
required: true,
|
||||||
|
max: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timing',
|
||||||
|
label: 'Timing',
|
||||||
|
required: false,
|
||||||
|
options: ['Urgent', 'Within 30 days', 'Within 90 days', 'Exploring'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preferredContact',
|
||||||
|
label: 'Preferred reply',
|
||||||
|
required: false,
|
||||||
|
options: ['Email', 'Phone'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The honeypot field name. NOT in `FIELDS`, and that is load-bearing: it is
|
||||||
|
* checked before validation and a non-empty value gets the SUCCESS page, not a
|
||||||
|
* rejection. Telling a bot it was detected is how the next version of the bot
|
||||||
|
* stops filling the field. `check:intake` asserts it is absent from `FIELDS`.
|
||||||
|
*/
|
||||||
|
export const HONEYPOT = 'company_website';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SECOND honeypot — a decoy checkbox that must arrive ABSENT. Also not in
|
||||||
|
* `FIELDS`, for the same reason, and `check:intake` asserts that too.
|
||||||
|
*
|
||||||
|
* ⚠️ **DIFFERENT TRAP, NOT A SECOND COPY.** `HONEYPOT` catches a bot that fills
|
||||||
|
* every text input; this catches one that sets every control it enumerates.
|
||||||
|
*
|
||||||
|
* ⚠️ **IT IS PROBABLY INERT AGAINST THE 2026-09-04 PAIR, AND THE COMMENT HERE
|
||||||
|
* SAID THE OPPOSITE FOR ONE ROUND.** They left `HONEYPOT` empty, so they skip
|
||||||
|
* hidden fields — and a bot that skips a hidden text input skips a hidden
|
||||||
|
* checkbox. `src/data/intake.ts` carries the full argument; this is defence in
|
||||||
|
* depth against a different class, not a counter to the observed one.
|
||||||
|
*
|
||||||
|
* ⚠️ **UNCHECKED SENDS NOTHING, so absence is the pass — and so is an empty
|
||||||
|
* value, because the handler tests for a non-empty one rather than for mere
|
||||||
|
* presence.** See
|
||||||
|
* `src/data/intake.ts` for the full reasoning; the two files state it separately
|
||||||
|
* because they are separately deployed and `check:intake` is what keeps the
|
||||||
|
* NAMES in step, not the comments.
|
||||||
|
*/
|
||||||
|
export const DECOY_CHECKBOX = 'updates_optin';
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
/**
|
||||||
|
* The intake handler. Spec: docs/05-backend-spec.md. Resource names, region,
|
||||||
|
* table and SES state: AGENTS.md §7 — this file reads them from the environment
|
||||||
|
* and does not restate them.
|
||||||
|
*
|
||||||
|
* ⚠️ THIS IS LIVE. Deployed at cutover on 2026-09-02 by `docs/09` Part 5, and
|
||||||
|
* `/api/intake` answers 303 to the Part 7.1 probe. It REPLACED a hand-built
|
||||||
|
* `adr-intake-handler` that predates this repo. **This banner read "THIS IS NOT
|
||||||
|
* DEPLOYED" until 2026-09-04**, which is the most dangerous thing a comment on
|
||||||
|
* this file can say: an edit made in that belief ships to a form real inquirers
|
||||||
|
* are using. Changes here reach production on the next `docs/09` Part 5 run.
|
||||||
|
*
|
||||||
|
* ⚠️ AND A BARE `POST /api/intake` RETURNS 403 BY DESIGN — the Origin check
|
||||||
|
* below. `docs/09` §7.1 is the only valid route probe; a 403 without that header
|
||||||
|
* is not evidence about the route. It has been misread as one twice.
|
||||||
|
*
|
||||||
|
* ── THE SHAPE, AND WHY IT IS POST-REDIRECT-GET ─────────────────────────────
|
||||||
|
*
|
||||||
|
* The site ships ZERO JavaScript (AGENTS.md §7, and it is not "minimal" — none).
|
||||||
|
* So the form is a plain HTML POST, and this handler answers with **303 See
|
||||||
|
* Other** and a `Location` on the site. That gives, with no script anywhere:
|
||||||
|
*
|
||||||
|
* - a working form with JavaScript disabled, which is the failure this whole
|
||||||
|
* project exists to fix;
|
||||||
|
* - no JSON response rendered as a raw page, which is what a plain POST to an
|
||||||
|
* API Gateway JSON endpoint shows the user;
|
||||||
|
* - no double submission on refresh, because the browser lands on a GET.
|
||||||
|
*
|
||||||
|
* docs/05's definition of done asks that the form "degrades to a mailto:
|
||||||
|
* fallback with JavaScript disabled". It does not need to: there is nothing to
|
||||||
|
* degrade FROM, because the form never used script. The email address is
|
||||||
|
* published on /contact/ regardless.
|
||||||
|
*
|
||||||
|
* ── WHAT THIS DELIBERATELY DOES NOT IMPLEMENT ──────────────────────────────
|
||||||
|
*
|
||||||
|
* ⚠️ **RE-ASKED 2026-09-04 AND STILL NOT IMPLEMENTABLE HERE.** Pouya ruled
|
||||||
|
* *"raise the timing floor"* after the first real spam. There is no floor to
|
||||||
|
* raise — the check has never existed — and the reason below is unchanged by
|
||||||
|
* the spam arriving: it is a property of a CDN-cached static page, not of how
|
||||||
|
* hard anyone has tried. What CAN carry a per-visitor clock is named in
|
||||||
|
* `docs/05` §Observed abuse and it is outside "handler + form only". §9 Q66.
|
||||||
|
*
|
||||||
|
* **THE 3-SECOND TIMESTAMP CHECK IS NOT IMPLEMENTED, AND THAT IS A DECISION.**
|
||||||
|
* docs/05 asks to "reject submissions completed in under 3 seconds". It cannot
|
||||||
|
* be done here and implementing it would produce a control that does nothing:
|
||||||
|
* the check needs to know when the form was SERVED to that visitor, and
|
||||||
|
* /contact/ is a static file cached at the CloudFront edge. A build-time
|
||||||
|
* timestamp is the same value for every visitor and is hours or days old, so
|
||||||
|
* `now - served` is always large — the check would pass for a bot exactly as it
|
||||||
|
* passes for a human. A per-visitor token needs either a dynamic origin or
|
||||||
|
* client-side script, and the site has neither by design.
|
||||||
|
*
|
||||||
|
* That is worse than omitting it: AGENTS.md Q22 and the Lighthouse row are both
|
||||||
|
* records of what a control that exists on paper and not in fact costs here. So
|
||||||
|
* it is omitted, said out loud, and the load is carried by the TWO honeypots,
|
||||||
|
* the Origin check, the aggregate API Gateway route throttle and the validation
|
||||||
|
* below — plus, since 2026-09-04, a score that LABELS and never rejects.
|
||||||
|
* (Aggregate, not per-IP — see above; the earlier wording here said "rate
|
||||||
|
* limit" and let the reader supply the stronger meaning.)
|
||||||
|
*
|
||||||
|
* ── WHAT MUST BE CONFIGURED OUTSIDE THIS FILE ──────────────────────────────
|
||||||
|
*
|
||||||
|
* - An AGGREGATE API Gateway route throttle. NOT per source IP: API Gateway
|
||||||
|
* throttling is per route and per stage across all callers, so docs/05's
|
||||||
|
* "5 requests / 5 minutes per source IP" is struck — per-IP needs AWS WAF.
|
||||||
|
* Never describe what ships as per-IP. docs/09 Part 6.3.
|
||||||
|
* - CloudFront behaviour: /api/* → the HTTP API origin §7 records.
|
||||||
|
* - CloudWatch alarms on Lambda `Errors` and on API Gateway 5xx for this
|
||||||
|
* route. NOT a dead-letter queue: `DeadLetterConfig` is used only for
|
||||||
|
* ASYNCHRONOUS invocations, API Gateway invokes synchronously, so a DLQ here
|
||||||
|
* would sit at depth 0 for ever and an alarm on it would be a permanently
|
||||||
|
* green light. docs/05 §Notification carries the replacement.
|
||||||
|
* - The `ses-alerts` SNS email subscription is CONFIRMED (§7) — R9 closed
|
||||||
|
* 2026-09-01, so the bounce and complaint alarms reach someone.
|
||||||
|
*/
|
||||||
|
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
|
||||||
|
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
/* The field table and BOTH honeypot names live in their own module so that
|
||||||
|
`npm run check:intake` can import them without this file's module-scope
|
||||||
|
`requireEnv()` calls running. See fields.mjs for why there are two tables. */
|
||||||
|
import { DECOY_CHECKBOX, FIELDS, HONEYPOT } from './fields.mjs';
|
||||||
|
/* Scoring lives in its own module so it can be unit-tested — this file throws at
|
||||||
|
import without a configured environment, so it cannot be. `node
|
||||||
|
backend/intake/spam-score.test.mjs`. ⚠️ IT IS A THIRD FILE IN THE ZIP:
|
||||||
|
`docs/09` Part 5.1 packages it explicitly, and a cold start would fail with
|
||||||
|
ERR_MODULE_NOT_FOUND if it were left out. */
|
||||||
|
import {
|
||||||
|
isPossibleSpam,
|
||||||
|
scoreSubmission,
|
||||||
|
SPAM_THRESHOLD,
|
||||||
|
} from './spam-score.mjs';
|
||||||
|
|
||||||
|
/* Region comes from the Lambda runtime, which sets AWS_REGION to the function's
|
||||||
|
own region — the one §7 records. Not hardcoded: a second copy of a fact §7
|
||||||
|
owns is the copy that goes stale. */
|
||||||
|
const ddb = new DynamoDBClient({});
|
||||||
|
const ses = new SESv2Client({});
|
||||||
|
|
||||||
|
const TABLE = requireEnv('INTAKE_TABLE');
|
||||||
|
const SITE_ORIGIN = requireEnv('SITE_ORIGIN');
|
||||||
|
const NOTIFY_TO = requireEnv('NOTIFY_TO');
|
||||||
|
const MAIL_FROM = requireEnv('MAIL_FROM');
|
||||||
|
|
||||||
|
/** 24 months, docs/05 §Retention. It must match /legal/privacy/ exactly.
|
||||||
|
* ⚠️ Writing this attribute is NOT the mechanism — TTL must be enabled on the
|
||||||
|
* table, and AGENTS.md §7 records whether it is. */
|
||||||
|
const RETENTION_MONTHS = 24;
|
||||||
|
|
||||||
|
/** The public commitment, §4 and Q27. It must read identically here, on
|
||||||
|
* /contact/, and in any bio. Injected rather than typed so one edit moves all
|
||||||
|
* three; the deploy step sets it from `CONTACT.responseTime`. */
|
||||||
|
const RESPONSE_TIME = requireEnv('RESPONSE_TIME');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ INJECTED FOR EXACTLY THE REASON ABOVE, AND IT WAS HAND-TYPED UNTIL
|
||||||
|
* 2026-08-31. The confirmation email spelled the no-retainer notice out in
|
||||||
|
* prose, which made it a **fourth** hand-copy of `NO_RETAINER_NOTICE` — and the
|
||||||
|
* copy **dropped the fourth clause the constant carries**, *"and does not itself
|
||||||
|
* create a conflict check"*, which `docs/01` §`/contact/` requires. It also used
|
||||||
|
* a hyphen where the constant uses an en dash.
|
||||||
|
*
|
||||||
|
* The reasoning three lines above applied to it identically and was not applied.
|
||||||
|
* `npm run check:intake` compares field tables only, so a future softening of
|
||||||
|
* the constant would never have reached this email and nothing would have
|
||||||
|
* failed — the silent-drift shape §4 flags for the whole commitment class.
|
||||||
|
* Found by `adversarial-reviewer`. The deploy step sets it from
|
||||||
|
* `NO_RETAINER_NOTICE` in `src/data/site.ts`.
|
||||||
|
*
|
||||||
|
* ⚠️ AND THAT SENTENCE USED TO END "`docs/06` names it", WHICH IT DID NOT.
|
||||||
|
* This variable became a `requireEnv` and reached no document — so the
|
||||||
|
* deployment list said five variables while this file required six, and the
|
||||||
|
* function would have thrown at cold start on every invocation. **The comment
|
||||||
|
* asserting the documentation existed is what made it invisible.** `docs/06` and
|
||||||
|
* `docs/05` now name all six. Found by `adversarial-reviewer` round 2.
|
||||||
|
*/
|
||||||
|
const NO_RETAINER_NOTICE = requireEnv('NO_RETAINER_NOTICE');
|
||||||
|
|
||||||
|
function requireEnv(name) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value) {
|
||||||
|
// Fail at cold start, not per request: a function missing configuration
|
||||||
|
// should not accept a submission it cannot store.
|
||||||
|
throw new Error(`intake handler: ${name} is not set`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip HTML before storage and before anything enters an email body (docs/05).
|
||||||
|
*
|
||||||
|
* NOT AN HTML SANITISER, AND IT DOES NOT NEED TO BE — every field is stored and
|
||||||
|
* rendered as PLAIN TEXT, never as markup, so the job is to make a value
|
||||||
|
* incapable of becoming markup later, not to allow safe markup now.
|
||||||
|
*
|
||||||
|
* ⚠️ AND FOR THAT REASON IT STRIPS ANGLE BRACKETS RATHER THAN ENTITY-ENCODING
|
||||||
|
* THEM. The first version of this function escaped `&` to `&`, which is
|
||||||
|
* correct only when the sink is HTML: both sinks here are plain text, so the
|
||||||
|
* reader of the confirmation email would have received the five literal
|
||||||
|
* characters `&` wherever they had typed an ampersand. Encoding for the
|
||||||
|
* wrong sink is a defect wearing the costume of a protection.
|
||||||
|
*/
|
||||||
|
function toPlainText(value) {
|
||||||
|
return (
|
||||||
|
value
|
||||||
|
// Control characters, including the CR/LF that would let a value forge a
|
||||||
|
// header line in an email, and the C1 range.
|
||||||
|
//
|
||||||
|
// `no-control-regex` is disabled ON PURPOSE and with the reason: that rule
|
||||||
|
// exists to catch a control character that reached a pattern by ACCIDENT,
|
||||||
|
// usually a mis-escaped literal. Here the control range IS the thing being
|
||||||
|
// matched, and it is the one part of this function that stops a submitted
|
||||||
|
// value from forging an email header. Rewriting it as a charCodeAt filter
|
||||||
|
// to satisfy the linter would make the intent less legible, not more.
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
|
||||||
|
// Angle brackets removed rather than entity-encoded. The destination is
|
||||||
|
// plain text — a DynamoDB string attribute and a text/plain email body —
|
||||||
|
// so `&` would REACH THE READER as the five characters "&", which
|
||||||
|
// is a defect rather than a protection. Encoding is right when the sink is
|
||||||
|
// HTML; here the requirement is only that the value can never become
|
||||||
|
// markup if it is later put into one, and no `<` satisfies that
|
||||||
|
// permanently. Nothing else in the value is altered.
|
||||||
|
.replace(/[<>]/g, '')
|
||||||
|
.replace(/[ \t]{2,}/g, ' ')
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email validation, server side. Deliberately structural rather than clever:
|
||||||
|
* one @, something either side, a dot in the domain, no whitespace, no angle
|
||||||
|
* brackets, within the RFC 5321 length. A regex that tries to implement RFC 5322
|
||||||
|
* rejects real addresses, and the confirmation email in D18 is the real check —
|
||||||
|
* if it does not arrive, the address was wrong whatever a regex said.
|
||||||
|
*/
|
||||||
|
function looksLikeEmail(value) {
|
||||||
|
return (
|
||||||
|
value.length <= 254 &&
|
||||||
|
/^[^\s@<>]+@[^\s@<>.]+(\.[^\s@<>.]+)+$/.test(value) &&
|
||||||
|
!value.includes('..')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBody(event) {
|
||||||
|
const raw = event.isBase64Encoded
|
||||||
|
? Buffer.from(event.body ?? '', 'base64').toString('utf8')
|
||||||
|
: (event.body ?? '');
|
||||||
|
const type = headerOf(event, 'content-type') ?? '';
|
||||||
|
if (type.includes('application/x-www-form-urlencoded')) {
|
||||||
|
return Object.fromEntries(new URLSearchParams(raw));
|
||||||
|
}
|
||||||
|
// JSON is accepted so the endpoint stays testable with curl, and because a
|
||||||
|
// future island could post JSON without changing this handler.
|
||||||
|
if (type.includes('application/json')) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (
|
||||||
|
parsed === null ||
|
||||||
|
typeof parsed !== 'object' ||
|
||||||
|
Array.isArray(parsed)
|
||||||
|
) {
|
||||||
|
throw new Error('body is not an object');
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
throw new Error(`unsupported content-type: ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ THE UNFORGEABLE VALUE, AND NOT THE USEFUL ONE. `requestContext.http
|
||||||
|
* .sourceIp` is the TCP peer, which behind the CloudFront behaviour that routes
|
||||||
|
* /api/* is a CloudFront EDGE — so this records AWS rather than the inquirer.
|
||||||
|
*
|
||||||
|
* IT READ `x-forwarded-for` FOR ONE REVISION AND THAT WAS WORSE. CloudFront
|
||||||
|
* APPENDS the viewer address to a client-supplied XFF rather than replacing it,
|
||||||
|
* so the leftmost entry is whatever the client sent: a submission with
|
||||||
|
* `X-Forwarded-For: 8.8.8.8` stored `8.8.8.8`. That turns a field held for abuse
|
||||||
|
* investigation into one that can be made to name an uninvolved third party, and
|
||||||
|
* /legal/privacy/ promises the record holds "your IP address". A forgeable value
|
||||||
|
* presented as an identification is worse than an honest useless one.
|
||||||
|
*
|
||||||
|
* The right value is CloudFront's own `CloudFront-Viewer-Address`, which
|
||||||
|
* CloudFront generates and overwrites. Reaching it needs a CUSTOM origin request
|
||||||
|
* policy on the /api/* behaviour — the managed AllViewerAndCloudFrontHeaders
|
||||||
|
* forwards Host, which 403s every request at API Gateway.
|
||||||
|
*
|
||||||
|
* ⚠️ THAT POLICY IS NOW WRITTEN — `infra/cloudfront/configure.mjs` section 5,
|
||||||
|
* Pouya's ruling of 2026-09-04 — SO THE HEADER MAY ARRIVE. THIS FUNCTION STILL
|
||||||
|
* DOES NOT READ IT, AND THAT IS THE RULING, NOT AN OMISSION: *measured, not yet
|
||||||
|
* acted on*. What the record holds is published field by field on
|
||||||
|
* /legal/privacy/, so storing a different address is a DISCLOSURE change
|
||||||
|
* governed by `docs/09` §7.2's decision table — an infrastructure change
|
||||||
|
* forwards a header; only a privacy-policy change may store one. Do not "fix"
|
||||||
|
* this from any header without that measurement and that edit.
|
||||||
|
*/
|
||||||
|
function viewerIp(event) {
|
||||||
|
return event.requestContext?.http?.sourceIp ?? 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function headerOf(event, name) {
|
||||||
|
const headers = event.headers ?? {};
|
||||||
|
// API Gateway HTTP API lowercases header keys; a direct invoke or a test
|
||||||
|
// harness may not, so this does not assume it.
|
||||||
|
const hit = Object.keys(headers).find((k) => k.toLowerCase() === name);
|
||||||
|
return hit ? headers[hit] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirect = (path) => ({
|
||||||
|
statusCode: 303,
|
||||||
|
headers: {
|
||||||
|
Location: `${SITE_ORIGIN}${path}`,
|
||||||
|
// A redirect that a CDN or a browser caches would send the next visitor
|
||||||
|
// straight to the confirmation page without submitting anything.
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
},
|
||||||
|
body: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const SUCCESS = '/contact/received/';
|
||||||
|
const FAILURE = '/contact/could-not-send/';
|
||||||
|
|
||||||
|
export async function handler(event) {
|
||||||
|
/**
|
||||||
|
* ORIGIN CHECK, AND IT IS THE CONTROL CORS IS USUALLY MISTAKEN FOR. A form
|
||||||
|
* POST is a top-level navigation: it is exempt from CORS preflight, so an
|
||||||
|
* `Access-Control-Allow-Origin` setting on the endpoint does not stop another
|
||||||
|
* site from posting a form here. Checking the header does.
|
||||||
|
*
|
||||||
|
* Firefox omits `Origin` on some same-origin form navigations, so `Referer` is
|
||||||
|
* accepted as a fallback — both must MATCH the site origin when present, and
|
||||||
|
* a request with neither is refused.
|
||||||
|
*/
|
||||||
|
const origin = headerOf(event, 'origin');
|
||||||
|
const referer = headerOf(event, 'referer');
|
||||||
|
const originOk = origin
|
||||||
|
? origin === SITE_ORIGIN
|
||||||
|
: referer
|
||||||
|
? referer.startsWith(`${SITE_ORIGIN}/`)
|
||||||
|
: false;
|
||||||
|
if (!originOk) {
|
||||||
|
return {
|
||||||
|
statusCode: 403,
|
||||||
|
headers: { 'Cache-Control': 'no-store' },
|
||||||
|
body: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = parseBody(event);
|
||||||
|
} catch {
|
||||||
|
return redirect(FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE HONEYPOT GETS THE SUCCESS PAGE, NOT AN ERROR. Telling a bot it was
|
||||||
|
* detected is how the next version of the bot stops filling the field. A
|
||||||
|
* human cannot reach this field — it is `display: none`, `tabindex="-1"` and
|
||||||
|
* `aria-hidden` — so a non-empty value is not a mistake anyone made.
|
||||||
|
*/
|
||||||
|
/* ⚠️ COERCED, NOT TYPE-CHECKED. `parseBody` accepts JSON, so a value can
|
||||||
|
arrive as `true` or `1` rather than a string — and `typeof === 'string'`
|
||||||
|
let exactly that through both traps for one round. `String(v).trim()`
|
||||||
|
catches every non-empty shape and still treats absence as a pass. */
|
||||||
|
if (body[HONEYPOT] !== undefined && String(body[HONEYPOT]).trim() !== '') {
|
||||||
|
/* LOGGED, BECAUSE THIS IS ONE OF ONLY TWO PATHS THAT DISCARD A SUBMISSION
|
||||||
|
AND ANSWER WITH THE SUCCESS PAGE. Unlogged, a honeypot that starts firing
|
||||||
|
on real visitors — a stylesheet that 404s, an autofiller, a template edit
|
||||||
|
that unhides the wrapper — is indistinguishable from quiet weeks, and the
|
||||||
|
only signal is inquiries that were never mentioned again. The FIELD NAME
|
||||||
|
only: the value is whatever a bot chose and nothing about the submission
|
||||||
|
is kept, which is what makes this safe to log at all. */
|
||||||
|
console.warn('intake: discarded by honeypot', { field: HONEYPOT });
|
||||||
|
return redirect(SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE SECOND HONEYPOT, AND IT TRAPS A DIFFERENT BEHAVIOUR. A checkbox no
|
||||||
|
* person can see; an unchecked box sends nothing at all, so a VALUE arrives
|
||||||
|
* only because something ticked it. The value itself is not compared —
|
||||||
|
* `=1`, `=yes` and `=on` are all a tick — only that there is one.
|
||||||
|
*
|
||||||
|
* Same silent SUCCESS as above, and for the same reason.
|
||||||
|
*
|
||||||
|
* ⚠️ ABSENCE IS THE PASS, AND SO IS AN EMPTY VALUE. Both directions matter and
|
||||||
|
* they fail differently:
|
||||||
|
*
|
||||||
|
* - Requiring the field to ARRIVE would turn every dropped-field path — an
|
||||||
|
* extension, a proxy, a template edit — into a lost inquiry reported as
|
||||||
|
* sent.
|
||||||
|
* - Trapping on mere PRESENCE (`!== undefined`) would catch a form
|
||||||
|
* serialiser that emits `updates_optin=` for a hidden checkbox without
|
||||||
|
* reading its checked state. That is rare and it is not impossible, and
|
||||||
|
* the cost of being wrong is a real legal inquiry discarded in silence.
|
||||||
|
*
|
||||||
|
* So the test is the same shape as the honeypot above — a non-empty value —
|
||||||
|
* while the BEHAVIOUR it catches is the opposite one. That is the distinction
|
||||||
|
* that matters: filling text fields versus ticking boxes, not `undefined`
|
||||||
|
* versus `''`.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
body[DECOY_CHECKBOX] !== undefined &&
|
||||||
|
String(body[DECOY_CHECKBOX]).trim() !== ''
|
||||||
|
) {
|
||||||
|
console.warn('intake: discarded by honeypot', { field: DECOY_CHECKBOX });
|
||||||
|
return redirect(SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clean = {};
|
||||||
|
const errors = [];
|
||||||
|
for (const field of FIELDS) {
|
||||||
|
const rawValue = body[field.name];
|
||||||
|
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
||||||
|
if (value === '') {
|
||||||
|
if (field.required) errors.push(`${field.name} is required`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// REJECT over the cap rather than truncating (docs/05). A silently
|
||||||
|
// truncated matter summary is a file read wrongly.
|
||||||
|
if (field.max && value.length > field.max) {
|
||||||
|
errors.push(`${field.name} exceeds ${field.max} characters`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (field.options && !field.options.includes(value)) {
|
||||||
|
errors.push(`${field.name} is not one of the offered values`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (field.name === 'email' && !looksLikeEmail(value)) {
|
||||||
|
errors.push('email is not a well-formed address');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
clean[field.name] = toPlainText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit, unchecked by default, and required (docs/05). An unchecked box
|
||||||
|
// sends no value at all, so absence is the failure case.
|
||||||
|
if (body.consent !== 'on' && body.consent !== 'true') {
|
||||||
|
errors.push('consent was not given');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
// Logged for the operator, never returned to the caller: an error list is a
|
||||||
|
// description of the validation rules, which is a gift to whoever is
|
||||||
|
// probing them.
|
||||||
|
console.warn('intake rejected', { errors });
|
||||||
|
return redirect(FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const id = randomUUID();
|
||||||
|
/* NO `|| 0` FALLBACK (removed 2026-08-31): DynamoDB will not expire an item
|
||||||
|
whose TTL is more than five years past, so `ttl: 0` means RETAINED FOREVER
|
||||||
|
while /legal/privacy/ promises deletion. Let a bad value fail the write. */
|
||||||
|
const ttl = Math.floor(
|
||||||
|
Date.UTC(
|
||||||
|
now.getUTCFullYear(),
|
||||||
|
now.getUTCMonth() + RETENTION_MONTHS,
|
||||||
|
now.getUTCDate(),
|
||||||
|
now.getUTCHours(),
|
||||||
|
now.getUTCMinutes(),
|
||||||
|
now.getUTCSeconds(),
|
||||||
|
) / 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DYNAMODB FIRST, THEN MAIL — docs/05: "SES failure must never lose the
|
||||||
|
* submission." The order is the whole guarantee. If SES fails after this
|
||||||
|
* write, the record exists and a resend has something to resend; if the write
|
||||||
|
* fails, nothing was accepted and the inquirer is told so. (This said "the DLQ
|
||||||
|
* replay" — there is no DLQ and there cannot usefully be one on a
|
||||||
|
* synchronously invoked function; see the note at the top of this file.)
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
await ddb.send(
|
||||||
|
new PutItemCommand({
|
||||||
|
TableName: TABLE,
|
||||||
|
Item: {
|
||||||
|
/* ⚠️ `submissionId` IS THE TABLE'S PARTITION KEY AND THERE IS NO SORT
|
||||||
|
KEY. A DynamoDB key schema cannot be altered after creation, so this
|
||||||
|
attribute name is fixed by the table `AGENTS.md` §7 names, not
|
||||||
|
chosen here — and an item missing it fails the whole write with
|
||||||
|
`ValidationException`, which this function converts into the failure
|
||||||
|
page. Verify against `describe-table` before changing either name;
|
||||||
|
`submittedAt` is an ordinary attribute and is free. */
|
||||||
|
submissionId: { S: id },
|
||||||
|
submittedAt: { S: now.toISOString() },
|
||||||
|
ttl: { N: String(ttl) },
|
||||||
|
// Abuse investigation only (docs/05). Named so a later reader does not
|
||||||
|
// repurpose them: they are not analytics and not part of the reply.
|
||||||
|
/* Behind CloudFront this is the EDGE address, not the inquirer's.
|
||||||
|
See `viewerIp()` — and read it before changing this. */
|
||||||
|
sourceIp: { S: viewerIp(event) },
|
||||||
|
userAgent: {
|
||||||
|
S: (headerOf(event, 'user-agent') ?? 'unknown').slice(0, 400),
|
||||||
|
},
|
||||||
|
consentAt: { S: now.toISOString() },
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(clean).map(([k, v]) => [k, { S: v }]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('intake: DynamoDB write failed', error);
|
||||||
|
return redirect(FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `f.label`, not `f.name` — see the note on `label` in fields.mjs. The
|
||||||
|
// notification to the operator gets the same rendering: one shape, so the two
|
||||||
|
// messages cannot describe the same submission differently.
|
||||||
|
const summaryLines = FIELDS.filter((f) => clean[f.name] !== undefined)
|
||||||
|
.map((f) => `${f.label}: ${clean[f.name]}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SCORING, AND IT LABELS RATHER THAN REJECTS — Pouya, 2026-09-04.
|
||||||
|
*
|
||||||
|
* ⚠️ THIS RUNS AFTER THE RECORD IS STORED, WHICH IS NOT AN ACCIDENT OF
|
||||||
|
* ORDERING. Nothing below can decline a submission: by the time it runs, the
|
||||||
|
* write has already succeeded and the only remaining question is what the
|
||||||
|
* OPERATOR's subject line says. There is deliberately no branch here that can
|
||||||
|
* reach `redirect(FAILURE)`.
|
||||||
|
*
|
||||||
|
* ⚠️ AND IT TOUCHES THE NOTIFICATION ONLY. The confirmation below is
|
||||||
|
* unchanged. A real inquirer wrongly scored must never be told that a machine
|
||||||
|
* thought they were a bot.
|
||||||
|
*/
|
||||||
|
/* ⚠️ WRAPPED, AND THE GUARD IS THE RULING RATHER THAN CAUTION. An exception
|
||||||
|
here would escape `handler`, API Gateway would answer 500, and the inquirer
|
||||||
|
would see a failure for a submission ALREADY WRITTEN to the table — a path
|
||||||
|
that costs an inquiry, decided by a labelling function. Pouya's constraint
|
||||||
|
is that nothing but a honeypot may cost one, so the scorer is allowed to
|
||||||
|
fail and the submission is not. Unlabelled is the safe default. */
|
||||||
|
let spam = { score: 0, signals: [] };
|
||||||
|
try {
|
||||||
|
spam = scoreSubmission(clean);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('intake: spam scoring failed; sending unlabelled', {
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const flagged = isPossibleSpam(spam);
|
||||||
|
const notificationBody = [
|
||||||
|
`Received ${now.toISOString()}`,
|
||||||
|
`submissionId ${id}`,
|
||||||
|
...(flagged
|
||||||
|
? [
|
||||||
|
'',
|
||||||
|
`Possible spam. Score ${spam.score} of threshold ${SPAM_THRESHOLD}. ` +
|
||||||
|
`Signals: ${spam.signals.join('; ')}.`,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
'',
|
||||||
|
summaryLines,
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TWO EMAILS — D18, and the second one is why the form beats a mailto: link.
|
||||||
|
* `Promise.allSettled`, not `Promise.all`: the record is already stored, so a
|
||||||
|
* failure on either message must be logged rather than lost, and one failing
|
||||||
|
* must not prevent the other from being attempted.
|
||||||
|
*/
|
||||||
|
const results = await Promise.allSettled([
|
||||||
|
ses.send(
|
||||||
|
new SendEmailCommand({
|
||||||
|
FromEmailAddress: MAIL_FROM,
|
||||||
|
Destination: { ToAddresses: [NOTIFY_TO] },
|
||||||
|
// Replyable to the inquirer (docs/05), which is what makes the
|
||||||
|
// notification usable without copying an address out of it.
|
||||||
|
ReplyToAddresses: [clean.email],
|
||||||
|
Content: {
|
||||||
|
Simple: {
|
||||||
|
/* The prefix is what Pouya filters on in Gmail, so it is the
|
||||||
|
FIRST thing in the subject and it is a fixed string. Do not make
|
||||||
|
it conditional on anything else, and do not vary its wording. */
|
||||||
|
Subject: {
|
||||||
|
Data:
|
||||||
|
`${flagged ? '[Possible spam] ' : ''}` +
|
||||||
|
`Intake — ${clean.name} (${clean.practiceArea})`,
|
||||||
|
},
|
||||||
|
Body: {
|
||||||
|
// The body carries the bare submissionId, because it is the
|
||||||
|
// partition key: that line gets pasted into the console to find
|
||||||
|
// the record, so it must be the key and not a rendering of it.
|
||||||
|
Text: { Data: notificationBody },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ses.send(
|
||||||
|
new SendEmailCommand({
|
||||||
|
FromEmailAddress: MAIL_FROM,
|
||||||
|
Destination: { ToAddresses: [clean.email] },
|
||||||
|
Content: {
|
||||||
|
Simple: {
|
||||||
|
Subject: { Data: 'Your inquiry has been received' },
|
||||||
|
Body: {
|
||||||
|
Text: {
|
||||||
|
Data: [
|
||||||
|
`Thank you — your inquiry has been received.`,
|
||||||
|
``,
|
||||||
|
RESPONSE_TIME,
|
||||||
|
``,
|
||||||
|
NO_RETAINER_NOTICE,
|
||||||
|
``,
|
||||||
|
`What you sent:`,
|
||||||
|
``,
|
||||||
|
summaryLines,
|
||||||
|
``,
|
||||||
|
`How this information is handled, and how to ask for it to be`,
|
||||||
|
`deleted: ${SITE_ORIGIN}/legal/privacy/`,
|
||||||
|
``,
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
results.forEach((result, i) => {
|
||||||
|
if (result.status === 'rejected') {
|
||||||
|
console.error(
|
||||||
|
`intake: SES send ${i === 0 ? 'notification' : 'confirmation'} failed`,
|
||||||
|
{ id, reason: result.reason },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The submission is stored. Mail failures are an operator problem, not the
|
||||||
|
// inquirer's, and telling them it failed would invite a second submission of
|
||||||
|
// a record that already exists.
|
||||||
|
return redirect(SUCCESS);
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* Spam SCORING for the intake handler. Pouya's ruling, 2026-09-04.
|
||||||
|
*
|
||||||
|
* ⚠️ **THIS MODULE NEVER REJECTS ANYTHING, AND THAT IS THE WHOLE DESIGN.** It
|
||||||
|
* returns a score and a list of signal names. The handler stores the record and
|
||||||
|
* sends both emails either way; above the threshold it prefixes the OPERATOR
|
||||||
|
* notification's subject with `[Possible spam] ` and adds one line naming the
|
||||||
|
* signals. Pouya filters in Gmail. His words: *"Nothing is dropped; a false
|
||||||
|
* positive costs him one glance."*
|
||||||
|
*
|
||||||
|
* That asymmetry is why the thresholds below can be tuned aggressively. The cost
|
||||||
|
* of a false positive is a subject-line prefix; the cost of a false negative is
|
||||||
|
* one unlabelled email. Neither loses an inquiry — which a filter that rejected
|
||||||
|
* would, and a legal inquiry lost silently is the one outcome this form must not
|
||||||
|
* produce.
|
||||||
|
*
|
||||||
|
* ⚠️ **NOTHING HERE IS STORED.** The score and the signals do not enter the
|
||||||
|
* DynamoDB item. `/legal/privacy/` publishes what the record holds, field by
|
||||||
|
* field, and adding an attribute would make that list wrong — a disclosure
|
||||||
|
* defect, not a schema change. The label lives only in the operator
|
||||||
|
* notification. ⚠️ **THAT MAILBOX IS DELEGATED, NOT PERSONAL** — §9 Q63 and
|
||||||
|
* `/legal/privacy/` §Who can see it both say so, and an earlier draft of this
|
||||||
|
* comment said the label "lives in an email that only Pouya reads", which is the
|
||||||
|
* exclusivity Q63 struck. It reaches whoever reads `info@smlcompany.ca`. If a
|
||||||
|
* stored score is ever wanted, the page changes first.
|
||||||
|
*
|
||||||
|
* ⚠️ **AND THE INQUIRER NEVER SEES ANY OF THIS.** The confirmation email is
|
||||||
|
* untouched. A person wrongly scored must not be told a machine thought they
|
||||||
|
* were a bot.
|
||||||
|
*
|
||||||
|
* WHY SCORING RATHER THAN MORE REJECTION. The two submissions of 2026-09-04
|
||||||
|
* (`docs/05` §Observed abuse) passed the honeypot. Every rule that would have
|
||||||
|
* caught them — a foreign phone, a link in the summary, a disposable-looking
|
||||||
|
* address — is a rule some real inquirer also trips: this practice takes
|
||||||
|
* cross-border commercial work, so a `+44` number is a client, not a bot. A
|
||||||
|
* rejecting rule set built from those signals would eventually discard a real
|
||||||
|
* dispute and report success while doing it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ **NOT MEASURED FROM A CORPUS — THERE IS NO CORPUS.** No genuine inquiry has
|
||||||
|
* arrived through this form yet, so there is nothing to measure a normal summary
|
||||||
|
* length against, and a number presented as measured when it is not is the
|
||||||
|
* defect `AGENTS.md` keeps paying for.
|
||||||
|
*
|
||||||
|
* It is DERIVED, and the derivation is the form's own instruction: the `summary`
|
||||||
|
* field's hint reads *"A few sentences is enough."* This floor sits **below** what
|
||||||
|
* that invites, so it fires on a summary that does not attempt the question
|
||||||
|
* rather than on one that answers it briefly. `[assumed 2026-09-04]`
|
||||||
|
*
|
||||||
|
* ⚠️ **TUNE IT DOWN WHEN IN DOUBT, NEVER UP.** An unlabelled spam costs nothing
|
||||||
|
* that matters; a labelled real inquiry spends the reader's trust in the label.
|
||||||
|
* *"Shareholder dispute, two directors, Ontario CBCA company."* is 57 characters
|
||||||
|
* and is exactly what the hint asks for — a floor above that scores the form's
|
||||||
|
* own instruction as a spam signal.
|
||||||
|
*
|
||||||
|
* **Pouya can replace this with a measurement whenever he likes** — the two spam
|
||||||
|
* records of 2026-09-04 are still in the table, and their `summary` lengths are
|
||||||
|
* the first real data this number could rest on. §9 Q65 records that.
|
||||||
|
*/
|
||||||
|
export const SHORT_SUMMARY_CHARS = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Above this, the notification is labelled. Weights below are 1 for a signal a
|
||||||
|
* real inquirer plausibly trips and 2 for one they rarely do, so the threshold
|
||||||
|
* of 2 means: **one strong signal, or two weak ones.**
|
||||||
|
*
|
||||||
|
* Worked, because a threshold nobody has worked through is a guess with a number
|
||||||
|
* on it:
|
||||||
|
* - Ontario counsel, local number, three-line summary → 0, clean
|
||||||
|
* - Cross-border counsel, `+44` number, three-line summary → 1, clean
|
||||||
|
* - Cross-border counsel, `+44` number, one-line summary → 2, LABELLED
|
||||||
|
* - A four-part real name at gmail.com → 1, clean
|
||||||
|
* - Anyone pasting a link to a public tender document → 2, LABELLED
|
||||||
|
* - foreign number + a short scraped summary carrying a link → 4, LABELLED
|
||||||
|
*
|
||||||
|
* The third and fourth rows are the accepted false positives. Both are real
|
||||||
|
* shapes, both cost one glance, and both were preferred to missing the fifth.
|
||||||
|
*
|
||||||
|
* ⚠️ **THE LAST ROW IS A SHAPE, NOT A MEASUREMENT OF THE TWO 2026-09-04
|
||||||
|
* SUBMISSIONS. THOSE RECORDS WERE NEVER READ.** What the attested signature
|
||||||
|
* guarantees is a non-NANP phone — **one weak signal** — and whether either is
|
||||||
|
* labelled turns on facts only the two rows in the table hold. §9 Q65 records
|
||||||
|
* that they are still there and are the only real data any of these numbers
|
||||||
|
* could rest on.
|
||||||
|
*/
|
||||||
|
export const SPAM_THRESHOLD = 2;
|
||||||
|
|
||||||
|
/** `https://…` or `www.…` only. A bare `acme.com` is NOT matched: an inquirer
|
||||||
|
* writing "the dispute concerns acme.com's supply contract" is describing a
|
||||||
|
* party, and matching that would label ordinary commercial prose. */
|
||||||
|
const URL_IN_TEXT = /\b(?:https?:\/\/|www\.)\S/i;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NANP: an explicit `+<cc>` settles it; otherwise ten digits, or eleven
|
||||||
|
* beginning with 1, after the tail is dropped.
|
||||||
|
*
|
||||||
|
* ⚠️ **A DIGIT COUNT ALONE CANNOT DO THIS.** `416-555-0123 ext 22` is twelve
|
||||||
|
* digits, `416-555-0123 or 416-555-0124` is twenty, and both are ordinary
|
||||||
|
* Toronto numbers that a bare count calls foreign — a signal saying the opposite
|
||||||
|
* of the truth. The tail is dropped at the first extension marker or
|
||||||
|
* second-number separator, and the marker list is deliberately generous.
|
||||||
|
*
|
||||||
|
* **The country code is read FIRST because it is the only unambiguous thing in
|
||||||
|
* the field.** `+44 …` and `+7 …` are settled without counting anything, which
|
||||||
|
* is what a pure shape test cannot do: `+7 912 345 6789` is grouped 3-3-4
|
||||||
|
* exactly like a NANP number, so matching the shape would call it Canadian.
|
||||||
|
* Only when there is no explicit country code does the digit count run, and then
|
||||||
|
* the tail is dropped at the first extension marker or second-number separator.
|
||||||
|
*/
|
||||||
|
function looksNorthAmerican(phone) {
|
||||||
|
const trimmed = phone.trim();
|
||||||
|
/* An explicit international prefix is decisive in both directions. */
|
||||||
|
const cc = trimmed.match(/^\+\s*(\d{1,3})/);
|
||||||
|
if (cc) return cc[1] === '1';
|
||||||
|
/* Longest alternative FIRST: regex alternation is leftmost-first, so `ext`
|
||||||
|
placed before `extension` matches the first three letters and then relies on
|
||||||
|
backtracking. Ordering it correctly is cheaper than depending on that.
|
||||||
|
`\bx\b` would NOT match the `x` in `x22` — the digit after it is a word
|
||||||
|
character, so there is no boundary — which is how `(416) 555-0123 x22` scored
|
||||||
|
foreign for one round. The marker is matched by what FOLLOWS it. */
|
||||||
|
const digits = trimmed
|
||||||
|
.split(/\s*(?:extension|extn|ext|x)[.:-]?\s*\d|[#,;]|\bor\b/i)[0]
|
||||||
|
.replace(/\D/g, '');
|
||||||
|
return digits.length === 10 || (digits.length === 11 && digits[0] === '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Gmail dot trick: one mailbox, unlimited distinct-looking addresses,
|
||||||
|
* because Gmail ignores dots in the local part.
|
||||||
|
*
|
||||||
|
* ⚠️ **A DOT IS NOT THE SIGNAL, AND TREATING IT AS ONE WOULD LABEL MOST REAL
|
||||||
|
* GMAIL USERS.** `first.last@gmail.com` is the single most ordinary form a Gmail
|
||||||
|
* address takes. What distinguishes the trick is dot DENSITY: **three or more
|
||||||
|
* dots**, and nothing else.
|
||||||
|
*
|
||||||
|
* ⚠️ **AND THE WEIGHT IS 1, NOT 2, WHICH MATTERS MORE THAN THE BOUNDARY DOES.**
|
||||||
|
* `mary.jane.o.brien@gmail.com` and `maria.de.la.cruz@gmail.com` are three-dot
|
||||||
|
* REAL names — compound surnames and middle initials are ordinary, not rare —
|
||||||
|
* and weight 2 is defined here as what a real inquirer rarely trips. At weight 1
|
||||||
|
* nothing can be labelled on the shape of its owner's name alone; a genuine
|
||||||
|
* dot-trick address reaches the threshold as soon as it trips anything else,
|
||||||
|
* which spam reliably does. **Do not raise it back.**
|
||||||
|
*/
|
||||||
|
function looksLikeGmailDotTrick(email) {
|
||||||
|
const at = email.lastIndexOf('@');
|
||||||
|
if (at < 1) return false;
|
||||||
|
const local = email.slice(0, at);
|
||||||
|
const domain = email.slice(at + 1).toLowerCase();
|
||||||
|
if (domain !== 'gmail.com' && domain !== 'googlemail.com') return false;
|
||||||
|
const dots = local.split('.').length - 1;
|
||||||
|
return dots >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string>} fields the handler's `clean` map — validated,
|
||||||
|
* plain-texted values, keyed by field name. Absent fields are simply absent.
|
||||||
|
* @returns {{score: number, signals: string[]}} `signals` are written for a
|
||||||
|
* human reading one line of an email, not for a machine.
|
||||||
|
*/
|
||||||
|
export function scoreSubmission(fields) {
|
||||||
|
const signals = [];
|
||||||
|
let score = 0;
|
||||||
|
const add = (weight, label) => {
|
||||||
|
score += weight;
|
||||||
|
signals.push(label);
|
||||||
|
};
|
||||||
|
|
||||||
|
const summary = fields.summary ?? '';
|
||||||
|
const phone = fields.phone ?? '';
|
||||||
|
const email = fields.email ?? '';
|
||||||
|
|
||||||
|
/* Only when a summary exists. An absent one is a validation failure the
|
||||||
|
handler has already turned into the failure page, so scoring an empty
|
||||||
|
string here would be scoring a submission that never got this far. */
|
||||||
|
if (summary !== '' && summary.length < SHORT_SUMMARY_CHARS) {
|
||||||
|
add(1, `summary under ${SHORT_SUMMARY_CHARS} characters`);
|
||||||
|
}
|
||||||
|
/* `phone` is OPTIONAL. Not giving one is not a signal — most inquirers will
|
||||||
|
not — so this fires only on a number that is present and not North
|
||||||
|
American. Treating absence as suspicious would label the quiet majority. */
|
||||||
|
if (phone !== '' && !looksNorthAmerican(phone)) {
|
||||||
|
add(1, 'phone is not a Canadian or US number');
|
||||||
|
}
|
||||||
|
if (URL_IN_TEXT.test(summary)) {
|
||||||
|
add(2, 'link in the dispute summary');
|
||||||
|
}
|
||||||
|
if (looksLikeGmailDotTrick(email)) {
|
||||||
|
add(1, 'Gmail address using the dot trick');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { score, signals };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the operator notification should carry the label. */
|
||||||
|
export const isPossibleSpam = ({ score }) => score >= SPAM_THRESHOLD;
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* Unit test for the intake spam scorer. `node backend/intake/spam-score.test.mjs`.
|
||||||
|
*
|
||||||
|
* Same shape and same reasoning as `infra/cloudfront/router.test.mjs`: the real
|
||||||
|
* check is a real submission, this one runs in a second and catches the branch
|
||||||
|
* mistakes that a regex change makes silently.
|
||||||
|
*
|
||||||
|
* ⚠️ **EVERY SIGNAL SHIPS WITH A NEGATIVE FIXTURE**, which is the discipline
|
||||||
|
* `CLAUDE.md` imposes on `check:claims` and applies here for the same reason:
|
||||||
|
* this scorer's failure mode is not missing spam, it is labelling a real
|
||||||
|
* inquiry. The pairs below are the nearest legitimate submission to each trap —
|
||||||
|
* `j.k.smith@gmail.com` beside the dot trick, an extension-carrying Toronto
|
||||||
|
* number beside a Russian one, ordinary commercial prose naming a company
|
||||||
|
* beside a pasted link.
|
||||||
|
*
|
||||||
|
* ⚠️ **EACH CASE ASSERTS THE SIGNAL NAMES, NOT ONLY THE SCORE.** Asserting the
|
||||||
|
* total alone lets two rules swap weights, or one rule fire in place of
|
||||||
|
* another, with every case still passing — the suite would then be checking
|
||||||
|
* arithmetic rather than behaviour. `expected` is the exact signal set.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
scoreSubmission,
|
||||||
|
isPossibleSpam,
|
||||||
|
SPAM_THRESHOLD,
|
||||||
|
SHORT_SUMMARY_CHARS,
|
||||||
|
} from './spam-score.mjs';
|
||||||
|
|
||||||
|
const SHORT = `summary under ${SHORT_SUMMARY_CHARS} characters`;
|
||||||
|
const PHONE = 'phone is not a Canadian or US number';
|
||||||
|
const LINK = 'link in the dispute summary';
|
||||||
|
const GMAIL = 'Gmail address using the dot trick';
|
||||||
|
|
||||||
|
const MID =
|
||||||
|
'A construction lien dispute over a delayed fit-out. Counsel are engaged ' +
|
||||||
|
'on both sides and we want a mediator.';
|
||||||
|
|
||||||
|
const LONG =
|
||||||
|
'The parties are in dispute over a delayed fit-out on a Toronto office ' +
|
||||||
|
'tower. The subcontract was terminated in June and the holdback has not ' +
|
||||||
|
'been released. Counsel are engaged on both sides and we are looking for a ' +
|
||||||
|
'mediator with construction experience.';
|
||||||
|
|
||||||
|
/* [label, fields, expected signals] — score and labelled are DERIVED from the
|
||||||
|
weights below, so a weight change fails every affected case by name rather
|
||||||
|
than silently re-balancing the totals. */
|
||||||
|
const WEIGHTS = { [SHORT]: 1, [PHONE]: 1, [LINK]: 2, [GMAIL]: 1 };
|
||||||
|
|
||||||
|
const CASES = [
|
||||||
|
// ---- clean submissions, which is the half that matters most -------------
|
||||||
|
[
|
||||||
|
'ordinary Ontario inquiry',
|
||||||
|
{ summary: LONG, phone: '416-555-0123', email: 'a.counsel@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
['no phone given at all', { summary: LONG, email: 'counsel@firm.ca' }, []],
|
||||||
|
[
|
||||||
|
'+1 with punctuation',
|
||||||
|
{ summary: LONG, phone: '+1 (647) 555-0188', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ten digits, no punctuation',
|
||||||
|
{ summary: LONG, phone: '6475550188', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Toronto number with an extension',
|
||||||
|
{ summary: LONG, phone: '416-555-0123 ext 22', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'extension written x22',
|
||||||
|
{ summary: LONG, phone: '(416) 555-0123 x22', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'extension written Ext:',
|
||||||
|
{ summary: LONG, phone: '416-555-0123 Ext: 4501', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'extension spelled out',
|
||||||
|
{ summary: LONG, phone: '416-555-0123 extension 22', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'extension hyphenated',
|
||||||
|
{ summary: LONG, phone: '416-555-0123 ext-22', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'two numbers in one field',
|
||||||
|
{
|
||||||
|
summary: LONG,
|
||||||
|
phone: '416-555-0123 or 416-555-0124',
|
||||||
|
email: 'c@firm.ca',
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ordinary gmail, one dot',
|
||||||
|
{ summary: LONG, phone: '416-555-0123', email: 'first.last@gmail.com' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gmail, single initial',
|
||||||
|
{ summary: LONG, phone: '416-555-0123', email: 'j.smith@gmail.com' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gmail, TWO initials and a surname',
|
||||||
|
{ summary: LONG, email: 'j.k.smith@gmail.com' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
/* ⚠️ NON-GMAIL, THREE DOTS — this pins the DOMAIN GUARD, which nothing did.
|
||||||
|
Deleting `if (domain !== 'gmail.com' && …) return false` left all 30 cases
|
||||||
|
passing: the nearest legitimate submission to a three-dot trap is a
|
||||||
|
three-dot address at a firm domain, and it was the one fixture missing. */
|
||||||
|
[
|
||||||
|
'law-firm address, three dots',
|
||||||
|
{ summary: LONG, email: 'j.p.van.dam@blakes.com' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'four-part real name at gmail',
|
||||||
|
{
|
||||||
|
summary: LONG,
|
||||||
|
phone: '416-555-0123',
|
||||||
|
email: 'mary.jane.o.brien@gmail.com',
|
||||||
|
},
|
||||||
|
[GMAIL],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'company named in prose, no link',
|
||||||
|
{ summary: `${LONG} The respondent is acme.com Ltd.`, email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'googlemail, one dot',
|
||||||
|
{ summary: LONG, email: 'first.last@googlemail.com' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ---- one weak signal: still clean ---------------------------------------
|
||||||
|
[
|
||||||
|
'cross-border counsel, UK number',
|
||||||
|
{ summary: LONG, phone: '+44 20 7946 0958', email: 'c@firm.co.uk' },
|
||||||
|
[PHONE],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'the concise summary the hint invites',
|
||||||
|
{
|
||||||
|
summary: 'Shareholder dispute, two directors, Ontario CBCA company.',
|
||||||
|
phone: '416-555-0123',
|
||||||
|
email: 'c@firm.ca',
|
||||||
|
},
|
||||||
|
[SHORT],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ---- boundaries ----------------------------------------------------------
|
||||||
|
/* PINS THE FLOOR'S VALUE, which the two boundary cases below cannot: they
|
||||||
|
derive their lengths from `SHORT_SUMMARY_CHARS`, so they move with it and
|
||||||
|
a floor raised back to 140 passed them silently. This one is a literal
|
||||||
|
109-character summary of the kind the form's hint invites, and it fails the
|
||||||
|
moment the floor rises above it. */
|
||||||
|
[
|
||||||
|
'a realistic 109-character summary',
|
||||||
|
{ summary: MID, email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary exactly at the floor',
|
||||||
|
{ summary: 'x'.repeat(SHORT_SUMMARY_CHARS), email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary one under the floor',
|
||||||
|
{ summary: 'x'.repeat(SHORT_SUMMARY_CHARS - 1), email: 'c@firm.ca' },
|
||||||
|
[SHORT],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'eleven digits not starting 1',
|
||||||
|
{ summary: LONG, phone: '+7 912 345 6789', email: 'c@firm.ca' },
|
||||||
|
[PHONE],
|
||||||
|
],
|
||||||
|
/* ⚠️ TEN DIGITS IN TOTAL, AND FOREIGN — Iceland writes +354 followed by seven.
|
||||||
|
This is the ONE case that pins the country-code branch: without it the
|
||||||
|
digit count reads 10 and calls this a NANP number. Every other foreign
|
||||||
|
fixture here has 11+ digits, so the count agrees by accident and the
|
||||||
|
branch could be deleted with the whole suite still green. */
|
||||||
|
[
|
||||||
|
'ten-digit international number',
|
||||||
|
{ summary: LONG, phone: '+354 555 1234', email: 'c@firm.is' },
|
||||||
|
[PHONE],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gmail, exactly two dots',
|
||||||
|
{ summary: LONG, email: 'a.b.smith@gmail.com' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gmail, exactly three dots',
|
||||||
|
{ summary: LONG, email: 'a.b.c.smith@gmail.com' },
|
||||||
|
[GMAIL],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ---- two weak signals: labelled ------------------------------------------
|
||||||
|
[
|
||||||
|
'foreign number and terse summary',
|
||||||
|
{
|
||||||
|
summary: 'Need a mediator.',
|
||||||
|
phone: '+7 912 345 6789',
|
||||||
|
email: 'c@firm.ru',
|
||||||
|
},
|
||||||
|
[SHORT, PHONE],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ---- one strong signal: labelled -----------------------------------------
|
||||||
|
[
|
||||||
|
'link in the summary',
|
||||||
|
{ summary: `${LONG} See https://example.com/tender`, email: 'c@firm.ca' },
|
||||||
|
[LINK],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'www link in the summary',
|
||||||
|
{ summary: `${LONG} See www.example.com/tender`, email: 'c@firm.ca' },
|
||||||
|
[LINK],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dot trick, four dots',
|
||||||
|
{ summary: LONG, email: 'j.o.h.nsmith@gmail.com' },
|
||||||
|
[GMAIL],
|
||||||
|
],
|
||||||
|
/* ⚠️ WEIGHT 1, SO IT DOES NOT LABEL ALONE. That is the whole point of the
|
||||||
|
weight change, and this is the case that fails if it goes back to 2. */
|
||||||
|
[
|
||||||
|
'dot trick alone does not label',
|
||||||
|
{ summary: LONG, email: 'r.a.n.d.om@gmail.com' },
|
||||||
|
[GMAIL],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ---- the shape the 2026-09-04 pair is described as ------------------------
|
||||||
|
// NOT a measurement of those records: their `summary` values were never read.
|
||||||
|
[
|
||||||
|
'scraped text, foreign number, link',
|
||||||
|
{
|
||||||
|
summary: 'Buy now at https://spam.example/offer',
|
||||||
|
phone: '+7 912 345 6789',
|
||||||
|
email: 'r.a.n.d.om@gmail.com',
|
||||||
|
},
|
||||||
|
[SHORT, PHONE, LINK, GMAIL],
|
||||||
|
],
|
||||||
|
/* The module's own worked example of a legitimate concise summary, beside a
|
||||||
|
Toronto direct line. It scored 2 and shipped `[Possible spam]` while the
|
||||||
|
extension strip was incomplete. */
|
||||||
|
[
|
||||||
|
'concise summary + Toronto extension',
|
||||||
|
{
|
||||||
|
summary: 'Shareholder dispute, two directors, Ontario CBCA company.',
|
||||||
|
phone: '416-555-0123 ext: 4501',
|
||||||
|
email: 'c@firm.ca',
|
||||||
|
},
|
||||||
|
[SHORT],
|
||||||
|
],
|
||||||
|
// The attested signature ALONE — a non-NANP phone and nothing else known —
|
||||||
|
// is one weak signal and is NOT labelled. Kept as a case so the limit of what
|
||||||
|
// the observed evidence supports is asserted rather than described.
|
||||||
|
[
|
||||||
|
'attested signature alone',
|
||||||
|
{ summary: LONG, phone: '+7 912 345 6789', email: 'random@gmail.com' },
|
||||||
|
[PHONE],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ---- absent fields must not throw or score -------------------------------
|
||||||
|
['empty object', {}, []],
|
||||||
|
[
|
||||||
|
'summary absent, phone local',
|
||||||
|
{ phone: '416-555-0123', email: 'c@firm.ca' },
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
['email absent', { summary: LONG }, []],
|
||||||
|
['malformed email, no @', { summary: LONG, email: 'not-an-address' }, []],
|
||||||
|
['gmail with no local part', { summary: LONG, email: '@gmail.com' }, []],
|
||||||
|
];
|
||||||
|
|
||||||
|
let pass = 0;
|
||||||
|
const failures = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const [label, fields, expected] of CASES) {
|
||||||
|
const result = scoreSubmission(fields);
|
||||||
|
expected.forEach((sig) => seen.add(sig));
|
||||||
|
const wantScore = expected.reduce((n, sig) => n + WEIGHTS[sig], 0);
|
||||||
|
const wantLabelled = wantScore >= SPAM_THRESHOLD;
|
||||||
|
const gotSignals = [...result.signals].sort();
|
||||||
|
const wantSignals = [...expected].sort();
|
||||||
|
const ok =
|
||||||
|
result.score === wantScore &&
|
||||||
|
isPossibleSpam(result) === wantLabelled &&
|
||||||
|
JSON.stringify(gotSignals) === JSON.stringify(wantSignals);
|
||||||
|
if (ok) {
|
||||||
|
pass += 1;
|
||||||
|
} else {
|
||||||
|
failures.push(
|
||||||
|
` ${label}\n` +
|
||||||
|
` expected score ${wantScore}, labelled ${wantLabelled}, signals ${JSON.stringify(wantSignals)}\n` +
|
||||||
|
` got score ${result.score}, labelled ${isPossibleSpam(result)}, signals ${JSON.stringify(gotSignals)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COVERAGE, ASSERTED RATHER THAN ASSUMED. A rule with no positive case is a rule
|
||||||
|
nobody has run, and it would still show a green suite. */
|
||||||
|
for (const sig of Object.keys(WEIGHTS)) {
|
||||||
|
if (!seen.has(sig)) {
|
||||||
|
failures.push(` no case exercises the "${sig}" signal — it is untested.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The threshold is part of the contract the cases above were written against.
|
||||||
|
Changing it without re-deriving them would leave every expectation a
|
||||||
|
statement about a threshold that no longer exists. */
|
||||||
|
if (SPAM_THRESHOLD !== 2) {
|
||||||
|
failures.push(
|
||||||
|
` SPAM_THRESHOLD is ${SPAM_THRESHOLD}, not 2 — the weights and expectations ` +
|
||||||
|
'above were written against 2. Re-derive them before changing it.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error(`spam-score: ${failures.length} FAILED of ${CASES.length}`);
|
||||||
|
console.error(failures.join('\n'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`spam-score: ${pass} of ${CASES.length} cases pass; all ${Object.keys(WEIGHTS).length} signals exercised`,
|
||||||
|
);
|
||||||
+320
-53
@@ -49,7 +49,49 @@ decision, not an aesthetic one.
|
|||||||
/legal/terms/ Terms of use
|
/legal/terms/ Terms of use
|
||||||
```
|
```
|
||||||
|
|
||||||
Nineteen fixed URLs plus one per article.
|
Nineteen fixed URLs plus one per article — **and four more, all `noindex` and all
|
||||||
|
excluded from the sitemap in `astro.config.mjs`.** They are utilities rather than
|
||||||
|
pages in the IA above, which is why they are listed here rather than in it:
|
||||||
|
|
||||||
|
```
|
||||||
|
/contact/received/ The intake form's success landing page
|
||||||
|
/contact/could-not-send/ Its failure landing page
|
||||||
|
/bio/ Source of the one-page PDF (R16)
|
||||||
|
/404/ Emitted as dist/404.html — see below
|
||||||
|
```
|
||||||
|
|
||||||
|
The two `/contact/` pages exist because the site ships **zero JavaScript**, so
|
||||||
|
the form is a plain POST and the handler answers `303 See Other` to a real URL —
|
||||||
|
`docs/05` §Build step 8 has the reasoning. `/bio/` exists so the PDF circulated
|
||||||
|
with an appointment proposal is a *rendering of a reviewed page* rather than a
|
||||||
|
document outside this project's review apparatus.
|
||||||
|
|
||||||
|
⚠️ **`/404/` IS THE ONE ROUTE THAT BREAKS THIS DOCUMENT'S OWN URL RULES, AND IT
|
||||||
|
HAS TO.** The rules above are lower-case, hyphenated, trailing slash, no file
|
||||||
|
extensions. Astro emits `src/pages/404.astro` as **`dist/404.html`** — a file at
|
||||||
|
the root, outside `build.format: 'directory'` — because that is the object name a
|
||||||
|
CDN custom error response can point at. `SEO.astro` still sees the path as
|
||||||
|
`/404/`, which is why its `OG_CARDS` key is `/404/` while the URL a tool fetches
|
||||||
|
is `/404.html`. Added 2026-09-01; `docs/04` had asked for the page since before
|
||||||
|
build step 1 and it did not exist.
|
||||||
|
|
||||||
|
**So: 23 built pages plus one per published article.**
|
||||||
|
|
||||||
|
⚠️ **AND THIS SENTENCE USED TO END WITH A REASSURANCE THAT WAS DISPROVEN THE DAY
|
||||||
|
THE 404 PAGE WAS ADDED.** It read: *"`npm run lighthouse` enumerates them from
|
||||||
|
`dist/` rather than from this list, which is why this count being stale could never
|
||||||
|
make the gate miss a page."* Both `scripts/lighthouse.mjs` and
|
||||||
|
`scripts/og-proof.mjs` enumerated **`index.html` under `dist/`**, not every page —
|
||||||
|
so both missed `/404/`, and `og:proof` reported it backwards, as an orphaned card
|
||||||
|
rather than an unchecked page. The count being stale was not the failure mode; the
|
||||||
|
**definition of "a page"** was. Both now take any `.html` at the root as well, and
|
||||||
|
`check:claims` always did, which is why the new page's copy was inside the claim
|
||||||
|
register from its first build.
|
||||||
|
|
||||||
|
**The rule that replaces the reassurance:** a route that does not live at
|
||||||
|
`<dir>/index.html` is invisible to anything that looks for `index.html`. If a
|
||||||
|
future page is emitted outside the directory convention, grep the three scripts
|
||||||
|
for `index.html` before trusting any of them.
|
||||||
|
|
||||||
### URL rules
|
### URL rules
|
||||||
|
|
||||||
@@ -81,8 +123,15 @@ the ceiling before a nav stops being scannable.
|
|||||||
## Deliberate omission: Indigenous engagement
|
## Deliberate omission: Indigenous engagement
|
||||||
|
|
||||||
The strategy brief (§III.4) rates Indigenous engagement, IBA, and consultation-
|
The strategy brief (§III.4) rates Indigenous engagement, IBA, and consultation-
|
||||||
breakdown mediation as *"strategically the most valuable single niche"* for a
|
breakdown mediation as *"strategically the most valuable single niche"*.
|
||||||
Q.Med on the C.Med-Arb pathway.
|
|
||||||
|
⚠️ **THAT SENTENCE USED TO CONTINUE "for a Q.Med on the C.Med-Arb pathway", AND
|
||||||
|
BOTH HALVES WERE DEFECTIVE.** *"pathway"* is a struck stage form (§4, 2026-08-29),
|
||||||
|
and the clause sat **outside** the quotation marks in `docs/01`'s own voice while
|
||||||
|
a parenthetical called the whole thing *"the brief's framing, quoted"* — a
|
||||||
|
quotation boundary the marks do not draw, which is the gloss defect inverted.
|
||||||
|
Only the four quoted words come from the brief, and the brief is not in this
|
||||||
|
repository (R14). The reasoning below never turned on either half.
|
||||||
|
|
||||||
There is no page for it at launch, on the following reasoning:
|
There is no page for it at launch, on the following reasoning:
|
||||||
|
|
||||||
@@ -99,7 +148,7 @@ Revisit at month 12–18, once there is relationship history to point to.
|
|||||||
|
|
||||||
## Not a practice area yet: tax-adjacent disputes
|
## Not a practice area yet: tax-adjacent disputes
|
||||||
|
|
||||||
**Canadian Tax Foundation membership is verified** (`AGENTS.md` §4, 2026-08-26)
|
**Canadian Tax Foundation membership is verified** (`AGENTS.md` §4 — read the stamp there; a second copy of a currency stamp goes stale where nobody re-reads it, and this one had)
|
||||||
and it is the one credential none of the six areas above touch. Tax-adjacent
|
and it is the one credential none of the six areas above touch. Tax-adjacent
|
||||||
disputes are genuinely ADR territory — valuation and purchase-price disputes on
|
disputes are genuinely ADR territory — valuation and purchase-price disputes on
|
||||||
a share sale, indemnity and earn-out fights that turn on a tax position,
|
a share sale, indemnity and earn-out fights that turn on a tax position,
|
||||||
@@ -139,7 +188,11 @@ four audiences to its surface.
|
|||||||
stands; do not lift the phrase into copy** — it reached `/` once already.
|
stands; do not lift the phrase into copy** — it reached `/` once already.
|
||||||
State the asymmetry instead: `docs/03` §The credential row. Infinity mark as the visual anchor.
|
State the asymmetry instead: `docs/03` §The credential row. Infinity mark as the visual anchor.
|
||||||
4. **Two practices.** Mediation and Arbitration cards → `/mediation/`, `/arbitration/`.
|
4. **Two practices.** Mediation and Arbitration cards → `/mediation/`, `/arbitration/`.
|
||||||
Med-Arb named here as the long-term arc, linking to `/med-arb/`.
|
Med-Arb named here, linking to `/med-arb/`. ⚠️ **This item read "named here as
|
||||||
|
the long-term arc" until 2026-08-29.** There is no arc — C.Med-Arb is struck
|
||||||
|
(§4) — and Med-Arb has had its own §4 Offerings row since 2026-08-27 (Q35),
|
||||||
|
so it is named as a **present offering**. `index.astro` cited this item as
|
||||||
|
amended before it was; both are correct now.
|
||||||
5. **Practice areas.** Six-card grid → `/practice/*`. This is the most important
|
5. **Practice areas.** Six-card grid → `/practice/*`. This is the most important
|
||||||
block on the page for search, because it distributes authority to the pages
|
block on the page for search, because it distributes authority to the pages
|
||||||
that can actually rank.
|
that can actually rank.
|
||||||
@@ -167,17 +220,49 @@ to an appointment. This page carries the verifiable record.
|
|||||||
engineering, operating a company — told as one arc rather than three lists.
|
engineering, operating a company — told as one arc rather than three lists.
|
||||||
3. **Credentials**, structured and scannable: designations, education,
|
3. **Credentials**, structured and scannable: designations, education,
|
||||||
certifications, memberships. Every line from `AGENTS.md` §4 Verified.
|
certifications, memberships. Every line from `AGENTS.md` §4 Verified.
|
||||||
4. **The credentialing arc.** Q.Med held → Q.Arb in progress → C.Med-Arb as the
|
4. ~~**The credentialing arc.**~~ ⚠️ **STRUCK 2026-08-29 — Pouya, and the
|
||||||
endpoint. The brief (§V) treats the arc itself as part of the story; say so
|
section is deleted from the page, not rewritten.** *"Two held designations,
|
||||||
openly rather than implying a finished state.
|
no journey… An arc invites 'where are you on it'; two designations don't."*
|
||||||
|
Q.Arb is held and C.Med-Arb is off the site entirely, so the item had no
|
||||||
|
subject. **Do not restore it from this outline or from the brief (§V), which
|
||||||
|
treated the arc as part of the story.** What it carried belongs in item 3,
|
||||||
|
which is a list of held things.
|
||||||
5. **Languages and cross-cultural practice.**
|
5. **Languages and cross-cultural practice.**
|
||||||
6. **Speaking and publications.** Omit the section entirely until there is
|
6. **Speaking and publications.** Omit the section entirely until there is
|
||||||
something in it. An empty "Speaking" heading is worse than no heading.
|
something in it. An empty "Speaking" heading is worse than no heading.
|
||||||
7. `Person` JSON-LD. Downloadable one-page PDF bio — brief §VIII lists this as
|
7. `Person` JSON-LD. Downloadable one-page PDF bio — brief §VIII lists this as
|
||||||
an asset for circulation with appointment proposals.
|
an asset for circulation with appointment proposals.
|
||||||
|
|
||||||
> **The PDF bio does NOT ship with build step 3, and the omission is stated
|
> ✅ **SHIPPED AT BUILD STEP 9, 2026-08-31. R16 / Q45 DISCHARGED.**
|
||||||
> rather than silent** — `AGENTS.md` Q45, opened 2026-08-28. No such file exists
|
> `public/pouya-lajevardi-bio.pdf` exists, is committed, and this page links it
|
||||||
|
> between the biography and the credentials.
|
||||||
|
>
|
||||||
|
> **The two decisions R16 left open are both taken, and the second makes the
|
||||||
|
> first safe.** *(a)* Neither "generated at build" nor "authored once": the bio
|
||||||
|
> is a **page**, `src/pages/bio.astro`, so every line is reviewed by the same
|
||||||
|
> apparatus as every other page — and `npm run bio:pdf` renders the PDF from the
|
||||||
|
> built page through the Chrome that Lighthouse already requires, so it adds no
|
||||||
|
> dependency. It is **not** part of `astro build`, because CI has no Chrome.
|
||||||
|
> *(b)* It carries **nothing the site does not** — every line renders from
|
||||||
|
> `CREDENTIALS`, `ROLE`, `BOUTIQUE`, `PRACTICE_AREAS`, `FEES` and `CONTACT`. No
|
||||||
|
> matter list (which R16 correctly said would collide with §4 Forbidden), no
|
||||||
|
> referees, no figure that is not on `/fees/`.
|
||||||
|
>
|
||||||
|
> ⚠️ **`npm run bio:pdf` asserts ONE PAGE and writes nothing if the count is
|
||||||
|
> wrong.** And reading the rendered PDF caught a breach the source review had
|
||||||
|
> not: its opening clause scoped **mediation** commercial, which Q56 leaves
|
||||||
|
> unscoped deliberately. Nothing in the build regenerates the PDF — `docs/06`'s
|
||||||
|
> cutover checklist carries the re-render.
|
||||||
|
|
||||||
|
> *Original deferral note, kept because its reasoning is why this is R16 rather
|
||||||
|
> than a to-do.* **The PDF bio ships at BUILD STEP 9, alongside `/fees/` —
|
||||||
|
> deferred by Pouya 2026-08-28 (Q45), tracked as `AGENTS.md` §12 **R16**.** His reasoning: it is a
|
||||||
|
> derived artefact, so building it before `/about/` and `/fees/` are final means
|
||||||
|
> building it twice, and an appointment proposal needs the fee card as much as
|
||||||
|
> the bio. The two decisions below are **not** settled by the deferral and travel
|
||||||
|
> with R16. Original note follows, and its reasoning is why this is R16 rather
|
||||||
|
> than a to-do. **The PDF bio does NOT ship with build step 3, and the omission
|
||||||
|
> is stated rather than silent** — `AGENTS.md` Q45, opened 2026-08-28. No such file exists
|
||||||
> in the repo, and a link to one that does not exist is a broken link on the
|
> in the repo, and a link to one that does not exist is a broken link on the
|
||||||
> page an appointing body reads. It is also not a formatting job: a one-page bio
|
> page an appointing body reads. It is also not a formatting job: a one-page bio
|
||||||
> is a **credential document** whose every line has to trace to §4 exactly as a
|
> is a **credential document** whose every line has to trace to §4 exactly as a
|
||||||
@@ -192,63 +277,170 @@ to an appointment. This page carries the verifiable record.
|
|||||||
**Job:** convert counsel who have already decided on mediation and are choosing a
|
**Job:** convert counsel who have already decided on mediation and are choosing a
|
||||||
neutral.
|
neutral.
|
||||||
**Search intent:** `commercial mediator Toronto`, `ADRIC mediation rules`,
|
**Search intent:** `commercial mediator Toronto`, `ADRIC mediation rules`,
|
||||||
`what happens at mediation Ontario`.
|
`what happens at mediation Ontario`. **These are queries, not the page's scope —
|
||||||
|
Q56, 2026-08-30.** The mediation offering is **not** scoped commercial (§4's row
|
||||||
|
is unscoped; the arbitration scope is a legal gate and does not transfer). Do not
|
||||||
|
read the first query back into the `<title>` or the copy, which is where
|
||||||
|
*"Commercial Mediation"* came from in the first place.
|
||||||
|
|
||||||
1. What the service is; the neutral's role stated plainly.
|
1. What the service is; the neutral's role stated plainly.
|
||||||
2. **Formats:** full-day, half-day, shuttle, remote, hybrid.
|
2. **Formats:** full-day, half-day, shuttle, remote, hybrid.
|
||||||
3. **Rules:** ADRIC Model Mediation Rules, or a bespoke protocol agreed by the
|
3. **Rules:** the **ADRIC National Mediation Rules**, or a bespoke protocol
|
||||||
parties.
|
agreed by the parties.
|
||||||
|
⚠️ *This item read "ADRIC Model Mediation Rules" until 2026-08-28 and that is
|
||||||
|
not the name of anything ADRIC publishes* — **0 occurrences** across all four
|
||||||
|
of its rules pages, against **10** of "National Mediation Rules" on the
|
||||||
|
document's own page. "Model" belongs to the **Model Dispute Resolution
|
||||||
|
Clause**, a contract clause inside the rules. Sourced and reproducible:
|
||||||
|
`docs/reference/adric-rules.md` Finding 1. The spec would have put a wrong
|
||||||
|
institutional name on a public page, which is the `Chartered
|
||||||
|
Mediator-Arbitrator` shape a second time — caught here only because R14 sent
|
||||||
|
the fetch out before the copy was written.
|
||||||
4. **What parties should bring** — briefs, documents, authority to settle.
|
4. **What parties should bring** — briefs, documents, authority to settle.
|
||||||
5. **Confidentiality and without-prejudice framing.**
|
5. **Confidentiality and without-prejudice framing.**
|
||||||
6. Practice areas → `/practice/*`.
|
6. Practice areas → `/practice/*`.
|
||||||
7. Fees → `/fees/`. Booking → `/contact/`.
|
7. Fees → `/fees/`. Booking → `/contact/`.
|
||||||
|
|
||||||
|
⚠️ **This page carries no first-person conduct commitment, and that is
|
||||||
|
deliberate.** How Pouya handles caucus material, and what he undertakes about a
|
||||||
|
bespoke protocol, are claims about his practice with no §4 row. Two of them
|
||||||
|
shipped here for one pass and were removed. Drafted for his ruling as **Q54(d)**
|
||||||
|
and **(e)**; until he rules, the Confidentiality and Rules sections describe what
|
||||||
|
an *agreement* settles, not what he promises.
|
||||||
|
|
||||||
### `/arbitration/`
|
### `/arbitration/`
|
||||||
|
|
||||||
**Job:** the same, for arbitration — and to state the Q.Arb position honestly.
|
**Job:** the same, for arbitration. *(This read "and to state the Q.Arb
|
||||||
|
position honestly" until 2026-08-29; Q.Arb is held and there is no position to
|
||||||
|
state.)*
|
||||||
**Search intent:** `sole arbitrator Ontario`, `expedited arbitration Canada`,
|
**Search intent:** `sole arbitrator Ontario`, `expedited arbitration Canada`,
|
||||||
`documents-only arbitration`.
|
`documents-only arbitration`.
|
||||||
|
|
||||||
1. What the service is; sole-arbitrator and party-appointed
|
1. What the service is; sole-arbitrator and party-appointed
|
||||||
appointments.
|
appointments.
|
||||||
2. **Tracks:** documents-only, expedited, full hearing.
|
2. **Tracks:** documents-only, expedited, full hearing.
|
||||||
3. **Rules:** ADRIC, ADR Chambers, ad hoc.
|
3. **Rules:** ADRIC, ad hoc. ✅ **Both are named on the page.**
|
||||||
|
⚠️ **ADR CHAMBERS WAS STRUCK FROM THIS ITEM AND FROM THE PAGE ON 2026-08-30
|
||||||
|
— Pouya's ruling, and do not reinstate it from an earlier reading of this
|
||||||
|
spec.** This item listed it as a third rule option for most of the project.
|
||||||
|
`docs/reference/adr-institution-names.md` sources what the firm *publishes*;
|
||||||
|
it does not source that an outside neutral can be appointed under those
|
||||||
|
rules, and the firm's own model clause reads *"at ADR Chambers"* — so naming
|
||||||
|
it here implied a relationship the repository does not establish. The ADRIC
|
||||||
|
edition date (1 March 2025) is published on the page because ADRIC publishes
|
||||||
|
one.
|
||||||
4. Awards — form, reasoning, timing.
|
4. Awards — form, reasoning, timing.
|
||||||
5. **Credentialing status, stated plainly.** The Q.Arb pathway is in progress;
|
5. ~~**Credentialing status, stated plainly.**~~ ⚠️ **STRUCK 2026-08-29 —
|
||||||
the page says so. **What is available now is all three forms — sole,
|
Pouya. Q.Arb is HELD, and §4's paired-disclosure condition dissolved with the
|
||||||
party-appointed and co-arbitration** — and `AGENTS.md` §4 Offerings carries a
|
stage it required.** *"It existed only because Q.Arb was in progress. There
|
||||||
row for each `[verified 2026-08-26 — Pouya]`. The page states that alongside
|
is no stage left to disclose."* The page's whole credentialing-stage section
|
||||||
the credentialing stage: Q.Arb commenced August 2026, C.Med-Arb is the
|
is deleted, its `<h1>` no longer reads *"Available now, and open about the
|
||||||
endpoint. §4 Offerings: **neither half may be dropped.** Honesty here is a
|
stage"*, and **no residue of the condition may be reintroduced** — a page
|
||||||
differentiator, not a weakness — and misstating it in either direction is a
|
that offers arbitration and then reaches for something qualifying to say is
|
||||||
conduct problem.
|
reproducing it from memory. **What survives:** all three forms — sole,
|
||||||
|
party-appointed and co-arbitration — are offered now, with an `AGENTS.md` §4
|
||||||
|
Offerings row for each `[verified 2026-08-26 — Pouya]`, and the scope is
|
||||||
|
**commercial** (Q39, Pouya's own choice, never part of this condition).
|
||||||
*(This paragraph read "(co-arbitration, co-arbitration)" until 2026-08-26 —
|
*(This paragraph read "(co-arbitration, co-arbitration)" until 2026-08-26 —
|
||||||
edited without being re-read — and then carried a caveat against Q36 for
|
edited without being re-read — and then carried a caveat against Q36 for
|
||||||
several hours after Q36 closed. Both are recorded because the pattern is the
|
several hours after Q36 closed. Both are recorded because the pattern is the
|
||||||
same one: an edit that was not re-read against the register.)*
|
same one: an edit that was not re-read against the register.)*
|
||||||
6. Fees, booking.
|
6. Fees, booking.
|
||||||
|
|
||||||
|
⚠️ **No first-person conduct commitment on this page either.** The Rules and
|
||||||
|
Awards sections say what a process and an award *should* settle and contain, not
|
||||||
|
what Pouya undertakes to do — **Q54(e)** and **(f)** are drafted and unruled, and
|
||||||
|
both shipped here in the first person for one pass. The `<title>` names a
|
||||||
|
**service**, never "Sole Arbitrator": §4 grants exactly one practised role and it
|
||||||
|
is "Mediator".
|
||||||
|
|
||||||
### `/med-arb/`
|
### `/med-arb/`
|
||||||
|
|
||||||
**Job:** own a term few Canadian neutrals explain well, and frame the C.Med-Arb
|
**Job:** own a term few Canadian neutrals explain well. *(This read "and frame
|
||||||
endpoint.
|
the C.Med-Arb endpoint" until 2026-08-29; C.Med-Arb is off the site.)*
|
||||||
**Search intent:** `med-arb Canada`, `what is med-arb`, `arb-med`.
|
**Search intent:** `med-arb Canada`, `what is med-arb`, `arb-med`.
|
||||||
|
|
||||||
1. What Med-Arb is; how it differs from Arb-Med.
|
1. What Med-Arb is; how it differs from Arb-Med.
|
||||||
|
⚠️ *As built, the page flags the confusion and does **not define arb-med***.
|
||||||
|
No source for a definition of arb-med is committed, and this repository does
|
||||||
|
not publish a definition of a third party's process from recall (R14). The
|
||||||
|
page says only that the two are one syllable apart, that the processes are
|
||||||
|
not interchangeable, and that a reader should check which one their contract
|
||||||
|
names — against the rule set the contract adopts, not against this page. **It
|
||||||
|
states no differentia at all**, because on a page that defines med-arb as
|
||||||
|
mediation→arbitration, "the phases run in a different order" *is* a
|
||||||
|
definition of arb-med by inversion. That wording shipped for one pass and
|
||||||
|
`claims-auditor` caught it. Define arb-med when a source is committed, or add
|
||||||
|
an **Arb-Med** row to `AGENTS.md` §11 — not before.
|
||||||
2. The procedural fairness objection, addressed head-on rather than elided.
|
2. The procedural fairness objection, addressed head-on rather than elided.
|
||||||
|
⚠️ *The page answers it at the level of **process design** — what a med-arb
|
||||||
|
agreement has to settle before the mediation phase begins. It does **not**
|
||||||
|
carry Pouya's own protocol commitments, which are claims about his practice
|
||||||
|
with no §4 row.* Three are drafted for his approval in **Q54**; the section is
|
||||||
|
incomplete until he rules.
|
||||||
3. When it fits and when it does not.
|
3. When it fits and when it does not.
|
||||||
4. The C.Med-Arb designation and why it is the practice's stated endpoint.
|
4. ~~The C.Med-Arb designation and why it is the practice's stated endpoint.~~
|
||||||
|
⚠️ **STRUCK 2026-08-29 — Pouya. C.Med-Arb is out entirely, and this is a
|
||||||
|
deliberate deviation from the strategy brief**, which made it *"the explicit
|
||||||
|
long-term professional narrative"*. His reasoning: *"Pouya holds Q.Med and
|
||||||
|
Q.Arb, which is what med-arb requires. The page says he conducts med-arb and
|
||||||
|
stops."* The section is **rewritten, not edited** — it earned its credibility
|
||||||
|
from an incomplete credential and now says what med-arb demands of one
|
||||||
|
neutral. ⚠️ **AND IT NO LONGER RESTATES THE DESIGNATIONS EITHER —
|
||||||
|
2026-09-02, `claims-auditor` D20 pass, finding F.** §Why this practice held a
|
||||||
|
bare `DESIGNATIONS_HELD_LINE` sentence sitting directly beneath §Rules'
|
||||||
|
quotation of ADRIC requiring *"a high level of practitioner competence"*, so
|
||||||
|
the designations read as meeting a bar ADRIC answers with the Chartered
|
||||||
|
Med-Arb — struck from this site entirely. The section now makes **no
|
||||||
|
credential claim at all**; `/about/` publishes them and the JSON-LD carries
|
||||||
|
them. Do not reinstate one here.
|
||||||
|
5. **The ADRIC Med-Arb Rules**, which this spec did not mention and which exist:
|
||||||
|
~~a published rule set, presented to ADRIC's membership~~ — **CORRECTED
|
||||||
|
2026-09-02, `claims-auditor` D20 pass, finding 6.** The source says a
|
||||||
|
**discussion draft** was presented, not the rules; `adric-rules.md` Finding 2
|
||||||
|
records no adoption and no effective date, in deliberate contrast to the
|
||||||
|
Arbitration Rules ("effective March 1, 2025") and the Mediation Rules ("the
|
||||||
|
existing rules remain in effect"). The page was corrected on 2026-09-01 and
|
||||||
|
**this line still instructed the struck form for a day** — the Q.Arb lesson
|
||||||
|
exactly: page fixed, spec still telling the next implementer to write the
|
||||||
|
false version. Write it as **presented to the membership as a discussion
|
||||||
|
draft at ADRIC's 2019 annual conference** — drafted for domestic commercial
|
||||||
|
disputes and designed to work with its Mediation and Arbitration Rules.
|
||||||
|
Sourced: `docs/reference/adric-rules.md` Finding 2. Quote ADRIC's own
|
||||||
|
framing rather than paraphrasing it, and **keep its superlatives inside the
|
||||||
|
quotation marks** — Finding 5 lists the two not to lift.
|
||||||
|
|
||||||
This page is a strong candidate for the best-performing page on the site.
|
This page is a strong candidate for the best-performing page on the site.
|
||||||
Search demand exists, competition is thin, and it maps exactly to the brand's
|
Search demand exists and competition is thin. *(This closed "and it maps exactly
|
||||||
long-term narrative.
|
to the brand's long-term narrative" — the C.Med-Arb narrative struck thirteen
|
||||||
|
lines above on 2026-08-29. The two reasons that survive are the two that were
|
||||||
|
ever measurable.)*
|
||||||
|
|
||||||
### `/practice/` — index
|
### `/practice/` — index
|
||||||
|
|
||||||
Six cards, one paragraph each, linking onward. Also the natural home for the
|
Six cards, one paragraph each, linking onward. Also the natural home for the
|
||||||
"also offered" strip: **early neutral evaluation, dispute-system design, and
|
"also offered" strip: **early neutral evaluation, dispute-system design, and
|
||||||
pre-dispute technical advisory** — three, and each now has an `AGENTS.md` §4
|
pre-dispute technical advisory** — three, and each has an `AGENTS.md` §4
|
||||||
Offerings row, which is what the strip needs before it may ship.
|
Offerings row.
|
||||||
|
|
||||||
|
> ✅ **THE STRIP MAY SHIP — Q46(a) closed 2026-08-28, on Pouya's second
|
||||||
|
> ruling.** All three §4 Offerings rows now read **PUBLISHABLE**. Read the ENE
|
||||||
|
> row before writing the strip: gate 1 (*is the activity gated?*) is closed as
|
||||||
|
> **`[Pouya's stated basis 2026-08-28]`** and **not** as `[verified]`, because no
|
||||||
|
> source for any of the three exists in `docs/reference/`. That is a deliberate,
|
||||||
|
> attributed position of the architect's, not a sourced fact — do not describe it
|
||||||
|
> on the page or in a comment as settled law.
|
||||||
|
>
|
||||||
|
> **Two framing constraints survive the ruling and are not discretionary:**
|
||||||
|
>
|
||||||
|
> 1. **ENE is an assessment delivered to BOTH parties**, never advice to one. It
|
||||||
|
> is the offering nearest §4's NOT-NEGOTIABLE boundary — a neutral assessment
|
||||||
|
> of the *merits* sits closest to *"providing legal services"*.
|
||||||
|
> 2. **Pre-dispute advisory carries a conflict caution**, and it is practice
|
||||||
|
> management rather than a publication gate: advisory work for one
|
||||||
|
> organisation can conflict against a later appointment in the same matter.
|
||||||
|
> **No copy may imply the offering is free of that tension** — this strip is
|
||||||
|
> where the temptation to imply it will arise.
|
||||||
|
|
||||||
> **`settlement counsel` IS STRUCK FROM THIS STRIP AND MUST NOT BE RESTORED.**
|
> **`settlement counsel` IS STRUCK FROM THIS STRIP AND MUST NOT BE RESTORED.**
|
||||||
> `AGENTS.md` Q42, 2026-08-27. Pouya struck it as his own error in this document:
|
> `AGENTS.md` Q42, 2026-08-27. Pouya struck it as his own error in this document:
|
||||||
@@ -294,10 +486,40 @@ practice.
|
|||||||
**Search intent:** `Bill 40 dispute`, `IESO dispute resolution`,
|
**Search intent:** `Bill 40 dispute`, `IESO dispute resolution`,
|
||||||
`OEB leave to construct dispute`, `grid connection dispute Ontario`.
|
`OEB leave to construct dispute`, `grid connection dispute Ontario`.
|
||||||
|
|
||||||
Grid connection and allocation, leave-to-construct, proponent–municipality
|
Connection assessment and approval, leave to construct, proponent–municipality
|
||||||
disputes, IESO market participation, data-centre connection allocation. Brief
|
disputes, IESO market participation, and the large-load / data-centre connection
|
||||||
§III.2 frames this as a 24–36 month build. **Write it as a genuine position, not
|
regime. Brief §III.2 frames this as a 24–36 month build. **Write it as a genuine
|
||||||
a claim of existing volume.**
|
position, not a claim of existing volume.**
|
||||||
|
|
||||||
|
> ⚠️ **"CONNECTION ALLOCATION" WAS THIS SECTION'S WORDING AND IT IS NOT AN
|
||||||
|
> ONTARIO TERM. Corrected 2026-08-29**, against the IESO's own pages
|
||||||
|
> (`docs/reference/ontario-energy-regulatory.md`), which contain **zero**
|
||||||
|
> occurrences of "allocation" of any kind across the connection process. It also
|
||||||
|
> reached `src/data/site.ts` and shipped in the six-card grid on three pages.
|
||||||
|
>
|
||||||
|
> **Use the terms these bodies use:** the IESO's own words are *"the IESO's and
|
||||||
|
> transmitter's connection assessment and approval (CAA) process"*, within which
|
||||||
|
> the IESO performs a *System Impact Assessment (SIA)* and the transmitter a
|
||||||
|
> *Customer Impact Assessment (CIA)*. ⚠️ **This read "CAA is the umbrella" until
|
||||||
|
> 2026-09-03** — which is the extract's own COMMENTARY, not the IESO's, and
|
||||||
|
> `CLAUDE.md` is explicit that commentary around a quotation is this
|
||||||
|
> repository's voice. The pages took the attribution from here and gave the
|
||||||
|
> process to the IESO alone. **And it is the CONNECTION PROCESS that runs to
|
||||||
|
> *up to* six stages, not the CAA** — the source scopes the count by connection
|
||||||
|
> type, and CAA is stage 2 of that process rather than a name for it. Naming the
|
||||||
|
> wrong subject here is how the conflation reaches a page. **Ontario has no
|
||||||
|
> interconnection queue** — the IESO says so in terms and works from "committed
|
||||||
|
> projects" instead, so "our place in the queue" describes nothing. The
|
||||||
|
> genuinely adjacent term, the OEB's *Capacity Allocation Model* in the
|
||||||
|
> Distribution System Code, is about housing-development connections and is a
|
||||||
|
> different thing.
|
||||||
|
>
|
||||||
|
> **Bill 40 is identified**, and the identification was not safe to assume: it
|
||||||
|
> is Bill 40 of the **44th Parliament, 1st Session — the Protect Ontario by
|
||||||
|
> Securing Affordable Energy for Generations Act, 2025**, Royal Assent
|
||||||
|
> 11 December 2025. Bill numbers are reused every parliament and most Ontario
|
||||||
|
> Bill 40s are unrelated to energy (43-1 is a highway-traffic bill). Cite the
|
||||||
|
> parliament and session, never the bare number.
|
||||||
|
|
||||||
### `/practice/insurance/`
|
### `/practice/insurance/`
|
||||||
|
|
||||||
@@ -316,22 +538,38 @@ a claim of existing volume.**
|
|||||||
> - **LAT Rule 2.4:** *"'Case Conference' has the same meaning as 'Pre-Hearing
|
> - **LAT Rule 2.4:** *"'Case Conference' has the same meaning as 'Pre-Hearing
|
||||||
> Conference' as defined in the SPPA."* **"Pre-hearing" is the Tribunal's own
|
> Conference' as defined in the SPPA."* **"Pre-hearing" is the Tribunal's own
|
||||||
> label**, and what it labels is a case conference.
|
> label**, and what it labels is a case conference.
|
||||||
> - **Rule 14.3:** a **Member** presides and is then disqualified from the
|
> - **Rule 14.3:** a **Member** presides and does not then sit on the hearing
|
||||||
> hearing panel; **Rule 14.6:** parties must attend. The neutral is the
|
> panel except with the consent of the parties; **Rule 14.6:** parties must
|
||||||
> Tribunal's. A privately retained one is not appointed to it and cannot be.
|
> attend. The neutral is the Tribunal's. A privately retained one is not
|
||||||
|
> appointed to it and cannot be.
|
||||||
> - The LAT Rules contain **zero** occurrences of `mediat` or `arbitrat` —
|
> - The LAT Rules contain **zero** occurrences of `mediat` or `arbitrat` —
|
||||||
> 0 in 66,593 characters. The concept is not in them.
|
> 0 in 66,593 characters. The concept is not in them.
|
||||||
> - The LAT-AABS page itself, though, says: *"Before you apply to the LAT-AABS,
|
> - The LAT-AABS page itself, though, says: *"Before you apply to the LAT-AABS,
|
||||||
> you may want to consider negotiation or mediation services… including before
|
> you may want to consider negotiation or mediation services."*
|
||||||
> filing at the LAT-AABS, and continuing… after a claim has been filed."*
|
> **That is the affirmative basis for the offering, in the Tribunal's words** —
|
||||||
> **That is the affirmative basis for the offering, in the Tribunal's words.**
|
> and it is the whole of it.
|
||||||
|
>
|
||||||
|
> ⚠️ **THE QUOTATION ABOVE WAS ELLIPSED, AND THE ELLIPSIS REMOVED THE WORD THAT
|
||||||
|
> SCOPED IT. Corrected 2026-08-29.** It read *"…consider negotiation or
|
||||||
|
> mediation services… including before filing at the LAT-AABS, and continuing…
|
||||||
|
> after a claim has been filed."* The Tribunal's second sentence is
|
||||||
|
> *"Parties are encouraged to attempt to **negotiate** the claim at all times,
|
||||||
|
> including before filing at the LAT-AABS, and continuing **negotiation**
|
||||||
|
> discussions after a claim has been filed."* — **negotiation, twice.** The
|
||||||
|
> second ellipsis deleted the second "negotiation" and made a sentence about
|
||||||
|
> negotiation read as one about mediation. The full passage is quoted verbatim
|
||||||
|
> in `docs/reference/lat-case-conference.md`, which now carries the correction
|
||||||
|
> and the reason it survived three checks.
|
||||||
>
|
>
|
||||||
> **The page must state that the mediation offered is PRIVATE, retained by the
|
> **The page must state that the mediation offered is PRIVATE, retained by the
|
||||||
> parties, and is not the Tribunal's case conference.** Published blurb:
|
> parties, and is not the Tribunal's case conference.** Published blurb:
|
||||||
> *"Accident benefits and SABS entitlement, MIG disputes, and private mediation
|
> *"Accident benefits and SABS entitlement, MIG disputes, and private mediation
|
||||||
> alongside a LAT application, before filing or after."* If Pouya holds a roster
|
> retained by the parties, not the Tribunal's case conference."* **Do not
|
||||||
> position that makes more than that true, it is a §4 addition — absent a row,
|
> restore the "before filing or after" form** — it rested on the elided half.
|
||||||
> it is not.
|
> The page may quote the Tribunal's actual sentence, which supports mediation
|
||||||
|
> **before an application**; it may not attribute the after-filing frame to the
|
||||||
|
> Tribunal. If Pouya holds a roster position that makes more than that true, it
|
||||||
|
> is a §4 addition — absent a row, it is not.
|
||||||
|
|
||||||
Highest realistic near-term volume — it flows directly from the existing
|
Highest realistic near-term volume — it flows directly from the existing
|
||||||
personal-injury and SABS work, and brief §IV.7 notes the segment is
|
personal-injury and SABS work, and brief §IV.7 notes the segment is
|
||||||
@@ -389,9 +627,11 @@ commitments, the first matter that slips makes the page false."*
|
|||||||
**Unblocked — `AGENTS.md` Q4/Q14 answered (D14). Build from the confirmed card
|
**Unblocked — `AGENTS.md` Q4/Q14 answered (D14). Build from the confirmed card
|
||||||
in `docs/07-fees.md`; still do not invent numbers.**
|
in `docs/07-fees.md`; still do not invent numbers.**
|
||||||
|
|
||||||
Hourly rate; half-day and full-day mediation; preparation time policy;
|
Hourly rate; half-day and full-day mediation; **med-arb, billed by phase and
|
||||||
cancellation terms; administrative fee; HST treatment; who pays and how costs
|
carrying no figure of its own** (added 2026-09-03, `docs/07` §Med-arb, INTERIM
|
||||||
are shared between parties; payment terms. A real page with real numbers, or a
|
against R5 — the page shipped it and this outline did not name it); preparation
|
||||||
|
time policy; cancellation terms; administrative fee; HST treatment; who pays and
|
||||||
|
how costs are shared between parties; payment terms. A real page with real numbers, or a
|
||||||
`TODO(pouya)` — nothing in between.
|
`TODO(pouya)` — nothing in between.
|
||||||
|
|
||||||
### `/for-parties/`
|
### `/for-parties/`
|
||||||
@@ -399,8 +639,15 @@ are shared between parties; payment terms. A real page with real numbers, or a
|
|||||||
**Job:** serve the self-represented tier without diluting the counsel-facing
|
**Job:** serve the self-represented tier without diluting the counsel-facing
|
||||||
pages. Plain language, short sentences, no jargon.
|
pages. Plain language, short sentences, no jargon.
|
||||||
|
|
||||||
What mediation is · what it is not · who the mediator is and is not (not your
|
What mediation is · what it is not · who the mediator is and is not —
|
||||||
lawyer, not a judge) · what happens on the day · what it costs · what happens if
|
**render `NEUTRAL_ROLE_LINE`, and read `docs/03` §For parties before writing a
|
||||||
|
word of it.** ⚠️ **THIS LINE READ *"(not your lawyer, not a judge)"* UNTIL
|
||||||
|
2026-08-30 AND IT INSTRUCTED A FORM STRUCK ON 2026-08-28.** *"Not your lawyer"*
|
||||||
|
**presupposes lawyer status**, which §4 Forbidden bars and D13 treats as hard as
|
||||||
|
assertion — a negation still carries the presupposition. `docs/03` struck it
|
||||||
|
with that reasoning two days earlier and the sweep never reached this file;
|
||||||
|
build step 6 is the step that consumes this line, and it was found here by
|
||||||
|
`adversarial-reviewer` while the page itself avoided the trap · what happens on the day · what it costs · what happens if
|
||||||
you do not settle · how to prepare.
|
you do not settle · how to prepare.
|
||||||
|
|
||||||
### `/insights/` and `/insights/[slug]/`
|
### `/insights/` and `/insights/[slug]/`
|
||||||
@@ -464,8 +711,28 @@ Dependency-ordered, so nothing is blocked mid-stream:
|
|||||||
4. `/mediation/`, `/arbitration/`, `/med-arb/`
|
4. `/mediation/`, `/arbitration/`, `/med-arb/`
|
||||||
5. `/practice/` and the six area pages
|
5. `/practice/` and the six area pages
|
||||||
6. `/process/`, `/for-parties/`
|
6. `/process/`, `/for-parties/`
|
||||||
7. `/insights/` plumbing, then the drafted articles
|
7. ✅ `/insights/` plumbing, then the drafted articles — **built 2026-08-31.**
|
||||||
8. `/contact/` and the intake backend
|
Step 7a returned Lighthouse (`AGENTS.md` §7, R11); 7b built the OG card
|
||||||
9. `/fees/` — last, though no longer blocked: D14 confirmed the card
|
generator (R15) and the Insights routes; 7c drafted the five launch articles.
|
||||||
10. `/legal/*` — written to match the backend as actually built
|
**The section is not live and cannot be**: D9 and `src/content.config.ts`
|
||||||
11. Audit and cutover (`06-deployment.md`)
|
between them mean an article publishes only when Pouya sets both flags, and
|
||||||
|
`SiteHeader` keeps Insights out of the nav until two are live
|
||||||
|
8. ✅ `/contact/` — **the page is built and the pipe behind it is LIVE as of
|
||||||
|
2026-09-02.** The handler is deployed and the CloudFront `/api/*` behaviour is
|
||||||
|
in place; `AGENTS.md` §7 holds the state and this list does not restate it.
|
||||||
|
`docs/05` §Build step 8 records three deliberate deviations from that spec, and
|
||||||
|
§Observed abuse records the first real spam and what was added for it.
|
||||||
|
⚠️ **This item read *"the pipe behind it is not"* until 2026-09-04** — written
|
||||||
|
under D11, true then, and left asserting an undeployed backend for two days
|
||||||
|
after cutover
|
||||||
|
9. ✅ `/fees/` — **built 2026-08-31 on Q59's ruling**, which settled where the
|
||||||
|
overtime hour starts (the session cap) and supplied the reservation point that
|
||||||
|
answers the rate card's arithmetic anomaly. The PDF bio shipped with it (R16)
|
||||||
|
10. ✅ `/legal/privacy/` and `/legal/terms/` — **built 2026-08-31, written to the
|
||||||
|
backend as actually built.** Three of the privacy policy's statements are
|
||||||
|
DERIVED — the collected-data list from `INTAKE_FIELDS`, the retention period
|
||||||
|
from the handler's own figure, the analytics paragraph from
|
||||||
|
`ANALYTICS.installed` — so they cannot drift from the implementation
|
||||||
|
11. Audit and cutover (`06-deployment.md`) — **not started. Nothing is deployed.**
|
||||||
|
`claims-auditor`'s single pass over the whole finished site (D20) is a blocking
|
||||||
|
item there and has not run
|
||||||
|
|||||||
+953
-14
File diff suppressed because it is too large
Load Diff
+287
-37
@@ -13,6 +13,47 @@ table before writing any number, statistic, or superlative. If you need a fact
|
|||||||
you do not have, write `TODO(pouya): <exact question>` and log it in §9. Do not
|
you do not have, write `TODO(pouya): <exact question>` and log it in §9. Do not
|
||||||
infer, do not soften, do not carry anything over from the old site.
|
infer, do not soften, do not carry anything over from the old site.
|
||||||
|
|
||||||
|
### When a fact is `[unestablished]`, write around the capacity question
|
||||||
|
|
||||||
|
**Ratified by Pouya 2026-08-28 as a reusable pattern, with the instruction that
|
||||||
|
it will recur.** §4 records licence status as `[unestablished]` and says: *do not
|
||||||
|
assert it, do not deny it.* Copy that addresses the question at all must
|
||||||
|
therefore pick neither side — and the trap is that **both obvious phrasings pick
|
||||||
|
one.**
|
||||||
|
|
||||||
|
The worked example, which took three attempts and two audits:
|
||||||
|
|
||||||
|
| Attempt | Copy | Why it failed |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | "I **do not** give legal advice" | *"Do not"* is an **election** — it implies entitlement withheld by choice. Flagged by audit 1 |
|
||||||
|
| 2 | "I **cannot** give legal advice" | *"Cannot"* is a **denial** of capacity. Flagged by audit 2 |
|
||||||
|
| 3 ✅ | "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." | Makes **no capacity claim at all** |
|
||||||
|
|
||||||
|
**Both audits were right, and that is why the third version works.** Attempts 1
|
||||||
|
and 2 are opposite answers to the same question, so flagging both is not
|
||||||
|
inconsistency — it is the correct finding twice. Pouya: *"'do not' elects,
|
||||||
|
'cannot' denies, and §4 says do neither. The shipped sentence makes no capacity
|
||||||
|
claim at all — it describes the role and points the reader somewhere useful."*
|
||||||
|
|
||||||
|
**The pattern, stated for reuse:**
|
||||||
|
|
||||||
|
> Do not look for a *softer* way to answer the capacity question. Any word that
|
||||||
|
> answers it — *cannot, do not, am not, is not permitted, is not qualified* —
|
||||||
|
> takes a side of a fact this register does not hold. Instead write the sentence
|
||||||
|
> so the question **does not arise**: state the **role**, state the
|
||||||
|
> **consequence** for the reader, and stop.
|
||||||
|
|
||||||
|
Three tests before shipping such a sentence. It fails if any is yes:
|
||||||
|
|
||||||
|
1. Could a reader infer from it that he **is** entitled to do the thing?
|
||||||
|
2. Could a reader infer from it that he is **not**?
|
||||||
|
3. Does it contain a verb of capacity or permission attached to him at all?
|
||||||
|
|
||||||
|
**Where this will come up next:** `/practice/` (step 5) and `/for-parties/`, both
|
||||||
|
of which have to tell an unrepresented party what the neutral will and will not
|
||||||
|
do for them — the exact place the "cannot" phrasing feels most natural and is
|
||||||
|
most wrong.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Voice
|
## Voice
|
||||||
@@ -38,13 +79,73 @@ detect padding instantly and discount everything after it.
|
|||||||
|
|
||||||
The shape of the bullet still stands, so here is a sentence that fits it and
|
The shape of the bullet still stands, so here is a sentence that fits it and
|
||||||
clears the register: *"I accept sole, party-appointed and co-arbitration
|
clears the register: *"I accept sole, party-appointed and co-arbitration
|
||||||
appointments. The Q.Arb designation commenced in August 2026; C.Med-Arb is the
|
appointments **in commercial matters**."* ⚠️ **THE SECOND SENTENCE IS STRUCK
|
||||||
endpoint."* The limit being named is the **stage of the arc**, stated plainly —
|
(2026-08-29).** It read *"The Q.Arb **pathway** commenced in August 2026;
|
||||||
Pouya's instruction is that being open about it is the differentiator, so do
|
C.Med-Arb is the endpoint"*; Q.Arb is held, C.Med-Arb is off the site, and §4's
|
||||||
not hedge it into vagueness and do not drop it. (**No tribunal-secretary
|
paired-disclosure condition that required a second sentence at all is
|
||||||
work** — D14
|
dissolved. **The limit this bullet names is now the SCOPE, not the stage** —
|
||||||
removed the rate and bars offering it; see `docs/07-fees.md`.) Precision about
|
which is the durable half. **"in commercial matters" is not
|
||||||
what you cannot yet do makes the rest believable.
|
optional** — every §4 Offerings arbitration row is scoped commercial, family
|
||||||
|
arbitration has its own NOT OFFERED row, and §4's NOT-NEGOTIABLE boundary
|
||||||
|
records the unscoped class form as the struck universal (Q39). This model
|
||||||
|
sentence was unscoped until 2026-08-28 while the shipped page it feeds was
|
||||||
|
scoped; found by `claims-auditor` on re-audit, one file over from the same
|
||||||
|
defect fixed in `docs/04` earlier the same day.
|
||||||
|
|
||||||
|
⚠️ **AND THE MEDIATION HALF IS THE OPPOSITE — Q56, ruled by Pouya 2026-08-30.
|
||||||
|
DO NOT MIRROR THIS SCOPE ONTO MEDIATION.** The scope above exists because of a
|
||||||
|
**legal gate**: family arbitration in Ontario requires prescribed training, so
|
||||||
|
"in commercial matters" is load-bearing there. **Mediation has no such gate,
|
||||||
|
§4's mediation row is unscoped on purpose.** **He mediates in all six published
|
||||||
|
practice areas** — construction, technology, energy, insurance, shareholder,
|
||||||
|
cross-cultural — and §4's row now enumerates them (`PRACTICE_AREAS` in
|
||||||
|
`src/data/site.ts`), each named under **Q35(c)'s subject-matter publication
|
||||||
|
gate**. ⚠️ **The six are the PUBLISHED areas, not the authorised
|
||||||
|
subject-matter list, and Q35(c)'s gate is not spent by having been used six
|
||||||
|
times.** A seventh may be named where it clears that gate.
|
||||||
|
|
||||||
|
*(Q56's ruling had named five, which was four of the six areas plus the word
|
||||||
|
"commercial", and this block called the list "illustrative of breadth, not a
|
||||||
|
closed set" for one day. Pouya struck the hedge on 2026-08-31: "the register
|
||||||
|
should match the site; 'illustrative' is a hedge covering a gap that has a
|
||||||
|
correct value." Technology and energy are the two that were missing. The
|
||||||
|
non-exhaustiveness clause was struck with the hedge in the same pass and
|
||||||
|
restored the same day — his ruling supplied a correct value, it did not close
|
||||||
|
the class.)*
|
||||||
|
|
||||||
|
The site-wide *"Commercial Mediation"* framing was **under-describing the
|
||||||
|
offering**, and it was corrected rather than ratified as positioning. The
|
||||||
|
asymmetry between the two halves is designed; a later editor tidying them into
|
||||||
|
a matching pair would be reintroducing the defect.
|
||||||
|
|
||||||
|
⚠️ **AND DO NOT TREAT ANY LIST OF THE CORRECTED SURFACES AS COMPLETE.** Q56
|
||||||
|
named four. The sweep that implemented it changed **thirteen shipped strings
|
||||||
|
across five files**, and `adversarial-reviewer` then found **three more it had
|
||||||
|
missed** — the worst on `/practice/`, where *"These describe the process the
|
||||||
|
parties are choosing between, in commercial matters"* scoped mediation with
|
||||||
|
the two words never appearing in the same element, so no proximity grep could
|
||||||
|
reach it. The command, and its limit:
|
||||||
|
|
||||||
|
```
|
||||||
|
git grep -nEi 'commercial[^.]{0,60}mediat|mediat[^.]{0,60}commercial' -- src/
|
||||||
|
```
|
||||||
|
|
||||||
|
That finds the **adjacent** form only. For the split form there is no pattern —
|
||||||
|
read every occurrence of `commercial` in `src/` and in `dist/`, and ask what
|
||||||
|
each one is scoping.
|
||||||
|
|
||||||
|
⚠️ **TEN LINES WERE DELETED HERE ON 2026-08-30, AND THE DELETION IS THE
|
||||||
|
POINT.** They read *"'Pathway', never 'designation'"*, cited the **struck**
|
||||||
|
Forbidden row as live, instructed that *"the limit being named is the stage of
|
||||||
|
the arc, stated plainly"*, and closed *"Precision about what you cannot yet do
|
||||||
|
makes the rest believable."* Every one of those directs an implementer to
|
||||||
|
write the form §4 now bars — **eleven lines below this bullet's own strike
|
||||||
|
notice**, so one bullet said both things. The Q25 shape, in the copy deck an
|
||||||
|
implementer is told to lift verbatim. **The 2026-08-29 sweep missed it because
|
||||||
|
`git grep -nE 'Q\.?Arb'` is line-anchored and not one of those ten lines
|
||||||
|
contained the string.** Found by `claims-auditor` on the round-2 audit.
|
||||||
|
(**No tribunal-secretary work** — D14 removed the rate and bars offering it;
|
||||||
|
see `docs/07-fees.md`.)
|
||||||
- Plain words over Latin. "Without prejudice" survives because it is a term of
|
- Plain words over Latin. "Without prejudice" survives because it is a term of
|
||||||
art; *inter alia* does not.
|
art; *inter alia* does not.
|
||||||
|
|
||||||
@@ -58,8 +159,12 @@ detect padding instantly and discount everything after it.
|
|||||||
**The approved phrasing is "active litigation exposure" or "involvement in
|
**The approved phrasing is "active litigation exposure" or "involvement in
|
||||||
litigation and ADR matters" — never "practice" in that context.** Pouya's
|
litigation and ADR matters" — never "practice" in that context.** Pouya's
|
||||||
wording, 2026-08-26. So: *Director of Firm Operations at a Toronto litigation
|
wording, 2026-08-26. So: *Director of Firm Operations at a Toronto litigation
|
||||||
and ADR boutique, with active exposure to construction, personal injury, POA,
|
and ADR boutique, with **active litigation exposure** across construction,
|
||||||
and SABS matters.* Accurate, specific, and it claims nothing it should not.
|
personal injury, POA, and SABS matters.* Accurate, specific, and it claims
|
||||||
|
nothing it should not. *(This example read "with active exposure to" until
|
||||||
|
2026-08-28 — a **third** variant of the phrasing, three lines below the
|
||||||
|
sentence naming the only two approved forms, in the one place where the wording
|
||||||
|
IS the compliance. Found by `claims-auditor` on re-audit.)*
|
||||||
|
|
||||||
This framing is **interim** — see `AGENTS.md` §12 R1. Raise it with Pouya
|
This framing is **interim** — see `AGENTS.md` §12 R1. Raise it with Pouya
|
||||||
rather than letting it settle in by default.
|
rather than letting it settle in by default.
|
||||||
@@ -83,7 +188,7 @@ detect padding instantly and discount everything after it.
|
|||||||
Reused, adapted, across the hero, the About page, and the PDF bio:
|
Reused, adapted, across the hero, the About page, and the PDF bio:
|
||||||
|
|
||||||
> The dispute resolution practice of Pouya Lajevardi — a credentialed neutral
|
> The dispute resolution practice of Pouya Lajevardi — a credentialed neutral
|
||||||
> who is also close to live litigation and a practising machine-learning and
|
> with active litigation exposure and a practising machine-learning and
|
||||||
> infrastructure engineer. Built for commercial, construction, energy,
|
> infrastructure engineer. Built for commercial, construction, energy,
|
||||||
> technology, and cross-cultural disputes that turn on the contract, the code,
|
> technology, and cross-cultural disputes that turn on the contract, the code,
|
||||||
> the engineering documents, and the regulatory overlay around them.
|
> the engineering documents, and the regulatory overlay around them.
|
||||||
@@ -174,17 +279,18 @@ session that wrote them. Consume the constant on any page that needs the
|
|||||||
sentence. Do not retype it, do not paraphrase it, and do not quote a variant of
|
sentence. Do not retype it, do not paraphrase it, and do not quote a variant of
|
||||||
it in a spec.
|
it in a spec.
|
||||||
|
|
||||||
Fourth slot where the layout has one: **Q.Arb — commenced August 2026.** Use
|
Fourth slot where the layout has one: **`Q.Arb` over `ADRIC / ADRIO designation`**
|
||||||
that wording, not "in progress": §4 pins it, and the weaker form drifts toward
|
— the label is byte-identical to the Q.Med slot's, because the two render side by
|
||||||
"nearly complete", which §4 Forbidden bars outright.
|
side and any difference reads as a distinction being drawn. ⚠️ **AMENDED 2026-08-29.** It read *"Q.Arb — commenced August
|
||||||
|
2026"*, and every stage form — "commenced", "in progress", "pathway", "not yet" —
|
||||||
|
is now barred: Q.Arb is **held** (`AGENTS.md` §4), so a stage word understates a
|
||||||
|
held credential. **The acquisition date is recorded in §4 and is not published.**
|
||||||
|
|
||||||
**On the home page the fourth slot IS used, and it is not optional there.**
|
**The fourth slot is no longer MANDATORY anywhere.** It was, on `/`, under §4's
|
||||||
`docs/01` §`/` says "Three slots"; §4's paired-disclosure condition is the higher
|
paired-disclosure condition — that page says *arbitrator* in its opening
|
||||||
authority and requires that wherever the site offers arbitration it "states
|
sentence and the stage had to appear beside the offering. The condition is
|
||||||
plainly" the stage of the arc. `/` says *arbitrator* in its opening sentence, so
|
dissolved. `/` keeps the slot because §4's substitution principle wants a fourth
|
||||||
the stage belongs on the same page rather than only in the footer. Rendered as
|
credential where the layout has one, not because anything requires it.
|
||||||
value `Q.Arb` over label `Commenced August 2026` — the same wording, with the
|
|
||||||
em-dash carried by the layout instead of by the string.
|
|
||||||
|
|
||||||
The substitution principle (`AGENTS.md` §4): wherever the design wants a "how
|
The substitution principle (`AGENTS.md` §4): wherever the design wants a "how
|
||||||
many", substitute a longer-arc credential. These are all true at launch and stay
|
many", substitute a longer-arc credential. These are all true at launch and stay
|
||||||
@@ -213,17 +319,32 @@ redrawing the loop into a line.* First person: "my mark", not "our mark".
|
|||||||
400–600 words of narrative, then structured credentials. Tell the three tracks
|
400–600 words of narrative, then structured credentials. Tell the three tracks
|
||||||
as one arc, not three lists: a JD and active litigation exposure; a parallel
|
as one arc, not three lists: a JD and active litigation exposure; a parallel
|
||||||
career in machine learning and infrastructure engineering; a company run
|
career in machine learning and infrastructure engineering; a company run
|
||||||
alongside both. The arc is the point — the credentialing pathway from Q.Med
|
alongside the practice — §4's wording; "alongside both" was a concurrency claim
|
||||||
through Q.Arb to C.Med-Arb is stated openly as in progress. The brief treats
|
the register does not make.
|
||||||
that arc as part of the story rather than something to obscure.
|
|
||||||
|
⚠️ **THE CREDENTIALING ARC IS STRUCK (2026-08-29, Pouya), AND SO IS `/about/`'s
|
||||||
|
ARC SECTION.** This paragraph required *"the credentialing pathway from Q.Med
|
||||||
|
through Q.Arb to C.Med-Arb… stated openly, with Q.Arb described as commenced
|
||||||
|
August 2026"*. Q.Arb is held; C.Med-Arb is off the site; there is no pathway.
|
||||||
|
**The three-track story stays** — law, engineering, a company — because that arc
|
||||||
|
is about his working life, not about a credential. Only the credentialing arc
|
||||||
|
goes.
|
||||||
|
|
||||||
|
The designation names expand as **Qualified Mediator** and **Qualified
|
||||||
|
Arbitrator** — ADRIO's own forms, sourced in
|
||||||
|
`docs/reference/adrio-designations.md`. *(**Chartered Med-Arbitrator** was here
|
||||||
|
too; the designation is real and stays in `AGENTS.md` §11 as a definition, but
|
||||||
|
nothing on the site names it.)* Never "Chartered Mediator-Arbitrator".
|
||||||
|
|
||||||
Omit any section that would be empty. No "Speaking" heading until there is a
|
Omit any section that would be empty. No "Speaking" heading until there is a
|
||||||
talk to list.
|
talk to list.
|
||||||
|
|
||||||
### Mediation / Arbitration / Med-Arb
|
### Mediation / Arbitration / Med-Arb
|
||||||
Procedural, specific, unembellished. Name the rules. Describe the formats. State
|
Procedural, specific, unembellished. Name the rules. Describe the formats. State
|
||||||
what a party should expect to do and when. On `/arbitration/`, state the Q.Arb
|
what a party should expect to do and when. *(This closed with "On
|
||||||
position in plain terms — what is available now versus what follows designation.
|
`/arbitration/`, state the Q.Arb position in plain terms — what is available now
|
||||||
|
versus what follows designation." Struck 2026-08-29: Q.Arb is held, and nothing
|
||||||
|
follows designation.)*
|
||||||
|
|
||||||
`/med-arb/` addresses the procedural-fairness objection directly: the same
|
`/med-arb/` addresses the procedural-fairness objection directly: the same
|
||||||
neutral who heard a party's confidential caucus later decides the matter. Do not
|
neutral who heard a party's confidential caucus later decides the matter. Do not
|
||||||
@@ -233,8 +354,38 @@ Meeting the strongest objection is what makes the page worth reading.
|
|||||||
### Practice areas
|
### Practice areas
|
||||||
Each page: dispute types, why this practice fits, what the process looks like,
|
Each page: dispute types, why this practice fits, what the process looks like,
|
||||||
and the market context that makes the area live. Context comes from the strategy
|
and the market context that makes the area live. Context comes from the strategy
|
||||||
brief §III–IV — Ontario's megaproject pipeline, Bill 40 and grid connection, the
|
brief §III–IV — Ontario's megaproject pipeline, Bill 40 and grid connection,
|
||||||
2026 privacy statute, LAT volumes.
|
~~the 2026 privacy statute~~, LAT volumes.
|
||||||
|
|
||||||
|
> ⚠️ **"THE 2026 PRIVACY STATUTE" DOES NOT EXIST. Struck rather than corrected
|
||||||
|
> in place, so the phrase is not re-invented. Checked 2026-08-29; sourced in
|
||||||
|
> `docs/reference/canada-privacy-technology.md`.**
|
||||||
|
>
|
||||||
|
> **Nothing enacted federally or in Ontario in 2025 or 2026 is a privacy
|
||||||
|
> statute.** Bill C-27 — which would have enacted the Consumer Privacy
|
||||||
|
> Protection Act and the Artificial Intelligence and Data Act — **died without
|
||||||
|
> royal assent** when the 44th Parliament's first session ended, and was never
|
||||||
|
> reinstated. **PIPEDA remains the operative federal private-sector statute**,
|
||||||
|
> and **Canada has no federal AI statute.** The nearest real thing is federal
|
||||||
|
> **Bill C-36 (45-1)**, introduced 15 June 2026, which *would* enact the
|
||||||
|
> Protecting Privacy and Consumer Data Act — a bill, at second reading, not law.
|
||||||
|
>
|
||||||
|
> **Caught before it reached a page, and only because the phrase was checked
|
||||||
|
> rather than trusted.** Same failure mode as `docs/01`'s "Model Mediation
|
||||||
|
> Rules" and the LAT gloss corrected the same day: a spec naming an instrument
|
||||||
|
> from recall, and an implementer with no reason to doubt it. **Bill 40 in the
|
||||||
|
> same sentence turned out to be real** — Bill 40 of the 44th Parliament, 1st
|
||||||
|
> Session, the *Protect Ontario by Securing Affordable Energy for Generations
|
||||||
|
> Act, 2025* — but the number alone did not establish that, because bill numbers
|
||||||
|
> are reused every parliament. Cite the parliament and session.
|
||||||
|
>
|
||||||
|
> **What `/practice/technology/` publishes instead** is the real position, and
|
||||||
|
> it is better copy than the invented one: what is in force, what died, what is
|
||||||
|
> only a bill — and the genuinely useful part for a commercial audience, that
|
||||||
|
> **no Canadian statute requires personal data to be stored in Canada.** That is
|
||||||
|
> the assumption a great many data-residency clauses are drafted against, and it
|
||||||
|
> is quoted from the Privacy Commissioner's own guidance rather than concluded
|
||||||
|
> here.
|
||||||
|
|
||||||
**Frame as positioning, not as history.** "Built to facilitate procurement and
|
**Frame as positioning, not as history.** "Built to facilitate procurement and
|
||||||
subcontract disputes on Ontario's megaproject pipeline" — not "extensive
|
subcontract disputes on Ontario's megaproject pipeline" — not "extensive
|
||||||
@@ -272,9 +423,26 @@ evasions, no "contact for pricing" after promising a rate card.
|
|||||||
|
|
||||||
### For parties
|
### For parties
|
||||||
The one page in second person. Grade-9 reading level. Short sentences. Says
|
The one page in second person. Grade-9 reading level. Short sentences. Says
|
||||||
explicitly: the mediator is not your lawyer and cannot give you legal advice; the
|
explicitly: **the mediator is a neutral and does not act for either party; each
|
||||||
mediator does not decide who is right. Answers what it costs and what happens if
|
party should have their own legal advice;** and the mediator does not decide who
|
||||||
you do not settle.
|
is right. Answers what it costs and what happens if you do not settle.
|
||||||
|
|
||||||
|
> ⚠️ **THIS DIRECTIVE READ *"the mediator is not your lawyer and cannot give you
|
||||||
|
> legal advice"* UNTIL 2026-08-28, AND IT BREACHED D13 TWICE** — in the file that
|
||||||
|
> warns, fifty lines above, that `/for-parties/` is *"the exact place the
|
||||||
|
> 'cannot' phrasing feels most natural and is most wrong"*.
|
||||||
|
>
|
||||||
|
> 1. **"not your lawyer" presupposes lawyer status.** §4 Forbidden bars *the word
|
||||||
|
> "lawyer" used of Pouya*, and D13 treats implication as hard as assertion. A
|
||||||
|
> negation still carries the presupposition.
|
||||||
|
> 2. **"cannot give you legal advice" is attempt 2** in this file's own ratified
|
||||||
|
> table — *"a **denial** of capacity"* — and it fails test 3 and the
|
||||||
|
> compliance-checklist item, both added to this file in the same change set
|
||||||
|
> that left this line untouched.
|
||||||
|
>
|
||||||
|
> The replacement is the ratified pattern applied verbatim: **role, then
|
||||||
|
> consequence for the reader, and no verb of capacity.** Found by
|
||||||
|
> `claims-auditor` on re-audit; the original sweep did not reach this section.
|
||||||
|
|
||||||
### Insights
|
### Insights
|
||||||
1,200–1,800 words, monthly cadence (brief §VIII). Territories from §VII:
|
1,200–1,800 words, monthly cadence (brief §VIII). Territories from §VII:
|
||||||
@@ -303,13 +471,95 @@ Drafted by Claude, **every word reviewed by Pouya before publication**:
|
|||||||
|
|
||||||
## Compliance checklist — before any page ships
|
## Compliance checklist — before any page ships
|
||||||
|
|
||||||
|
⚠️ **EVERY ITEM HERE THAT BARS SOMETHING CITES ITS `AGENTS.md` §4 ROW. IT DOES
|
||||||
|
NOT RESTATE IT — STRUCTURAL FIX, Pouya, 2026-08-31, after the third instance.**
|
||||||
|
|
||||||
|
His ruling: *"the checklist must CITE the §4 row, not restate it. Same
|
||||||
|
single-source rule as §7 and operational facts."* **This file is what gets
|
||||||
|
grepped; §4 is what is correct.** Three times a line here paraphrased a §4 row,
|
||||||
|
dropped a qualifier, and then flagged the copy a spec **requires** — *"no dollar
|
||||||
|
figures"* for §4's *"attached to **past matters**"*, which forbade the rate card
|
||||||
|
D8 commits to; *"no testimonials"* for §4's bar on endorsements **of the practice
|
||||||
|
or of Pouya**, which forbade the institutional quotations `docs/01` directs; and
|
||||||
|
the licence-capacity item, which rejected the exact sentence the section above
|
||||||
|
exists to bless. Each was caught by review, never by the checklist, and the row
|
||||||
|
was never wrong.
|
||||||
|
|
||||||
|
**So an item below names what to look for on the page, and which row decides it.
|
||||||
|
Not both halves of the rule.**
|
||||||
|
|
||||||
- [ ] Every factual claim appears in `AGENTS.md` §4 Verified
|
- [ ] Every factual claim appears in `AGENTS.md` §4 Verified
|
||||||
- [ ] No matter counts, settlement rates, dollar figures, or time-to-award stats
|
- [ ] **Numbers that describe the practice** — any count, rate, percentage, time
|
||||||
- [ ] No testimonials, endorsements, or third-party quotes
|
or dollar figure about matters, hours, years, or outcomes. Decided by §4
|
||||||
- [ ] No superlatives and no guarantee language
|
Forbidden's *counts of matters closed / hours mediated / years in ADR
|
||||||
- [ ] No claim or implication of legal licensure anywhere (D13)
|
practice*, *settlement rates, resolution percentages, median time to
|
||||||
- [ ] Q.Arb described as commenced August 2026, never as held or nearly complete
|
award*, *aggregate value resolved; any dollar figure attached to past
|
||||||
- [ ] Nothing implies a firm, a team, or offices that do not exist
|
matters*, and *named or describable past matters*. `npm run check:claims`
|
||||||
|
`counts-and-tenure` sweeps `dist/`. ⚠️ **The rate card is a different
|
||||||
|
thing and no row reaches it:** D8 commits to publishing it in full, D14
|
||||||
|
confirms the figures `[verified 2026-08-26 — Pouya]`, and `docs/01`
|
||||||
|
requires `/for-parties/` to answer *"what it costs"*. A published **price**
|
||||||
|
is an offer; a published **statistic about past matters** is what the old
|
||||||
|
site fabricated
|
||||||
|
- [ ] **Third-party voices** — decided by §4 Forbidden's *testimonials,
|
||||||
|
endorsements, quotes from counterparties*. ⚠️ **Scope is the whole
|
||||||
|
question:** that row reaches a third party praising the practice or Pouya.
|
||||||
|
It does not reach an institution quoted **about its own rules**, from a
|
||||||
|
committed source — which `docs/01` §`/med-arb/` **directs**, and which
|
||||||
|
`/mediation/`, `/arbitration/` and `/process/` all do. **Keep the source's
|
||||||
|
superlatives inside the quotation marks**
|
||||||
|
- [ ] **Superlatives and guarantee language** — decided by §4 Forbidden's
|
||||||
|
*guarantees of outcome, or superlatives*. `check:claims` `superlatives`
|
||||||
|
sweeps `dist/`
|
||||||
|
- [ ] **Licensure, asserted or implied** — decided by §4 Forbidden's two
|
||||||
|
licensure rows and D13. The second of the two is the one that catches
|
||||||
|
copy: it reaches phrasing that *implies* entitlement without saying so.
|
||||||
|
`check:claims` `licensure-of-pouya` and `acting-for-a-party` sweep `dist/`
|
||||||
|
- [ ] **The licence-capacity question answered in EITHER direction** — see *When
|
||||||
|
a fact is `[unestablished]`* above, and §4's own note that its two
|
||||||
|
licensure rows are verified **directives not to publish**, not a verified
|
||||||
|
status. Check for *cannot*, *do not*, *am not*, *not permitted*, *not
|
||||||
|
qualified* **attached to giving legal advice, practising law, or holding a
|
||||||
|
licence.** ⚠️ **The objection is to answering the capacity question, not
|
||||||
|
to the words themselves** — the approved sentence contains "do not" and
|
||||||
|
passes: *"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."* That is **role
|
||||||
|
conduct**, which he may state freely. `check:claims` `capacity-phrasing`
|
||||||
|
sweeps `dist/`
|
||||||
|
- [ ] **Q.Arb** — decided by §4's Q.Arb Verified row and its Forbidden row. The
|
||||||
|
publishable form is `Q.Arb (ADRIC / ADRIO)`; the acquisition date is
|
||||||
|
recorded in §4 and is not published. `check:claims` `q-arb-as-a-stage`
|
||||||
|
enforces the stage words and a date near the designation, on `dist/`.
|
||||||
|
⚠️ It cannot catch a stage expressed **without naming the designation** —
|
||||||
|
the struck h1 *"Available now, and open about the stage"* matches nothing —
|
||||||
|
and that gap belongs to the cutover claims pass, not to this checklist
|
||||||
|
- [ ] **C.Med-Arb appears nowhere** — decided by §4's struck *C.Med-Arb as a
|
||||||
|
long-term designation goal* row, which carries the reasoning and the
|
||||||
|
deliberate deviation from the strategy brief. `check:claims`
|
||||||
|
`c-med-arb-struck` sweeps `dist/`
|
||||||
|
- [ ] **A class statement about what Ontario law does or does not gate behind an
|
||||||
|
arbitral designation** — decided by §4 Forbidden's *"Anyone may be appointed
|
||||||
|
an arbitrator in Ontario" / "nothing in law gates the role"* row, which
|
||||||
|
carries the committed source and the reason the scoped replacement is
|
||||||
|
**attributed to Pouya rather than stamped**. It bars the claim in **both**
|
||||||
|
directions; this repository does not conclude a proposition of law.
|
||||||
|
`check:claims` `struck-universal-q39` sweeps `dist/`. *(Added 2026-08-31,
|
||||||
|
when Pouya ruled the §4 row into existence: this line could not exist before
|
||||||
|
it, because an item here cites a row rather than restating a bar.)*
|
||||||
|
- [ ] **A firm, a team, or offices that do not exist** — §4 Forbidden rows the
|
||||||
|
specific false artefacts (*"Since 2009"*, *"sixteen years"*, the London
|
||||||
|
and New York offices, the company number, and the fictitious founder);
|
||||||
|
the general rule is **D16** — the boutique is never named, and the
|
||||||
|
publishable form is *Director of Firm Operations at a Toronto litigation
|
||||||
|
and ADR boutique*. `check:claims` `fabricated-founder` sweeps `dist/`
|
||||||
- [ ] Contact page states that an inquiry creates no retainer and no
|
- [ ] Contact page states that an inquiry creates no retainer and no
|
||||||
mediator–party relationship
|
mediator–party relationship
|
||||||
- [ ] Any comparative claim is factual and verifiable
|
- [ ] Any comparative claim is factual and verifiable — **Q41(b), closed
|
||||||
|
2026-08-27**, which struck a comparative claim about other neutrals from
|
||||||
|
this file's own positioning statement. Pouya: *"comparative claims must be
|
||||||
|
factual and verifiable… assert his capability, not the field's
|
||||||
|
incapability."*
|
||||||
|
- [ ] Abbreviations expanded on §11 Glossary's authority only — and expansions
|
||||||
|
for the five ADR designations checked against
|
||||||
|
`docs/reference/adrio-designations.md`, not from memory. "Chartered
|
||||||
|
**Med**-Arbitrator", never "Mediator-Arbitrator"
|
||||||
|
|||||||
+103
-30
@@ -48,8 +48,12 @@ D1.
|
|||||||
Every page passes through one `SEO` component. A page without it is not finished.
|
Every page passes through one `SEO` component. A page without it is not finished.
|
||||||
|
|
||||||
```
|
```
|
||||||
title 50–60 chars, unique. Pattern: "<Page> · Pouya Lajevardi"
|
title 50–60 chars, unique. Pattern: "<Page> · Pouya Lajevardi".
|
||||||
Home: "Pouya Lajevardi · Mediation & Arbitration · Toronto"
|
`/` composes its own from the constants — `SITE.name` +
|
||||||
|
`SITE.tagline` — rather than a literal, so the masthead and
|
||||||
|
the title cannot drift. This spec carried the literal with an
|
||||||
|
ampersand after the composition shipped with interpuncts;
|
||||||
|
cite the constants, do not restate them (AGENTS.md §7 rule).
|
||||||
ARTICLES ARE THE EXCEPTION: no " · Pouya Lajevardi" suffix.
|
ARTICLES ARE THE EXCEPTION: no " · Pouya Lajevardi" suffix.
|
||||||
The suffix is 18 chars, so a headline that already reads
|
The suffix is 18 chars, so a headline that already reads
|
||||||
50–60 renders at 68–78 — over this ceiling. Measured against
|
50–60 renders at 68–78 — over this ceiling. Measured against
|
||||||
@@ -84,16 +88,38 @@ So:
|
|||||||
|
|
||||||
| Pages | Card |
|
| Pages | Card |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/` and `/about/` | The **portrait** crop, `src/assets/og-portrait.jpg`. Not an interim — the decided answer |
|
| `/` and `/about/` | The **portrait** crop, `src/assets/og-portrait.jpg`. Not an interim — the decided answer. Resolved from `PORTRAIT_PAGES` in `src/data/og-cards.ts`, not from a per-page prop |
|
||||||
| Every other page | Generated at build with `satori` or `astro-og-canvas`, using the site's own type and palette: display headline on cream, infinity mark, designation line |
|
| Every other page | **Generated at build by `src/pages/og/[...slug].jpg.ts`** from `satori` + `sharp`, in the site's own type and palette: display headline on cream, infinity mark, designation line |
|
||||||
| Each article | Per-article card from the same generator — the reason the two jobs are one build |
|
| Each article | Per-article card from the same endpoint — the reason the two jobs were one build |
|
||||||
|
|
||||||
**Until step 7 every page shares the portrait, and that is a RECORDED interim
|
✅ **BUILT — step 7b, 2026-08-31. R15 IS DISCHARGED.** `satori@0.33.4` was chosen
|
||||||
that blocks cutover, not build step 3.** It is tracked as **R15** in
|
over `astro-og-canvas@0.13.0` (both 0 vulnerabilities, verified that day): `sharp`
|
||||||
`AGENTS.md` §12 with its removal trigger, because a link preview nobody on the
|
is already a dependency to rasterise satori's SVG, so it adds one library rather
|
||||||
team ever sees is exactly the kind of interim that becomes permanent by
|
than a CanvasKit wasm blob, and it renders with this site's own fonts and tokens
|
||||||
never being raised. The dependency choice is made against R11 on the day, not
|
rather than approximating them.
|
||||||
recalled from this paragraph.
|
|
||||||
|
**Four things about the implementation are load-bearing and are not style
|
||||||
|
choices.** Each is recorded because a later reader would otherwise "tidy" it:
|
||||||
|
|
||||||
|
1. **Colours are parsed out of `src/styles/tokens.css` at build time**, not
|
||||||
|
copied into the generator. `CLAUDE.md` requires every colour to come from a
|
||||||
|
token; the alternative was a duplicated hex table, which is the SES-DKIM shape.
|
||||||
|
A missing token throws rather than falling back.
|
||||||
|
2. **The fonts are `@fontsource`'s static `.woff` cuts, not `public/fonts/`.**
|
||||||
|
satori parses TTF/OTF/WOFF and not WOFF2, and decompressing the site's own
|
||||||
|
subset **variable** Geist to TTF *throws* inside satori's `opentype.js` fork —
|
||||||
|
Fontsource's subsetting drops the `name` records the `fvar` table points at.
|
||||||
|
Same typeface, same upstream version, same weight; build-time only.
|
||||||
|
3. **Every card's headline is its page's own `<h1>`, character for character, and
|
||||||
|
`npm run og:proof` enforces it** against the built HTML. This is a compliance
|
||||||
|
mechanism, not a convenience: **text baked into a JPEG cannot be grepped by
|
||||||
|
`npm run check:claims`**, which under D20 is the only per-step claims control
|
||||||
|
there is. A card must not carry a claim its page does not already make in
|
||||||
|
auditable HTML. The same check confirms every page's `og:image` resolves to a
|
||||||
|
file that exists — a 404 preview is invisible from inside the repo.
|
||||||
|
4. **A page with no card entry is a BUILD ERROR, not a fallback to the portrait.**
|
||||||
|
R15's failure mode was never the wrong image; it was the wrong image shipping
|
||||||
|
*invisibly* and reading as intentional. A silent fallback recreates it exactly.
|
||||||
|
|
||||||
## Structured data
|
## Structured data
|
||||||
|
|
||||||
@@ -101,16 +127,24 @@ JSON-LD only. Validate against Google's Rich Results Test before cutover.
|
|||||||
|
|
||||||
| Type | Where | Notes |
|
| Type | Where | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `Person` | `/about/`, referenced site-wide | `name`, `jobTitle`, `description`, `alumniOf` (Bond University), `knowsLanguage` (en, fa), `hasCredential` (Q.Med), `sameAs` (LinkedIn), `image`. **`jobTitle` = "Director of Firm Operations"; omit `worksFor`** — populating it either names the boutique (D16) or misstates the employer |
|
| `Person` | `/about/`, referenced site-wide | **Emitted:** `name`, `url`, `jobTitle`, `description`, `alumniOf` (Bond University), `knowsLanguage` (en, fa), `hasCredential` (**Q.Med, Q.Arb** — both, since 2026-08-29), `sameAs` (LinkedIn), `email`, `image`. **Emitted on `/about/` only:** `memberOf` — the four §4 memberships as `Organization` nodes (Q53, ruled 2026-08-28). `/` shows no memberships, so its Person node omits it: structured data represents the page it sits on. **Withheld:** `worksFor` — Q49(b) declined the row 2026-08-28 and Pouya confirmed the reading 2026-08-29, so it is settled rather than pending; `provider → Person → worksFor` would assert a same-entity claim §4 does not row. *(This enumeration listed `worksFor` as emitted while the same cell said it was withheld, and omitted `url` and `email`, which are — wrong in both directions. The enumeration is the part an implementer copies. Found by `adversarial-reviewer`.)* **CHANGED 2026-08-28 — Q47.** This row read *"`jobTitle` = 'Director of Firm Operations'; omit `worksFor`"*, which put the boutique title on a node whose `url` is this ADR practice's `/about/` — so a consumer could attach it to this entity. Pouya's ruling reframes the field: `jobTitle` describes **this practice**, not the boutique role, which D16 keeps unnamed. The visible role line is unchanged and still reads "Director of Firm Operations at a Toronto litigation and ADR boutique". **THE VALUE IS `PRACTICE_JOB_TITLE` IN `src/data/site.ts` AND THIS ROW DOES NOT RESTATE IT** — §7's rule, applied to a string with a live revert trigger on it: this row carried the literal text for one pass, and `adversarial-reviewer` noted it would go stale the moment the constant moved. Cite, do not copy. **`worksFor` IS WITHHELD** — set for one pass under Q47, then reverted: `ProfessionalService.provider` is this Person, so `provider → Person → worksFor` asserts the same-entity claim `schema.ts` explicitly declines, and §4 says "alongside the practice" where the ruling says "operates through". **`memberOf` is emitted** — see the sentence above; Q53 closed 2026-08-28. *(This cell asserted `memberOf` was both emitted and withheld for one pass, which is the defect it already records itself being caught for on `worksFor`, in the opposite direction. The enumeration is the part an implementer copies.)* See `src/data/schema.ts` |
|
||||||
| `ProfessionalService` | Home | `areaServed` Toronto/Ontario, `serviceType` Mediation/Arbitration, `provider` → Person, `priceRange` once `/fees/` is real. **Never `LegalService`** — schema.org defines it as a business providing legal advice and *representation*, which asserts in machine-readable form exactly what D13 bars and §4 Forbidden calls out |
|
| `ProfessionalService` | Home | `areaServed` Toronto/Ontario, `serviceType` **Mediation / Commercial arbitration / Mediation-arbitration (med-arb)** — *scoped 2026-08-28 on `claims-auditor`'s finding; this row instructed the unscoped class form "Mediation/Arbitration" that Q39 struck and that `schema.ts` deliberately does not follow. Family arbitration carries prescribed training and has its own NOT OFFERED row, so unscoped "Arbitration" is the struck universal in a field nobody reads. Do not widen these strings without a §4 row to widen them from* — `provider` → Person, ⚠️ **`priceRange` DECLINED 2026-08-31 — this row said *"once `/fees/` is real"*, the page became real at step 9, the field went in, and it came out the same day.** Its own defence rejected a `min`/`max` over `FEES` because *"a range whose ends mean different units is a range that misinforms"* — and the ends it chose had different units too: the floor was the hourly rate, the ceiling a flat documents-only fee. The floor misinformed in the direction that matters, because the least anyone pays for the headline service is **$2,000**. **Nothing on the site states a price in machine-readable form**, and no `Offer` node either: every figure on `/fees/` is conditional on session length, party count or format, and schema.org's `Offer` models one price for one item. This row gates the field; it does not require it. **Never `LegalService`** — schema.org defines it as a business providing legal advice and *representation*, which asserts in machine-readable form exactly what D13 bars and §4 Forbidden calls out |
|
||||||
| `Service` | Each practice page | `serviceType`, `provider` → Person, `areaServed` |
|
| `Service` | **`/mediation/`, `/arbitration/`, `/med-arb/`** and each practice page | `serviceType`, `provider` → Person, `areaServed`. **The Person node travels in the same `@graph`** so `provider: {'@id'}` resolves in one document rather than relying on a crawler joining two — `homeGraph`'s reasoning, applied. `serviceType` is scoped where §4 scopes it: *Commercial arbitration*, never a bare "Arbitration". No `BreadcrumbList` on the three — one hop from the root, no visible breadcrumb, and this spec requires the markup to match the visible one |
|
||||||
| `Article` | Each article | `headline`, `description`, `datePublished`, `dateModified`, `author` → Person, `image` |
|
| `Article` | Each article | `headline`, `description`, `datePublished`, `dateModified`, `author` → Person, `image` |
|
||||||
| `BreadcrumbList` | All nested pages | Matches visible breadcrumbs |
|
| `BreadcrumbList` | All nested pages | Matches visible breadcrumbs |
|
||||||
| `FAQPage` | `/for-parties/`, `/med-arb/` | Only where the visible page genuinely is Q&A. Never fabricate questions to farm a rich result |
|
| `FAQPage` | `/for-parties/`, `/med-arb/` | Only where the visible page genuinely is Q&A. Never fabricate questions to farm a rich result |
|
||||||
|
|
||||||
**`hasCredential` must reflect reality.** Q.Med is held. Q.Arb is not. Marking an
|
**`hasCredential` must reflect reality.** ⚠️ **Q.Med AND Q.Arb are both held as
|
||||||
unheld credential as held in structured data is a misrepresentation that happens
|
of 2026-08-29** (`AGENTS.md` §4) and the field carries both — it was Q.Med-only
|
||||||
to be machine-readable.
|
while Q.Arb was a commenced pathway. The rule is unchanged and cuts both ways:
|
||||||
|
marking an unheld credential as held is a misrepresentation that happens to be
|
||||||
|
machine-readable, and **omitting a held one understates the record in a field
|
||||||
|
whose whole meaning is "holds"**. `personNode` maps `CREDENTIALS.designations`
|
||||||
|
rather than indexing it, so a designation added to that constant reaches the
|
||||||
|
graph automatically. **That links the CONSTANT to the graph, not §4 to the
|
||||||
|
graph** — a designation added to §4 and not to `src/data/site.ts` still drifts
|
||||||
|
silently, and no mechanism catches it. `src/data/schema.ts` states the condition
|
||||||
|
in full; this sentence overstated it for one pass.
|
||||||
|
|
||||||
## Crawlability
|
## Crawlability
|
||||||
|
|
||||||
@@ -119,7 +153,20 @@ here.** This spec used to reproduce the file inline and the reproduction had
|
|||||||
already drifted from it by 2026-08-26, which is the failure mode the `AGENTS.md`
|
already drifted from it by 2026-08-26, which is the failure mode the `AGENTS.md`
|
||||||
§7 rule exists to stop.
|
§7 rule exists to stop.
|
||||||
|
|
||||||
**It disallows nothing, and that is deliberate.** This spec previously
|
**It disallows exactly one path, and everything about that exception is in the
|
||||||
|
file.** ⚠️ **THIS READ "It disallows nothing, and that is deliberate" UNTIL
|
||||||
|
2026-09-04.** `Disallow: /pouya-lajevardi-bio.pdf` was added that day as the
|
||||||
|
stand-in for `X-Robots-Tag: noindex` on `*.pdf`, which needs a CloudFront
|
||||||
|
response-headers policy the distribution's pricing plan forbids (`AGENTS.md` §7).
|
||||||
|
|
||||||
|
⚠️ **IT IS A SUBSTITUTE, NOT AN EQUIVALENT, AND THE RULE BELOW IS WHY.** It stops
|
||||||
|
the PDF being *fetched* — so its contents are never indexed and the
|
||||||
|
duplicate-of-`/bio/` problem is solved — but it does **not** de-index the URL,
|
||||||
|
and the PDF is linked from `/bio/` and `/about/`, so a bare listing remains
|
||||||
|
possible. That residual is accepted deliberately. **The rule below is unchanged
|
||||||
|
and this is its exception, not its repeal.**
|
||||||
|
|
||||||
|
The general rule: this spec previously
|
||||||
prescribed `Disallow: /legal/` alongside `noindex` on those pages, and the two
|
prescribed `Disallow: /legal/` alongside `noindex` on those pages, and the two
|
||||||
cancel each other: a crawler forbidden to *fetch* a URL never reads the
|
cancel each other: a crawler forbidden to *fetch* a URL never reads the
|
||||||
`noindex` on it. `/legal/privacy/` and `/legal/terms/` are linked from the
|
`noindex` on it. `/legal/privacy/` and `/legal/terms/` are linked from the
|
||||||
@@ -155,16 +202,41 @@ Core Web Vitals are a ranking input, and the current build fails all of them.
|
|||||||
| CLS | < 0.05 |
|
| CLS | < 0.05 |
|
||||||
| INP | < 150 ms |
|
| INP | < 150 ms |
|
||||||
| JS per route | < 100 KB |
|
| JS per route | < 100 KB |
|
||||||
| Lighthouse (mobile) | ≥ 95 all four categories — **not measurable until step 7, see below** |
|
| Lighthouse (mobile) | ≥ 95 all four categories — **measurable again as of 2026-08-31, see below** |
|
||||||
|
|
||||||
> ⚠️ **Lighthouse verification is UNAVAILABLE until build step 7.** `@lhci/cli`
|
> ✅ **THE INSTRUMENT IS BACK — build step 7a, 2026-08-31. `npm run lighthouse`,
|
||||||
> was removed on 2026-08-26 — it was the sole source of all 10 `npm audit`
|
> and it is `lighthouse` rather than `@lhci/cli`.** R11's re-add trigger said to
|
||||||
> findings (7 high), `0.15.1` is `latest` so there was no clean upgrade, and it
|
> put `@lhci/cli` back; this is a deliberate deviation from its literal wording
|
||||||
> could not run at all with no pages and no `lighthouserc`. The budget below is
|
> and `AGENTS.md` §7 records both the reason and what it costs.
|
||||||
> not suspended; the tool that measures it is absent. Re-add at step 7 under
|
>
|
||||||
> `AGENTS.md` R11, checking for a patched release rather than assuming `0.15.1`
|
> **The reason is that §7's advisory attribution was wrong, and it was the
|
||||||
> is still the ceiling. Until then, a run that skips this is skipping something
|
> attribution that made the tool look unusable.** §7 recorded the ten findings as
|
||||||
> known — not something forgotten. `AGENTS.md` §7 has the state.
|
> arriving *"via `lighthouse → puppeteer-core → extract-zip`"*. Measured from two
|
||||||
|
> probe lockfiles: `@lhci/cli@0.15.1` carries 10 (7 high) and pins **lighthouse
|
||||||
|
> 12.6.1**, and the two high carriers are `tmp@0.1.0` — *its own direct
|
||||||
|
> dependency* — and `extract-zip@2.0.1` via `@puppeteer/browsers`.
|
||||||
|
> `lighthouse@13.4.1` standalone is 109 packages, and both are **absent**:
|
||||||
|
> `npm audit` returns 0. So Lighthouse was never the carrier, and the budget was
|
||||||
|
> unmeasurable for five days on a cause nobody re-derived.
|
||||||
|
>
|
||||||
|
> **What it does not do: run in CI.** Standalone Lighthouse drives an installed
|
||||||
|
> browser and the Gitea runner has none (§7, Q23). So it is a local gate plus a
|
||||||
|
> blocking item on `docs/06`'s cutover checklist, and it is deliberately not
|
||||||
|
> wired into `npm run build` or either deploy path — a check described as running
|
||||||
|
> where it cannot is the defect Q22 turned out to be.
|
||||||
|
>
|
||||||
|
> ⚠️ **THE ACCESSIBILITY CATEGORY IS MEASURED WITH `prefers-reduced-motion`
|
||||||
|
> FORCED, and that is a deviation that has to travel with the number.** Measured
|
||||||
|
> twice per condition on `/process/`: motion on gives **96** with
|
||||||
|
> `color-contrast` failing on **24 nodes**; motion off gives **100** with 0. The
|
||||||
|
> 24 were the scroll-driven reveal caught mid-flight — axe reported foregrounds
|
||||||
|
> like `#d0cbc4` on `#f8f4ed`, and neither is in this palette; they are the real
|
||||||
|
> colours blended toward the background by an in-progress `opacity` keyframe. A
|
||||||
|
> category reporting 24 known-false nodes on ten of fourteen pages cannot surface
|
||||||
|
> the twenty-fifth real one. The reduced-motion rendering is the branch
|
||||||
|
> `global.css` ships for a real user setting, and it is the one where every
|
||||||
|
> element sits at its final colour. Palette ratios are computed in
|
||||||
|
> `docs/02-design-system.md`; `scripts/lighthouse.mjs` carries the measurement.
|
||||||
|
|
||||||
How: static HTML, self-hosted preloaded subset fonts, AVIF/WebP with explicit
|
How: static HTML, self-hosted preloaded subset fonts, AVIF/WebP with explicit
|
||||||
dimensions, critical CSS inlined, no third-party scripts on any page except the
|
dimensions, critical CSS inlined, no third-party scripts on any page except the
|
||||||
@@ -188,6 +260,7 @@ nothing more.
|
|||||||
- [ ] OG preview renders correctly in LinkedIn Post Inspector and Slack
|
- [ ] OG preview renders correctly in LinkedIn Post Inspector and Slack
|
||||||
- [ ] Sitemap submitted to Google Search Console and Bing
|
- [ ] Sitemap submitted to Google Search Console and Bing
|
||||||
- [ ] No page returns 200 for a URL that should 404
|
- [ ] No page returns 200 for a URL that should 404
|
||||||
- [ ] Lighthouse ≥ 95 mobile on `/`, `/about/`, one practice page, one article
|
- [ ] Lighthouse ≥ 95 mobile on **every built page** — `npm run lighthouse`,
|
||||||
— **blocked until `@lhci/cli` is re-added at step 7.** Do not tick this box
|
which enumerates `dist/` rather than taking a list, so the set cannot go
|
||||||
from a manual Chrome DevTools run and call it the same check
|
stale as pages are added. Do not tick this box from a manual Chrome
|
||||||
|
DevTools run and call it the same check
|
||||||
|
|||||||
+351
-22
@@ -16,6 +16,108 @@ a verified sender on `smlcompany.ca`. `[verified 2026-08-26 — AGENTS.md §7]`
|
|||||||
|
|
||||||
The shape is right. This is a hardening and rework pass, not a replacement.
|
The shape is right. This is a hardening and rework pass, not a replacement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build step 8, as actually built — 2026-08-31
|
||||||
|
|
||||||
|
**What is in the repository:** `/contact/` with the intake form, two
|
||||||
|
POST-redirect-GET landing pages, and `backend/intake/handler.mjs` +
|
||||||
|
`backend/intake/fields.mjs` + `backend/intake/spam-score.mjs` — the handler that
|
||||||
|
**replaced** the hand-built `adr-intake-handler` §7 records.
|
||||||
|
|
||||||
|
🟢 **IT IS ALL LIVE AS OF 2026-09-02, AND THIS PARAGRAPH SAID THE OPPOSITE UNTIL
|
||||||
|
2026-09-04.** It read *"the handler is not deployed, and the CloudFront `/api/*`
|
||||||
|
behaviour the form posts to does not exist"* — true when written under D11, false
|
||||||
|
from the moment `docs/09` Parts 3, 5 and 6 ran at cutover, and two days stale in
|
||||||
|
the document an implementer reads before touching the handler. **Measured
|
||||||
|
2026-09-04:** the function carries `handler.handler` with six environment
|
||||||
|
variables and its deployed source entries match a commit **§7 names**; the API has
|
||||||
|
exactly one route, `POST /api/intake`; §7 holds the full state and this spec does
|
||||||
|
not restate it. ⚠️ **THIS SENTENCE NAMED THE COMMIT — `02739ad` — IN THE SAME
|
||||||
|
BREATH AS DISCLAIMING RESTATEMENT, AND THE 2026-09-04 REDEPLOY MADE IT FALSE**
|
||||||
|
(three source entries now, matching a later commit). The count is gone with it:
|
||||||
|
both were facts §7 owns. `/contact/` still publishes the email address beside the form,
|
||||||
|
which is now a courtesy rather than a fallback.
|
||||||
|
|
||||||
|
### The form is a plain HTML POST, and it answers 303
|
||||||
|
|
||||||
|
The site ships **zero** JavaScript (§7 — none, not "minimal"), so the form is a
|
||||||
|
`<form method="post">` and the handler replies **303 See Other** to a page on the
|
||||||
|
site. That buys three things with no script anywhere: it works with JavaScript
|
||||||
|
disabled, which is the failure this whole project exists to fix; the visitor never
|
||||||
|
sees a raw JSON body rendered as a page; and a refresh cannot resubmit, because
|
||||||
|
the browser lands on a GET.
|
||||||
|
|
||||||
|
Two pages exist for the two outcomes — `/contact/received/` and
|
||||||
|
`/contact/could-not-send/`. Both are `noindex` and both are excluded from the
|
||||||
|
sitemap in `astro.config.mjs`. **The failure page names no field**, because the
|
||||||
|
handler deliberately does not return the error list (an enumeration of the
|
||||||
|
validation rules is a gift to whoever is probing them) and because a static page
|
||||||
|
cannot read `?error=` without script.
|
||||||
|
|
||||||
|
### It posts to `/api/intake`, not to the execute-api hostname
|
||||||
|
|
||||||
|
Same-origin, with a CloudFront behaviour routing `/api/*` to the HTTP API origin
|
||||||
|
§7 records. Four consequences, and the fourth is the one that matters day to day:
|
||||||
|
`form-action 'self'` alone satisfies the CSP below; there is no cross-origin POST
|
||||||
|
to reason about; the endpoint id stays out of the HTML and out of the repo; and
|
||||||
|
**submitting the form from `astro dev` does nothing**, because there is no
|
||||||
|
`/api/` route locally. Under the alternative, clicking Submit on a laptop would
|
||||||
|
write a real DynamoDB record and send two real emails.
|
||||||
|
|
||||||
|
### ⚠️ Three deviations from this spec, each deliberate
|
||||||
|
|
||||||
|
**1. The 3-second timestamp check is NOT implemented.** It cannot be, and
|
||||||
|
implementing it would produce a control that does nothing. The check needs to know
|
||||||
|
when the form was *served to that visitor*; `/contact/` is a static file cached at
|
||||||
|
the CloudFront edge, so a build-time timestamp is the same value for every visitor
|
||||||
|
and is hours or days old. `now − served` is therefore always large, and the check
|
||||||
|
passes for a bot exactly as it passes for a human. A per-visitor token needs a
|
||||||
|
dynamic origin or client-side script, and the site has neither by design.
|
||||||
|
|
||||||
|
A control that exists on paper and not in fact is worse than a stated gap — that
|
||||||
|
is what `AGENTS.md` Q22 and the Lighthouse row both cost. So it is omitted and
|
||||||
|
said out loud, and the load is carried by the honeypot, the `Origin` check, the
|
||||||
|
**aggregate** API Gateway route throttle and server-side validation. (Aggregate,
|
||||||
|
not per-IP — see §Validation. "Rate limit" was the wording here and let the reader
|
||||||
|
supply the stronger meaning.)
|
||||||
|
|
||||||
|
**2. CORS is not what protects the form, and the `Origin` check is.** A form POST
|
||||||
|
is a top-level navigation: it is exempt from CORS preflight, so an
|
||||||
|
`Access-Control-Allow-Origin` setting cannot stop another site posting a form
|
||||||
|
here. The handler compares `Origin` (falling back to `Referer`, which Firefox
|
||||||
|
sends where it omits `Origin`) against the site origin and refuses anything else.
|
||||||
|
The CORS restriction in this spec is still right — it governs *scripted* calls to
|
||||||
|
the endpoint — but it is a different control and was being relied on for this one.
|
||||||
|
|
||||||
|
**3. There is no `mailto:` fallback, because there is nothing to fall back FROM.**
|
||||||
|
This spec's definition of done asks that the form "degrades to a `mailto:`
|
||||||
|
fallback with JavaScript disabled". The form never used script, so it does not
|
||||||
|
degrade. The email address is published on `/contact/` regardless, and the failure
|
||||||
|
page routes to it.
|
||||||
|
|
||||||
|
### Two field tables, cross-checked
|
||||||
|
|
||||||
|
`src/data/intake.ts` builds the form. `backend/intake/fields.mjs` is what the
|
||||||
|
handler validates against. **The duplication is architectural**, because this
|
||||||
|
spec's own rule is that the Lambda re-validates everything: a server validating
|
||||||
|
against a list the client shipped it is asking the caller what the rules are. And
|
||||||
|
the Lambda is a separately deployed zip that cannot import from `src/`.
|
||||||
|
|
||||||
|
**`npm run check:intake` is what keeps them honest** — it imports both and asserts
|
||||||
|
they agree on every field name, on which are required, on every length cap, and on
|
||||||
|
every closed option set. Probed with three deliberate mismatches (a changed cap, a
|
||||||
|
dropped field, a changed option); each was caught, exit 1.
|
||||||
|
|
||||||
|
### Analytics: decided, not installed
|
||||||
|
|
||||||
|
D15 chose Plausible. **§7 records that no script is on any page**, and
|
||||||
|
`ANALYTICS.installed` in `src/data/site.ts` is `false`. `/legal/privacy/` renders
|
||||||
|
its analytics paragraph from that flag, so today the policy says the site sets no
|
||||||
|
cookies and runs no analytics — which is the fact. **Flipping the flag is a change
|
||||||
|
to a published disclosure**, not a config edit: the policy changes on the same
|
||||||
|
build and its last-updated date moves with it.
|
||||||
|
|
||||||
## What this data actually is
|
## What this data actually is
|
||||||
|
|
||||||
The form collects, in a live legal dispute: the inquirer's identity and contact
|
The form collects, in a live legal dispute: the inquirer's identity and contact
|
||||||
@@ -52,7 +154,7 @@ might reasonably treat as privileged. The intake call is for that.
|
|||||||
|
|
||||||
### Consent text
|
### Consent text
|
||||||
|
|
||||||
> I consent to Pouya Lajevardi storing and using the information in this form to
|
> I consent to SML Company Ltd storing and using the information in this form to
|
||||||
> respond to my inquiry and to run a conflicts check. I understand that
|
> respond to my inquiry and to run a conflicts check. I understand that
|
||||||
> submitting this form does not create a retainer, does not appoint a neutral,
|
> submitting this form does not create a retainer, does not appoint a neutral,
|
||||||
> and does not itself establish a mediator–party relationship.
|
> and does not itself establish a mediator–party relationship.
|
||||||
@@ -64,13 +166,124 @@ Client-side validation is a convenience. **The Lambda re-validates everything.**
|
|||||||
- Required fields present; email well-formed; lengths within bounds
|
- Required fields present; email well-formed; lengths within bounds
|
||||||
- Reject any field over its cap rather than truncating silently
|
- Reject any field over its cap rather than truncating silently
|
||||||
- **Honeypot** field, hidden from sighted and screen-reader users, must be empty
|
- **Honeypot** field, hidden from sighted and screen-reader users, must be empty
|
||||||
- **Timestamp check** — reject submissions completed in under 3 seconds
|
- **Second honeypot** — a hidden CHECKBOX that must arrive **absent**, added
|
||||||
- **Rate limit** by source IP at API Gateway: 5 requests / 5 minutes
|
2026-09-04. A different trap, not a second copy: the first catches a bot that
|
||||||
- No CAPTCHA. It is a third-party script on a page collecting legal information,
|
fills every text input, this one catches a bot that sets every control it
|
||||||
and the two controls above stop the traffic that matters
|
enumerates. ⚠️ **IT IS PROBABLY INERT AGAINST THE TRAFFIC THAT PROMPTED IT —
|
||||||
|
see §Observed abuse, which retracts in full the argument this bullet made for
|
||||||
|
one round** (*"which anything reaching validation must do, because the consent
|
||||||
|
box is required and unchecked by default"*). The retraction was written sixty
|
||||||
|
lines below this bullet and did not reach it. **Unchecked sends nothing, so
|
||||||
|
absence is the pass —
|
||||||
|
and so is an empty value**, because the handler tests for a non-empty one
|
||||||
|
rather than for presence: no dropped-field path and no blind form serialiser
|
||||||
|
can turn it into a lost inquiry. It carries its **own** wrapper class (not the
|
||||||
|
first honeypot's), the `hidden` attribute as well as the CSS rule, and a label
|
||||||
|
that tells a human not to tick it — see `src/pages/contact.astro`, where each
|
||||||
|
of the three is a correction rather than a precaution
|
||||||
|
- **Spam SCORING that labels and never rejects**, added 2026-09-04. See
|
||||||
|
§Observed abuse. It changes the operator notification's subject line and
|
||||||
|
nothing else
|
||||||
|
- ~~**Timestamp check** — reject submissions completed in under 3 seconds~~
|
||||||
|
⚠️ **STRUCK, and it was recorded as unimplementable in three other places
|
||||||
|
while this line stayed an unqualified imperative** — the handler's header,
|
||||||
|
§Three deviations above, and the definition of done below. §Three deviations
|
||||||
|
has the reasoning: `/contact/` is a CDN-cached static file, so a build-time
|
||||||
|
timestamp is the same value for every visitor and `now − served` is always
|
||||||
|
large. **This is the unstruck-imperative shape `CLAUDE.md` names** — and it
|
||||||
|
survived in the same list whose sibling bullet was struck correctly, which is
|
||||||
|
the sweep failure exactly. Found by `adversarial-reviewer` round 2
|
||||||
|
- ~~**Rate limit** by source IP at API Gateway: 5 requests / 5 minutes~~
|
||||||
|
⚠️ **STRUCK 2026-09-01: API GATEWAY CANNOT RATE-LIMIT BY SOURCE IP, SO THIS
|
||||||
|
ASKED FOR A CONTROL THAT CANNOT BE BUILT WHERE IT SAYS TO BUILD IT.** HTTP API
|
||||||
|
throttling is **aggregate** — a rate and a burst, per route and per stage,
|
||||||
|
across all callers. Per-IP limiting needs **AWS WAF** with a rate-based rule on
|
||||||
|
the distribution, which is a paid service and therefore a decision rather than
|
||||||
|
a step. What ships instead is the aggregate throttle
|
||||||
|
(`docs/09-cutover-runbook.md` Part 6.3), and it must never be described as
|
||||||
|
per-IP. This is deviation 1's own argument turned on this spec: *"a control that
|
||||||
|
exists on paper and not in fact is worse than a stated gap"* — the throttle is
|
||||||
|
real and bounds total volume; the per-IP claim was neither
|
||||||
|
- No CAPTCHA. It is a third-party script on a page collecting legal information.
|
||||||
|
⚠️ **THIS BULLET USED TO END "and the two controls above stop the traffic that
|
||||||
|
matters", WHICH THE FIRST REAL SPAM FALSIFIED** — see §Observed abuse. The
|
||||||
|
reason to keep CAPTCHA out is unchanged and stands on its own; the claim that
|
||||||
|
what ships is sufficient was an untested prediction and has been removed rather
|
||||||
|
than reworded
|
||||||
- CORS restricted to `https://adr.smlcompany.ca` — no wildcard
|
- CORS restricted to `https://adr.smlcompany.ca` — no wildcard
|
||||||
- Strip HTML from every field before storage and before it enters an email body
|
- Strip HTML from every field before storage and before it enters an email body
|
||||||
|
|
||||||
|
## Observed abuse
|
||||||
|
|
||||||
|
**First real-world spam: 2026-09-04.** Two automated submissions, **10:51Z** and
|
||||||
|
**12:16Z**, `submissionId` prefixes `50cda580…` and `e3e21122…`. Recorded here
|
||||||
|
rather than in the Change Log alone because this section's controls were
|
||||||
|
specified against an imagined attacker and this is the first measured one.
|
||||||
|
|
||||||
|
**Both passed the honeypot**, and neither was stopped by anything else that
|
||||||
|
ships: the aggregate route throttle is 1 request/second with a burst of 5
|
||||||
|
(`AGENTS.md` §7), and two submissions ninety minutes apart are nowhere near it.
|
||||||
|
|
||||||
|
**The signature, as Pouya recorded it:**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Names | random |
|
||||||
|
| Email | random Gmail addresses — **one using the dot trick** |
|
||||||
|
| Phone | Russian format |
|
||||||
|
| Organisation | big-brand names |
|
||||||
|
| Dispute summary | scraped text |
|
||||||
|
|
||||||
|
⚠️ **THE HONEYPOT WAS NOT DEFEATED BY CLEVERNESS — IT WAS NOT ENGAGED.** A bot
|
||||||
|
that submits only the fields it recognises never touches a decoy text input.
|
||||||
|
|
||||||
|
🛑 **AND THAT CUTS BOTH WAYS. THE SECOND HONEYPOT IS PROBABLY INERT AGAINST THIS
|
||||||
|
PAIR, AND THIS SECTION CLAIMED THE OPPOSITE FOR ONE ROUND.** It said the checkbox
|
||||||
|
*"is aimed at a behaviour the traffic must have"*, reasoning that the consent box
|
||||||
|
is required so anything that validated must have been ticking checkboxes.
|
||||||
|
**Sending `consent=on` shows only that it knows one field name.** A bot selective
|
||||||
|
enough to skip a hidden text input is selective enough to skip a hidden checkbox,
|
||||||
|
and the same evidence that explains the first honeypot's silence predicts the
|
||||||
|
second's. It is **defence in depth against a different and common class** — the
|
||||||
|
bot that enumerates controls and sets all of them — which is worth adding and is
|
||||||
|
not a counter to what was observed. Nothing in this repository has yet caught a
|
||||||
|
bot with it.
|
||||||
|
|
||||||
|
**What was added, and the ordering rule Pouya set:** *"Nothing is dropped; a
|
||||||
|
false positive costs him one glance."*
|
||||||
|
|
||||||
|
1. A second honeypot — above.
|
||||||
|
2. **Scoring that labels.** `backend/intake/spam-score.mjs`, unit-tested at
|
||||||
|
`spam-score.test.mjs`. Signals and weights: summary under a floor **(1)**,
|
||||||
|
phone present and not North American **(1)**, a link in the summary **(2)**,
|
||||||
|
a Gmail address with dot-trick density **(2)**; **threshold 2**. Above it the
|
||||||
|
record is still stored, both emails are still sent, and only the operator
|
||||||
|
notification changes — subject prefixed `[Possible spam] `, plus one line
|
||||||
|
naming the signals. **The confirmation to the inquirer is untouched.**
|
||||||
|
3. **Nothing is stored.** The score and signals do not enter the DynamoDB item,
|
||||||
|
because §Storage's attribute list is published on `/legal/privacy/` and adding
|
||||||
|
one would make that page wrong.
|
||||||
|
|
||||||
|
⚠️ **THE TIMING FLOOR WAS RULED AND COULD NOT BE BUILT — §9 Q66.** Pouya's ruling
|
||||||
|
of 2026-09-04 asked to *"raise the timing floor"*. **There is no floor to raise:**
|
||||||
|
the timestamp check is struck above and has never existed, for a reason unchanged
|
||||||
|
by the spam arriving — `/contact/` is a CDN-cached static file, so no per-visitor
|
||||||
|
"served at" value exists to subtract from. Nothing inside *"handler + form only,
|
||||||
|
zero-JS preserved"* can produce one, and the three mechanisms that could each
|
||||||
|
break one of his constraints:
|
||||||
|
|
||||||
|
| mechanism | what it costs |
|
||||||
|
|---|---|
|
||||||
|
| Client-side script timing the fill | **Breaks zero JavaScript** (§7, and it is *none*, not *minimal*) |
|
||||||
|
| A CloudFront Function on viewer-response setting a signed short-lived cookie, read by the handler | Outside *"handler + form only"*, and it puts a **cookie** on a site whose privacy policy turns on there being none — a `/legal/privacy/` change and a consent question this repository must not answer for itself |
|
||||||
|
| A dynamic origin for `/contact/` | Reverses D1's `output: 'static'` |
|
||||||
|
|
||||||
|
**A fourth is worse than doing nothing:** shipping a build-time timestamp and
|
||||||
|
calling it a timing check. `now − served` would be hours or days for every
|
||||||
|
caller, so it would pass for a bot exactly as it passes for a human — the control
|
||||||
|
that exists on paper and not in fact, which is what deviation 1 and `AGENTS.md`
|
||||||
|
Q22 are both records of.
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
DynamoDB, in the region `AGENTS.md` §7 records. **Canadian data residency is
|
DynamoDB, in the region `AGENTS.md` §7 records. **Canadian data residency is
|
||||||
@@ -79,23 +292,64 @@ handing over sensitive material, and where it comes to rest is a fair question
|
|||||||
for them to ask. Confirm the existing table's region and migrate if it is
|
for them to ask. Confirm the existing table's region and migrate if it is
|
||||||
elsewhere — §7 has the table name and region.
|
elsewhere — §7 has the table name and region.
|
||||||
|
|
||||||
|
⚠️ **THE KEY SCHEMA IS THE TABLE'S, NOT THIS SPEC'S — CORRECTED 2026-09-01, AND
|
||||||
|
THE UNCORRECTED VERSION WOULD HAVE LOST EVERY SUBMISSION.** This table specified
|
||||||
|
`pk: INTAKE#<uuid>` and `sk: <timestamp>`, and `handler.mjs` was written to it.
|
||||||
|
The table `AGENTS.md` §7 names has a single partition key **`submissionId` (S)`
|
||||||
|
and no sort key** `[verified 2026-09-01 — aws dynamodb describe-table]`. A
|
||||||
|
`PutItem` missing the key attribute fails the whole write with
|
||||||
|
`ValidationException`, the handler catches it and answers the failure page — so
|
||||||
|
the form would have looked broken to every inquirer while the record went
|
||||||
|
nowhere, from the moment `/api/*` was wired. **A DynamoDB key schema cannot be
|
||||||
|
altered after creation**, so the handler was changed to the table rather than the
|
||||||
|
reverse; the alternative, a new table matching the old shape, was declined
|
||||||
|
because it would re-open the §7-verified TTL and PITR state on a fresh resource
|
||||||
|
at cutover to buy a sort key nothing queries. Verify with `describe-table`, not
|
||||||
|
against this row.
|
||||||
|
|
||||||
| Attribute | |
|
| Attribute | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `pk` | `INTAKE#<uuid>` |
|
| `submissionId` | `<uuid>` — **the partition key.** Fixed by the table; the notification email prints this value verbatim so it can be pasted into the console |
|
||||||
| `sk` | `<ISO-8601 timestamp>` |
|
| `submittedAt` | `<ISO-8601 timestamp>` — an ordinary attribute, not a sort key |
|
||||||
| fields | as above |
|
| fields | as above |
|
||||||
| `sourceIp`, `userAgent` | abuse investigation only |
|
| `sourceIp`, `userAgent` | ~~abuse investigation only~~ — ⚠️ **AMENDED 2026-09-02: that purpose holds for `userAgent` and `submittedAt`, and NOT for `sourceIp`.** Behind the `/api/*` behaviour `requestContext.http.sourceIp` is a CloudFront edge, so it identifies the network rather than the sender and cannot serve an abuse investigation. `/legal/privacy/` now states the two purposes separately — timestamp and user-agent for abuse, the address as something that simply arrives with the request. **A spec and a page disagreeing about WHY data is held is the disclosure PIPEDA actually turns on**, and this row said one thing while the page said another for a day. `docs/09` Part 7.2 measures the field; if it holds the reader's own address, this row and that paragraph both change. `adversarial-reviewer`, round 2 |
|
||||||
| `ttl` | epoch seconds — **automatic deletion** |
|
| `consentAt` | `<ISO-8601 timestamp>` — when the consent box was submitted |
|
||||||
|
| `ttl` | epoch seconds — **the input to automatic deletion; see §Retention for why writing it is not the mechanism** |
|
||||||
|
|
||||||
**Encryption at rest** with a customer-managed KMS key. **Point-in-time recovery
|
**Encryption at rest** with a customer-managed KMS key. **Point-in-time recovery
|
||||||
on.** Table access limited to the Lambda role and one named administrative
|
on.** ⚠️ **THE THIRD LINE HERE WAS *"table access limited to the Lambda role and
|
||||||
principal.
|
one named administrative principal"*, AND IT WAS THE Q62 FALSEHOOD — struck
|
||||||
|
2026-09-02.** It is false on both halves: `adr-intake-lambda-role` holds
|
||||||
|
`PutItem` **only** and cannot read the table at all, and access is not one
|
||||||
|
principal. **`AGENTS.md` §7's `Intake table — who can read it` row is the answer
|
||||||
|
and this spec does not restate it** — a duplicated fact is one that goes wrong in
|
||||||
|
the copy nobody re-reads, which is what happened here: the Q62 sweep ran over
|
||||||
|
`src/` and never reached a spec, and `check:claims` carries this exact sentence
|
||||||
|
as a string that reached `dist/`. It survived the sweep this file's own
|
||||||
|
definition-of-done claims to have completed (`adversarial-reviewer`, round 2).
|
||||||
|
|
||||||
|
⚠️ **TWO OF THOSE THREE ARE THE STATE OF THE RUNNING TABLE AND ONE IS NOT.**
|
||||||
|
PITR is **on** `[verified 2026-09-01 — describe-continuous-backups,
|
||||||
|
PointInTimeRecoveryStatus: ENABLED, 35-day window]`. Encryption at rest is on
|
||||||
|
with the **AWS-owned key, not a customer-managed KMS key** `[verified
|
||||||
|
2026-09-01 — describe-table returns no SSEDescription]`. That gap is
|
||||||
|
deliberately not a cutover blocker: `/legal/privacy/` says "encrypted at rest",
|
||||||
|
which is unconditionally true of every DynamoDB table, and it does not claim a
|
||||||
|
customer-managed key — so nothing published depends on it. It stays on
|
||||||
|
`docs/06`'s checklist as the improvement it is.
|
||||||
|
|
||||||
### Retention
|
### Retention
|
||||||
|
|
||||||
**24 months, enforced by DynamoDB TTL.** Not a policy someone remembers — a
|
**24 months, enforced by DynamoDB TTL.** Not a policy someone remembers — a
|
||||||
mechanism that runs whether anyone remembers or not.
|
mechanism that runs whether anyone remembers or not.
|
||||||
|
|
||||||
|
⚠️ **WRITING THE ATTRIBUTE IS NOT THE MECHANISM.** The handler supplies `ttl`;
|
||||||
|
TTL must also be **enabled on the table**, and **`AGENTS.md` §7 records whether
|
||||||
|
it is — this section deliberately does not.** So the paragraph above is a
|
||||||
|
statement about the design and not about the running system until the cutover
|
||||||
|
item below is ticked on **both** halves: `ENABLED` by command, and a test record
|
||||||
|
observed to disappear.
|
||||||
|
|
||||||
Rationale: long enough to serve conflicts screening across a normal matter
|
Rationale: long enough to serve conflicts screening across a normal matter
|
||||||
lifecycle; short enough to be defensible under PIPEDA's requirement to retain
|
lifecycle; short enough to be defensible under PIPEDA's requirement to retain
|
||||||
personal information only as long as necessary. Whatever number ships must match
|
personal information only as long as necessary. Whatever number ships must match
|
||||||
@@ -187,11 +441,45 @@ no visibility.
|
|||||||
— silently, months later.
|
— silently, months later.
|
||||||
|
|
||||||
Failure handling: SES failure must never lose the submission. Write to DynamoDB
|
Failure handling: SES failure must never lose the submission. Write to DynamoDB
|
||||||
first, then send. A dead-letter queue on the Lambda, and a CloudWatch alarm on
|
first, then send. ~~A dead-letter queue on the Lambda, and a CloudWatch alarm on
|
||||||
DLQ depth ≥ 1.
|
DLQ depth ≥ 1.~~
|
||||||
|
|
||||||
|
⚠️ **THE DLQ IS STRUCK, 2026-09-01, AND IT WOULD HAVE BEEN A CONTROL THAT
|
||||||
|
RECEIVED NOTHING.** Lambda's `DeadLetterConfig` is used **only for asynchronous
|
||||||
|
invocations** (and event-source failures). API Gateway invokes this function
|
||||||
|
**synchronously** and the error is returned to the caller, so a DLQ configured on
|
||||||
|
`adr-intake-handler` would sit at depth 0 for ever and an alarm on it would be a
|
||||||
|
green light that means nothing — the third instance of this project's most
|
||||||
|
expensive shape, after `AGENTS.md` Q22 and the Lighthouse row.
|
||||||
|
|
||||||
|
What actually protects a submission is already built and is not a queue: the
|
||||||
|
handler **writes to DynamoDB before sending mail**, so a mail failure cannot lose
|
||||||
|
a record, and a write failure returns the visitor to `/contact/could-not-send/`
|
||||||
|
rather than telling them an inquiry was received. What is missing is **detection**,
|
||||||
|
and the replacement is two CloudWatch alarms rather than one:
|
||||||
|
|
||||||
|
- **Lambda `Errors` ≥ 1** on `adr-intake-handler` — this is what a DLQ alarm was
|
||||||
|
reaching for and it fires on a synchronous failure, which a DLQ cannot see.
|
||||||
|
- **API Gateway `5xx` ≥ 1** on the `POST /api/intake` route — it catches the one
|
||||||
|
failure the Lambda cannot report, a permission or integration fault where the
|
||||||
|
function is never entered at all (`docs/09-cutover-runbook.md` Part 6.1 is the
|
||||||
|
step whose omission causes exactly that).
|
||||||
|
|
||||||
|
Both notify the `ses-alerts` topic, whose email subscription is **confirmed** as
|
||||||
|
of `AGENTS.md` §7 — so unlike the DLQ alarm, these reach someone.
|
||||||
|
|
||||||
## Booking
|
## Booking
|
||||||
|
|
||||||
|
**PARKED — R6, and `/contact/` ships without it.** Pouya parked the booking tool
|
||||||
|
on 2026-08-26; build step 8 shipped the form and no embed. The "reserved slot"
|
||||||
|
`docs/01` asks for is `CONTACT.bookingUrl` being `null`: nothing renders, and a
|
||||||
|
URL there brings the block back without a rebuild of the page.
|
||||||
|
|
||||||
|
**Nothing on `/contact/` mentions booking**, deliberately — a page that says
|
||||||
|
"book a call" with no way to book one is worse than a page that says to email.
|
||||||
|
D10 committed to booking because it removes the back-and-forth that loses
|
||||||
|
appointments, so the form alone is a partial answer and R6 stays live.
|
||||||
|
|
||||||
An embedded scheduler for the 30–45 minute confidential intake call
|
An embedded scheduler for the 30–45 minute confidential intake call
|
||||||
(**Q5** — tool not yet chosen).
|
(**Q5** — tool not yet chosen).
|
||||||
|
|
||||||
@@ -231,6 +519,13 @@ Content-Security-Policy: default-src 'self'; img-src 'self' data:;
|
|||||||
base-uri 'self'; frame-ancestors 'none'
|
base-uri 'self'; frame-ancestors 'none'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
⚠️ **`form-action` IS NOW `'self'` ALONE, and that is tighter than the line
|
||||||
|
above.** Build step 8 posts the intake form to the same-origin path `/api/intake`
|
||||||
|
rather than to the execute-api hostname, so no third-party origin needs to appear
|
||||||
|
in the policy. Drop `<api-endpoint>` from `form-action` when the policy is
|
||||||
|
written. `frame-src <booking-provider>` is also unnecessary while R6 keeps the
|
||||||
|
embed parked — add it with the embed, not before.
|
||||||
|
|
||||||
Tighten CSP once the booking provider is chosen. `unsafe-inline` on styles is
|
Tighten CSP once the booking provider is chosen. `unsafe-inline` on styles is
|
||||||
tolerable for critical CSS; `unsafe-inline` on scripts is not — use a hash or
|
tolerable for critical CSS; `unsafe-inline` on scripts is not — use a hash or
|
||||||
nonce for the reveal script.
|
nonce for the reveal script.
|
||||||
@@ -253,13 +548,47 @@ Plausible or Fathom, cookieless, no consent banner.
|
|||||||
|
|
||||||
## Definition of done
|
## Definition of done
|
||||||
|
|
||||||
- [ ] Server-side validation independent of the client
|
- [x] **Server-side validation independent of the client** — `backend/intake/fields.mjs`, cross-checked by `npm run check:intake`
|
||||||
- [ ] Honeypot and timing checks live; rate limit configured
|
- [x] **The first honeypot is live** — the hidden text input that must arrive
|
||||||
- [ ] CORS restricted to the production origin
|
empty. Deployed since cutover. ⚠️ **The timing check is NOT implemented** —
|
||||||
- [ ] TTL set and verified by test record
|
see deviation 1 above and §Observed abuse; it is unimplementable on a
|
||||||
- [ ] KMS encryption and PITR enabled
|
CDN-cached static page and would be a control that does nothing.
|
||||||
|
**Re-ruled and re-blocked 2026-09-04, §9 Q66**
|
||||||
|
- [ ] 🛑 **THE SECOND HONEYPOT AND THE SPAM SCORING ARE WRITTEN AND NOT
|
||||||
|
DEPLOYED** — 2026-09-04. Both live in `backend/intake/`, and **a site
|
||||||
|
deploy does not carry `backend/`**: `scripts/deploy-local.sh` is an S3 sync
|
||||||
|
and an invalidation, nothing more. They need `docs/09` Part 5 (and Part 5.5,
|
||||||
|
which is the path in production). ⚠️ **THIS LINE READ `[x]` … "live" FOR ONE
|
||||||
|
ROUND, ON AN UNCOMMITTED WORKING TREE**, while §7's own row recorded the
|
||||||
|
running function as last modified 2026-09-02 with source digests matching
|
||||||
|
`HEAD` — the spec asserting a control that its neighbour proved absent.
|
||||||
|
`node backend/intake/spam-score.test.mjs` returns **39 of 39** and all four
|
||||||
|
signals are exercised `[verified 2026-09-04]`; that is a statement about
|
||||||
|
the repository, not about production. ⚠️ **The only paths that discard a
|
||||||
|
submission are the two honeypots**, and both answer with the success page
|
||||||
|
rather than an error. Validation failures redirect to
|
||||||
|
`/contact/could-not-send/`, which is a told failure, not a silent one
|
||||||
|
- [x] ✅ **Throttle configured — `POST /api/intake` at rate 1.0 req/s, burst 5, detailed metrics on** `[verified 2026-09-04 — get-stage]`. ⚠️ **IT IS A `RouteSettings` ENTRY, NOT THE STAGE DEFAULT**, and a query projecting `DefaultRouteSettings` alone returns only `DetailedMetricsEnabled` and reads as *no throttle configured* — which is how §7 came to say so. Read `RouteSettings` before concluding it is absent. An **aggregate** route throttle, not the per-source-IP limit this spec used to ask for; see §Validation above for why that is not buildable at API Gateway and what it would take. Not expressible in handler code. `docs/09-cutover-runbook.md` Part 6.3
|
||||||
|
- [x] **The form's own protection is the `Origin` check, not CORS** — see deviation 2. CORS on the endpoint still to be restricted for scripted calls
|
||||||
|
- [ ] **TTL set and verified by test record.** ⚠️ **THIS ONE BACKS A PUBLISHED PROMISE.** `/legal/privacy/` states that records are deleted automatically after 24 months, and it asserts the **mechanism**, not only the period. The handler writes the `ttl` attribute — epoch seconds, 24 months, confirmed against this spec `[verified 2026-08-31]` — and **writing the attribute is not the mechanism**: TTL must also be enabled on the table, which is a table setting the code cannot see. **`AGENTS.md` §7 holds that status and its stamp; this line does not restate it** — it restated it once, went stale within the day, and had to be pulled back (§12 R19). **The test record is what closes this item, not the status:** `ENABLED` proves the setting, a record written with a near-future `ttl` and observed to vanish proves the behaviour. Tracked as §9 Q60
|
||||||
|
- [x] **PITR enabled** — `ENABLED`, 35-day window `[verified 2026-09-01 — describe-continuous-backups]`
|
||||||
|
- [ ] KMS customer-managed key. **Not on the table: encryption at rest is with the AWS-owned key** `[verified 2026-09-01 — describe-table returns no SSEDescription]`. **Not claimed on `/legal/privacy/`** — the page says "encrypted at rest", which is unconditionally true of every DynamoDB table and does not mention a customer-managed key, so nothing published depends on it. An improvement, not a blocker
|
||||||
|
- [x] ✅ **Table access matches what `/legal/privacy/` says about it — 2026-09-02.** **The access is unchanged; the page now states it.** Pouya ruled *state the truth* rather than *remove the access* (§9 Q62), so the page states the truth about access rather than a false exclusivity. ⚠️ **WHAT IT STATES CHANGED TWICE MORE THAT DAY AND THIS LINE IS WRITTEN AGAINST THE SHIPPED BYTES, NOT AGAINST THE RULING.** §9 Q63 took the human headcount off (a simulation counts identities and the page was reading them as people), and a second ruling then cut §Who can see it to **four plain statements**. The page now says: *"The record in the table: me, and the small number of people who administer the account it sits in with me"*; that the receiving system *"can only add a record — it cannot read back what is stored"*; where the notification goes and who reads it; and that the confirmation sits with the reader's own provider. **§Where it is stored carries the shared-account disclosure** — *"an Amazon Web Services account that also runs systems unrelated to this practice"*. ⚠️ **`adr-sml-deploy` is `implicitDeny` on all seven read AND write actions — MEASURED, TRUE, AND NO LONGER ON THE PAGE**; it went with the mechanics cut and it is §7's claim now, not the policy's. Do not tick this item against a page that states it. Evidence and commands: `docs/reference/intake-table-access-verification.md`, whose enumeration was **extended on 2026-09-02** — the original screened roles by `list-attached-role-policies` alone, missing that 23 of 26 non-service-linked roles carry inline policies and that the two CDK `lookup` roles can read the table. Four roles can, not two; every one of them is reachable only by those administrators. ⚠️ **Do not restate that as a count of PEOPLE** — this line said *"all four terminate at the same two people"* until 2026-09-02, which is the inference §9 Q63 struck. ⚠️ **THIS LINE SAID "IT DOES NOT" FOR A DAY AFTER THE PAGE WAS CORRECTED, AND IT IS A DEFINITION-OF-DONE LIST SOMEONE FOLLOWS AT CUTOVER** — the Q62 sweep was run over `src/` only, so it could not reach a spec. `adversarial-reviewer`, round 1. The sweep across `docs/` is in the Change Log entry
|
||||||
- [ ] Both emails send; SPF/DKIM/DMARC aligned; inbox-tested, not spam-tested
|
- [ ] Both emails send; SPF/DKIM/DMARC aligned; inbox-tested, not spam-tested
|
||||||
- [ ] DLQ and CloudWatch alarm configured
|
- [ ] **CloudWatch alarms on Lambda `Errors` and API Gateway `5xx`** — replacing the DLQ item, which is struck: a DLQ on a **synchronously** invoked function never receives anything, so the alarm on its depth would have been permanently green. See §Notification. The handler writes to DynamoDB **before** sending mail, so the protection this item was pointing at is in the code rather than in a queue
|
||||||
- [ ] Form usable by keyboard only; errors announced with `role="alert"`
|
- [x] **Form usable by keyboard only.** Errors are announced by the browser's own validation, which with no script is the only thing that can announce them inline — `role="alert"` needs a live region and something to write into it
|
||||||
- [ ] Form degrades to a `mailto:` fallback with JavaScript disabled
|
- [x] **Works with JavaScript disabled** — replacing the `mailto:` degradation item; see deviation 3
|
||||||
- [ ] Privacy policy matches the implementation line for line
|
- [x] **Privacy policy matches the implementation** — and three of its statements are DERIVED rather than written, so they cannot drift: the collected-data list renders from `INTAKE_FIELDS`, the retention period from the handler's own figure, and the analytics paragraph from `ANALYTICS.installed`
|
||||||
|
> ✅ **THE THREE ITEMS BELOW WERE COMMANDS AND ALL THREE HAVE RUN — cutover,
|
||||||
|
> 2026-09-02**, verified against the live account 2026-09-04. They are ticked
|
||||||
|
> below and the reasoning is kept because it is what made them non-obvious.
|
||||||
|
> The commands are in `docs/09-cutover-runbook.md` — Parts 5, 6 and 3
|
||||||
|
> respectively, each with its verification and the output to expect. Two things that spec found by reading the
|
||||||
|
> running system rather than the specs, and both would have lost every
|
||||||
|
> submission: the API route needs its **own** Lambda invoke permission, because
|
||||||
|
> the existing one is `SourceArn`-scoped to the old `/submissions` path; and the
|
||||||
|
> handler's item shape had to change, because the table's partition key is
|
||||||
|
> `submissionId` and a key schema cannot be altered after creation (§Storage).
|
||||||
|
|
||||||
|
- [x] ✅ **CloudFront `/api/*` behaviour created** `[verified 2026-09-04 — get-distribution-config: 1 cache behaviour, 2 origins, 1 function association, 1 custom error response]`, routing to the HTTP API origin §7 records. The form does not work without it. **And two other distribution changes are prerequisites of the site working at all**, neither of which is intake: a viewer-request function for `trailingSlash: 'always'`, without which 22 of 23 pages return S3's `AccessDenied`, and the 404 mapping `docs/04` requires
|
||||||
|
- [x] ✅ **Handler deployed 2026-09-02**, replacing the hand-built `adr-intake-handler` `[verified 2026-09-04 — get-function-configuration: `handler.handler`, 15 s, 512 MB, six variables; and the deployed zip downloaded and read]`. ⚠️ **Ticking it does NOT mean the current working tree is deployed** — the running artefact matches `HEAD`, and `backend/` changes reach production only through Part 5. With **SIX** variables set: `INTAKE_TABLE`, `SITE_ORIGIN`, `NOTIFY_TO`, `MAIL_FROM`, `RESPONSE_TIME` and `NO_RETAINER_NOTICE`. It throws at cold start on any missing one, deliberately. ⚠️ **This item said five while the handler required six.** `NO_RETAINER_NOTICE` became a `requireEnv` and reached no document, so an operator following the list would have deployed a function that throws on every invocation — 5xx from API Gateway, and every inquiry lost from the moment `/api/*` was wired. Found by `adversarial-reviewer`, 2026-08-31. **Two of the six must be verbatim from `src/data/site.ts`**, because both are published commitments: `RESPONSE_TIME` from `CONTACT.responseTime`, and `NO_RETAINER_NOTICE` from the constant of the same name — whose fourth clause (*"does not itself create a conflict check"*, required by `docs/01` §`/contact/`) a hand-typed copy in the handler had dropped
|
||||||
|
|||||||
+1240
-47
File diff suppressed because it is too large
Load Diff
+232
-11
@@ -70,7 +70,7 @@ Three observations that drive the recommendation.
|
|||||||
## Where this practice should sit
|
## Where this practice should sit
|
||||||
|
|
||||||
**Not at the floor.** Pouya's stack — JD, an operating role inside a litigation
|
**Not at the floor.** Pouya's stack — JD, an operating role inside a litigation
|
||||||
and ADR boutique, Q.Med held, Q.Arb commenced, and a working engineering career —
|
and ADR boutique, Q.Med and Q.Arb held, and a working engineering career —
|
||||||
is not a junior generalist profile. Entering at roster rates would anchor him
|
is not a junior generalist profile. Entering at roster rates would anchor him
|
||||||
into SABS volume work and make the commercial rate very hard to raise later.
|
into SABS volume work and make the commercial rate very hard to raise later.
|
||||||
Published rates are close to unrecoverable once set: raising them looks
|
Published rates are close to unrecoverable once set: raising them looks
|
||||||
@@ -97,21 +97,147 @@ All figures **plus HST**.
|
|||||||
|
|
||||||
| Item | Fee |
|
| Item | Fee |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Half day — up to 3.5 h, including 2 h preparation | **$2,000** |
|
| Half day — up to 3 hours of session. Fee includes up to 2 hours of preparation | **$2,000** |
|
||||||
| Full day — up to 7 h, including 3 h preparation | **$4,000** |
|
| Full day — up to 6 hours of session. Fee includes up to 3 hours of preparation | **$4,000** |
|
||||||
| Each party beyond two | **$500** |
|
| Each party beyond two | **$500** |
|
||||||
| Overtime, per hour | **$500** |
|
| Overtime, per hour | **$500** |
|
||||||
|
|
||||||
|
⚠️ **THE TWO ROWS ABOVE WERE AMBIGUOUS UNTIL 2026-08-31, AND THE AMBIGUITY WAS
|
||||||
|
IN THIS DOCUMENT RATHER THAN IN ANY COPY — Q58, ruled by Pouya.** They read
|
||||||
|
*"Half day — up to 3.5 h, including 2 h preparation"* and *"Full day — up to
|
||||||
|
7 h, including 3 h preparation"*. Read literally that makes 3.5 the **whole
|
||||||
|
billed envelope** and the time in the room **1.5 h**. Pouya's ruling: *"3.5 was
|
||||||
|
meant as the TOTAL time committed, of which 2 is preparation — leaving 1.5 hours
|
||||||
|
in the room… The intended reading is the market's, and my wording obscured
|
||||||
|
it."*
|
||||||
|
|
||||||
|
**What found it was arithmetic, not reading:** 3.5 and 7 are exactly 2×, which
|
||||||
|
they would not be if preparation sat inside them, **because preparation does not
|
||||||
|
scale with session length**. Under the literal reading the room time was 1.5 h
|
||||||
|
and 4 h, which is not 2× anything.
|
||||||
|
|
||||||
|
**The corrected numbers are corroborated by this file's own research table**, one
|
||||||
|
section up — which is the reason they are 3 and 6 rather than a round guess:
|
||||||
|
|
||||||
|
- **Patey** publishes **3 h** half-day and **6 h** full-day in *both* tiers.
|
||||||
|
- **Zuber** publishes **3 h** video half-day and **6 h** full day.
|
||||||
|
- **ADR Chambers**' roster rate covers *"one half hour of preparation time per
|
||||||
|
party **and** up to three hours of mediation"* — preparation counted
|
||||||
|
**separately from** a three-hour session, which is the shape this card now
|
||||||
|
has.
|
||||||
|
|
||||||
|
Pouya: *"Selling 1.5 hours of room time as a half day would be an outlier nobody
|
||||||
|
would recognise."* ⚠️ **One provenance note, because this file is the authority
|
||||||
|
on money:** he recalled Patey and Zuber as publishing *"all or part of 3 hours"*.
|
||||||
|
The extract above records their **hours** but not that phrase, so **the hours are
|
||||||
|
what this file relies on** — do not quote the phrase as theirs.
|
||||||
|
|
||||||
|
**THE PREPARATION ALLOWANCE IS CAPPED, AND MUST BE PUBLISHED AS CAPPED.**
|
||||||
|
*"Including **up to** 2 hours of preparation."* Never *"including 2 hours"*,
|
||||||
|
which reads as a flat entitlement, and never *"preparation included"*, which
|
||||||
|
sells an uncapped one. See §All parameters confirmed below.
|
||||||
|
|
||||||
|
**Consequences, applied 2026-08-31:** `FEES.mediation.*.hours` in
|
||||||
|
`src/data/site.ts` means the **session** and is corrected 3.5 → 3 and 7 → 6, and
|
||||||
|
**`/fees/` is unblocked for build step 9** on the question Q58 asked.
|
||||||
|
|
||||||
|
✅ **WHERE OVERTIME BEGINS — RULED. Q59, Pouya, 2026-08-31. IT RUNS FROM THE
|
||||||
|
SESSION CAP**: the fourth hour of a half day, the seventh of a full day. Not the
|
||||||
|
billed envelope. The two candidates were the session cap (3 h / 6 h) and the
|
||||||
|
envelope (5 h / 9 h), and this file could not choose between them — a fee term is
|
||||||
|
a fact we do not have, not an inference. A first pass at this paragraph asserted
|
||||||
|
the session cap as applied fact and `adversarial-reviewer` struck it in the same
|
||||||
|
change set that wrote it; the strike was right, and the ruling has now supplied
|
||||||
|
the value the strike was waiting for.
|
||||||
|
|
||||||
|
⚠️ **AND THE RULING'S SECOND HALF IS THE PART THAT MATTERS MOST, BECAUSE IT
|
||||||
|
ANSWERS THE ARITHMETIC ANOMALY BELOW RATHER THAN RESTATING IT.** His words:
|
||||||
|
|
||||||
|
> "a full day reserves the day; half-day overtime is subject to availability"
|
||||||
|
|
||||||
|
**The full-day fee buys the DAY, not six hours of it.** That is what a reader
|
||||||
|
doing the arithmetic in the table below is missing: `2000 + 500 × 3 = 3500`
|
||||||
|
against `4000` looks like a $500 penalty for booking properly, and it is not —
|
||||||
|
the two are different products. Half-day overtime depends on the time after the
|
||||||
|
session still being free, and on a booked day it is not.
|
||||||
|
|
||||||
|
**So the reservation sentence is published ADJACENT TO THE OVERTIME ROW on
|
||||||
|
`/fees/`, not in a footnote**, and it is rendered from
|
||||||
|
`FEES.mediation.reservation` rather than retyped. Structurally the same rule as
|
||||||
|
`PROCESS_FRAMING` beside the five timings under Q43: a reader who takes the
|
||||||
|
number and skips the framing has read a different offer.
|
||||||
|
|
||||||
|
⚠️ **THE ANOMALY IS NOT CLOSED BY THIS.** The gap is still in D14's own figures —
|
||||||
|
the half-to-full step is $2,000 and three hours of overtime is $1,500 — and the
|
||||||
|
reservation point explains what the gap buys without removing it. It stays on
|
||||||
|
**§12 R5**'s 12-month review, and §Recorded dissent below carries the table for
|
||||||
|
that review to test against.
|
||||||
|
|
||||||
|
**And the reason it cannot be quietly chosen is that the choice is visible in the
|
||||||
|
arithmetic.** Take the trigger as the session cap. The half-day route costs
|
||||||
|
`2000 + 500 × max(0, n − 3)`; the full-day route is **flat $4,000 until hour 6**
|
||||||
|
and `4000 + 500 × (n − 6)` after it:
|
||||||
|
|
||||||
|
| Session run to | Book a half day + overtime | Book a full day | Gap |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 3 h | **$2,000** | **$4,000** | **$2,000** |
|
||||||
|
| 4 h | $2,000 + 1 × $500 = **$2,500** | **$4,000** | **$1,500** |
|
||||||
|
| 5 h | $2,000 + 2 × $500 = **$3,000** | **$4,000** | **$1,000** |
|
||||||
|
| 6 h | $2,000 + 3 × $500 = **$3,500** | **$4,000** | **$500** |
|
||||||
|
| 7 h | $2,000 + 4 × $500 = **$4,000** | $4,000 + 1 × $500 = **$4,500** | **$500** |
|
||||||
|
|
||||||
|
**Booking a half day and paying overtime is cheaper at every length — by $2,000
|
||||||
|
at three hours, narrowing to $500 from six hours on — and the full-day rate is
|
||||||
|
never the cheaper choice.** The gap is in D14's figures rather than in the
|
||||||
|
trigger: the half-to-full step is $2,000 and three hours of overtime is $1,500.
|
||||||
|
|
||||||
|
⚠️ **A FIRST PASS AT THIS PARAGRAPH GOT THE ARITHMETIC WRONG, AND WRONG IN THE
|
||||||
|
DIRECTION THAT MATTERED.** It generalised the full-day route as `500n + 1000` for
|
||||||
|
all `n ≥ 3`, which is **only valid from hour 6** — the route is flat until then —
|
||||||
|
and concluded *"$500 cheaper at every length"*. The real spread is **up to four
|
||||||
|
times larger and is largest at three to five hours, which is the band a half-day
|
||||||
|
booking actually overruns into.** The document's own table contradicted the
|
||||||
|
formula three lines above it. Found by `adversarial-reviewer` on round 2, in the
|
||||||
|
change set that wrote it. *A measurement is a claim about your instrument, and a
|
||||||
|
formula is an instrument.*
|
||||||
|
|
||||||
|
**Moving the trigger out to the 5 h / 9 h billed envelope does not fix it and is
|
||||||
|
not uniformly better either** — the gap stays at $2,000 through five hours and is
|
||||||
|
$1,500 at six, worse than the session-cap trigger there, but it closes to **zero**
|
||||||
|
from nine hours on, where the session-cap trigger holds a permanent $500. So the
|
||||||
|
two triggers trade one band against another and neither removes the anomaly. **It
|
||||||
|
was never a defect this file could fix by picking a trigger** — which is why the
|
||||||
|
trigger went to Pouya and the step went to R5.
|
||||||
|
|
||||||
|
**Both halves came back. He ruled the session cap AND supplied the reservation
|
||||||
|
point**, which is the answer the arithmetic alone cannot give: the table compares
|
||||||
|
prices for two things that are not the same product. Read the table as a price
|
||||||
|
comparison and the full-day rate looks strictly worse; read it knowing a full day
|
||||||
|
reserves the day and half-day overtime is subject to availability, and the
|
||||||
|
$2,000-to-$500 spread is the price of certainty rather than a mistake. The
|
||||||
|
anomaly stays on R5 because the *size* of that spread is still a judgement about
|
||||||
|
D14's figures, and it is largest at three to five hours — the band a half-day
|
||||||
|
booking actually overruns into.
|
||||||
|
|
||||||
### Arbitration
|
### Arbitration
|
||||||
|
|
||||||
Sole, party-appointed and co-arbitration appointments are all accepted now —
|
Sole, party-appointed and co-arbitration appointments **in commercial matters**
|
||||||
`AGENTS.md` §4 Offerings carries a row for each `[verified 2026-08-26 — Pouya]`.
|
are all accepted now — `AGENTS.md` §4 Offerings carries a row for each
|
||||||
|
`[verified 2026-08-26 — Pouya]`. *("in commercial matters" added 2026-08-28: the
|
||||||
|
line was unscoped, and every §4 arbitration row is scoped commercial with family
|
||||||
|
arbitration separately NOT OFFERED — Q39's struck universal.)*
|
||||||
*(This line previously read "sole appointments follow the Q.Arb designation",
|
*(This line previously read "sole appointments follow the Q.Arb designation",
|
||||||
which understated the offering, and carried a caveat against a since-closed
|
which understated the offering, and carried a caveat against a since-closed
|
||||||
Q36.)* Whatever `/fees/` says about arbitration must state the Q.Arb stage
|
Q36.)*
|
||||||
plainly alongside it — §4 Offerings, "neither half may be dropped": the Q.Arb
|
|
||||||
designation commenced August 2026, with C.Med-Arb as the endpoint. See
|
⚠️ **`/fees/` HAS NO CREDENTIALING DISCLOSURE TO MAKE (amended 2026-08-29).**
|
||||||
`03-content-spec.md` for the wording.
|
This paragraph required that whatever the page says about arbitration *"must
|
||||||
|
state the Q.Arb stage plainly alongside it — §4 Offerings, 'neither half may be
|
||||||
|
dropped'"*. **Q.Arb is held and that condition is dissolved.** `/fees/` is
|
||||||
|
unbuilt (step 9), so this is the one place the amendment lands before the page
|
||||||
|
exists rather than after — do not build the page against the struck form. `03-content-spec.md`'s
|
||||||
|
model sentence is now the scope half only — the stage half was struck with the
|
||||||
|
paragraph this one used to point at.
|
||||||
|
|
||||||
| Item | Fee |
|
| Item | Fee |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -123,11 +249,60 @@ designation commenced August 2026, with C.Med-Arb as the endpoint. See
|
|||||||
**No tribunal-secretary rate.** Removed by Pouya. Do not reinstate it, and do not
|
**No tribunal-secretary rate.** Removed by Pouya. Do not reinstate it, and do not
|
||||||
offer tribunal-secretary work on the site.
|
offer tribunal-secretary work on the site.
|
||||||
|
|
||||||
|
### Med-arb — billed by phase
|
||||||
|
|
||||||
|
⚠️ **INTERIM. Set by Pouya 2026-09-03; reviewed at the twelve-month fee review,
|
||||||
|
`AGENTS.md` §12 R5.** It is stamped interim because it is the only rule on this
|
||||||
|
page set after the card was published rather than with it, and because it prices
|
||||||
|
an offering by reference to two other rows — if either moves at R5, this moves
|
||||||
|
with them and nobody will be reminded by a figure changing.
|
||||||
|
|
||||||
|
**The rule, and it carries no figure of its own:**
|
||||||
|
|
||||||
|
- Med-arb is billed **by phase**. The mediation phase is charged at the
|
||||||
|
**mediation** rates above. If the matter proceeds to arbitration, that phase is
|
||||||
|
charged at the **arbitration** rates above.
|
||||||
|
- **There is no separate med-arb fee.**
|
||||||
|
- The additional-party and cancellation terms apply to each phase **as they
|
||||||
|
apply to that process on its own**.
|
||||||
|
|
||||||
|
**Why this rule exists at all, because a fee page does not usually need one.**
|
||||||
|
`/fees/` opens *"Every figure is on this page"*, and `AGENTS.md` §4 Offerings
|
||||||
|
carries a **Med-Arb** row that this document priced nowhere. The promise was
|
||||||
|
therefore wider than the card — the D20 cutover claims pass, finding 10. Pouya
|
||||||
|
closed it by **pricing the offering rather than narrowing the promise**, which is
|
||||||
|
the more expensive of the two fixes and the one that leaves the page saying the
|
||||||
|
stronger thing.
|
||||||
|
|
||||||
|
⚠️ **DO NOT GIVE MED-ARB A RATE ROW.** A med-arb figure would be a fourth price
|
||||||
|
for a process that is already priced twice, and the first thing it would do is
|
||||||
|
disagree with one of them. The rule is expressed as a pointer to the two cards
|
||||||
|
above **on purpose**; that is what keeps the count of published figures the same
|
||||||
|
as the count of published rates.
|
||||||
|
|
||||||
|
⚠️ **"AS THEY APPLY TO THAT PROCESS ON ITS OWN" IS NOT "TO BOTH PHASES".** The
|
||||||
|
additional-party fee is a **mediation** row; the arbitration card has no
|
||||||
|
equivalent. The wording above invents nothing. *"The additional-party term
|
||||||
|
applies throughout"* would invent an additional-party charge in the arbitral
|
||||||
|
phase, which no ruling has set.
|
||||||
|
|
||||||
|
`FEES.medArb` in `src/data/site.ts` holds the three sentences and `/fees/`
|
||||||
|
renders them, so the rule is not retyped into the template.
|
||||||
|
|
||||||
### Other services — hourly
|
### Other services — hourly
|
||||||
|
|
||||||
Early neutral evaluation, dispute-system design, and pre-dispute technical
|
Early neutral evaluation, dispute-system design, and pre-dispute technical
|
||||||
advisory: **$500 / hour**.
|
advisory: **$500 / hour**.
|
||||||
|
|
||||||
|
> ✅ **PUBLISHABLE — Q46(a) closed 2026-08-28.** All three §4 Offerings rows
|
||||||
|
> read PUBLISHABLE on Pouya's second ruling, so the line item may go on
|
||||||
|
> `/fees/`. **Read the §4 ENE row first:** gate 1 is closed as **`[Pouya's
|
||||||
|
> stated basis 2026-08-28]`**, not `[verified]` — there is still no source in
|
||||||
|
> `docs/reference/` — and the framing constraints in `docs/01` §`/practice/`
|
||||||
|
> travel with the offering onto this page. In particular, **ENE is priced as an
|
||||||
|
> assessment delivered to both parties**, and nothing on `/fees/` may read as a
|
||||||
|
> rate for advising one of them.
|
||||||
|
|
||||||
**THREE services, not four. `settlement counsel` is struck and must not be
|
**THREE services, not four. `settlement counsel` is struck and must not be
|
||||||
priced** — `AGENTS.md` Q42, Pouya 2026-08-27, correcting his own entry in
|
priced** — `AGENTS.md` Q42, Pouya 2026-08-27, correcting his own entry in
|
||||||
`docs/01`:
|
`docs/01`:
|
||||||
@@ -164,14 +339,29 @@ page is an offer.
|
|||||||
### All parameters confirmed
|
### All parameters confirmed
|
||||||
|
|
||||||
Q15, Q16, and Q17 were closed on 2026-08-26. **Preparation time is bundled and
|
Q15, Q16, and Q17 were closed on 2026-08-26. **Preparation time is bundled and
|
||||||
must be stated on the page** — "including 2 hours of preparation", "including
|
must be stated on the page, IN HOURS AND AS A CAP** — "including **up to** 2
|
||||||
3 hours of preparation". Do not quietly fold it into the hours figure. At these
|
hours of preparation", "including **up to** 3 hours of preparation". Do not
|
||||||
|
quietly fold it into the session figure, and do not drop the "up to": the
|
||||||
|
allowance is capped, so the unqualified form sells an uncapped one. At these
|
||||||
rates, saying preparation is included is the selling point, not a footnote.
|
rates, saying preparation is included is the selling point, not a footnote.
|
||||||
|
|
||||||
|
*(The "up to" was added 2026-08-31 with Q58's ruling. This paragraph previously
|
||||||
|
prescribed the flat form, and `/for-parties/` shipped it — the one page written
|
||||||
|
for a reader with no counsel to catch it.)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Recorded dissent — for the 12-month review (R5)
|
## Recorded dissent — for the 12-month review (R5)
|
||||||
|
|
||||||
|
⚠️ **SECOND ITEM FOR R5, ADDED 2026-09-04 — MED-ARB, AND IT IS NOT A DISSENT.**
|
||||||
|
It is here because **R5 names this section as where its items live**, and the
|
||||||
|
med-arb rule was stamped INTERIM against R5 in §Med-arb above and written into no
|
||||||
|
list the review actually reads. **The rule is derived** — each phase at the rates
|
||||||
|
for that process, no figure of its own — so **moving any mediation or arbitration
|
||||||
|
number at R5 moves the med-arb price with it, silently, with no diff on the
|
||||||
|
med-arb rule.** Nothing else on this page has that property. Check it against
|
||||||
|
whatever the review does to the two cards above.
|
||||||
|
|
||||||
Claude recommended a two-tier card; Pouya set a single rate. The reasoning is
|
Claude recommended a two-tier card; Pouya set a single rate. The reasoning is
|
||||||
recorded here so the 12-month review has something to test against, not to
|
recorded here so the 12-month review has something to test against, not to
|
||||||
re-open a settled decision.
|
re-open a settled decision.
|
||||||
@@ -209,6 +399,37 @@ unrecoverable, and it is far easier to add a lower tier later than to raise a
|
|||||||
headline rate. Setting the ceiling first and discounting privately preserves
|
headline rate. Setting the ceiling first and discounting privately preserves
|
||||||
more optionality than the reverse.
|
more optionality than the reverse.
|
||||||
|
|
||||||
|
**And a fourth item, added 2026-08-31: the half-day-plus-overtime route is
|
||||||
|
strictly cheaper than the full-day rate, at every session length.** This is the
|
||||||
|
one item in this section that is arithmetic rather than judgement, so it is the
|
||||||
|
one the review can settle without new market data.
|
||||||
|
|
||||||
|
| Session run to | Half day + overtime | Full day | Gap |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 3 h | $2,000 | $4,000 | **$2,000** |
|
||||||
|
| 4 h | $2,500 | $4,000 | **$1,500** |
|
||||||
|
| 5 h | $3,000 | $4,000 | **$1,000** |
|
||||||
|
| 6 h | $3,500 | $4,000 | **$500** |
|
||||||
|
| 7 h | $4,000 | $4,500 | **$500** |
|
||||||
|
|
||||||
|
*(Session-cap trigger; the trigger itself is `AGENTS.md` **Q59**, ruled and
|
||||||
|
closed 2026-08-31 — this line said "open" for a day after line 144 of this same
|
||||||
|
file recorded the ruling.)* The
|
||||||
|
cause is the relationship between two of D14's own numbers rather than anything
|
||||||
|
about the trigger: **the half-to-full step is $2,000 and three hours of overtime
|
||||||
|
is $1,500.** Any trigger leaves a gap; the envelope trigger closes it only from
|
||||||
|
nine hours on.
|
||||||
|
|
||||||
|
**What the review has to decide is whether that is a choice.** It is defensible
|
||||||
|
as one — a full-day booking buys certainty and a reserved diary, and a client who
|
||||||
|
knows they need six hours may prefer to pay $500 for not having to watch the
|
||||||
|
clock. It is also exactly the kind of thing counsel comparing published cards
|
||||||
|
finds in under a minute, and this practice's whole pricing argument is that the
|
||||||
|
card is published in full and means what it says. **The lever, if it is not a
|
||||||
|
choice:** either raise the overtime rate so the routes converge, or narrow the
|
||||||
|
half-to-full step. Both are rate moves, which is why they belong here and not in
|
||||||
|
a footnote to the card.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ it does not need restating in every prompt.
|
|||||||
|
|
||||||
That single command runs: plan → implement → adversarial review → resolve →
|
That single command runs: plan → implement → adversarial review → resolve →
|
||||||
verify → record. It requests deep reasoning, reads `AGENTS.md` and the specs,
|
verify → record. It requests deep reasoning, reads `AGENTS.md` and the specs,
|
||||||
stops if the task conflicts with a locked decision, invokes two independent
|
stops if the task conflicts with a locked decision, invokes `adversarial-reviewer`
|
||||||
reviewers on the finished diff, resolves what they find, runs the checks, and
|
on the finished diff, resolves what it finds, runs the checks, and appends the
|
||||||
appends the Change Log entry.
|
Change Log entry.
|
||||||
|
|
||||||
You do not need to ask for thinking, for review, or for the record to be updated.
|
You do not need to ask for thinking, for review, or for the record to be updated.
|
||||||
Those are the agreement, not the request.
|
Those are the agreement, not the request.
|
||||||
@@ -26,29 +26,53 @@ Those are the agreement, not the request.
|
|||||||
| Command | Does |
|
| Command | Does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/build <task>` | The full loop. Use for every substantive change |
|
| `/build <task>` | The full loop. Use for every substantive change |
|
||||||
| `/review [scope]` | The review pass alone, on the working tree or a named scope. Reports; fixes nothing without your say-so |
|
| `/review [scope]` | The **code** review alone, on the working tree or a named scope. Reports; fixes nothing without your say-so. Under D20 it does not run the claims pass |
|
||||||
| `/wrap` | End of session — updates `AGENTS.md` under its constitution and leaves the tree clean |
|
| `/wrap` | End of session — updates `AGENTS.md` under its constitution and leaves the tree clean |
|
||||||
|
|
||||||
## The two reviewers
|
## The two reviewers, and when each of them runs — D20
|
||||||
|
|
||||||
Both are defined in `.claude/agents/` and run in parallel on the diff.
|
Both are defined in `.claude/agents/`. **They no longer run together.**
|
||||||
|
|
||||||
**`adversarial-reviewer`** reads the code: correctness and edge cases,
|
**`adversarial-reviewer` runs on every build step.** It reads the code:
|
||||||
accessibility, crawlability, performance budgets, security, and whether a
|
correctness and edge cases, accessibility, crawlability, performance budgets,
|
||||||
materially simpler correct version exists.
|
security, and whether a materially simpler correct version exists.
|
||||||
|
|
||||||
**`claims-auditor`** reads the copy against `AGENTS.md` §4 and nothing else. It
|
**`claims-auditor` runs once, at cutover, over the whole finished site.** It reads
|
||||||
extracts every factual assertion — credentials, roles, numbers, languages,
|
the copy against `AGENTS.md` §4 and nothing else — extracting every factual
|
||||||
locations, capabilities, and the JSON-LD — and traces each to the Verified table.
|
assertion (credentials, roles, numbers, languages, locations, capabilities, and
|
||||||
Anything untraceable is reported and does not ship.
|
the JSON-LD) and tracing each to the Verified table. Anything untraceable is
|
||||||
|
reported and does not ship. `docs/06`'s cutover checklist carries it as a blocking
|
||||||
|
item, alongside your own read of every page against §4.
|
||||||
|
|
||||||
It is a separate agent on purpose. A general-purpose reviewer will happily
|
It is a separate agent on purpose. A general-purpose reviewer will happily
|
||||||
approve elegant code containing a claim that should never have been published,
|
approve elegant code containing a claim that should never have been published,
|
||||||
because professional-conduct compliance is not what it is looking at. On this
|
because professional-conduct compliance is not what it is looking at. On this
|
||||||
project that is the highest-stakes failure mode, so it gets its own pass.
|
project that is the highest-stakes failure mode, so it keeps its own pass.
|
||||||
|
|
||||||
**Verifying they are loaded.** `.claude/agents/` is the correct location. To
|
### Why the claims pass moved, and what it costs
|
||||||
confirm the agents are live, invoke one directly:
|
|
||||||
|
Your ruling, 2026-08-30. `AGENTS.md` D20 carries it in full; the short form:
|
||||||
|
|
||||||
|
- **Nothing has shipped.** Every claims finding to date has been about a page no
|
||||||
|
visitor can reach. The risk is deferred to cutover whether or not the audit is.
|
||||||
|
- **One pass over twenty finished pages catches more than nine over drafts**,
|
||||||
|
because it reads the site as a visitor 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.
|
||||||
|
A claims defect does not compound; it sits there until someone reads it.
|
||||||
|
|
||||||
|
**What it costs, stated rather than glossed:** a claims defect can now live in an
|
||||||
|
unpublished draft for weeks. Two things carry that risk in the meantime, and
|
||||||
|
neither is a substitute for the cutover pass — **`npm run check:claims`**, which
|
||||||
|
is unchanged and runs on every build and both deploy paths, and **you reading the
|
||||||
|
copy as it is built.**
|
||||||
|
|
||||||
|
## Verifying the agents are loaded
|
||||||
|
|
||||||
|
**`.claude/agents/` is the correct location.** To confirm the agents are live,
|
||||||
|
invoke one directly:
|
||||||
|
|
||||||
```
|
```
|
||||||
Use the claims-auditor agent to audit README.md against AGENTS.md §4.
|
Use the claims-auditor agent to audit README.md against AGENTS.md §4.
|
||||||
@@ -57,20 +81,29 @@ Use the claims-auditor agent to audit README.md against AGENTS.md §4.
|
|||||||
A verdict table back means both are wired. "No such agent" means the frontmatter
|
A verdict table back means both are wired. "No such agent" means the frontmatter
|
||||||
needs looking at.
|
needs looking at.
|
||||||
|
|
||||||
|
**Agent definitions load at session start.** An edit to `.claude/agents/*.md`
|
||||||
|
does not reach a running session — the version in force is the one that was on
|
||||||
|
disk when the session began. So after changing a brief, restart before relying on
|
||||||
|
it, and **say in the report which version actually ran.** This is not theoretical:
|
||||||
|
the gloss lens was added to `claims-auditor` on 2026-08-30 and the agent
|
||||||
|
reconstructed it from the Change Log rather than having it in its brief, because
|
||||||
|
the edit and the run were in the same session.
|
||||||
|
|
||||||
**Both are instructed to treat uncertainty as a defect.** They will sometimes be
|
**Both are instructed to treat uncertainty as a defect.** They will sometimes be
|
||||||
wrong. That is the intended trade: explaining why a finding is mistaken costs
|
wrong. That is the intended trade: explaining why a finding is mistaken costs
|
||||||
minutes, and a missed defect on this project's public marketing pages
|
minutes, and a missed defect on this project's public marketing pages
|
||||||
costs a great deal more.
|
costs a great deal more. **This weighs heavier on the cutover pass, not lighter**
|
||||||
|
— there is nothing behind it, so D19's two-round cap does not apply there.
|
||||||
|
|
||||||
## The rule that makes it work
|
## The rule that makes it work
|
||||||
|
|
||||||
**The reviewers are given the diff and the specs — never the implementer's
|
**A reviewer is given the diff and the specs — never the implementer's
|
||||||
explanation of why the work is correct.**
|
explanation of why the work is correct.**
|
||||||
|
|
||||||
A rationale anchors the reviewer. Told why something is right, a reviewer looks
|
A rationale anchors the reviewer. Told why something is right, a reviewer looks
|
||||||
for confirmation and finds it; given only the artefact, it forms an independent
|
for confirmation and finds it; given only the artefact, it forms an independent
|
||||||
view. That independence is the entire mechanism. Every other detail of this
|
view. That independence is the entire mechanism. Every other detail of this
|
||||||
protocol is adjustable. This one is not.
|
protocol is adjustable — D20 just adjusted one of them. This one is not.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -135,3 +168,7 @@ see that judgement was exercised rather than the finding missed.
|
|||||||
**A check reported as passing that was not run.** "Should pass" is not a result.
|
**A check reported as passing that was not run.** "Should pass" is not a result.
|
||||||
If a build, a Lighthouse run, or a JavaScript-disabled render was not actually
|
If a build, a Lighthouse run, or a JavaScript-disabled render was not actually
|
||||||
executed, it must say so.
|
executed, it must say so.
|
||||||
|
|
||||||
|
**A report that says "reviewed" without naming which agent ran.** Under D20 a
|
||||||
|
build step gets `adversarial-reviewer` and not the claims pass; "reviewed" reads
|
||||||
|
as both. The report must name the one that ran.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,886 @@
|
|||||||
|
# The exact published names of ADRIC, ADRIO and ADR Chambers rule sets, designations and codes
|
||||||
|
|
||||||
|
Committed under AGENTS.md R14 and the CLAUDE.md rule it encodes: **anything a
|
||||||
|
spec makes a claim about must be reachable from the repository.** Every fact
|
||||||
|
the six `/practice/*` pages state about the world is checkable here or it is
|
||||||
|
not published.
|
||||||
|
|
||||||
|
**Retrieved 2026-08-29.** Fetched from the primary sources listed below and
|
||||||
|
extracted with quotations pasted verbatim. This file is the artefact; the pages
|
||||||
|
cite it. Do not paraphrase a fact into a page that is not stated here.
|
||||||
|
|
||||||
|
> ⚠️ **A statute, a regulation and a tribunal page all move.** Every consolidation
|
||||||
|
> date and effective date below is as at the retrieval date and nothing more.
|
||||||
|
> Re-check before cutover, and before any republish that turns on one of them.
|
||||||
|
|
||||||
|
> ### R18 re-check — cutover pass, 2026-09-01
|
||||||
|
>
|
||||||
|
> The trigger fired. `AGENTS.md` §12 R18 holds the per-limb findings and
|
||||||
|
> the sources; this stamp does not restate them. **The quoted bytes below
|
||||||
|
> are still the original retrieval and were not re-fetched** — what was
|
||||||
|
> re-checked is whether the *facts* they support have moved.
|
||||||
|
>
|
||||||
|
> - **(g) ADRIC's Code of Ethics, quoted verbatim on `/process/` — NOT
|
||||||
|
> RE-RETRIEVED.** Held unchanged on a cadence judgement `[assumed
|
||||||
|
> 2026-09-01 — Pouya]`. It is the slowest-moving of the seven; the live link
|
||||||
|
> on `/process/` is what makes a stale quotation visible to a reader.
|
||||||
|
|
||||||
|
**Topic as researched:** The Canadian ADR institutions this practice names, and the exact form of their names — ADRIC / ADRIO rule sets, designations, codes; ADR Chambers; early neutral evaluation and dispute-system design
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
| Kind | Source | URL |
|
||||||
|
|---|---|---|
|
||||||
|
| institution | Rules & Codes - ADR Institute of Canada | <https://adric.ca/rules-codes/> |
|
||||||
|
| institution | National Mediation Rules - ADR Institute of Canada | <https://adric.ca/rules-codes/national-mediation-rules/> |
|
||||||
|
| institution | ADR Institute of Canada, Inc. — National Mediation Rules (PDF linked from the National Mediation Rules page) | <https://adric.ca/pdf/ADRMEDIATIONRULES2014.pdf> |
|
||||||
|
| institution | ADRIC Arbitration Rules - ADR Institute of Canada | <https://adric.ca/rules-codes/arbrules/> |
|
||||||
|
| institution | ADRIC ARBITRATION RULES — Effective 01 March 2025 (PDF, 42 pp.) | <https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf> |
|
||||||
|
| institution | ADRIC ARBITRATOR APPOINTMENT PROTOCOL — Effective 01 March 2025 (PDF) | <https://adric.ca/rules/ADRIC-Arbitration-Protocol-2025.pdf> |
|
||||||
|
| institution | ADRIC Med-Arb Rules - ADR Institute of Canada | <https://adric.ca/rules-codes/adric-med-arb-rules/> |
|
||||||
|
| institution | ADRIC Med-Arb Rules (PDF, 8.5 x 11 format) | <https://adric.ca/wp-content/uploads/2023/12/ADRIC_Med_Arb_Rules_2020_8_5-X-11-p.-1.pdf> |
|
||||||
|
| institution | Code of Conduct - ADR Institute of Canada | <https://adric.ca/rules-codes/code-of-conduct/> |
|
||||||
|
| institution | Code of Ethics - ADR Institute of Canada | <https://adric.ca/rules-codes/code-of-ethics/> |
|
||||||
|
| institution | Ethics & Professional Practice - ADR Institute of Canada | <https://adric.ca/ethics-professional-practice/> |
|
||||||
|
| institution | Professional Designations - ADR Institute of Canada | <https://adric.ca/professional-designations/> |
|
||||||
|
| institution | Chartered Med-Arb - ADR Institute of Canada | <https://adric.ca/professional-designations/chartered-med-arb/> |
|
||||||
|
| institution | Arbitrator Designations - ADR Institute of Canada | <https://adric.ca/professional-designations/arbitrator-designations/> |
|
||||||
|
| institution | Mediator Designations - ADR Institute of Canada | <https://adric.ca/professional-designations/mediator-designations/> |
|
||||||
|
| institution | Services - ADR Institute of Canada | <https://adric.ca/services/> |
|
||||||
|
| institution | Custom ADR Systems and Roster Development - ADR Institute of Canada | <https://adric.ca/services/custom-adr-systems-rosters/> |
|
||||||
|
| institution | ADRIC Professional Practice Manuals - ADR Institute of Canada | <https://adric.ca/adric-professional-practice-manuals/> |
|
||||||
|
| institution | ADRIC Sponsored Professional Liability Insurance Program brochure (PDF, PDF creation date 2016) | <https://adric.ca/wp-content/uploads/2015/05/ADR-Brochure-EN-final.pdf> |
|
||||||
|
| institution | Professional Designations – The ADR Institute of Ontario | <https://adr-ontario.ca/professional-designations/> |
|
||||||
|
| institution | Chartered Med-Arbitrator (C.Med-Arb) – The ADR Institute of Ontario | <https://adr-ontario.ca/chartered-med-arbitrator-c-med-arb/> |
|
||||||
|
| institution | Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb) – The ADR Institute of Ontario | <https://adr-ontario.ca/qualified-mediator-q-med-qualified-arbitrator-q-arb/> |
|
||||||
|
| institution | Rules & Codes – The ADR Institute of Ontario | <https://adr-ontario.ca/rules-codes/> |
|
||||||
|
| institution | Code of Ethics – The ADR Institute of Ontario | <https://adr-ontario.ca/code-of-ethics/> |
|
||||||
|
| proponent | About ADR Chambers — Trusted ADR Experts in Canada | <https://adrchambers.com/about-adr-chambers/> |
|
||||||
|
| proponent | Arbitration rules - ADR Chambers | <https://adrchambers.com/arbitration/rules/> |
|
||||||
|
| proponent | ADR Chambers Arbitration Rules — current version PDF (Revised February 10, 2026) | <https://adrchambers.com/wp-content/uploads/2026/02/Revised-ADRC-Arbitration-Rules-Feb-10-2026.pdf> |
|
||||||
|
| proponent | Mediation Rules - ADR Chambers | <https://adrchambers.com/mediation/rules/> |
|
||||||
|
| proponent | ADR Chambers Mediation Rules (PDF) | <https://adrchambers.com/wp-content/uploads/2017/11/Mediation-Rules.pdf> |
|
||||||
|
| proponent | ADR Chambers Expedited Arbitration Rules and Efficiency | <https://adrchambers.com/expedited-arbitration/rules/> |
|
||||||
|
| proponent | ADR Chambers Expedited Arbitration Rules (PDF, revised April 2026) | <https://adrchambers.com/wp-content/uploads/2023/04/ADRC-Expedited-Arbitration-Rules-Revised-April-2026.pdf> |
|
||||||
|
| proponent | Mediation Model Clauses - ADR Chambers | <https://adrchambers.com/mediation-model-clause/> |
|
||||||
|
| proponent | ADR Chambers Neutral Evaluation – Expert Case Assessment | <https://adrchambers.com/neutral-evaluation/> |
|
||||||
|
| proponent | ADR Systems Design - ADR Chambers | <https://adrchambers.com/adr-systems-design/> |
|
||||||
|
| proponent | ADR Chambers International – International Arbitration and Mediation | <https://adrchambersinternational.com/> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verbatim quotations
|
||||||
|
|
||||||
|
### Rules & Codes - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/rules-codes/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Rules & Codes - ADR Institute of Canada
|
||||||
|
|
||||||
|
> ADRIC By-laws
|
||||||
|
> Federation MoU
|
||||||
|
> ADRIC Arbitration Rules
|
||||||
|
> National Mediation Rules
|
||||||
|
> ADRIC Med-Arb Rules
|
||||||
|
> Ethics & Professional Practice
|
||||||
|
> Code of Ethics
|
||||||
|
> Code of Conduct
|
||||||
|
> Conflict of Interest
|
||||||
|
> Complaints & Discipline Policy
|
||||||
|
> Privacy Policy
|
||||||
|
> Online Dispute Resolution (ODR) Vision
|
||||||
|
|
||||||
|
> ADRIC By-Laws
|
||||||
|
> Learn More
|
||||||
|
> Federation MoU
|
||||||
|
> Learn More
|
||||||
|
> ADRIC Arbitration Rules
|
||||||
|
> Learn More
|
||||||
|
> ADRIC Mediation Rules
|
||||||
|
> Learn More
|
||||||
|
> ADRIC Med-Arb Rules
|
||||||
|
> Learn More
|
||||||
|
|
||||||
|
> <h2 style="color: #FFFFFF;text-align: center;font-family:Montserrat;font-weight:400;font-style:normal" class="vc_custom_heading">ADRIC Mediation Rules</h2>
|
||||||
|
|
||||||
|
> Chartered Med-Arbitrator
|
||||||
|
|
||||||
|
> ADR Institute of Canada, Inc. 705-130 Albert Street, Ottawa, ON K1P5G4 1-877-475-4353 info@adric.ca
|
||||||
|
|
||||||
|
### National Mediation Rules - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/rules-codes/national-mediation-rules/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>National Mediation Rules - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> <h1><strong>The ADRIC National Mediation Rules</strong></h1>
|
||||||
|
|
||||||
|
> The National Mediation Rules provide rules for initiating mediations, including the appointment of a mediator should the parties be unable to come to an agreement.
|
||||||
|
|
||||||
|
> The National Mediation Rules document contains the following:
|
||||||
|
> Mediation Rules including Code of Conduct
|
||||||
|
> Standard Form Agreement to Mediate (Schedule B)
|
||||||
|
> Administration fees payable to the ADR Institute of Canada- see Schedule A of the Rules
|
||||||
|
|
||||||
|
> All disputes arising out of or in connection with this agreement, or in respect of any legal relationship associated with or derived from this agreement, shall be mediated pursuant to the National Mediation Rules of the ADR Institute of Canada, Inc.
|
||||||
|
|
||||||
|
> Important Update on the ADR Institute of Canada (ADRIC) National Mediation Rules: As of 2025, the ADRIC Mediation Committee is currently reviewing the Mediation Rules to ensure they remain current, practical, and aligned with best practices in the field. In the meantime, the existing rules remain in effect and should continue to be used until any updates are formally adopted.
|
||||||
|
|
||||||
|
> Commercial contracts drafted by law firms of all sizes across Canada commonly contain a clause indicating that any dispute that arises with respect to the contract will be administered by ADR Canada, or one of its affiliates, pursuant to the National Mediation Rules or the ADRIC Arbitration Rules of the ADR Institute of Canada.
|
||||||
|
|
||||||
|
### ADR Institute of Canada, Inc. — National Mediation Rules (PDF linked from the National Mediation Rules page)
|
||||||
|
|
||||||
|
<https://adric.ca/pdf/ADRMEDIATIONRULES2014.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADR INSTITUTE OF CANADA, INC.
|
||||||
|
> NATIONAL MEDIATION RULES
|
||||||
|
|
||||||
|
> ADR INSTITUTE OF CANADA, INC.
|
||||||
|
> CODE OF CONDUCT FOR MEDIATORS
|
||||||
|
|
||||||
|
> ADR Institute of Canada, Inc. © As amended August 3, 2012
|
||||||
|
|
||||||
|
> (g) "Rules" means the National Mediation Rules of the Institute.
|
||||||
|
|
||||||
|
> Parties who agree to mediate under the National Mediation Rules may use the following clause in their agreement:
|
||||||
|
|
||||||
|
### ADRIC Arbitration Rules - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/rules-codes/arbrules/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>ADRIC Arbitration Rules - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> <h1><strong>The ADRIC Arbitration Rules</strong></h1>
|
||||||
|
|
||||||
|
> ADRIC has adopted new Arbitration Rules and a new Arbitrator Appointment Protocol, effective March 1, 2025. This modernization effort aims to support both domestic and international arbitration with up-to-date procedures and streamlined institutional support. The ADRIC Arbitration Rules have been the leading choice for Canadian businesses since 2002.
|
||||||
|
|
||||||
|
> ADRIC Arbitration Rules – Effective 2025
|
||||||
|
> ADRIC Arbitrator Appointment Protocol
|
||||||
|
|
||||||
|
> All disputes arising out of or in connection with this agreement, or in relation to any legal relationship associated with or derived from this agreement, will be resolved by final and binding arbitration under the Arbitration Rules of the ADR Institute of Canada, Inc. [or the Simplified Arbitration Rules of the ADR Institute of Canada, Inc.] The Seat of Arbitration will be [specify]. The language of the arbitration will be [specify].
|
||||||
|
|
||||||
|
> — Comment by William G. Horton, C.Arb, co-chair and discussion leader of the ADRIC Modernization Committee that drafted the new Arbitration Rules.
|
||||||
|
|
||||||
|
### ADRIC ARBITRATION RULES — Effective 01 March 2025 (PDF, 42 pp.)
|
||||||
|
|
||||||
|
<https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADRIC
|
||||||
|
> ARBITRATION RULES
|
||||||
|
> Effective 01 March 2025
|
||||||
|
|
||||||
|
> ADR INSTITUTE OF CANADA, INC.
|
||||||
|
>
|
||||||
|
> ARBITRATION RULES
|
||||||
|
|
||||||
|
> IV. CURRENT VERSION OF THE RULES AND UPDATES
|
||||||
|
> Go to https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf for the most current version of the Rules.
|
||||||
|
|
||||||
|
> 6.2 EXPEDITED/SIMPLIFIED ARBITRATION PROCEDURE ............................................................................... 19
|
||||||
|
|
||||||
|
> 6.2.1 If the parties agree in writing or the Tribunal orders, the arbitration will follow the Expedited/Simplified
|
||||||
|
|
||||||
|
### ADRIC ARBITRATOR APPOINTMENT PROTOCOL — Effective 01 March 2025 (PDF)
|
||||||
|
|
||||||
|
<https://adric.ca/rules/ADRIC-Arbitration-Protocol-2025.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADRIC
|
||||||
|
> ARBITRATOR APPOINTMENT PROTOCOL
|
||||||
|
>
|
||||||
|
> Effective 01 March 2025
|
||||||
|
> Revised: 2026-01-30
|
||||||
|
|
||||||
|
> ADR Institute of Canada (ADRIC)
|
||||||
|
>
|
||||||
|
> ADRIC Arbitrator Appointment Protocol
|
||||||
|
|
||||||
|
> ADRIC makes arbitrator appointments ("Direct Appointments") and generates lists of candidates for appointment by the parties ("Candidate Lists") in accordance with the ADRIC Arbitration Rules (the "Rules")
|
||||||
|
|
||||||
|
> At least three members must have an ADRIC Chartered Arbitrator designation.
|
||||||
|
|
||||||
|
### ADRIC Med-Arb Rules - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/rules-codes/adric-med-arb-rules/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>ADRIC Med-Arb Rules - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> <h1><strong>The ADRIC Med-Arb Rules</strong></h1>
|
||||||
|
|
||||||
|
> The Rules are designed to work in tandem with ADRIC's existing Mediation Rules and Arbitration Rules, integrating seamlessly.
|
||||||
|
|
||||||
|
> Download the ADRIC Med-Arb Rules:
|
||||||
|
> Booklet format
|
||||||
|
> 8.5 x 11 format
|
||||||
|
|
||||||
|
> Designation : We have also developed criteria for a specialized designation: the Chartered Med-Arb
|
||||||
|
|
||||||
|
> Course : We have worked with course designers to develop the Med-Arb Foundational Course.
|
||||||
|
|
||||||
|
> All disputes arising out of or in connection with this agreement, or in respect of any legal relationship associated with or derived from this agreement, will be finally resolved by Med-Arb under the Med-Arb Rules of the ADR Institute of Canada, Inc. The Seat of Arbitration under the ADRIC Arbitration Rules will be [specify]. The language of the Med-Arb will be [specify].
|
||||||
|
|
||||||
|
### ADRIC Med-Arb Rules (PDF, 8.5 x 11 format)
|
||||||
|
|
||||||
|
<https://adric.ca/wp-content/uploads/2023/12/ADRIC_Med_Arb_Rules_2020_8_5-X-11-p.-1.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADRIC Med-Arb Rules
|
||||||
|
|
||||||
|
> MED-ARB
|
||||||
|
> RULES
|
||||||
|
|
||||||
|
> ADRIC Med-Arb Rules
|
||||||
|
> Published by the ADR Institute of Canada
|
||||||
|
|
||||||
|
> Version 1 - 1 June 2020
|
||||||
|
> You are welcome to use and modify these Rules.
|
||||||
|
> We request that ADRIC be acknowledged.
|
||||||
|
|
||||||
|
> THE ADRIC MED-ARB RULES
|
||||||
|
> ADRIC is pleased to have the ADRIC Med-Arb Rules take their place alongside ADRIC's other flagship ADR rules.
|
||||||
|
|
||||||
|
> ADRIC's Rules are state of the art; Canada's first and foremost for the Canadian jurisdiction. The ADRIC Arbitration Rules (which include expedited arbitration), Mediation Rules and Med-Arb Rules: designed to integrate seamlessly.
|
||||||
|
|
||||||
|
> ADRIC also provides dispute resolution services such as ADR systems design, roster development, education, training and certification, and issues publications for practitioners, parties and counsel.
|
||||||
|
|
||||||
|
### Code of Conduct - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/rules-codes/code-of-conduct/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Code of Conduct - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> ADRIC members are held accountable to practice standards that include a Code of Conduct for Mediators and a National Code of Ethics. In effect, ADRIC provides an infrastructure that allows ADR practitioners to be self-regulating professionals.
|
||||||
|
|
||||||
|
> To view the Code of Conduct for Mediators in booklet form, please click here .
|
||||||
|
|
||||||
|
> CODE OF CONDUCT FOR MEDIATORS
|
||||||
|
|
||||||
|
> This Code of Conduct for Mediators (the "Code") applies in its entirety to every Mediator who is a member of the ADR Institute of Canada, Inc. (the "Institute") or any of its Regional Affiliates, or who accepts from the Institute an appointment as Mediator.
|
||||||
|
|
||||||
|
> 12.1 Nothing in the Code replaces or supersedes any other ethical standard or code that may govern the Mediator.
|
||||||
|
|
||||||
|
### Code of Ethics - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/rules-codes/code-of-ethics/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Code of Ethics - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> This code is applicable to all members of the ADR Institute of Canada.
|
||||||
|
|
||||||
|
> A member shall uphold and abide by the Code of Ethics, the Code of Conduct for Mediators, the regulations, and other professional requirements adopted by the ADR Institute of Canada.
|
||||||
|
|
||||||
|
> A Member shall uphold the integrity and fairness of the arbitration and mediation processes.
|
||||||
|
|
||||||
|
> A Member shall be faithful to the relationship of trust and confidentiality inherent in the office of arbitrator or mediator.
|
||||||
|
|
||||||
|
### Ethics & Professional Practice - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/ethics-professional-practice/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Code of Ethics
|
||||||
|
> Learn More
|
||||||
|
> Code of Conduct
|
||||||
|
> Learn More
|
||||||
|
> Conflict of Interest
|
||||||
|
> Learn More
|
||||||
|
> Complaints & Discipline Policy
|
||||||
|
> Learn More
|
||||||
|
|
||||||
|
> ADR Institute of Canada members are held accountable to practice standards that include a Mediator's Code of Conduct and a National Code of Ethics.
|
||||||
|
|
||||||
|
> Complaints about members who breach the standards set out in these documents can be brought to the attention of the Board of Directors of the affiliate or national for investigation and potential discipline as per the procedure set out in Regulations issued under the By-laws of the ADR Institute of Canada, or by disciplinary processes set by the affiliate.
|
||||||
|
|
||||||
|
### Professional Designations - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/professional-designations/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Entry-level <em>Qualified</em> Designations</strong></p>
|
||||||
|
> <ul>
|
||||||
|
> <li><a href="https://adric.ca/professional-designations/arbitrator-designations/">Qualified Arbitrator – Q.Arb</a></li>
|
||||||
|
> <li><a href="https://adric.ca/professional-designations/mediator-designations/">Qualified Mediator – Q.Med</a></li>
|
||||||
|
> </ul>
|
||||||
|
> <p><strong>Senior-level <em>Chartered</em> Designations</strong></p>
|
||||||
|
> <ul>
|
||||||
|
> <li><a href="https://adric.ca/professional-designations/arbitrator-designations/">Chartered Arbitrator – C.Arb</a></li>
|
||||||
|
> <li><a href="https://adric.ca/professional-designations/mediator-designations/">Chartered Mediator – C.Med</a></li>
|
||||||
|
> <li><a href="https://adric.ca/professional-designations/chartered-med-arb/">Chartered Med-Arbitrator – C.Med-Arb</a></li>
|
||||||
|
> </ul>
|
||||||
|
|
||||||
|
> Mediators , Arbitrators and Med-Arbitrators
|
||||||
|
|
||||||
|
> Entry-level Qualified Designations
|
||||||
|
> Qualified Arbitrator – Q.Arb
|
||||||
|
> Qualified Mediator – Q.Med
|
||||||
|
> Senior-level Chartered Designations
|
||||||
|
> Chartered Arbitrator – C.Arb
|
||||||
|
> Chartered Mediator – C.Med
|
||||||
|
> Chartered Med-Arbitrator – C.Med-Arb
|
||||||
|
|
||||||
|
> Specialized Designations:
|
||||||
|
> Family Practice Designations (available in some regions – check with your affiliate)
|
||||||
|
> Qualified Arbitrator (Family) – Q.Arb(Fam) – coming soon
|
||||||
|
> Qualified Mediator (Family) – Q.Med(Fam)
|
||||||
|
> Chartered Arbitrator – C.Arb (Family) – C.Arb(Fam) – coming soon
|
||||||
|
> Chartered Mediator – C.Med (Family) – C.Med(Fam) – coming soon
|
||||||
|
> Construction Adjudication Designations
|
||||||
|
> Qualified Construction Adjudicator – Q.Adj (Const)
|
||||||
|
|
||||||
|
> Generalist Designations
|
||||||
|
> C.Arb or C.Med
|
||||||
|
> $ 280.00
|
||||||
|
> C.Arb and C.Med
|
||||||
|
> $ 420.00
|
||||||
|
> C.Med-Arb
|
||||||
|
> $ 420.00
|
||||||
|
> Q.Arb, Q.Med or Q.Adj (Const)
|
||||||
|
> $ 165.00
|
||||||
|
|
||||||
|
### Chartered Med-Arb - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/professional-designations/chartered-med-arb/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Chartered Med-Arb - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> <h3><strong>ADRIC Chartered Med-Arb Designation (C.Med-Arb)<br />
|
||||||
|
> </strong></h3>
|
||||||
|
|
||||||
|
> <a href="https://adric.ca/professional-designations/chartered-med-arb/">Chartered Med-Arbitrator</a>
|
||||||
|
|
||||||
|
> Chartered Med-Arb Criteria
|
||||||
|
> Chartered Med-Arb application form
|
||||||
|
|
||||||
|
> The ADRIC Med-Arb designation is unique in the ADR world and provides clearly defined criteria for those practitioners who wish to obtain it.
|
||||||
|
|
||||||
|
### Arbitrator Designations - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/professional-designations/arbitrator-designations/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Arbitrator Designations - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> Qualified Arbitrator (Q.Arb)
|
||||||
|
> The Qualified Arbitrator (Q.Arb) designation is an entry-level designation for arbitrators while they continue to learn and practice for the Chartered Arbitration designation. It recognizes member applicants who have completed an ADRIC-Accredited 40 hour arbitration training that includes a written exam.
|
||||||
|
|
||||||
|
> Chartered Arbitrator (C.Arb)
|
||||||
|
> Assessment Criteria
|
||||||
|
> The Chartered Arbitrator (C.Arb) designation recognizes competence at a high level.
|
||||||
|
|
||||||
|
> The C.Arb or Chartered Arbitrator designation is Canada's only official senior designation for experienced, practicing Arbitrators.
|
||||||
|
|
||||||
|
> ADR Institute of British Columbia (ADRBC)
|
||||||
|
> ADR Institute of Alberta (ADRIA)
|
||||||
|
> ADR Institute of Saskatchewan Inc. (ADRISK)
|
||||||
|
> ADR Institute of Manitoba (ADRIM)
|
||||||
|
> ADR Institute of Ontario, Inc. (ADRIO)
|
||||||
|
> L'Institut de médiation et d'arbitrage du Québec (IMAQ)
|
||||||
|
> ADR Atlantic Institute (ADRAI)
|
||||||
|
|
||||||
|
### Mediator Designations - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/professional-designations/mediator-designations/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Mediator Designations - ADR Institute of Canada</title>
|
||||||
|
|
||||||
|
> <h1><strong>ADRIC’s National Designations for Mediators</strong></h1>
|
||||||
|
|
||||||
|
> Qualified Mediator (Q.Med)
|
||||||
|
> The Qualified Mediator designation is an entry-level step for mediators while they continue to learn and practice for the Chartered Mediator designation. It is available to members who have completed at least 80 hours of mediation and related dispute resolution training and have some practice experience.
|
||||||
|
|
||||||
|
> Chartered Mediator (C.Med)
|
||||||
|
> Assessment Criteria
|
||||||
|
> T he Chartered Mediator (C.Med) designation recognizes competence at a high level .
|
||||||
|
|
||||||
|
> The C.Med or Chartered Mediator designation is Canada's most senior designation for mediators with a general or special practice.
|
||||||
|
|
||||||
|
### Services - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/services/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADRIC's Rules are state of the art; Canada's first and foremost for the Canadian jurisdiction. The ADRIC Arbitration Rules , Mediation Rules and the Med-Arb Rules are designed to integrate seamlessly.
|
||||||
|
|
||||||
|
> ADRIC also provides dispute resolution services such as ADR systems design, roster development, training and certification , and issues publications for practitioners, parties and counsel.
|
||||||
|
|
||||||
|
> Construction Adjudication
|
||||||
|
> Learn More
|
||||||
|
> CTA Arbitrator Roster
|
||||||
|
> Learn more
|
||||||
|
> Arbitrator Appointments
|
||||||
|
> Learn More
|
||||||
|
> Mediator Appointments
|
||||||
|
> Learn More
|
||||||
|
> ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
> Learn more
|
||||||
|
> Custom ADR Systems and Roster Development
|
||||||
|
> Learn More
|
||||||
|
> In-House Designations
|
||||||
|
> Learn More
|
||||||
|
|
||||||
|
### Custom ADR Systems and Roster Development - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/services/custom-adr-systems-rosters/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Custom ADR Systems and Roster Development
|
||||||
|
|
||||||
|
> ADRIC can assist organizations of all kinds to develop and administer ADR programs designed to resolve disputes between various parties.
|
||||||
|
|
||||||
|
> Option 1: Custom ADR Systems and Rosters Development
|
||||||
|
> ADRIC assists organizations in developing and administering practical and efficient dispute resolution programs.
|
||||||
|
|
||||||
|
> Option 2: Roster Selection Services
|
||||||
|
|
||||||
|
> Option 3: Roster Management Services
|
||||||
|
|
||||||
|
### ADRIC Professional Practice Manuals - ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/adric-professional-practice-manuals/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The ADRIC Arbitration Practice Handbook
|
||||||
|
|
||||||
|
> The ADRIC Commercial Mediation Practice Handbook
|
||||||
|
|
||||||
|
> The Mediation Handbook is designed to function as a guide for professionals working in the field of commercial mediation.
|
||||||
|
|
||||||
|
### ADRIC Sponsored Professional Liability Insurance Program brochure (PDF, PDF creation date 2016)
|
||||||
|
|
||||||
|
<https://adric.ca/wp-content/uploads/2015/05/ADR-Brochure-EN-final.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> •• Early Neutral Evaluation
|
||||||
|
|
||||||
|
> •• ADR Systems Design / Implementation and/or
|
||||||
|
> Management
|
||||||
|
|
||||||
|
> •• Pre-ADR Processes (assisting the parties to
|
||||||
|
> select an appropriate ADR process)
|
||||||
|
|
||||||
|
> THE INFORMATION PROVIDED ABOVE IS ONLY A GENERAL OUTLINE OF COVERAGES AVAILABLE. FOR EXACT TERMS, DEFINITIONS,
|
||||||
|
> LIMITATIONS, AND EXCLUSIONS, PLEASE SPEAK WITH YOUR LICENSED MARSH CANADA BROKER, OR REFER TO THE POLICY WORDING.
|
||||||
|
|
||||||
|
### Professional Designations – The ADR Institute of Ontario
|
||||||
|
|
||||||
|
<https://adr-ontario.ca/professional-designations/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Professional Designations – The ADR Institute of Ontario</title>
|
||||||
|
|
||||||
|
> Designations
|
||||||
|
> The Path to your Professional ADR designation
|
||||||
|
> ADRIC Accredited Courses
|
||||||
|
> Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
> Chartered Mediator (C.Med)
|
||||||
|
> Chartered Arbitrator (C.Arb)
|
||||||
|
> Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
> Insurance
|
||||||
|
> FAQ
|
||||||
|
|
||||||
|
> Professional Designations
|
||||||
|
> To read more about the path to designations, click here .
|
||||||
|
|
||||||
|
### Chartered Med-Arbitrator (C.Med-Arb) – The ADR Institute of Ontario
|
||||||
|
|
||||||
|
<https://adr-ontario.ca/chartered-med-arbitrator-c-med-arb/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Chartered Med-Arbitrator (C.Med-Arb) – The ADR Institute of Ontario</title>
|
||||||
|
|
||||||
|
> <h1 class="page-title"><span>Chartered Med-Arbitrator (C.Med-Arb)</span></h1>
|
||||||
|
|
||||||
|
> Med-Arb is a distinct, innovative standalone process that is not as well known or understood by consumers of ADR services compared to mediation and arbitration.
|
||||||
|
|
||||||
|
> To read more about the C.Med-Arb designation, including how to apply, click here .
|
||||||
|
|
||||||
|
### Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb) – The ADR Institute of Ontario
|
||||||
|
|
||||||
|
<https://adr-ontario.ca/qualified-mediator-q-med-qualified-arbitrator-q-arb/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <h1 class="page-title"><span>Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)</span></h1>
|
||||||
|
|
||||||
|
> Qualified Mediator (Q.Med) / Qualified Arbitrator (Q.Arb)
|
||||||
|
> These designations are to recognize practitioners who have completed sufficient mediation / arbitration and related dispute resolution training to be qualified to practice. They are an intermediate step for practitioners working to receive their Chartered designation.
|
||||||
|
|
||||||
|
> *Q.Med criteria vary across affiliates. For the criteria specific to Ontario, be sure to read the checklist on the application form.
|
||||||
|
|
||||||
|
### Rules & Codes – The ADR Institute of Ontario
|
||||||
|
|
||||||
|
<https://adr-ontario.ca/rules-codes/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Rules & Codes – The ADR Institute of Ontario</title>
|
||||||
|
|
||||||
|
> Rules & Codes
|
||||||
|
> Code of Ethics
|
||||||
|
> View Code of Ethics.
|
||||||
|
> Code of Conduct
|
||||||
|
> View Code of Conduct.
|
||||||
|
> Complaints Policy
|
||||||
|
> ...
|
||||||
|
> ADRIC Arbitration Rules
|
||||||
|
> View ADRIC Arbitration Rules.
|
||||||
|
> National Mediation Rules
|
||||||
|
> View Selection Protocols.
|
||||||
|
> ADRIO's Statement Re: Zero Tolerance for Unacceptable Behaviour
|
||||||
|
> ...
|
||||||
|
> ADRIO Policy Statement on Conduct & Communication
|
||||||
|
|
||||||
|
> Code of Ethics</h2>
|
||||||
|
> <p class="wp-block-paragraph"><a href="https://adr-ontario.ca/code-of-ethics/">View Code of Ethics.</a></p>
|
||||||
|
|
||||||
|
> <h2 ...>Code of Conduct</h2>
|
||||||
|
> <p class="wp-block-paragraph"><a href="https://adric.ca/rules-codes/code-of-conduct/">View Code of Conduct.</a></p>
|
||||||
|
|
||||||
|
> <h2 ...>ADRIC Arbitration Rules</h2>
|
||||||
|
> <p class="wp-block-paragraph"><a href="https://adric.ca/rules-codes/arbrules/">View ADRIC Arbitration Rules.</a></p>
|
||||||
|
|
||||||
|
> <h2 ...>National Mediation Rules</h2>
|
||||||
|
> <p class="wp-block-paragraph"><a href="https://adric.ca/rules-codes/national-mediation-rules/" data-type="URL" target="_blank" rel="noreferrer noopener">View Selection Protocols.</a></p>
|
||||||
|
|
||||||
|
### Code of Ethics – The ADR Institute of Ontario
|
||||||
|
|
||||||
|
<https://adr-ontario.ca/code-of-ethics/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Code of Ethics – The ADR Institute of Ontario</title>
|
||||||
|
|
||||||
|
> <h1 class="page-title"><span>Code of Ethics</span></h1>
|
||||||
|
|
||||||
|
> Code of Ethics
|
||||||
|
> The Code is applicable to all members of the Institute.
|
||||||
|
|
||||||
|
> A Member shall uphold and abide by the Rules of Conduct, regulations and other professional requirements adopted by the Institute.
|
||||||
|
|
||||||
|
> A Member shall uphold the integrity and fairness of the arbitration and mediation processes.
|
||||||
|
|
||||||
|
### About ADR Chambers — Trusted ADR Experts in Canada
|
||||||
|
|
||||||
|
<https://adrchambers.com/about-adr-chambers/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <h1>About ADR Chambers — Trusted ADR Experts in Canada</h1>
|
||||||
|
|
||||||
|
> For more than 30 years, ADR Chambers, the renowned arbitration mediation firm, has provided conflict resolution services across Canada and internationally.
|
||||||
|
|
||||||
|
> ADR Chambers' dispute resolution services include mediation , arbitration , ombuds services , workplace investigations , neutral evaluation , med/arb , fairness monitoring , workplace restoration , private appeals and independent assessment of workplace accommodation requests .
|
||||||
|
|
||||||
|
> The world's largest dispute resolution service provider *
|
||||||
|
|
||||||
|
> * Over the past 15 years, ADR Chambers has administered over 55,000 mediations and arbitrations through their reputable arbitration mediation firm.
|
||||||
|
|
||||||
|
### Arbitration rules - ADR Chambers
|
||||||
|
|
||||||
|
<https://adrchambers.com/arbitration/rules/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Arbitration rules - ADR Chambers</title>
|
||||||
|
|
||||||
|
> <h1>Arbitration Rules</h1>
|
||||||
|
|
||||||
|
> Download current Arbitration Rules (PDF).
|
||||||
|
> Download previous version of Rules : June 3, 2025 – February 9, 2026
|
||||||
|
> Download previous version of Rules : June 1, 2024 to June 2, 2025
|
||||||
|
> Download previous version of Rules : September 10, 2013 to May 31, 2024
|
||||||
|
|
||||||
|
> "ADR Chambers" means ADR Chambers Inc.
|
||||||
|
|
||||||
|
> "Rules" means the ADR Chambers Arbitration Rules in force at the time of the commencement of the arbitration.
|
||||||
|
|
||||||
|
> "Med-Arb" means a process by which the Parties agree to first submit their dispute to mediation and, in the event that the dispute does not fully settle at mediation, to an arbitration in accordance with these Rules, where the arbitrator will be the same individual as the mediator.
|
||||||
|
|
||||||
|
> 2.6 These Rules may be amended by ADR Chambers in its sole discretion. Amendments become effective when they are posted to the ADR Chambers website.
|
||||||
|
|
||||||
|
### ADR Chambers Arbitration Rules — current version PDF (Revised February 10, 2026)
|
||||||
|
|
||||||
|
<https://adrchambers.com/wp-content/uploads/2026/02/Revised-ADRC-Arbitration-Rules-Feb-10-2026.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> 14. "Rules" means the ADR Chambers Arbitration Rules in force at the time of the
|
||||||
|
|
||||||
|
> These Rules are Effective February 10, 2026 and are Subject to Change Without Notice.
|
||||||
|
|
||||||
|
> In accordance with Rule 10.1 of the ADR Chambers Arbitration Rules, a preliminary
|
||||||
|
|
||||||
|
> 12. Mediation
|
||||||
|
> 13. Med/Arb
|
||||||
|
|
||||||
|
### Mediation Rules - ADR Chambers
|
||||||
|
|
||||||
|
<https://adrchambers.com/mediation/rules/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>Mediation Rules - ADR Chambers</title>
|
||||||
|
|
||||||
|
> <h1>Mediation Rules</h1>
|
||||||
|
|
||||||
|
> Download Mediation Rules (PDF).
|
||||||
|
|
||||||
|
> 1.1 These Rules, and all amendments to them, shall be deemed to have been made a part of any agreement which provides for mediation with ADR Chambers.
|
||||||
|
|
||||||
|
> Appendix A: Sample Agreement to Mediate
|
||||||
|
|
||||||
|
### ADR Chambers Mediation Rules (PDF)
|
||||||
|
|
||||||
|
<https://adrchambers.com/wp-content/uploads/2017/11/Mediation-Rules.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADR Chambers Mediation Rules
|
||||||
|
|
||||||
|
> 1. Agreement of Parties
|
||||||
|
> 1.1 These Rules, and all amendments to them, shall be deemed to have been made a part
|
||||||
|
> of any agreement which provides for mediation with ADR Chambers.
|
||||||
|
|
||||||
|
### ADR Chambers Expedited Arbitration Rules and Efficiency
|
||||||
|
|
||||||
|
<https://adrchambers.com/expedited-arbitration/rules/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>ADR Chambers Expedited Arbitration Rules and Efficiency</title>
|
||||||
|
|
||||||
|
> <h1>Expedited Arbitration Rules</h1>
|
||||||
|
|
||||||
|
> Download Expedited Arbitration Rules (PDF).
|
||||||
|
> Download previous version of Rules : March 29, 2017 to April 17, 2026
|
||||||
|
|
||||||
|
> These Rules will apply whenever the parties agree in writing to have their dispute decided "under the Expedited Arbitration Rules of ADR Chambers" or words to that effect.
|
||||||
|
|
||||||
|
### ADR Chambers Expedited Arbitration Rules (PDF, revised April 2026)
|
||||||
|
|
||||||
|
<https://adrchambers.com/wp-content/uploads/2023/04/ADRC-Expedited-Arbitration-Rules-Revised-April-2026.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADR Chambers Expedited Arbitration Rules
|
||||||
|
|
||||||
|
> b) These Rules will apply whenever the parties agree in writing to have their dispute decided "under the
|
||||||
|
> Expedited Arbitration Rules of ADR Chambers" or words to that effect.
|
||||||
|
|
||||||
|
### Mediation Model Clauses - ADR Chambers
|
||||||
|
|
||||||
|
<https://adrchambers.com/mediation-model-clause/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <h1>Mediation Model Clauses</h1>
|
||||||
|
|
||||||
|
> the dispute will be resolved by arbitration at ADR Chambers pursuant to the general ADR Chambers Rules for Arbitration.
|
||||||
|
|
||||||
|
> shall be referred to and finally resolved by arbitration at ADR Chambers under the ADR Chambers Arbitration Rules.
|
||||||
|
|
||||||
|
> the Parties agree to resolve the dispute by arbitration at ADR Chambers using the ADR Chambers Expedited Arbitration Rules.
|
||||||
|
|
||||||
|
### ADR Chambers Neutral Evaluation – Expert Case Assessment
|
||||||
|
|
||||||
|
<https://adrchambers.com/neutral-evaluation/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADR Chambers Neutral Evaluation – Expert Case Assessment
|
||||||
|
|
||||||
|
> Neutral Evaluation Overview
|
||||||
|
> Neutral Evaluation enables parties to hear an expert's assessment of a likely trial outcome. Neutral Evaluation may involve briefs, oral presentations, and sometimes witnesses. The process can be tailored to the dispute.
|
||||||
|
|
||||||
|
> Neutral Evaluation Rules (PDF)
|
||||||
|
> Neutral Evaluation Sample Agreement (PDF)
|
||||||
|
|
||||||
|
> Neutral Evaluation fees range from $250 to $750 plus HST per hour, depending on the experience of the neutral.
|
||||||
|
|
||||||
|
### ADR Systems Design - ADR Chambers
|
||||||
|
|
||||||
|
<https://adrchambers.com/adr-systems-design/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADR Systems Design
|
||||||
|
|
||||||
|
> ADR Chambers offers dispute resolution systems design consultation services. Consultants help organizations that have problems managing disputes, whether because of their frequency, size, complexity, timing, or number. For more information, please contact ADR Chambers.
|
||||||
|
|
||||||
|
> What is Alternative Dispute Resolution System Design?
|
||||||
|
|
||||||
|
> ADR system design helps to match the appropriate dispute resolution process with the type of dispute and the culture of an organization.
|
||||||
|
|
||||||
|
### ADR Chambers International – International Arbitration and Mediation
|
||||||
|
|
||||||
|
<https://adrchambersinternational.com/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> <title>ADR Chambers International – International Arbitration and Mediation</title>
|
||||||
|
|
||||||
|
> ADR Chambers International ("ADRCI") is the leading Canadian organization that specializes in International Arbitration and Mediation. Through the use of the UNCITRAL Arbitration Rules as supplemented by its own state of the art rules, ADRCI provides practitioners and their clients uniformity and credibility in the field of international arbitration and mediation.
|
||||||
|
|
||||||
|
> Arbitration
|
||||||
|
> Arbitration Rules
|
||||||
|
> Roster of Arbitrators
|
||||||
|
> Mediation
|
||||||
|
> Mediation Rules
|
||||||
|
> Roster of Mediators
|
||||||
|
> Model Clause
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this establishes
|
||||||
|
|
||||||
|
Each item names the source it rests on. An item here that no quotation above
|
||||||
|
supports is a defect in this file, not a fact.
|
||||||
|
|
||||||
|
- ADRIC's corporate name, as it appears in its own site footer and in its rule documents, is "ADR Institute of Canada, Inc." — the comma and the "Inc." are part of the name.
|
||||||
|
*Source:* <https://adric.ca/rules-codes/>
|
||||||
|
- ADRIC's arbitration rules are titled "ADRIC Arbitration Rules". The web page's <h1> reads "The ADRIC Arbitration Rules" and the browser title is "ADRIC Arbitration Rules - ADR Institute of Canada".
|
||||||
|
*Source:* <https://adric.ca/rules-codes/arbrules/>
|
||||||
|
- The current arbitration rules edition is effective 1 March 2025. The page says "ADRIC has adopted new Arbitration Rules and a new Arbitrator Appointment Protocol, effective March 1, 2025" and labels the download "ADRIC Arbitration Rules – Effective 2025".
|
||||||
|
*Source:* <https://adric.ca/rules-codes/arbrules/>
|
||||||
|
- The PDF cover of the current edition reads "ADRIC / ARBITRATION RULES / Effective 01 March 2025" — ADRIC writes the date day-first on the artefact itself, and "March 1, 2025" in web prose. Its internal title page reads "ADR INSTITUTE OF CANADA, INC. / ARBITRATION RULES".
|
||||||
|
*Source:* <https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf>
|
||||||
|
- ADRIC publishes a companion document titled "ADRIC Arbitrator Appointment Protocol", also effective 01 March 2025, whose PDF cover carries "Revised: 2026-01-30".
|
||||||
|
*Source:* <https://adric.ca/rules/ADRIC-Arbitration-Protocol-2025.pdf>
|
||||||
|
- ADRIC's mediation rules are titled "National Mediation Rules". The page <h1> is "The ADRIC National Mediation Rules", the browser title is "National Mediation Rules - ADR Institute of Canada", and the model clause reads "pursuant to the National Mediation Rules of the ADR Institute of Canada, Inc."
|
||||||
|
*Source:* <https://adric.ca/rules-codes/national-mediation-rules/>
|
||||||
|
- ADRIC is internally inconsistent about the mediation rules' name: the Rules & Codes index nav item reads "National Mediation Rules" while the index card on the same page reads "ADRIC Mediation Rules". Both strings appear in the same fetched document.
|
||||||
|
*Source:* <https://adric.ca/rules-codes/>
|
||||||
|
- The mediation rules PDF that ADRIC currently links carries no 2025-era effective date; its internal headings read "ADR INSTITUTE OF CANADA, INC. / NATIONAL MEDIATION RULES" and every page footer reads "ADR Institute of Canada, Inc. © As amended August 3, 2012".
|
||||||
|
*Source:* <https://adric.ca/pdf/ADRMEDIATIONRULES2014.pdf>
|
||||||
|
- As of the page state on 2026-08-29 the mediation rules are under review but unchanged: "the ADRIC Mediation Committee is currently reviewing the Mediation Rules ... In the meantime, the existing rules remain in effect and should continue to be used until any updates are formally adopted."
|
||||||
|
*Source:* <https://adric.ca/rules-codes/national-mediation-rules/>
|
||||||
|
- ADRIC's med-arb rules are titled "ADRIC Med-Arb Rules" — hyphenated "Med-Arb", not "Med/Arb" and not "Mediation-Arbitration". The page <h1> is "The ADRIC Med-Arb Rules".
|
||||||
|
*Source:* <https://adric.ca/rules-codes/adric-med-arb-rules/>
|
||||||
|
- The ADRIC Med-Arb Rules PDF is stamped "Version 1 - 1 June 2020" and its running head and title block read "ADRIC Med-Arb Rules" / "MED-ARB RULES" / "THE ADRIC MED-ARB RULES".
|
||||||
|
*Source:* <https://adric.ca/wp-content/uploads/2023/12/ADRIC_Med_Arb_Rules_2020_8_5-X-11-p.-1.pdf>
|
||||||
|
- The chartered med-arb designation is written by ADRIC as "Chartered Med-Arbitrator", abbreviated "C.Med-Arb". The Professional Designations page lists verbatim: "Chartered Med-Arbitrator – C.Med-Arb" (raw HTML: "Chartered Med-Arbitrator – C.Med-Arb"), and the site-wide nav item linking to that page reads "Chartered Med-Arbitrator".
|
||||||
|
*Source:* <https://adric.ca/professional-designations/>
|
||||||
|
- The string "Chartered Mediator-Arbitrator" does not appear anywhere on the ADRIC or ADRIO pages fetched. A grep for "Mediator-Arbitrator" across all 20+ extracted ADRIC/ADRIO page texts returned no matches; "Med-Arbitrator" matched on every page (nav) plus the designation list.
|
||||||
|
*Source:* <https://adric.ca/professional-designations/>
|
||||||
|
- ADRIC uses a second, shorter form for the same designation on the designation's own page: the heading is "ADRIC Chartered Med-Arb Designation (C.Med-Arb)", the browser title is "Chartered Med-Arb - ADR Institute of Canada", and the linked criteria are "Chartered Med-Arb Criteria".
|
||||||
|
*Source:* <https://adric.ca/professional-designations/chartered-med-arb/>
|
||||||
|
- The ADRIC Med-Arb Rules page uses a third form again: "we have also developed criteria for a specialized designation: the Chartered Med-Arb".
|
||||||
|
*Source:* <https://adric.ca/rules-codes/adric-med-arb-rules/>
|
||||||
|
- ADRIC's four generalist designations expand as: Qualified Arbitrator (Q.Arb), Qualified Mediator (Q.Med), Chartered Arbitrator (C.Arb), Chartered Mediator (C.Med). ADRIC groups the first two as "Entry-level Qualified Designations" and the latter two as "Senior-level Chartered Designations".
|
||||||
|
*Source:* <https://adric.ca/professional-designations/>
|
||||||
|
- ADRIC describes C.Arb as "Canada's only official senior designation for experienced, practicing Arbitrators" and Q.Arb as "an entry-level designation for arbitrators".
|
||||||
|
*Source:* <https://adric.ca/professional-designations/arbitrator-designations/>
|
||||||
|
- ADRIC describes C.Med as "Canada's most senior designation for mediators with a general or special practice" and Q.Med as "an entry-level step for mediators". The mediator designations page <h1> is "ADRIC's National Designations for Mediators".
|
||||||
|
*Source:* <https://adric.ca/professional-designations/mediator-designations/>
|
||||||
|
- ADRIC designations are conferred nationally but administered regionally: "You must be a 'Full' Member via one of ADRIC's Regional Affiliates before applying for an ADRIC designation."
|
||||||
|
*Source:* <https://adric.ca/professional-designations/>
|
||||||
|
- ADRIC names its seven regional affiliates verbatim as: ADR Institute of British Columbia (ADRBC), ADR Institute of Alberta (ADRIA), ADR Institute of Saskatchewan Inc. (ADRISK), ADR Institute of Manitoba (ADRIM), ADR Institute of Ontario, Inc. (ADRIO), L'Institut de médiation et d'arbitrage du Québec (IMAQ), ADR Atlantic Institute (ADRAI).
|
||||||
|
*Source:* <https://adric.ca/professional-designations/arbitrator-designations/>
|
||||||
|
- ADRIC publishes exactly two conduct/ethics instruments for practitioners, and their exact titles are "Code of Ethics" and "Code of Conduct for Mediators": "ADRIC members are held accountable to practice standards that include a Code of Conduct for Mediators and a National Code of Ethics."
|
||||||
|
*Source:* <https://adric.ca/rules-codes/code-of-conduct/>
|
||||||
|
- The Code of Ethics applies to all ADRIC members and covers both processes: "This code is applicable to all members of the ADR Institute of Canada" and "A Member shall uphold the integrity and fairness of the arbitration and mediation processes."
|
||||||
|
*Source:* <https://adric.ca/rules-codes/code-of-ethics/>
|
||||||
|
- ADRIC itself calls the same two documents by varying names: the Code of Ethics page says "the Code of Ethics, the Code of Conduct for Mediators"; the Code of Conduct page says "a Code of Conduct for Mediators and a National Code of Ethics"; the Ethics & Professional Practice page says "a Mediator's Code of Conduct and a National Code of Ethics".
|
||||||
|
*Source:* <https://adric.ca/ethics-professional-practice/>
|
||||||
|
- The Code of Conduct for Mediators is also bound into the National Mediation Rules booklet: "The National Mediation Rules document contains the following: Mediation Rules including Code of Conduct".
|
||||||
|
*Source:* <https://adric.ca/rules-codes/national-mediation-rules/>
|
||||||
|
- ADRIC's Ethics & Professional Practice section also names two further instruments: "Conflict of Interest" and "Complaints & Discipline Policy".
|
||||||
|
*Source:* <https://adric.ca/ethics-professional-practice/>
|
||||||
|
- ADRIO's own name, as it writes it, is "The ADR Institute of Ontario" in page titles and "ADR Institute of Ontario, Inc. (ADRIO)" in ADRIC's affiliate list.
|
||||||
|
*Source:* <https://adr-ontario.ca/professional-designations/>
|
||||||
|
- ADRIO writes the designation as "Chartered Med-Arbitrator (C.Med-Arb)" — that is the page <h1>, the browser title, and the nav item.
|
||||||
|
*Source:* <https://adr-ontario.ca/chartered-med-arbitrator-c-med-arb/>
|
||||||
|
- ADRIO's designation nav lists, verbatim: "Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb) / Chartered Mediator (C.Med) / Chartered Arbitrator (C.Arb) / Chartered Med-Arbitrator (C.Med-Arb)".
|
||||||
|
*Source:* <https://adr-ontario.ca/professional-designations/>
|
||||||
|
- ADRIO publishes its own "Code of Ethics" as a page on its own site, applicable "to all members of the Institute", but points to ADRIC for the Code of Conduct — its "Code of Conduct" entry links to https://adric.ca/rules-codes/code-of-conduct/.
|
||||||
|
*Source:* <https://adr-ontario.ca/rules-codes/>
|
||||||
|
- ADRIO does not publish its own mediation or arbitration rules: its Rules & Codes page lists "ADRIC Arbitration Rules" and "National Mediation Rules", both linking out to adric.ca.
|
||||||
|
*Source:* <https://adr-ontario.ca/rules-codes/>
|
||||||
|
- ADRIO also publishes two policy documents of its own, titled "ADRIO's Statement Re: Zero Tolerance for Unacceptable Behaviour" and "ADRIO Policy Statement on Conduct & Communication", plus a "Complaints Policy".
|
||||||
|
*Source:* <https://adr-ontario.ca/rules-codes/>
|
||||||
|
- ADR Chambers is a private dispute-resolution firm, not a professional body. Its own about page: "For more than 30 years, ADR Chambers, the renowned arbitration mediation firm, has provided conflict resolution services across Canada and internationally." Its rules define "'ADR Chambers' means ADR Chambers Inc."
|
||||||
|
*Source:* <https://adrchambers.com/about-adr-chambers/>
|
||||||
|
- ADR Chambers does publish its own rule sets. The exact titles are "ADR Chambers Arbitration Rules", "ADR Chambers Mediation Rules" and "ADR Chambers Expedited Arbitration Rules".
|
||||||
|
*Source:* <https://adrchambers.com/wp-content/uploads/2017/11/Mediation-Rules.pdf>
|
||||||
|
- The current ADR Chambers Arbitration Rules are stamped "These Rules are Effective February 10, 2026 and are Subject to Change Without Notice." The website also links three superseded versions (June 3 2025 – February 9 2026; June 1 2024 to June 2 2025; September 10 2013 to May 31 2024).
|
||||||
|
*Source:* <https://adrchambers.com/wp-content/uploads/2026/02/Revised-ADRC-Arbitration-Rules-Feb-10-2026.pdf>
|
||||||
|
- The ADR Chambers Arbitration Rules are self-amending without notice: "These Rules may be amended by ADR Chambers in its sole discretion. Amendments become effective when they are posted to the ADR Chambers website." Any citation to a specific edition should therefore carry a retrieval date.
|
||||||
|
*Source:* <https://adrchambers.com/arbitration/rules/>
|
||||||
|
- ADR Chambers is itself inconsistent about the arbitration rules' name: on a single model-clauses page it writes both "the general ADR Chambers Rules for Arbitration" and "the ADR Chambers Arbitration Rules". The rules document's own definition uses "ADR Chambers Arbitration Rules".
|
||||||
|
*Source:* <https://adrchambers.com/mediation-model-clause/>
|
||||||
|
- The ADR Chambers Arbitration Rules contain a med-arb regime internally, spelled "Med-Arb" in the definitions and "Med/Arb" in the table of contents heading 13; the definition reads "a process by which the Parties agree to first submit their dispute to mediation and, in the event that the dispute does not fully settle at mediation, to an arbitration in accordance with these Rules, where the arbitrator will be the same individual as the mediator."
|
||||||
|
*Source:* <https://adrchambers.com/arbitration/rules/>
|
||||||
|
- ADR Chambers offers a distinct "Neutral Evaluation" service — its own name for it does not include the word "Early" — and publishes "Neutral Evaluation Rules (PDF)" and a "Neutral Evaluation Sample Agreement (PDF)".
|
||||||
|
*Source:* <https://adrchambers.com/neutral-evaluation/>
|
||||||
|
- ADR Chambers offers dispute-system design under the name "ADR Systems Design": "ADR Chambers offers dispute resolution systems design consultation services."
|
||||||
|
*Source:* <https://adrchambers.com/adr-systems-design/>
|
||||||
|
- ADR Chambers International ("ADRCI") is a separate, differently-named entity with its own site and its own arbitration and mediation rules, built on the UNCITRAL Arbitration Rules. It must not be conflated with ADR Chambers Inc.
|
||||||
|
*Source:* <https://adrchambersinternational.com/>
|
||||||
|
- ADRIC does name dispute-system design as a service it provides: "ADRIC also provides dispute resolution services such as ADR systems design, roster development, training and certification , and issues publications for practitioners, parties and counsel."
|
||||||
|
*Source:* <https://adric.ca/services/>
|
||||||
|
- ADRIC's dispute-system-design service has a formal page title: "Custom ADR Systems and Roster Development" (the site nav renders it "Custom ADR Systems and Rosters Development"), covering systems development, roster selection and roster management.
|
||||||
|
*Source:* <https://adric.ca/services/custom-adr-systems-rosters/>
|
||||||
|
- ADRIC publishes two practitioner manuals, titled "The ADRIC Arbitration Practice Handbook" and "The ADRIC Commercial Mediation Practice Handbook".
|
||||||
|
*Source:* <https://adric.ca/adric-professional-practice-manuals/>
|
||||||
|
- The only ADRIC-hosted document found mentioning early neutral evaluation is its sponsored professional-liability insurance brochure, which lists "Early Neutral Evaluation" and "ADR Systems Design / Implementation and/or Management" among insurable ADR activities. This is a schedule of coverage, not a rule set, standard or designation, and the brochure's own PDF creation date is 2016.
|
||||||
|
*Source:* <https://adric.ca/wp-content/uploads/2015/05/ADR-Brochure-EN-final.pdf>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this does NOT establish
|
||||||
|
|
||||||
|
**Read this section before writing copy.** It is the half that keeps a page
|
||||||
|
honest, and on this project it is the half that has twice been skipped.
|
||||||
|
|
||||||
|
- **Does ADRIC publish anything about EARLY NEUTRAL EVALUATION as a recognised neutral service — rules, standards, a designation, or a definitional page?**
|
||||||
|
- *Searched:* WebSearch for 'adric.ca "early neutral evaluation" ADR Institute of Canada' (which surfaced https://adric.ca/ufaqs/what-is-early-neutral-evaluation/); direct curl of that URL; direct curl of https://adric.ca/ufaqs/; ADRIC's own site search at https://adric.ca/?s=early+neutral+evaluation; a grep for 'neutral evaluation' across the extracted text of all 20+ ADRIC and ADRIO pages fetched; a grep for 'neutral evaluation' through the full text of the 42-page ADRIC Arbitration Rules 2025 PDF.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — and the search-result page is dead. https://adric.ca/ufaqs/what-is-early-neutral-evaluation/ returns HTTP 404 and https://adric.ca/ufaqs/ returns HTTP 404 (both verified by curl reading the status code, stderr not suppressed). ADRIC's own site search returns only loosely-matching journal articles, no ENE page. 'Neutral evaluation' appears nowhere in ADRIC's rules, codes, designations or services pages. The ONLY ADRIC-hosted artefact naming it is the insurance brochure listed in sources — a schedule of insurable activities, roughly a decade old. Do NOT write that ADRIC recognises, defines, or publishes on early neutral evaluation. ADR Chambers, by contrast, does offer it (as 'Neutral Evaluation', without 'Early') and does publish Neutral Evaluation Rules.
|
||||||
|
- **Is there a separately published rule set titled 'Simplified Arbitration Rules of the ADR Institute of Canada, Inc.'? ADRIC's own model arbitration clause offers it as an alternative.**
|
||||||
|
- *Searched:* The model clause text on https://adric.ca/rules-codes/arbrules/ and on page 1 of the 2025 Rules PDF; a case-insensitive grep for 'simplified' and 'expedited' through the full 42-page 2025 Rules PDF; a grep of the arbrules page HTML for any href containing 'simplified' (exit status 1 — no match); ADRIC site search at https://adric.ca/?s=Simplified+Arbitration+Rules; speculative curl of https://adric.ca/rules/ADRIC-Simplified-Arbitration-Rules-2025.pdf and https://adric.ca/rules-codes/simplified-arbitration-rules/.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — both speculative URLs returned HTTP 404, the arbrules page links no such document, and the site search surfaced only articles. What actually exists is Rule 6.2 INSIDE the ADRIC Arbitration Rules, headed 'EXPEDITED/SIMPLIFIED ARBITRATION PROCEDURE', plus 'Appendix R2' and a 'SAMPLE EXPEDITED/SIMPLIFIED PROCEDURE'. Treat 'Simplified Arbitration Rules' as a phrase in ADRIC's model clause, not as the title of a document you can cite or link. If copy needs to mention it, say it is a procedure within the ADRIC Arbitration Rules.
|
||||||
|
- **What is the current effective date or edition of the ADRIC National Mediation Rules?**
|
||||||
|
- *Searched:* https://adric.ca/rules-codes/national-mediation-rules/ page text and HTML (no effective-date string present); every PDF link on that page; the linked rules PDF at https://adric.ca/pdf/ADRMEDIATIONRULES2014.pdf — pdfinfo metadata and pdftotext of pages 1–6 and 20–22.
|
||||||
|
- *Outcome:* NOT ESTABLISHED as a current-edition date. The page publishes no effective date. The PDF's own footers read 'As amended August 3, 2012'; its filename says 2014; its PDF metadata Title is 'ADR MEDIATION RULES 2011 Cover.p65' with a CreationDate of 24 Feb 2014. Three different years attach to one artefact. The page states the rules are under review by the ADRIC Mediation Committee as of 2025 and that the existing rules remain in effect. Recommendation: name the rules, do not date them.
|
||||||
|
- **Does ADRIC publish a code of conduct for ARBITRATORS, parallel to its Code of Conduct for Mediators?**
|
||||||
|
- *Searched:* https://adric.ca/rules-codes/ (full Rules & Codes index, nav and cards), https://adric.ca/ethics-professional-practice/, https://adric.ca/rules-codes/code-of-conduct/ (full text) and https://adric.ca/rules-codes/code-of-ethics/ (full text); the table of contents of the National Mediation Rules booklet PDF.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — no arbitrator-specific code of conduct was found. ADRIC's own framing is consistently two documents: 'a Code of Conduct for Mediators and a National Code of Ethics'. The Code of Ethics is the instrument that covers arbitrators ('the integrity and fairness of the arbitration and mediation processes'; 'the office of arbitrator or mediator'). Do not write that ADRIC has a code of conduct for arbitrators. The 2025 Arbitration Rules do carry their own conflicts-disclosure standards and a 'Standard statement of arbitrator independence and impartiality', which is a different thing from a code of conduct.
|
||||||
|
- **Which single string is 'correct' for the chartered med-arb designation, given ADRIC uses more than one?**
|
||||||
|
- *Searched:* All ADRIC designation pages plus the Med-Arb Rules page and the site-wide nav; all five ADRIO designation pages; raw-HTML inspection of the designation list markup to confirm the dash character.
|
||||||
|
- *Outcome:* PARTIALLY ESTABLISHED, with a caution. 'Chartered Mediator-Arbitrator' is WRONG and appears nowhere — that much is settled. But ADRIC uses TWO forms of the correct name: 'Chartered Med-Arbitrator' (designation list, site nav) and 'Chartered Med-Arb' (that designation page's own heading, its criteria, and the Med-Arb Rules page). ADRIO uses 'Chartered Med-Arbitrator (C.Med-Arb)' only. Recommendation for copy: 'Chartered Med-Arbitrator (C.Med-Arb)' — the form both institutions share and the only one ADRIO uses. The separator in ADRIC's list is an EN DASH (raw HTML '–'), not a hyphen; the abbreviation 'C.Med-Arb' takes a hyphen.
|
||||||
|
- **The cover/front-matter title of the ADRIC Code of Conduct for Mediators as a standalone PDF.**
|
||||||
|
- *Searched:* curl of https://adric.ca/wp-content/uploads/2016/04/Code-of-Conduct-for-Mediators.pdf (HTTP 200, 779,235 bytes, 3 pages); pdftotext with -layout over pages 1-2 and over the whole file.
|
||||||
|
- *Outcome:* NOT ESTABLISHED from that PDF — pdftotext returned zero characters at exit 0, i.e. the file is a scanned image with no text layer. The title is nevertheless established from two other fetched artefacts: the HTML page renders the heading 'CODE OF CONDUCT FOR MEDIATORS' and the opening sentence 'This Code of Conduct for Mediators (the "Code")...', and the National Mediation Rules booklet's contents page carries 'ADR INSTITUTE OF CANADA, INC. / CODE OF CONDUCT FOR MEDIATORS'.
|
||||||
|
- **Whether ADR Chambers publishes rules for its med-arb service, under a distinct title.**
|
||||||
|
- *Searched:* curl of https://adrchambers.com/med-arb/ (HTTP 404); the ADR Chambers Arbitration Rules page and PDF; the model-clauses page; the sidebar link inventory on the mediation, arbitration and expedited-arbitration pages.
|
||||||
|
- *Outcome:* NOT ESTABLISHED as a separate document. Med-arb at ADR Chambers is governed INSIDE the ADR Chambers Arbitration Rules (Rule 13, 'Med/Arb', with 'Med-Arb', 'Med-Arb Agreement' and 'Notice of Request for Med-Arb' all defined in Rule 1.1), and the firm links a 'Notice of Request for Med-Arb' form. There is no standalone 'ADR Chambers Med-Arb Rules'. Note the firm's service menu spells it 'Med/Arb' while the rules text spells it 'Med-Arb'.
|
||||||
|
- **A WebFetch-based read of adric.ca, per the instructed method.**
|
||||||
|
- *Searched:* WebFetch of https://adric.ca/rules-codes/.
|
||||||
|
- *Outcome:* BLOCKED — WebFetch returned HTTP 403 Forbidden with no body (adric.ca appears to filter by user agent). All ADRIC content in this report was therefore fetched with curl -sSL through Bash, reading the HTTP status code and curl exit status on every request, with stderr NOT suppressed. adr-ontario.ca and adrchambers.com fetched cleanly the same way. HTML was converted to text with the repository's committed extractor at /Users/pouya/Dev/Websites/adr-sml/docs/reference/adrio-extract/extract.mjs, and PDFs with /opt/homebrew/bin/pdftotext -layout.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Searches run
|
||||||
|
|
||||||
|
- `WebSearch: ADR Institute of Canada arbitration rules mediation rules official titles adric.ca`
|
||||||
|
- `WebSearch: ADR Institute of Ontario ADRIO designations Q.Med C.Med chartered mediator official site`
|
||||||
|
- `WebSearch: ADR Chambers arbitration rules mediation rules adrchambers.com`
|
||||||
|
- `WebSearch: adric.ca "early neutral evaluation" ADR Institute of Canada`
|
||||||
|
- `WebFetch https://adric.ca/rules-codes/ — HTTP 403 Forbidden, no body (blocked; all ADRIC reads redone via curl)`
|
||||||
|
- `curl -sSL https://adric.ca/rules-codes/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/rules-codes/national-mediation-rules/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/rules-codes/arbrules/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/rules-codes/adric-med-arb-rules/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/rules-codes/code-of-ethics/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/rules-codes/code-of-conduct/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/ethics-professional-practice/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/professional-designations/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/professional-designations/arbitrator-designations/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/professional-designations/mediator-designations/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/professional-designations/chartered-med-arb/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/services/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/services/custom-adr-systems-rosters/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/adric-professional-practice-manuals/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/about/about-us/ — 200`
|
||||||
|
- `curl -sSL https://adric.ca/ufaqs/what-is-early-neutral-evaluation/ — 404`
|
||||||
|
- `curl -sSL https://adric.ca/ufaqs/ — 404`
|
||||||
|
- `curl -sSL https://adric.ca/rules/ADRIC-Simplified-Arbitration-Rules-2025.pdf — 404`
|
||||||
|
- `curl -sSL https://adric.ca/rules-codes/simplified-arbitration-rules/ — 404`
|
||||||
|
- `ADRIC site search: https://adric.ca/?s=early+neutral+evaluation — 200, no ENE page in results`
|
||||||
|
- `ADRIC site search: https://adric.ca/?s=Simplified+Arbitration+Rules — 200, no such document in results`
|
||||||
|
- `curl + pdftotext -layout: https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf (200, 42 pp.)`
|
||||||
|
- `curl + pdftotext -layout: https://adric.ca/rules/ADRIC-Arbitration-Protocol-2025.pdf (200)`
|
||||||
|
- `curl + pdftotext -layout: https://adric.ca/pdf/ADRMEDIATIONRULES2014.pdf (200, 22 pp.)`
|
||||||
|
- `curl + pdftotext -layout: https://adric.ca/wp-content/uploads/2023/12/ADRIC_Med_Arb_Rules_2020_8_5-X-11-p.-1.pdf (200)`
|
||||||
|
- `curl + pdftotext: https://adric.ca/wp-content/uploads/2016/04/Code-of-Conduct-for-Mediators.pdf (200, 3 pp., NO text layer — scanned image, zero characters extracted at exit 0)`
|
||||||
|
- `curl + pdftotext -layout: https://adric.ca/wp-content/uploads/2015/05/ADR-Brochure-EN-final.pdf (200)`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/professional-designations/ — 200`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/qualified-mediator-q-med-qualified-arbitrator-q-arb/ — 200`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/chartered-mediator-c-med/ — 200`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/chartered-arbitrator-c-arb/ — 200`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/chartered-med-arbitrator-c-med-arb/ — 200`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/rules-codes/ — 200`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/code-of-ethics/ — 200`
|
||||||
|
- `curl -sSL https://adr-ontario.ca/what-is-adr/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/about-us/ — 404 (correct path is /about-adr-chambers/)`
|
||||||
|
- `curl -sSL https://adrchambers.com/about-adr-chambers/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/arbitration/rules/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/mediation/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/mediation/rules/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/mediation-model-clause/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/model-adr-clauses/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/expedited-arbitration/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/expedited-arbitration/rules/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/med-arb/ — 404`
|
||||||
|
- `curl -sSL https://adrchambers.com/neutral-evaluation/ — 200`
|
||||||
|
- `curl -sSL https://adrchambers.com/adr-systems-design/ — 200`
|
||||||
|
- `curl -sSL https://adrchambersinternational.com/ — 200`
|
||||||
|
- `curl + pdftotext: ADR Chambers arbitration, mediation and expedited-arbitration rule PDFs (all 200)`
|
||||||
|
- `grep -rniE 'neutral evaluation' across all extracted page texts — matched only ADR Chambers pages and the ADRIC search-results page title`
|
||||||
|
- `grep -rniE 'systems? design|dispute system' across all extracted page texts — matched ADR Chambers pages and adric.ca/services/`
|
||||||
|
- `grep -n 'Med-Arbitrator' across all extracted page texts — matched ADRIC nav on every page plus the designation list; grep -c 'Mediator-Arbitrator' — zero matches on every file`
|
||||||
|
- `perl -0777 raw-HTML context reads of the ADRIC designation list and nav, to confirm the en dash and exact strings before quoting`
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# The extracted text behind `../adric-rules.md`
|
||||||
|
|
||||||
|
**Why this directory exists, and why it matters more here than for ADRIO.**
|
||||||
|
Every term count in `../adric-rules.md` — including Finding 1, which corrected a
|
||||||
|
rule-set name `docs/01` had directed onto a public page — was run against these
|
||||||
|
extracts, not against the raw HTML. R14: a count taken from an artefact nobody
|
||||||
|
can reach is unverifiable by construction.
|
||||||
|
|
||||||
|
**And unlike the ADRIO fetch, the HTML digests here do not reproduce.** The
|
||||||
|
served pages change on every request. `../adric-rules.md` §Provenance records
|
||||||
|
the two measured digests and the four causes read out of the diff. **The
|
||||||
|
extracts below are byte-stable across those re-fetches** — verified, not
|
||||||
|
assumed: a second fetch of the mediation-rules page produced different HTML and
|
||||||
|
an identical `.txt`.
|
||||||
|
|
||||||
|
So for these pages, **the `.txt` files are the artefact and the digests are only
|
||||||
|
a record of what was fetched.**
|
||||||
|
|
||||||
|
## Reproducing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -sSL -o <slug>.html "https://adric.ca/rules-codes/<path>/"
|
||||||
|
node ../adrio-extract/extract.mjs <slug>.html > <slug>.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The four URLs and their slugs are in the provenance table of
|
||||||
|
`../adric-rules.md`.
|
||||||
|
|
||||||
|
**`extract.mjs` is not duplicated here.** It is the same unmodified script
|
||||||
|
committed at `../adrio-extract/extract.mjs`, and a second copy is a second thing
|
||||||
|
to keep true — `CLAUDE.md`'s single-source rule applied to a tool rather than to
|
||||||
|
a fact. If you change it, both reference documents' counts have to be re-run.
|
||||||
|
|
||||||
|
## The one command that reproduces Finding 1
|
||||||
|
|
||||||
|
```sh
|
||||||
|
for f in *.txt; do
|
||||||
|
printf '%-40s %s\n' "$f" "$(grep -o -F 'Model Mediation Rules' "$f" | wc -l)"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect `0` on all four. `grep -o -F 'National Mediation Rules'` returns 10 on
|
||||||
|
`rules-codes__national-mediation-rules.txt`.
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
Rules & Codes - ADR Institute of Canada
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
ADR Connect - Directory of Professionals Affiliate Member Portals
|
||||||
|
ADRIC Member Portal
|
||||||
|
Language:
|
||||||
|
en
|
||||||
|
fr
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
ADRIC By-Laws
|
||||||
|
Learn More
|
||||||
|
Federation MoU
|
||||||
|
Learn More
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
Learn More
|
||||||
|
ADRIC Mediation Rules
|
||||||
|
Learn More
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Learn More
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Learn More
|
||||||
|
Privacy Policy
|
||||||
|
Learn More
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Learn More
|
||||||
|
Corporate Members
|
||||||
|
News
|
||||||
|
Abusive Arbitration Clauses in Contracts of Adhesion: The Québec Court of Appeal Draws the Line
|
||||||
|
The Role of Law in Mediation: Between Authority and Autonomy
|
||||||
|
Preserving Mediation’s Distinct Role in Civil Justice Reform: The Promise and Peril in Ontario’s Civil Rules Review
|
||||||
|
Contact
|
||||||
|
ADR Institute of Canada, Inc. 705-130 Albert Street, Ottawa, ON K1P5G4 1-877-475-4353 info@adric.ca
|
||||||
|
© 2026 ADR Institute of Canada, Inc. | Privacy Policy | Subscribe
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
ADRIC Med-Arb Rules - ADR Institute of Canada
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
ADR Connect - Directory of Professionals Affiliate Member Portals
|
||||||
|
ADRIC Member Portal
|
||||||
|
Language:
|
||||||
|
en
|
||||||
|
fr
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
The ADRIC Med-Arb Rules
|
||||||
|
As part of ADRIC’s role in protecting the public and promoting best practices in ADR, the need became apparent for specialized rules of procedure for med-arb, so a Task Force was formed with a dedicated working group of med-arb professionals. The Task Force completed an initial draft of the Rules, which were then referred to the Rules Committee for its consideration.
|
||||||
|
The Committee followed past practice by consulting with the membership at large as the Rules were being reviewed, and a discussion draft of the ADRIC Med-Arb Rules was presented to the membership at ADRIC’s Annual Conference in November 2019. The Committee received extensive and very helpful feedback. The Committee also followed past practice by sending the draft ADRIC Med-Arb Rules for “wordsmithing” by a plain language specialist and many of the recommended plain language revisions were adopted.
|
||||||
|
Med-Arb is not merely the merging of separate mediation and arbitration processes, but a unique process designed to meet the needs of particular disputants. It involves nuances and complexities that can be fine-tuned to the needs of the parties as a customized dispute resolution process, which requires a high level of practitioner competence to do successfully.
|
||||||
|
The Rules are designed to work in tandem with ADRIC’s existing Mediation Rules and Arbitration Rules, integrating seamlessly.
|
||||||
|
Download the ADRIC Med-Arb Rules:
|
||||||
|
Booklet format
|
||||||
|
8.5 x 11 format
|
||||||
|
Designation : We have also developed criteria for a specialized designation: the Chartered Med-Arb
|
||||||
|
Course : We have worked with course designers to develop the Med-Arb Foundational Course. See the training schedule .
|
||||||
|
Note: This course is presented in English.
|
||||||
|
ADRIC is grateful for the exceptional work of the following volunteers who gave so generously of their time and expertise to develop the Rules and Designation Criteria:
|
||||||
|
Sara Ahlstrom, C.Med
|
||||||
|
Anik Béland
|
||||||
|
Glen Bell, C.Arb
|
||||||
|
Colm Brannigan, FCIArb, C.Arb, C.Med
|
||||||
|
Genevieve Chornenki, C.Arb, C.Med
|
||||||
|
Olivier Després, C.Med, C.Arb
|
||||||
|
Barry Effler
|
||||||
|
Martina Faith
|
||||||
|
Angus Gunn, Q.C.
|
||||||
|
Arlene Henry, Q.C., C.Med
|
||||||
|
Jim McCartney, C.Arb, C.Med
|
||||||
|
David McCutcheon, C.Arb
|
||||||
|
Gerry Nera, C.Med, Q.Arb
|
||||||
|
Louise Novinger Grant, Q.C.
|
||||||
|
Elton Simoes, C.Med, Q.Arb
|
||||||
|
Marjorie Lee Thompson
|
||||||
|
Rick Weiler
|
||||||
|
Michael Welsh, Q.C., FCIArb, C.Med, Q.Arb
|
||||||
|
How can I comment to enhance the next revision?
|
||||||
|
Send email or correspondence to the Executive Director: [email protected]
|
||||||
|
MODEL DISPUTE RESOLUTION CLAUSE
|
||||||
|
Parties who agree to submit disputes under the Med-Arb Rules may use this clause in their agreement:
|
||||||
|
All disputes arising out of or in connection with this agreement, or in respect of any legal relationship associated with or derived from this agreement, will be finally resolved by Med-Arb under the Med-Arb Rules of the ADR Institute of Canada, Inc. The Seat of Arbitration under the ADRIC Arbitration Rules will be [specify]. The language of the Med-Arb will be [specify].
|
||||||
|
TYPES OF DISPUTES TO WHICH THE MED-ARB RULES APPLY
|
||||||
|
Although the Med-Arb Rules were drafted to assist in resolving domestic commercial disputes, parties may want to apply them to international or non-commercial disputes. [1]
|
||||||
|
Parties should examine the Med-Arb Rules to ensure that their provisions are appropriate and conform with applicable legislation.
|
||||||
|
[1] In Québec, Article 2639 of the Civil Code of Québec, CQLR, c. CCQ-1991, provides that disputes over the status and capacity of persons, family matters, or other matters of public order may not be submitted to arbitration.
|
||||||
|
ADRIC ADMINISTRATION SERVICES
|
||||||
|
To commence a med-arb, send the Notice of Request to med-arbitrate to each Respondent under the Agreement and to ADRIC at [email protected] , and courier the Commencement Fee as set out in Schedule A of the Rules plus HST to ADRIC. Cheques are to be made payable to ADR Institute of Canada.
|
||||||
|
The Case Service Fee is to be remitted by the Respondent(s) with their Statement of Defence and/or Counterclaim (see Schedule A).
|
||||||
|
Templates : We regret we are not able to provide templates as the content of any such agreements would need to be the subject of legal advice.
|
||||||
|
Corporate Members
|
||||||
|
News
|
||||||
|
Abusive Arbitration Clauses in Contracts of Adhesion: The Québec Court of Appeal Draws the Line
|
||||||
|
The Role of Law in Mediation: Between Authority and Autonomy
|
||||||
|
Preserving Mediation’s Distinct Role in Civil Justice Reform: The Promise and Peril in Ontario’s Civil Rules Review
|
||||||
|
Contact
|
||||||
|
ADR Institute of Canada, Inc. 705-130 Albert Street, Ottawa, ON K1P5G4 1-877-475-4353 info@adric.ca
|
||||||
|
© 2026 ADR Institute of Canada, Inc. | Privacy Policy | Subscribe
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
ADRIC Arbitration Rules - ADR Institute of Canada
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
ADR Connect - Directory of Professionals Affiliate Member Portals
|
||||||
|
ADRIC Member Portal
|
||||||
|
Language:
|
||||||
|
en
|
||||||
|
fr
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
The ADRIC Arbitration Rules
|
||||||
|
Important Update on the ADR Institute of Canada (ADRIC) Arbitration Rules:
|
||||||
|
ADRIC has adopted new Arbitration Rules and a new Arbitrator Appointment Protocol, effective March 1, 2025. This modernization effort aims to support both domestic and international arbitration with up-to-date procedures and streamlined institutional support. The ADRIC Arbitration Rules have been the leading choice for Canadian businesses since 2002.
|
||||||
|
ADRIC Arbitration Rules – Effective 2025
|
||||||
|
ADRIC Arbitrator Appointment Protocol
|
||||||
|
CLICK FOR ARBITRATION FORMS
|
||||||
|
The ADR Institute of Canada (ADRIC) has developed forms to aid parties pursuing arbitration under the ADRIC Arbitration Rules. To file your case with ADRIC, you will need to complete the appropriate form below.
|
||||||
|
Notice to Arbitrate
|
||||||
|
Request to Administer the Arbitration
|
||||||
|
Request for the appointment of an arbitrator
|
||||||
|
Application for Urgent Interim Measures
|
||||||
|
Application to Challenge an Arbitrator
|
||||||
|
Notice of Appeal
|
||||||
|
“The new Arbitration Rules reflect two years of thoughtful discussion and analysis by a committed group of arbitrators and arbitration counsel representing all regions in Canada. Their focus has been on providing a framework for arbitration as a means of resolving disputes in a full, fair and final manner using up-to-date arbitration procedures and streamlined institutional support when needed.”
|
||||||
|
— Comment by William G. Horton, C.Arb, co-chair and discussion leader of the ADRIC Modernization Committee that drafted the new Arbitration Rules.
|
||||||
|
Highlights of the New Rules:
|
||||||
|
Customized ADRIC services based on a flexible “à la carte” menu tailored to meet your specific needs
|
||||||
|
Elimination of distinction between international and non-international disputes
|
||||||
|
Enhanced arbitrator appointment process
|
||||||
|
Expedited and integrated challenge process
|
||||||
|
Conflicts disclosure processes and standards
|
||||||
|
Practical precedents that can be customized
|
||||||
|
Checklist for first procedural meeting
|
||||||
|
Draft first procedural order
|
||||||
|
Standard terms of appointment of an arbitrator
|
||||||
|
Standard statement of arbitrator independence and impartiality
|
||||||
|
Important Notice : Our team is here to provide clarity and support, ensuring you have the information you need. Questions should be directed to ADRIC Case Services at [email protected] . However, please note that the ultimate decision-maker in any arbitration process is the Tribunal. For personalized legal advice, we strongly recommend consulting with your own lawyer.
|
||||||
|
MODEL DISPUTE RESOLUTION CLAUSE
|
||||||
|
Parties who agree to arbitrate under the Rules may use the following clause in their agreement:
|
||||||
|
“All disputes arising out of or in connection with this agreement, or in relation to any legal relationship associated with or derived from this agreement, will be resolved by final and binding arbitration under the Arbitration Rules of the ADR Institute of Canada, Inc. [or the Simplified Arbitration Rules of the ADR Institute of Canada, Inc.] The Seat of Arbitration will be [specify]. The language of the arbitration will be [specify].”
|
||||||
|
ADRIC ARBITRATION INFORMATION TO ARBITRATORS
|
||||||
|
Do you wish to be considered for an arbitrator appointment? The arbitrator appointment criteria are listed in the arbitrator appointment protocol above.
|
||||||
|
A candidate who wishes to be considered for an arbitrator appointment must:
|
||||||
|
Be a member in good standing of ADRIC and a regional affiliate;
|
||||||
|
Confirm that they carry at least $1 million in arbitrator errors and omissions insurance;
|
||||||
|
Commit to responding promptly to inquiries from ADRIC concerning their willingness to accept an appointment, failing which they may not be considered for appointments;
|
||||||
|
Advise whether there is any equity, diversity, and inclusion information they would like to be considered by the sub-committee in making Direct Appointments or generating Candidate Lists; and
|
||||||
|
Submit an ADR Connect profile that contains the following information:
|
||||||
|
i. their area(s) of expertise;
|
||||||
|
ii. their professional arbitration designations, if any;
|
||||||
|
iii. whether they wish to be considered for Interim Arbitrator or Challenge Adjudicator appointments;
|
||||||
|
iv. their experience as arbitrator and/or arbitration counsel;
|
||||||
|
v. their standard rate(s);
|
||||||
|
vi. any other information they deem relevant; and
|
||||||
|
vii. at the candidate’s option, any equity, diversity, and inclusion information they would like to be considered.
|
||||||
|
Any member who wishes to be considered for appointment as a Challenge Adjudicator or Interim Arbitrator must confirm that they are willing to accept an appointment on the basis of Schedules B and C of the Rules.
|
||||||
|
All requests for Direct Appointments or to generate Candidate Lists must be sent by email to ADRIC Case Services at [email protected] .
|
||||||
|
All requests by one or more parties must include the following information:
|
||||||
|
Party Disclosure required under the Rules;
|
||||||
|
full names of all parties, their addresses, and other contact information, if known;
|
||||||
|
names of all legal counsel or party representatives, their addresses, and other contact information, if known;
|
||||||
|
brief description of the dispute, without argument;
|
||||||
|
copy of the arbitration agreement, if any;
|
||||||
|
amount(s) in dispute, if any;
|
||||||
|
any qualifications the parties request or require of the arbitrator, including any subject area expertise;
|
||||||
|
time constraints, if any, imposed by the parties, the arbitration agreement, or the nature of the dispute;
|
||||||
|
whether appointment of an Arbitrator, Interim Arbitrator or Challenge Adjudicator is requested and, if so, all information required under the Rules; and
|
||||||
|
any other information the party making the request considers necessary.
|
||||||
|
All party requests will also be copied to those identified in paragraphs 7b) and c) above, unless the request is for appointment of an ex parte Interim Arbitrator as permitted pursuant to the Rules.
|
||||||
|
We are grateful to the following individuals:
|
||||||
|
Arbitrator Appointment Committee:
|
||||||
|
Megan Keenberg, Joan Cotie, Olivier Després, David Eaton, Jim McLandress, Lisa Munro, and Rachel Howie.
|
||||||
|
Arbitration Modernization Project (AMP):
|
||||||
|
William G. Horton, Jim McCartney, Jim Musgrave, Glen Bell, Mary Comeau, Stephen Drymer, Bryan Duguid, Angus Gunn, Joshua Karton, Lisa Munro, David McCutcheon, Emily McMurtry, and Lauren Tomasich .
|
||||||
|
Advisory Committee to the AMP:
|
||||||
|
Brian Casey, Megan Keenberg, Jack Marshall, Eric Morgan, Murray Smith, Doug Stollery, and Hon. Neil Wittmann.
|
||||||
|
Arbitrator Appointment Protocol Sub-Committee:
|
||||||
|
Lisa Munro, Amy Crosbie, Stephen Drymer, Douglas Harrison, Matti Lemmens, Jim McCartney, Sabri Shawa, and Junior Sirivar.
|
||||||
|
Any comments or suggestions concerning the New Arbitration Rules may be emailed to the ADRIC Executive Director at [email protected] .
|
||||||
|
Click here for previous versions of the Rules.
|
||||||
|
Corporate Members
|
||||||
|
News
|
||||||
|
Abusive Arbitration Clauses in Contracts of Adhesion: The Québec Court of Appeal Draws the Line
|
||||||
|
The Role of Law in Mediation: Between Authority and Autonomy
|
||||||
|
Preserving Mediation’s Distinct Role in Civil Justice Reform: The Promise and Peril in Ontario’s Civil Rules Review
|
||||||
|
Contact
|
||||||
|
ADR Institute of Canada, Inc. 705-130 Albert Street, Ottawa, ON K1P5G4 1-877-475-4353 info@adric.ca
|
||||||
|
© 2026 ADR Institute of Canada, Inc. | Privacy Policy | Subscribe
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
National Mediation Rules - ADR Institute of Canada
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
ADR Connect - Directory of Professionals Affiliate Member Portals
|
||||||
|
ADRIC Member Portal
|
||||||
|
Language:
|
||||||
|
en
|
||||||
|
fr
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
Board of Directors
|
||||||
|
Staff & Contact Us
|
||||||
|
Operational Committees
|
||||||
|
Diversity, Equity, Inclusion (DEI)
|
||||||
|
ADRIC Regional Affiliates
|
||||||
|
McGowan Awards
|
||||||
|
Refund & Cancellation Policy
|
||||||
|
Partners
|
||||||
|
Job Opportunities
|
||||||
|
Rules & Codes
|
||||||
|
ADRIC By-laws
|
||||||
|
Federation MoU
|
||||||
|
ADRIC Arbitration Rules
|
||||||
|
National Mediation Rules
|
||||||
|
ADRIC Med-Arb Rules
|
||||||
|
Ethics & Professional Practice
|
||||||
|
Code of Ethics
|
||||||
|
Code of Conduct
|
||||||
|
Conflict of Interest
|
||||||
|
Complaints & Discipline Policy
|
||||||
|
Privacy Policy
|
||||||
|
Online Dispute Resolution (ODR) Vision
|
||||||
|
Membership
|
||||||
|
Become a Member
|
||||||
|
Member Benefits
|
||||||
|
Membership Renewals
|
||||||
|
Events
|
||||||
|
ADRIC 2026 National Conference
|
||||||
|
Webinars
|
||||||
|
ADRIC Past Conferences
|
||||||
|
ADRIC 2025: Annual National Conference
|
||||||
|
ADRIC 2024: Annual National Conference
|
||||||
|
ADRIC 2023: Annual National Conference
|
||||||
|
ADRIC 2022: Annual National Conference
|
||||||
|
ADRIC 2021 Conference Webinar Series
|
||||||
|
Home
|
||||||
|
Program
|
||||||
|
Sponsor
|
||||||
|
Publications & News
|
||||||
|
ADRIC News – Monthly Newsletter
|
||||||
|
ADR Perspectives
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Canadian Arbitration and Mediation Journal
|
||||||
|
Editorial Board
|
||||||
|
Book Reviews
|
||||||
|
Media Centre
|
||||||
|
Marketing Opportunities
|
||||||
|
ADRIC Annual Report
|
||||||
|
Announcements
|
||||||
|
ADR Training
|
||||||
|
Construction Adjudication Training Program
|
||||||
|
Correspondence Course in Arbitration
|
||||||
|
Med-Arb Workshop
|
||||||
|
National Introductory Arbitration Course
|
||||||
|
National Introductory Mediation Course
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
ADRIC Professional Practice Manuals
|
||||||
|
Disability Accessibility Guidebook
|
||||||
|
Designations & CEE
|
||||||
|
Continuing Education & Engagement
|
||||||
|
Professional Designations
|
||||||
|
Arbitrator Designations
|
||||||
|
Mediator Designations
|
||||||
|
Chartered Med-Arbitrator
|
||||||
|
Construction Adjudicator
|
||||||
|
Family Practice Mediation
|
||||||
|
IMI Recognition
|
||||||
|
Indigenous Practitioner Inclusion Initiative
|
||||||
|
Services
|
||||||
|
Construction Adjudication
|
||||||
|
CTA-OTC Arbitrator Roster
|
||||||
|
Arbitrator Appointments
|
||||||
|
Mediator Appointments
|
||||||
|
ADRIC Med-Arb Rules Case Administration Services
|
||||||
|
Custom ADR Systems and Rosters Development
|
||||||
|
In-House Designations
|
||||||
|
The ADRIC National Mediation Rules
|
||||||
|
The National Mediation Rules provide rules for initiating mediations, including the appointment of a mediator should the parties be unable to come to an agreement.
|
||||||
|
The National Mediation Rules document contains the following:
|
||||||
|
Mediation Rules including Code of Conduct
|
||||||
|
Standard Form Agreement to Mediate (Schedule B)
|
||||||
|
Administration fees payable to the ADR Institute of Canada- see Schedule A of the Rules
|
||||||
|
Model Dispute Resolution Clause
|
||||||
|
The Model Dispute Resolution Clause set out below is of particular importance to those drafting or entering into contracts. Commercial contracts drafted by law firms of all sizes across Canada commonly contain a clause indicating that any dispute that arises with respect to the contract will be administered by ADR Canada, or one of its affiliates, pursuant to the National Mediation Rules or the ADRIC Arbitration Rules of the ADR Institute of Canada.
|
||||||
|
The National Mediation Rules provide a Model Dispute Resolution Clause for Mediation and /or Arbitration:
|
||||||
|
All disputes arising out of or in connection with this agreement, or in respect of any legal relationship associated with or derived from this agreement, shall be mediated pursuant to the National Mediation Rules of the ADR Institute of Canada, Inc. The place of mediation shall be [specify City and Province of Canada]. The language of the mediation shall be English or French [specify language].
|
||||||
|
Important Update on the ADR Institute of Canada (ADRIC) National Mediation Rules: As of 2025, the ADRIC Mediation Committee is currently reviewing the Mediation Rules to ensure they remain current, practical, and aligned with best practices in the field. In the meantime, the existing rules remain in effect and should continue to be used until any updates are formally adopted.
|
||||||
|
Corporate Members
|
||||||
|
News
|
||||||
|
Abusive Arbitration Clauses in Contracts of Adhesion: The Québec Court of Appeal Draws the Line
|
||||||
|
The Role of Law in Mediation: Between Authority and Autonomy
|
||||||
|
Preserving Mediation’s Distinct Role in Civil Justice Reform: The Promise and Peril in Ontario’s Civil Rules Review
|
||||||
|
Contact
|
||||||
|
ADR Institute of Canada, Inc. 705-130 Albert Street, Ottawa, ON K1P5G4 1-877-475-4353 info@adric.ca
|
||||||
|
© 2026 ADR Institute of Canada, Inc. | Privacy Policy | Subscribe
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# ADRIC rules and codes — the published rule sets, in ADRIC's own words
|
||||||
|
|
||||||
|
**Why this file exists.** `docs/01-architecture.md` directs `/mediation/` to
|
||||||
|
name *"ADRIC Model Mediation Rules"* and `/arbitration/` to name *"ADRIC, ADR
|
||||||
|
Chambers, ad hoc"*. A rule set's **name** is a fact about a published document,
|
||||||
|
so under Q46(b)'s standard it may be published — but only from a source, not
|
||||||
|
from recall. R14: a claim whose artefact is unreachable is unverifiable by
|
||||||
|
construction. This is the same fetch-before-writing that caught
|
||||||
|
`Chartered Mediator-Arbitrator` (`docs/reference/adrio-designations.md`).
|
||||||
|
|
||||||
|
**It caught one immediately. See Finding 1.**
|
||||||
|
|
||||||
|
> ### R18 re-check — cutover pass, 2026-09-01
|
||||||
|
>
|
||||||
|
> The trigger fired. `AGENTS.md` §12 R18 holds the per-limb findings and
|
||||||
|
> the sources; this stamp does not restate them. **The quoted bytes below
|
||||||
|
> are still the original retrieval and were not re-fetched** — what was
|
||||||
|
> re-checked is whether the *facts* they support have moved.
|
||||||
|
>
|
||||||
|
> - **(f) the ADRIC National Mediation Rules, under review by ADRIC's own
|
||||||
|
> committee — NOT RE-RETRIEVED.** Held unchanged on a cadence judgement
|
||||||
|
> rather than a fresh fetch `[assumed 2026-09-01 — Pouya]`. The digests in
|
||||||
|
> the Provenance table below are the 2026-08-28 bytes and were not
|
||||||
|
> recomputed, so this stamp says nothing about whether the page changed.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Retrieved | **2026-08-28** |
|
||||||
|
| Method | `curl -sSL` — redirects followed, exit status read, stderr not suppressed |
|
||||||
|
| Text extracts | `docs/reference/adric-extract/`, produced by the committed `adrio-extract/extract.mjs` (same script, unmodified) |
|
||||||
|
|
||||||
|
| URL | HTTP | bytes | text | sha256 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `https://adric.ca/rules-codes/` | 200 | 120908 | 4579 | `e7281b28e3d739f8d91985d5094fbdff7dea24e3a360d00ec32e387e32e9a1c9` |
|
||||||
|
| `https://adric.ca/rules-codes/national-mediation-rules/` | 200 | 110894 | 6130 | `c55fe482da8aa7a69377703b9192bda0d926f012e45a90ca5ce5d27a7b0344ac` |
|
||||||
|
| `https://adric.ca/rules-codes/arbrules/` | 200 | 122211 | 11004 | `07b9850f41bbf40f6ce3a3778876853091ac25a6bb255ef0c4b2827af69a937e` |
|
||||||
|
| `https://adric.ca/rules-codes/adric-med-arb-rules/` | 200 | 114700 | 8445 | `153479af8235022170b31c9673262f4e10a51e9f99fcffcb3e0c7dd23d037393` |
|
||||||
|
|
||||||
|
⚠️ **THE HTML DIGESTS DRIFT ON EVERY REQUEST. THE TEXT EXTRACTS DO NOT.**
|
||||||
|
Unlike the ADRIO fetch, where the digests were stable and are therefore stamped
|
||||||
|
as the artefact, these pages change per response. **Measured, not assumed** — the
|
||||||
|
mediation-rules page was fetched twice, three minutes apart:
|
||||||
|
|
||||||
|
```
|
||||||
|
first : c55fe482da8aa7a69377703b9192bda0d926f012e45a90ca5ce5d27a7b0344ac
|
||||||
|
second: e9f74014ec04e8fcab0f1cbbd75ef4cb104c8d94992b93c832d41c8259ce78b2
|
||||||
|
text identical? YES
|
||||||
|
```
|
||||||
|
|
||||||
|
Four causes, read out of the diff rather than guessed at:
|
||||||
|
|
||||||
|
1. a per-render page-builder class suffix (`highend_6a91df2f53035` →
|
||||||
|
`highend_6a91dfca9a8be`);
|
||||||
|
2. a **rotating corporate-member logo carousel** in the footer — nine sponsor
|
||||||
|
logos, reordered per request;
|
||||||
|
3. **Cloudflare email obfuscation**, which re-keys the `email-protection#…`
|
||||||
|
hash per response;
|
||||||
|
4. a **Cloudflare challenge-platform token** (`__CF$cv$params` `r` and `t`).
|
||||||
|
|
||||||
|
*(This paragraph first asserted "a WordPress nonce and rotating asset query
|
||||||
|
strings" — a guess, written before the second fetch. None of the four causes is
|
||||||
|
that. Corrected before commit, and recorded because a guessed cause for an
|
||||||
|
observed difference is exactly what `CLAUDE.md` bars.)*
|
||||||
|
|
||||||
|
**So the stable artefact is `docs/reference/adric-extract/`**, and the term
|
||||||
|
counts below were run against those extracts. A digest mismatch on re-fetch is
|
||||||
|
**not** evidence the content changed — re-extract and compare the text.
|
||||||
|
|
||||||
|
Also on the record: `https://adric.ca/rules/` returns **403**, and
|
||||||
|
`/mediation-rules/` and `/arbitration-rules/` return **404**. The working paths
|
||||||
|
are all under `/rules-codes/`. Recorded because a 403 body is 52 bytes and reads
|
||||||
|
like an empty page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 1 — "ADRIC Model Mediation Rules" is NOT the name of anything
|
||||||
|
|
||||||
|
`docs/01-architecture.md` §`/mediation/` item 3 directed the page to name the
|
||||||
|
**"ADRIC Model Mediation Rules"**. The string does not exist in ADRIC's
|
||||||
|
materials.
|
||||||
|
|
||||||
|
| Term | rules-codes | national-mediation-rules | arbrules | adric-med-arb-rules |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `Model Mediation Rules` | 0 | 0 | 0 | 0 |
|
||||||
|
| `National Mediation Rules` | 2 | 10 | 2 | 2 |
|
||||||
|
| `ADRIC Mediation Rules` | 1 | 0 | 0 | 0 |
|
||||||
|
| `ADRIC Arbitration Rules` | 3 | 3 | 7 | 3 |
|
||||||
|
| `ADRIC Med-Arb Rules` | 5 | 4 | 4 | 9 |
|
||||||
|
|
||||||
|
**Where "Model" actually belongs.** ADRIC publishes a **Model Dispute Resolution
|
||||||
|
Clause** — a contract clause, inside the rules document. The spec's phrase
|
||||||
|
conflates the clause's name with the rules' name.
|
||||||
|
|
||||||
|
**The canonical title, from the page's own heading:** *"The ADRIC National
|
||||||
|
Mediation Rules"*. The Model Clause it publishes uses the same form —
|
||||||
|
*"shall be mediated pursuant to the **National Mediation Rules** of the ADR
|
||||||
|
Institute of Canada, Inc."*
|
||||||
|
|
||||||
|
**One inconsistency in ADRIC's own materials, recorded so it is not read later
|
||||||
|
as our error.** The `/rules-codes/` index has a card labelled *"ADRIC Mediation
|
||||||
|
Rules"* (1 occurrence, card link text) while its nav and the document itself say
|
||||||
|
*"National Mediation Rules"*. **Use `ADRIC National Mediation Rules`** — the
|
||||||
|
document's own title, and the form inside the model clause.
|
||||||
|
|
||||||
|
## Finding 2 — the three rule sets, with the facts a page may state
|
||||||
|
|
||||||
|
**ADRIC National Mediation Rules.** *"The National Mediation Rules provide rules
|
||||||
|
for initiating mediations, including the appointment of a mediator should the
|
||||||
|
parties be unable to come to an agreement."* The document contains: Mediation
|
||||||
|
Rules including a Code of Conduct; a Standard Form Agreement to Mediate
|
||||||
|
(Schedule B); ADRIC administration fees (Schedule A); and the Model Dispute
|
||||||
|
Resolution Clause.
|
||||||
|
|
||||||
|
⚠️ **Currency caveat, verbatim:** *"As of 2025, the ADRIC Mediation Committee is
|
||||||
|
currently reviewing the Mediation Rules… In the meantime, the existing rules
|
||||||
|
remain in effect and should continue to be used until any updates are formally
|
||||||
|
adopted."* So do not date them, and do not describe them as recently revised.
|
||||||
|
|
||||||
|
**ADRIC Arbitration Rules.** *"ADRIC has adopted new Arbitration Rules and a new
|
||||||
|
Arbitrator Appointment Protocol, effective March 1, 2025."* Published as
|
||||||
|
**"ADRIC Arbitration Rules – Effective 2025"**, alongside an **ADRIC Arbitrator
|
||||||
|
Appointment Protocol** and named forms: Notice to Arbitrate, Request to
|
||||||
|
Administer the Arbitration, Request for the appointment of an arbitrator,
|
||||||
|
Application for Urgent Interim Measures, Application to Challenge an Arbitrator,
|
||||||
|
Notice of Appeal.
|
||||||
|
|
||||||
|
**ADRIC Med-Arb Rules.** Developed by a **Task Force** — *"a Task Force was
|
||||||
|
formed with a dedicated working group of med-arb professionals. The Task Force
|
||||||
|
completed an initial draft of the Rules, which were then referred to the Rules
|
||||||
|
Committee"* — and a discussion draft was presented to the membership at
|
||||||
|
ADRIC's Annual Conference in **November 2019**. Two sentences are directly
|
||||||
|
useful to `/med-arb/`, both verbatim:
|
||||||
|
|
||||||
|
> "Med-Arb is not merely the merging of separate mediation and arbitration
|
||||||
|
> processes, but a unique process designed to meet the needs of particular
|
||||||
|
> disputants. It involves nuances and complexities that can be fine-tuned to the
|
||||||
|
> needs of the parties as a customized dispute resolution process, **which
|
||||||
|
> requires a high level of practitioner competence to do successfully**."
|
||||||
|
|
||||||
|
> "The Rules are designed to work in tandem with ADRIC's existing Mediation
|
||||||
|
> Rules and Arbitration Rules, integrating seamlessly."
|
||||||
|
|
||||||
|
**Scope, verbatim:** *"Although the Med-Arb Rules were drafted to assist in
|
||||||
|
resolving domestic **commercial** disputes, parties may want to apply them to
|
||||||
|
international or non-commercial disputes."* That matches §4's commercial scoping
|
||||||
|
without being cited for it — it is ADRIC's statement about its own rules, not
|
||||||
|
authority for what this practice offers.
|
||||||
|
|
||||||
|
ADRIC also publishes a **Med-Arb Foundational Course** (in English) and, per this
|
||||||
|
page, *"criteria for a specialized designation: the Chartered Med-Arb"*.
|
||||||
|
|
||||||
|
## Finding 3 — "Chartered Med-Arbitrator" is corroborated by a second body
|
||||||
|
|
||||||
|
`Chartered Med-Arbitrator` appears **2 times on every one of the four pages** —
|
||||||
|
it is ADRIC's own navigation label, under *Designations & CEE → Professional
|
||||||
|
Designations*. So the long form §11 now carries, sourced from **ADRIO**, is
|
||||||
|
independently the form **ADRIC** uses in its nav. The body prose on the Med-Arb
|
||||||
|
Rules page uses the short *"Chartered Med-Arb"*
|
||||||
|
**once** — *"criteria for a specialized designation: the Chartered Med-Arb"*.
|
||||||
|
|
||||||
|
⚠️ *This read "3 occurrences" for one pass. `grep -o -F 'Chartered Med-Arb'`
|
||||||
|
does return 3, but **two of the three are substrings of the nav label
|
||||||
|
"Chartered Med-Arbitrator"** counted separately one sentence above. `grep -o
|
||||||
|
'Chartered Med-Arb(?!itrator)'` returns 1. That is `CLAUDE.md`'s "a grep that
|
||||||
|
matches is not a finding until you read what it matched", committed inside the
|
||||||
|
document whose whole purpose is sourced counts. Found by
|
||||||
|
`adversarial-reviewer`.* **Neither body writes "Mediator-Arbitrator" anywhere in this
|
||||||
|
fetch.**
|
||||||
|
|
||||||
|
## Finding 4 — what is NOT in this fetch, and must not be inferred from it
|
||||||
|
|
||||||
|
- **The rules' actual provisions.** Every page above is a *landing page*; the
|
||||||
|
rules themselves are PDFs behind download links and were **not** fetched. So
|
||||||
|
this file supports naming a rule set and describing what a document contains
|
||||||
|
at the level its own landing page describes it. It supports **no statement of
|
||||||
|
what any rule requires, permits or provides** — including anything about
|
||||||
|
consent mechanics, caucus information, or the switch from mediation to
|
||||||
|
arbitration in med-arb. Write those as this practice's own protocol, or not at
|
||||||
|
all.
|
||||||
|
- **Anything about ADR Chambers.** It is not sourced here, and as of 2026-08-30
|
||||||
|
it is named on no page: Pouya struck it from `/arbitration/` and from
|
||||||
|
`docs/01` item 3. `docs/07-fees.md` cites it only for *published fee ranges*,
|
||||||
|
which is a different claim and is internal.
|
||||||
|
- **Legal effect.** Nothing here establishes the enforceability of an award, in
|
||||||
|
Ontario or anywhere. §4 bars this repository from concluding a proposition of
|
||||||
|
law.
|
||||||
|
|
||||||
|
## Finding 5 — ADRIC superlatives deliberately NOT lifted
|
||||||
|
|
||||||
|
Same treatment as `adrio-designations.md` Finding 5. These are ADRIC's marketing
|
||||||
|
claims about itself and **must not travel onto this site**, where §4 Forbidden
|
||||||
|
bars superlatives:
|
||||||
|
|
||||||
|
- *"The ADRIC Arbitration Rules have been **the leading choice** for Canadian
|
||||||
|
businesses since 2002."*
|
||||||
|
- *"integrating **seamlessly**"* — quoted above inside a quotation, and it stays
|
||||||
|
inside one.
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
# Reference — ADRIO's own expansions of the five ADR designations
|
||||||
|
|
||||||
|
**Why this file exists.** `AGENTS.md` Q46(b) asked whether §11 Glossary may be
|
||||||
|
published as the source for **definitional expansions** — what `Q.Med`, `Q.Arb`
|
||||||
|
and `C.Med-Arb` stand for. Pouya ratified the standard on 2026-08-28 and
|
||||||
|
attached a condition: *"Fetch and commit
|
||||||
|
https://adr-ontario.ca/members-practitioners/professional-designations to
|
||||||
|
docs/reference/ per R14."* This is that fetch.
|
||||||
|
|
||||||
|
It also settles a **correction Pouya raised in the same ruling**, and the
|
||||||
|
correction was his own: §11 had said *"Chartered Mediator-Arbitrator"* since the
|
||||||
|
file was created. ADRIO's term is **"Chartered Med-Arbitrator"**. See Finding 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Provenance — read this before quoting anything below
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| URL as given | `https://adr-ontario.ca/members-practitioners/professional-designations` |
|
||||||
|
| **Redirects to** | `https://adr-ontario.ca/professional-designations/` — HTTP **301**, one hop |
|
||||||
|
| Retrieved | **2026-08-28** |
|
||||||
|
| Method | `curl -sSL -o <file> '<url>'` — HTTP **200** on all five after the redirect |
|
||||||
|
|
||||||
|
**The redirect is recorded because the first fetch did not follow it.** `curl -sS`
|
||||||
|
without `-L` returned HTTP 301 and wrote a **0-byte file**, which a reader of
|
||||||
|
the output could easily have taken for "the page is empty". It is not; it is
|
||||||
|
one hop away. `CLAUDE.md`: *a command that did not run is not evidence of
|
||||||
|
absence* — and a command that ran and returned nothing useful looks the same.
|
||||||
|
|
||||||
|
### The five files, with digests
|
||||||
|
|
||||||
|
| Page | Bytes | Text | `sha256` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `professional-designations/` | 54,961 | 1,932 | `5eca45791b3629d16186dd2a10df43fdd5981cbe89a6ce23d8b43ec7c4c415a8` |
|
||||||
|
| `qualified-mediator-q-med-qualified-arbitrator-q-arb/` | 57,174 | 2,814 | `c8cb8ad556b7ffd193cb672f136d4c0b0bdffdcaa8ac20739f31c1ccef84301f` |
|
||||||
|
| `chartered-mediator-c-med/` | 58,929 | 4,151 | `47cb8edb20681865b55103e265c693405a8dfde369fd5f4eefd340fb58f0c056` |
|
||||||
|
| `chartered-arbitrator-c-arb/` | 58,013 | 3,524 | `44526cb40f3e99bec03e3b45b13d4dc4b6cb5fd4e13b388ed533f7baa6fb1199` |
|
||||||
|
| `chartered-med-arbitrator-c-med-arb/` | 55,966 | 2,504 | `4c3a3214383c80209884125703e89625e855b550f01ea8141df496603cdd69c7` |
|
||||||
|
|
||||||
|
*Text* = characters remaining after `<script>`/`<style>`/`<noscript>`/comments
|
||||||
|
and tags are stripped, entities unescaped, whitespace collapsed.
|
||||||
|
|
||||||
|
> 📁 **THE EXTRACTED TEXT AND THE SCRIPT THAT PRODUCED IT ARE COMMITTED** —
|
||||||
|
> `adrio-extract/`. Every term count in this file was run against those `.txt`
|
||||||
|
> files, and `extract.mjs` is the extraction. **The digests above cover the raw
|
||||||
|
> HTML; they said nothing about the extraction**, so the counts were unverifiable
|
||||||
|
> by construction (R14) even while the digests were not —
|
||||||
|
> `adversarial-reviewer`, 2026-08-28. `adrio-extract/README.md` carries the
|
||||||
|
> reproduction command and the caveat that a future digest mismatch is most
|
||||||
|
> likely a WordPress plugin bump rather than a changed designation name.
|
||||||
|
|
||||||
|
> ✅ **THESE DIGESTS ARE STAMPED, AND `lat-case-conference.md`'S ARE NOT — THE
|
||||||
|
> DIFFERENCE WAS CHECKED, NOT ASSUMED.** That file removed its hashes because the
|
||||||
|
> LAT pages could not reproduce one: `LAT-Rules.html` carries a per-request
|
||||||
|
> bot-detection nonce (`__uzdbm_1`, `__uzdbm_2`) and the AABS page carries
|
||||||
|
> rotating WordPress `?ver=` cache-busters.
|
||||||
|
>
|
||||||
|
> Both causes were looked for here and **both are absent**:
|
||||||
|
>
|
||||||
|
> - `__uzdbm` — **0 occurrences.**
|
||||||
|
> - `?ver=` — 17 occurrences, and they are **static plugin version strings**
|
||||||
|
> (`?ver=1.3.12`, `?ver=2.3.2`, `?ver=3.4.1`, `?ver=4.7.0`), not rotating
|
||||||
|
> values. Same string on every fetch.
|
||||||
|
>
|
||||||
|
> Then measured rather than reasoned about: **four independent fetches of each of
|
||||||
|
> the five URLs, and the file on disk re-hashed against a fresh fetch. All five
|
||||||
|
> MATCH.** The digests above are of the exact bytes the quotes below came from.
|
||||||
|
>
|
||||||
|
> **One instrument failure happened during that check and is recorded because of
|
||||||
|
> its shape.** The first comparison loop used `set -- $pair` under zsh, which
|
||||||
|
> does not word-split unquoted variables, so the loop body received one argument
|
||||||
|
> and every comparison silently failed — printing **`DIFFER` on all five rows**.
|
||||||
|
> That output reads exactly like *"the pages changed under me"*. It was the
|
||||||
|
> `timeout 60 ls "$DRIVE"` shape: the comparison had never run. Re-run with
|
||||||
|
> `while IFS=: read`, all five returned MATCH.
|
||||||
|
|
||||||
|
**Instrument note on JS-rendered shells.** The parent page extracts to **1,932
|
||||||
|
characters from 54,961 bytes — 3.5%**, which is the signature `adric.ca` produced
|
||||||
|
when it defeated an earlier attempt at this same sourcing (4,840 chars from
|
||||||
|
114,985). Here the low ratio is **not** a shell: the parent page really is nearly
|
||||||
|
empty — its body is a title and one sentence pointing elsewhere. The substance is
|
||||||
|
on the four child pages, which extract at 4.5–7.0% because ~1,800 characters of
|
||||||
|
the extraction on every page is the shared navigation. Read the ratio against the
|
||||||
|
page, not against a threshold.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 1 — all five expansions, verbatim
|
||||||
|
|
||||||
|
Present **twice** on the parent page (desktop and mobile navigation), and again
|
||||||
|
as the `<h1>` of each child page. ADRIO's own words, entities unescaped:
|
||||||
|
|
||||||
|
| Abbrev. | ADRIO's expansion | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| `Q.Med` | **Qualified Mediator** | nav + child `<h1>` |
|
||||||
|
| `Q.Arb` | **Qualified Arbitrator** | nav + child `<h1>` |
|
||||||
|
| `C.Med` | **Chartered Mediator** | nav + child `<h1>` |
|
||||||
|
| `C.Arb` | **Chartered Arbitrator** | nav + child `<h1>` |
|
||||||
|
| `C.Med-Arb` | **Chartered Med-Arbitrator** | nav + child `<h1>` |
|
||||||
|
|
||||||
|
The navigation markup, verbatim:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="https://adr-ontario.ca/qualified-mediator-q-med-qualified-arbitrator-q-arb/">Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)</a>
|
||||||
|
<a href="https://adr-ontario.ca/chartered-mediator-c-med/">Chartered Mediator (C.Med)</a>
|
||||||
|
<a href="https://adr-ontario.ca/chartered-arbitrator-c-arb/">Chartered Arbitrator (C.Arb)</a>
|
||||||
|
<a href="https://adr-ontario.ca/chartered-med-arbitrator-c-med-arb/">Chartered Med-Arbitrator (C.Med-Arb)</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
**The URL slugs corroborate independently of the link text** — `chartered-med-arbitrator-c-med-arb`, not `chartered-mediator-arbitrator`.
|
||||||
|
|
||||||
|
## Finding 2 — the expansion this repo had wrong
|
||||||
|
|
||||||
|
`AGENTS.md` §11 read **"Chartered Mediator-Arbitrator"** from the file's creation
|
||||||
|
until 2026-08-28. Pouya's ruling: *"§11 has said 'Chartered Mediator-Arbitrator'
|
||||||
|
throughout. ADRIO's own term is 'Chartered Med-Arbitrator'. I took the expansion
|
||||||
|
from the strategy brief and never sourced it."*
|
||||||
|
|
||||||
|
**It had reached a public page.** `src/pages/about.astro`'s credential arc
|
||||||
|
rendered it as body copy, and `grep -c 'Chartered Mediator-Arbitrator'
|
||||||
|
dist/about/index.html` returned **1** — so the built artefact carried it too. Four
|
||||||
|
instances in the repo, swept 2026-08-28.
|
||||||
|
|
||||||
|
The child page's own heading and first two sentences, verbatim:
|
||||||
|
|
||||||
|
> **Chartered Med-Arbitrator (C.Med-Arb)**
|
||||||
|
>
|
||||||
|
> Med-Arb is a distinct, innovative standalone process that is not as well known
|
||||||
|
> or understood by consumers of ADR services compared to mediation and
|
||||||
|
> arbitration.
|
||||||
|
>
|
||||||
|
> Med-Arb is not merely the merging of separate mediation and arbitration
|
||||||
|
> processes, but a unique process designed to meet the needs of particular
|
||||||
|
> disputants. It involves nuances and complexities that can be fine-tuned to the
|
||||||
|
> needs of the parties as a customized dispute resolution process, which requires
|
||||||
|
> a high level of practitioner competence to do successfully.
|
||||||
|
|
||||||
|
## Finding 3 — how ADRIO characterises the Q-level designations
|
||||||
|
|
||||||
|
Verbatim, `qualified-mediator-q-med-qualified-arbitrator-q-arb/`:
|
||||||
|
|
||||||
|
> **Qualified Mediator (Q.Med) / Qualified Arbitrator (Q.Arb)**
|
||||||
|
>
|
||||||
|
> These designations are to recognize practitioners who have completed sufficient
|
||||||
|
> mediation / arbitration and related dispute resolution training to be qualified
|
||||||
|
> to practice. They are an intermediate step for practitioners working to receive
|
||||||
|
> their Chartered designation.
|
||||||
|
>
|
||||||
|
> *Q.Med criteria vary across affiliates. For the criteria specific to Ontario,
|
||||||
|
> be sure to read the checklist on the application form.*
|
||||||
|
|
||||||
|
⚠️ **Do not lift "qualified to practice" into copy, in any form.** It is ADRIO
|
||||||
|
describing its own designation, and on this site — where §4 records licence
|
||||||
|
status as `[unestablished]` and D13 bars any phrasing that *implies* entitlement
|
||||||
|
to practise law — the phrase would do work ADRIO never intended it to do. The
|
||||||
|
quote is here so a reviewer can see the full sentence, not so it can be reused.
|
||||||
|
|
||||||
|
The *"intermediate step … working to receive their Chartered designation"* half
|
||||||
|
is the sourced basis for describing the arc as an arc. It is a fact about the
|
||||||
|
designation structure, not about Pouya.
|
||||||
|
|
||||||
|
## Finding 4 — two of the four pages state a retention condition, and the split is by PAGE TYPE, not by designation level
|
||||||
|
|
||||||
|
> ⚠️ **THIS FINDING'S FIRST VERSION WAS FRAMED WRONG, AND THE PAGE THAT
|
||||||
|
> FALSIFIED IT WAS ONE THIS FILE HAD ALREADY FETCHED.** It was headed *"the two
|
||||||
|
> Chartered pages state a retention condition; the Q page does not"* and argued a
|
||||||
|
> **Qualified-vs-Chartered** asymmetry. There are **three** Chartered pages. The
|
||||||
|
> count was never run on `chartered-med-arbitrator-c-med-arb/` — at the time,
|
||||||
|
> the designation §4 recorded as the practice's stated goal, and therefore the
|
||||||
|
> one most load-bearing for Q48. Found by `claims-auditor`, 2026-08-28.
|
||||||
|
> *(That §4 row was **struck 2026-08-29** — C.Med-Arb is off the site entirely.
|
||||||
|
> The counting defect this paragraph records is unaffected.)*
|
||||||
|
|
||||||
|
Counted on the extracted text of **all four** designation pages:
|
||||||
|
|
||||||
|
| Page | `good standing` | `annual` | `maintain` | `retain` | `member of the ADR Institute` |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `qualified-mediator-q-med-qualified-arbitrator-q-arb/` | 0 | 0 | 0 | 0 | 0 |
|
||||||
|
| `chartered-mediator-c-med/` | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| `chartered-arbitrator-c-arb/` | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| `chartered-med-arbitrator-c-med-arb/` | **0** | **0** | **0** | **0** | **0** |
|
||||||
|
|
||||||
|
Verbatim, `chartered-mediator-c-med/`:
|
||||||
|
|
||||||
|
> There is an annual fee to maintain your C.Med designation, payable to ADR
|
||||||
|
> Institute of Canada, Inc. The national organization bills you every year. You
|
||||||
|
> must also remain a member in good standing with the ADR Institute of Ontario™
|
||||||
|
> to retain this designation.
|
||||||
|
|
||||||
|
`chartered-arbitrator-c-arb/` says the same of C.Arb.
|
||||||
|
|
||||||
|
**The corrected reading.** C.Med-Arb scores zero on every term, exactly like the
|
||||||
|
Q-level page — so the line does **not** fall between Qualified and Chartered. It
|
||||||
|
falls between **page types**: `chartered-mediator-c-med/` and
|
||||||
|
`chartered-arbitrator-c-arb/` are long application FAQs with numbered sections
|
||||||
|
("Are there prerequisites?", "Is there an ongoing cost?"), while the Q page and
|
||||||
|
the C.Med-Arb page are short descriptions that link out. The C.Med-Arb page says
|
||||||
|
so in as many words: *"To read more about the C.Med-Arb designation, including
|
||||||
|
how to apply, click here."*
|
||||||
|
|
||||||
|
**And that cuts the opposite way from how the first version read it.** A stub
|
||||||
|
page's silence is evidence about the *page*, not about the designation — so
|
||||||
|
Q.Med's silence is most likely "this page does not cover fees or retention", not
|
||||||
|
"no such condition exists". ADRIC bills annually for the two designations that
|
||||||
|
say so, and ADRIO notes that *"Q.Med criteria vary across affiliates"*.
|
||||||
|
|
||||||
|
**Still: this source does not answer it, in either direction, and must not be
|
||||||
|
read as doing so.** Raised as **Q48** — if `Q.Med` retention is contingent on
|
||||||
|
ADRIO membership currency, then §4's Q.Med row, this site's central credential,
|
||||||
|
inherits the shelf life of the memberships Pouya has declined to track by date
|
||||||
|
(R10). The answer needs the Q.Med criteria document or ADRIC/ADRIO directly.
|
||||||
|
|
||||||
|
## Finding 5 — two superlatives, deliberately not lifted
|
||||||
|
|
||||||
|
The Chartered pages call C.Med *"Canada's most senior official designation for
|
||||||
|
practising mediators"* and C.Arb *"Canada's only official designation for
|
||||||
|
practising Arbitrators"*. Both are ADRIO's words about its own designations and
|
||||||
|
both are quotable in principle.
|
||||||
|
|
||||||
|
**Neither goes on the site.** §4 Forbidden bars superlatives outright
|
||||||
|
(*"best", "leading", "top-rated"*), and the reason given there is not only
|
||||||
|
verifiability — it is that *"they read as insecure to the audience this site is
|
||||||
|
for"*. A superlative does not stop being one because a third party said it first,
|
||||||
|
and the copy did not need it. *(This sentence justified the superlative's
|
||||||
|
removal by pointing at **"the designation this practice is built toward"** as
|
||||||
|
"Pouya's own stated goal" with "a §4 row". **That row was struck 2026-08-29** and
|
||||||
|
the phrase is off the site. The reason superlatives stay inside quotation marks
|
||||||
|
is unchanged and never depended on it.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this establishes, and what it does not
|
||||||
|
|
||||||
|
**Establishes:**
|
||||||
|
|
||||||
|
1. All five expansions, in ADRIO's own words, from ADRIO's own site, with
|
||||||
|
reproducible digests. **R14 is satisfied for Q46(b)** — the standard no longer
|
||||||
|
rests on §11 asserting its own content.
|
||||||
|
2. `C.Med-Arb` = **Chartered Med-Arbitrator**. The form this repo published was
|
||||||
|
wrong.
|
||||||
|
3. That the Q-level designations are, in ADRIO's characterisation, an
|
||||||
|
intermediate step toward the Chartered designations.
|
||||||
|
|
||||||
|
**Does not establish:**
|
||||||
|
|
||||||
|
- **Anything about Pouya.** Not that he holds Q.Med, not that he holds Q.Arb,
|
||||||
|
not that he is a member of ADRIO. *(This read "not that he has **commenced**
|
||||||
|
Q.Arb" until 2026-08-30 — a struck stage form surviving in the file an
|
||||||
|
implementer opens to write designation copy.)* Those are §4 Verified rows and this
|
||||||
|
file is not a substitute for one. The standard Pouya ratified is exactly this
|
||||||
|
split: *"§11 Glossary is the source for DEFINITIONAL expansions; §4 remains
|
||||||
|
the only source for claims about Pouya."*
|
||||||
|
- **Anything about gating** — see `AGENTS.md` §4's offering test, question 1.
|
||||||
|
ADRIO describes its designations as recognising completed training; it says
|
||||||
|
nothing about what any activity requires by law, and it is not a source that
|
||||||
|
could. Q46(a) is not advanced by this file.
|
||||||
|
- **Anything about Q.Med retention.** Finding 4. That is Q48.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# The extracted text behind `../adrio-designations.md`
|
||||||
|
|
||||||
|
**Why this directory exists.** `adversarial-reviewer` (2026-08-28, C10): the
|
||||||
|
`sha256` digests in that file cover the **raw HTML**, but Finding 4's term counts
|
||||||
|
were taken from **extracted text produced by an uncommitted script** — so the
|
||||||
|
counts were unverifiable by construction (R14) even though the digests were not.
|
||||||
|
|
||||||
|
`extract.mjs` is that script. `*.txt` are its outputs, which are what every
|
||||||
|
count in `../adrio-designations.md` was run against.
|
||||||
|
|
||||||
|
## Reproducing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -sSL -o <slug>.html "https://adr-ontario.ca/<slug>/"
|
||||||
|
node extract.mjs <slug>.html > <slug>.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The five slugs are listed in the provenance table of `../adrio-designations.md`,
|
||||||
|
which also carries each file's raw-HTML byte count and `sha256`.
|
||||||
|
|
||||||
|
**Expect small differences in the character totals if you re-implement the
|
||||||
|
extraction.** Both review agents reproduced every digest, every byte count and
|
||||||
|
every term count exactly; one got 1924 / 2806 / 4121 / 3510 / 2496 characters
|
||||||
|
against this script's 1932 / 2814 / 4151 / 3524 / 2504, because tag-stripping
|
||||||
|
inserts whitespace differently. **The term counts are the finding; the character
|
||||||
|
totals are only a size indicator.**
|
||||||
|
|
||||||
|
⚠️ **The digests are valid until ADRIO's next WordPress plugin update.** The
|
||||||
|
served HTML carries 17 `?ver=` strings that are static plugin versions today
|
||||||
|
(`?ver=1.3.12`, `?ver=2.3.2`, …) — which is *why* these pages hash reproducibly
|
||||||
|
where the LAT pages could not. Those strings will move when ADRIO updates a
|
||||||
|
plugin. **A future digest mismatch is most likely a plugin bump, not tampering
|
||||||
|
and not a changed designation name** — re-run the term counts before concluding
|
||||||
|
anything from a mismatch.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
Chartered Arbitrator (C.Arb) – The ADR Institute of Ontario
|
||||||
|
Skip to content
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
What is ADR?
|
||||||
|
Resolve a Dispute
|
||||||
|
ADRIO Board of Directors and Staff
|
||||||
|
Rules & Codes
|
||||||
|
ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
Event Calendar
|
||||||
|
Webcasts – Recorded Events
|
||||||
|
Approved Courses
|
||||||
|
Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
Communities of Practice and Sections
|
||||||
|
Committees
|
||||||
|
Membership & Benefits
|
||||||
|
Member Portal
|
||||||
|
Benefits and Application Process
|
||||||
|
Practical Ethics for Working Mediators
|
||||||
|
Insurance
|
||||||
|
Rules & Codes
|
||||||
|
FAQ
|
||||||
|
Designations
|
||||||
|
The Path to your Professional ADR designation
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
Chartered Mediator (C.Med)
|
||||||
|
Chartered Arbitrator (C.Arb)
|
||||||
|
Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
Insurance
|
||||||
|
FAQ
|
||||||
|
News & Blog
|
||||||
|
Blog
|
||||||
|
Updates
|
||||||
|
Newsletter
|
||||||
|
ADRIO E-Mailing List Signup
|
||||||
|
President’s Circle
|
||||||
|
ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
About
|
||||||
|
- About Us
|
||||||
|
- What is ADR?
|
||||||
|
- Resolve a Dispute
|
||||||
|
- ADRIO Board of Directors and Staff
|
||||||
|
- Rules & Codes
|
||||||
|
- ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
- Event Calendar
|
||||||
|
- Webcasts - Recorded Events
|
||||||
|
- Approved Courses
|
||||||
|
- Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
- Communities of Practice and Sections
|
||||||
|
- Committees
|
||||||
|
Membership & Benefits
|
||||||
|
- Member Portal
|
||||||
|
- Benefits and Application Process
|
||||||
|
- Practical Ethics for Working Mediators
|
||||||
|
- Insurance
|
||||||
|
- Rules & Codes
|
||||||
|
- FAQ
|
||||||
|
Designations
|
||||||
|
- The Path to your Professional ADR designation
|
||||||
|
- ADRIC Accredited Courses
|
||||||
|
- Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
- Chartered Mediator (C.Med)
|
||||||
|
- Chartered Arbitrator (C.Arb)
|
||||||
|
- Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
- Insurance
|
||||||
|
- FAQ
|
||||||
|
News & Blog
|
||||||
|
- Blog
|
||||||
|
- Updates
|
||||||
|
- Newsletter
|
||||||
|
- ADRIO E-Mailing List Signup
|
||||||
|
- President’s Circle
|
||||||
|
- ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
Chartered Arbitrator (C.Arb)
|
||||||
|
Please do not mail your applications to our head office. Please email your application to membership@adr-ontario.ca
|
||||||
|
1. What is a Chartered Arbitrator (C.Arb) designation?
|
||||||
|
The C.Arb Or Chartered Arbitrator designation is Canada’s only official designation for practising Arbitrators. Please click here for a more complete description of the requirements can be found.
|
||||||
|
2. Are there prerequisites?
|
||||||
|
The applicant must be a member of the ADR Institute of Ontario™ (or any other regional association).
|
||||||
|
There are extensive training and experience requirements which you should review before applying.
|
||||||
|
3. Where do I get the C.Arb Application Form?
|
||||||
|
The C.Arb application form is linked below.
|
||||||
|
4. Is there a fee for the C.Arb application?
|
||||||
|
Yes, the Application Fee is $500.00 (plus HST). This covers the costs of administering the accreditation process. This is a one-time application fee.
|
||||||
|
5. Is there an ongoing cost?
|
||||||
|
There is an annual fee to maintain your C.Arb designation. The national organization bills you every year. You must also remain a member in good standing with the ADR Institute of Ontario™ to retain this designation.
|
||||||
|
6. How do I apply?
|
||||||
|
Forward your completed application form, and payment along with any attachments necessary to verify the courses you have taken and the work you have done (think of it as marketing yourself to the accreditation committee), to:
|
||||||
|
Please do not mail your applications to our head office. Please email your application to membership@adr-ontario.ca
|
||||||
|
(Looking for courses that have been pre-approved to qualify for designation applications? Click here .)
|
||||||
|
View C.Arb Application Form
|
||||||
|
View C.Arb Criteria
|
||||||
|
Theme by Think Up Themes Ltd . Powered by WordPress .
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
Chartered Med-Arbitrator (C.Med-Arb) – The ADR Institute of Ontario
|
||||||
|
Skip to content
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
What is ADR?
|
||||||
|
Resolve a Dispute
|
||||||
|
ADRIO Board of Directors and Staff
|
||||||
|
Rules & Codes
|
||||||
|
ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
Event Calendar
|
||||||
|
Webcasts – Recorded Events
|
||||||
|
Approved Courses
|
||||||
|
Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
Communities of Practice and Sections
|
||||||
|
Committees
|
||||||
|
Membership & Benefits
|
||||||
|
Member Portal
|
||||||
|
Benefits and Application Process
|
||||||
|
Practical Ethics for Working Mediators
|
||||||
|
Insurance
|
||||||
|
Rules & Codes
|
||||||
|
FAQ
|
||||||
|
Designations
|
||||||
|
The Path to your Professional ADR designation
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
Chartered Mediator (C.Med)
|
||||||
|
Chartered Arbitrator (C.Arb)
|
||||||
|
Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
Insurance
|
||||||
|
FAQ
|
||||||
|
News & Blog
|
||||||
|
Blog
|
||||||
|
Updates
|
||||||
|
Newsletter
|
||||||
|
ADRIO E-Mailing List Signup
|
||||||
|
President’s Circle
|
||||||
|
ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
About
|
||||||
|
- About Us
|
||||||
|
- What is ADR?
|
||||||
|
- Resolve a Dispute
|
||||||
|
- ADRIO Board of Directors and Staff
|
||||||
|
- Rules & Codes
|
||||||
|
- ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
- Event Calendar
|
||||||
|
- Webcasts - Recorded Events
|
||||||
|
- Approved Courses
|
||||||
|
- Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
- Communities of Practice and Sections
|
||||||
|
- Committees
|
||||||
|
Membership & Benefits
|
||||||
|
- Member Portal
|
||||||
|
- Benefits and Application Process
|
||||||
|
- Practical Ethics for Working Mediators
|
||||||
|
- Insurance
|
||||||
|
- Rules & Codes
|
||||||
|
- FAQ
|
||||||
|
Designations
|
||||||
|
- The Path to your Professional ADR designation
|
||||||
|
- ADRIC Accredited Courses
|
||||||
|
- Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
- Chartered Mediator (C.Med)
|
||||||
|
- Chartered Arbitrator (C.Arb)
|
||||||
|
- Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
- Insurance
|
||||||
|
- FAQ
|
||||||
|
News & Blog
|
||||||
|
- Blog
|
||||||
|
- Updates
|
||||||
|
- Newsletter
|
||||||
|
- ADRIO E-Mailing List Signup
|
||||||
|
- President’s Circle
|
||||||
|
- ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
Med-Arb is a distinct, innovative standalone process that is not as well known or understood by consumers of ADR services compared to mediation and arbitration.
|
||||||
|
Med-Arb is not merely the merging of separate mediation and arbitration processes, but a unique process designed to meet the needs of particular disputants. It involves nuances and complexities that can be fine-tuned to the needs of the parties as a customized dispute resolution process, which requires a high level of practitioner competence to do successfully.
|
||||||
|
To read more about the C.Med-Arb designation, including how to apply, click here .
|
||||||
|
Theme by Think Up Themes Ltd . Powered by WordPress .
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
Chartered Mediator (C.Med) – The ADR Institute of Ontario
|
||||||
|
Skip to content
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
What is ADR?
|
||||||
|
Resolve a Dispute
|
||||||
|
ADRIO Board of Directors and Staff
|
||||||
|
Rules & Codes
|
||||||
|
ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
Event Calendar
|
||||||
|
Webcasts – Recorded Events
|
||||||
|
Approved Courses
|
||||||
|
Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
Communities of Practice and Sections
|
||||||
|
Committees
|
||||||
|
Membership & Benefits
|
||||||
|
Member Portal
|
||||||
|
Benefits and Application Process
|
||||||
|
Practical Ethics for Working Mediators
|
||||||
|
Insurance
|
||||||
|
Rules & Codes
|
||||||
|
FAQ
|
||||||
|
Designations
|
||||||
|
The Path to your Professional ADR designation
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
Chartered Mediator (C.Med)
|
||||||
|
Chartered Arbitrator (C.Arb)
|
||||||
|
Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
Insurance
|
||||||
|
FAQ
|
||||||
|
News & Blog
|
||||||
|
Blog
|
||||||
|
Updates
|
||||||
|
Newsletter
|
||||||
|
ADRIO E-Mailing List Signup
|
||||||
|
President’s Circle
|
||||||
|
ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
About
|
||||||
|
- About Us
|
||||||
|
- What is ADR?
|
||||||
|
- Resolve a Dispute
|
||||||
|
- ADRIO Board of Directors and Staff
|
||||||
|
- Rules & Codes
|
||||||
|
- ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
- Event Calendar
|
||||||
|
- Webcasts - Recorded Events
|
||||||
|
- Approved Courses
|
||||||
|
- Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
- Communities of Practice and Sections
|
||||||
|
- Committees
|
||||||
|
Membership & Benefits
|
||||||
|
- Member Portal
|
||||||
|
- Benefits and Application Process
|
||||||
|
- Practical Ethics for Working Mediators
|
||||||
|
- Insurance
|
||||||
|
- Rules & Codes
|
||||||
|
- FAQ
|
||||||
|
Designations
|
||||||
|
- The Path to your Professional ADR designation
|
||||||
|
- ADRIC Accredited Courses
|
||||||
|
- Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
- Chartered Mediator (C.Med)
|
||||||
|
- Chartered Arbitrator (C.Arb)
|
||||||
|
- Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
- Insurance
|
||||||
|
- FAQ
|
||||||
|
News & Blog
|
||||||
|
- Blog
|
||||||
|
- Updates
|
||||||
|
- Newsletter
|
||||||
|
- ADRIO E-Mailing List Signup
|
||||||
|
- President’s Circle
|
||||||
|
- ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
Chartered Mediator (C.Med)
|
||||||
|
Please do not mail your applications to our head office. Please email your application to membership@adr-ontario.ca
|
||||||
|
1. What is a Chartered Mediator (C.Med) designation?
|
||||||
|
The “C.Med” or “Chartered Mediator” designation is Canada’s most senior official designation for practising mediators.
|
||||||
|
Clarification re Footnote #4 in the C.Med Criteria “The intended interpretation of Footnote #4 relating to a “Paid Mediation” is as follows: The criteria in Footnote #4 is intended to be applied individually to each of the 15 mediations presented by the applicant, and specifically where a mediation is not fee-paid, the RCMAC shall look at whether the mediation is demonstrably complex and involved, and may at its discretion accept the mediation being considered toward the 15 total mediations. There is no specific minimum number of fee paid mediations of the 15 total mediations required, providing this criteria is applied to each unpaid mediation.”
|
||||||
|
Use the Application for Chartered Mediator designation linked below.
|
||||||
|
2. Are there prerequisites?
|
||||||
|
The applicant must be a member of the ADR Institute of Ontario™.
|
||||||
|
There are extensive training and experience requirements which you should review before you consider applying.
|
||||||
|
3. Is there a fee for the C.Med application?
|
||||||
|
Yes, the Application Fee is $500.00 (plus HST) made payable to the ADR Institute of Ontario, Inc. This covers the costs of administering the accreditation process. This is a one-time application fee.
|
||||||
|
4. Is there an ongoing cost?
|
||||||
|
Yes. There is an annual fee to maintain your C.Med designation, payable to ADR Institute of Canada, Inc. The national organization bills you every year. You must also remain a member in good standing with the ADR Institute of Ontario™ to retain this designation.
|
||||||
|
6. How do I apply?
|
||||||
|
Forward your completed application form along with any attachments necessary to verify the courses you have taken and the work you have done (think of it as marketing yourself to the accreditation committee), to:
|
||||||
|
Please do not mail your applications to our head office. Please email your application to membership@adr-ontario.ca
|
||||||
|
(Looking for courses that have been pre-approved to qualify for designation applications? Click here .)
|
||||||
|
View C.Med Application Form
|
||||||
|
View C.Med Criteria
|
||||||
|
Theme by Think Up Themes Ltd . Powered by WordPress .
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
const raw = readFileSync(process.argv[2], 'utf8');
|
||||||
|
let t = raw
|
||||||
|
.replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
|
||||||
|
.replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
|
||||||
|
.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, ' ')
|
||||||
|
.replace(/<!--[\s\S]*?-->/g, ' ')
|
||||||
|
.replace(/<[^>]+>/g, ' ');
|
||||||
|
const ents = { amp:'&', lt:'<', gt:'>', quot:'"', apos:"'", nbsp:' ', ndash:'–', mdash:'—', rsquo:'’', lsquo:'‘', ldquo:'“', rdquo:'”' };
|
||||||
|
t = t.replace(/&([a-zA-Z]+);/g, (m,n)=> n in ents ? ents[n] : m)
|
||||||
|
.replace(/&#(\d+);/g, (_m,d)=> String.fromCodePoint(+d))
|
||||||
|
.replace(/&#x([0-9a-f]+);/gi, (_m,h)=> String.fromCodePoint(parseInt(h,16)));
|
||||||
|
t = t.replace(/[ \t ]+/g,' ').replace(/\s*\n\s*/g,'\n').replace(/\n{2,}/g,'\n').trim();
|
||||||
|
process.stdout.write(t);
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
Professional Designations – The ADR Institute of Ontario
|
||||||
|
Skip to content
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
What is ADR?
|
||||||
|
Resolve a Dispute
|
||||||
|
ADRIO Board of Directors and Staff
|
||||||
|
Rules & Codes
|
||||||
|
ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
Event Calendar
|
||||||
|
Webcasts – Recorded Events
|
||||||
|
Approved Courses
|
||||||
|
Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
Communities of Practice and Sections
|
||||||
|
Committees
|
||||||
|
Membership & Benefits
|
||||||
|
Member Portal
|
||||||
|
Benefits and Application Process
|
||||||
|
Practical Ethics for Working Mediators
|
||||||
|
Insurance
|
||||||
|
Rules & Codes
|
||||||
|
FAQ
|
||||||
|
Designations
|
||||||
|
The Path to your Professional ADR designation
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
Chartered Mediator (C.Med)
|
||||||
|
Chartered Arbitrator (C.Arb)
|
||||||
|
Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
Insurance
|
||||||
|
FAQ
|
||||||
|
News & Blog
|
||||||
|
Blog
|
||||||
|
Updates
|
||||||
|
Newsletter
|
||||||
|
ADRIO E-Mailing List Signup
|
||||||
|
President’s Circle
|
||||||
|
ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
About
|
||||||
|
- About Us
|
||||||
|
- What is ADR?
|
||||||
|
- Resolve a Dispute
|
||||||
|
- ADRIO Board of Directors and Staff
|
||||||
|
- Rules & Codes
|
||||||
|
- ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
- Event Calendar
|
||||||
|
- Webcasts - Recorded Events
|
||||||
|
- Approved Courses
|
||||||
|
- Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
- Communities of Practice and Sections
|
||||||
|
- Committees
|
||||||
|
Membership & Benefits
|
||||||
|
- Member Portal
|
||||||
|
- Benefits and Application Process
|
||||||
|
- Practical Ethics for Working Mediators
|
||||||
|
- Insurance
|
||||||
|
- Rules & Codes
|
||||||
|
- FAQ
|
||||||
|
Designations
|
||||||
|
- The Path to your Professional ADR designation
|
||||||
|
- ADRIC Accredited Courses
|
||||||
|
- Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
- Chartered Mediator (C.Med)
|
||||||
|
- Chartered Arbitrator (C.Arb)
|
||||||
|
- Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
- Insurance
|
||||||
|
- FAQ
|
||||||
|
News & Blog
|
||||||
|
- Blog
|
||||||
|
- Updates
|
||||||
|
- Newsletter
|
||||||
|
- ADRIO E-Mailing List Signup
|
||||||
|
- President’s Circle
|
||||||
|
- ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
Professional Designations
|
||||||
|
To read more about the path to designations, click here .
|
||||||
|
Theme by Think Up Themes Ltd . Powered by WordPress .
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb) – The ADR Institute of Ontario
|
||||||
|
Skip to content
|
||||||
|
About
|
||||||
|
About Us
|
||||||
|
What is ADR?
|
||||||
|
Resolve a Dispute
|
||||||
|
ADRIO Board of Directors and Staff
|
||||||
|
Rules & Codes
|
||||||
|
ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
Event Calendar
|
||||||
|
Webcasts – Recorded Events
|
||||||
|
Approved Courses
|
||||||
|
Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
Communities of Practice and Sections
|
||||||
|
Committees
|
||||||
|
Membership & Benefits
|
||||||
|
Member Portal
|
||||||
|
Benefits and Application Process
|
||||||
|
Practical Ethics for Working Mediators
|
||||||
|
Insurance
|
||||||
|
Rules & Codes
|
||||||
|
FAQ
|
||||||
|
Designations
|
||||||
|
The Path to your Professional ADR designation
|
||||||
|
ADRIC Accredited Courses
|
||||||
|
Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
Chartered Mediator (C.Med)
|
||||||
|
Chartered Arbitrator (C.Arb)
|
||||||
|
Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
Insurance
|
||||||
|
FAQ
|
||||||
|
News & Blog
|
||||||
|
Blog
|
||||||
|
Updates
|
||||||
|
Newsletter
|
||||||
|
ADRIO E-Mailing List Signup
|
||||||
|
President’s Circle
|
||||||
|
ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
About
|
||||||
|
- About Us
|
||||||
|
- What is ADR?
|
||||||
|
- Resolve a Dispute
|
||||||
|
- ADRIO Board of Directors and Staff
|
||||||
|
- Rules & Codes
|
||||||
|
- ADRIO DEI Policy Framework
|
||||||
|
Professional Development
|
||||||
|
- Event Calendar
|
||||||
|
- Webcasts - Recorded Events
|
||||||
|
- Approved Courses
|
||||||
|
- Lead a Professional Development Workshop with us!
|
||||||
|
Sections
|
||||||
|
- Communities of Practice and Sections
|
||||||
|
- Committees
|
||||||
|
Membership & Benefits
|
||||||
|
- Member Portal
|
||||||
|
- Benefits and Application Process
|
||||||
|
- Practical Ethics for Working Mediators
|
||||||
|
- Insurance
|
||||||
|
- Rules & Codes
|
||||||
|
- FAQ
|
||||||
|
Designations
|
||||||
|
- The Path to your Professional ADR designation
|
||||||
|
- ADRIC Accredited Courses
|
||||||
|
- Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
- Chartered Mediator (C.Med)
|
||||||
|
- Chartered Arbitrator (C.Arb)
|
||||||
|
- Chartered Med-Arbitrator (C.Med-Arb)
|
||||||
|
- Insurance
|
||||||
|
- FAQ
|
||||||
|
News & Blog
|
||||||
|
- Blog
|
||||||
|
- Updates
|
||||||
|
- Newsletter
|
||||||
|
- ADRIO E-Mailing List Signup
|
||||||
|
- President’s Circle
|
||||||
|
- ADRIO STAR Award Recipients
|
||||||
|
Contact & Staff
|
||||||
|
Qualified Mediator (Q.Med) & Qualified Arbitrator (Q.Arb)
|
||||||
|
Qualified Mediator (Q.Med) / Qualified Arbitrator (Q.Arb)
|
||||||
|
These designations are to recognize practitioners who have completed sufficient mediation / arbitration and related dispute resolution training to be qualified to practice. They are an intermediate step for practitioners working to receive their Chartered designation.
|
||||||
|
Please do not mail your applications to our head office. Please email your application to membership@adr-ontario.ca
|
||||||
|
(Looking for courses that have been pre-approved to qualify for designation applications? Click here .)
|
||||||
|
Read more by downloading the following resources:
|
||||||
|
Qualified Arbitrator (Q.Arb) Requirements
|
||||||
|
Qualified Arbitrator (Q.Arb) Application Form
|
||||||
|
Qualified Mediator (Q.Med) Criteria and Application Form*
|
||||||
|
*Q.Med criteria vary across affiliates. For the criteria specific to Ontario, be sure to read the checklist on the application form.
|
||||||
|
Theme by Think Up Themes Ltd . Powered by WordPress .
|
||||||
@@ -5,9 +5,22 @@ is `CLAUDE.md`'s rule and `AGENTS.md` R14, and this file exists because the
|
|||||||
infinity mark was reconstructed wrongly and **two adversarial review passes could
|
infinity mark was reconstructed wrongly and **two adversarial review passes could
|
||||||
not catch it**, since the real artwork was not in the repo to compare against.
|
not catch it**, since the real artwork was not in the repo to compare against.
|
||||||
|
|
||||||
**Every measurement below is `[verified 2026-08-26]`** — computed with `sharp`
|
**The measurements here are computed with `sharp` against the files in this
|
||||||
against the files in this repository, and re-derivable by anyone from the
|
repository**, and are re-derivable by anyone from the commands given.
|
||||||
commands given. Nothing here is quoted from an external source.
|
|
||||||
|
**Unless a section says otherwise, every figure below is `[verified
|
||||||
|
2026-08-26]`. Where a section carries its own stamp, that stamp wins** — the
|
||||||
|
render ladders are `[measured 2026-08-27]` and §The icon set is `[verified
|
||||||
|
2026-09-02]`. ⚠️ *This line has been wrong in both directions in one week: first
|
||||||
|
as a blanket 2026-08-26 that was already stale for the ladders, then as
|
||||||
|
"each section carries its own stamp" when five sections carry none. It is a floor
|
||||||
|
plus overrides because that is the only shape that covers every section without
|
||||||
|
mis-dating one* (`adversarial-reviewer`, rounds 1 and 2).
|
||||||
|
|
||||||
|
⚠️ **Two things in §The icon set are NOT read off this repository and are
|
||||||
|
labelled where they appear:** the tab-strip colours used in the contrast figures,
|
||||||
|
and iOS's handling of a transparent touch icon. Neither is derivable from
|
||||||
|
anything committed, so neither is stamped as if it were.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
@@ -103,6 +116,138 @@ Passing an explicit `width` is load-bearing: without it Astro emits the
|
|||||||
untouched 2668 px master as the `<img src>` fallback — **1,146,406 bytes** —
|
untouched 2668 px master as the `<img src>` fallback — **1,146,406 bytes** —
|
||||||
which any client without AVIF or WebP support would actually download.
|
which any client without AVIF or WebP support would actually download.
|
||||||
|
|
||||||
|
## The icon set
|
||||||
|
|
||||||
|
`[verified 2026-09-02 — every figure below read off the icon files in this
|
||||||
|
repository, EXCEPT the two external constants flagged inline]`
|
||||||
|
|
||||||
|
| File | What it is | Ground |
|
||||||
|
|---|---|---|
|
||||||
|
| `public/favicon.ico` | 16, 32 and 48 px frames, each a PNG-encoded 32-bit RGBA image inside the ICO container | **Transparent** |
|
||||||
|
| `public/apple-touch-icon.png` | 180 × 180 | **Opaque cream `#faf7f2`, and that is deliberate — see below** |
|
||||||
|
|
||||||
|
**The two grounds differ on purpose, and this is the line that stops someone
|
||||||
|
"fixing" it.** ⚠️ *The reason is an external platform behaviour, not a repository
|
||||||
|
fact, and it is recorded here as what it is:* **iOS does not honour transparency
|
||||||
|
in a touch icon — it composites it onto black**, so a transparent touch icon
|
||||||
|
ships a black tile on the home screen. Stated by Pouya in his ruling of
|
||||||
|
**2026-09-02** and not independently re-tested here; **it has not been checked on
|
||||||
|
a handset in this repository, and the place that does check it is the unticked
|
||||||
|
`docs/06` item "Tested on iOS Safari…".** The tab favicon has the opposite
|
||||||
|
requirement: a tab strip is dark for many readers, and an opaque ground shows
|
||||||
|
there as a visible rectangle around the mark. So the favicon is transparent, the
|
||||||
|
touch icon is matted, and **neither should be changed to match the other.**
|
||||||
|
|
||||||
|
### Composition
|
||||||
|
|
||||||
|
The mark spans **7/8 of the canvas width**, centred on both axes — 14/16, 28/32,
|
||||||
|
42/48, and 158/180 on the touch icon. That was measured off the icons as they
|
||||||
|
already shipped, so a regeneration reproduces the composition rather than
|
||||||
|
restyling the mark.
|
||||||
|
|
||||||
|
### Regenerating
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run icons
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/icons.mjs` resizes `src/assets/brand/sml-infinity-mark.png` onto a
|
||||||
|
transparent square canvas and assembles the ICO container itself, because
|
||||||
|
`sharp` does not write `.ico`. **It writes the favicon only** and never touches
|
||||||
|
the touch icon. It refuses to run if the render source has no alpha channel, and
|
||||||
|
it asserts that the source is still byte-identical to the documented crop of
|
||||||
|
`sml-infinity-mark-master.png` — R14, so the icon stays traceable to committed
|
||||||
|
artwork. It verifies a **candidate** file and renames it into place only on
|
||||||
|
success, so a rejected build cannot replace a good favicon.
|
||||||
|
|
||||||
|
**It also carries the halo test**, because the check that matters was living
|
||||||
|
only in prose here while the generator could not run it
|
||||||
|
(`adversarial-reviewer`, round 2). For each frame it takes every painted pixel
|
||||||
|
touching a fully transparent one and measures how many sit within 20 of cream:
|
||||||
|
|
||||||
|
| | boundary px | near cream | share |
|
||||||
|
|---|---|---|---|
|
||||||
|
| correct, 16 / 32 / 48 px | 61 / 146 / 258 | 1 / 2 / 5 | **1.6 / 1.4 / 1.9 %** |
|
||||||
|
| haloed fixture, 16 px | 56 | 11 | **19.6 %** |
|
||||||
|
|
||||||
|
The gate is **10 %**, roughly 5× clear of both. ⚠️ **The first version of this
|
||||||
|
guard inspected only partial-alpha pixels and MISSED the fixture completely** —
|
||||||
|
a knockout sets alpha per pixel and leaves **no partial alpha at all**, so there
|
||||||
|
was nothing for it to look at. The fixture is built by matting the mark on cream
|
||||||
|
and then knocking the ground out by alpha, which is the defect exactly; it fires
|
||||||
|
at 19.6 % and leaves `public/favicon.ico` untouched. **A guard that cannot see
|
||||||
|
the defect it is named for is worse than none**, and this one passed the real
|
||||||
|
icon while blind, which is the shape that ends a check instead of starting one.
|
||||||
|
|
||||||
|
### Why the favicon was regenerated, 2026-09-02
|
||||||
|
|
||||||
|
It had **no transparency at all**: all three frames declared a 32-bit alpha
|
||||||
|
channel and then carried `alpha = 255` on every one of their 256 / 1,024 / 2,304
|
||||||
|
pixels, with the ground opaque cream `rgba(250,247,242,255)`. Found by Pouya on a
|
||||||
|
read-through, confirmed by parsing the container directly.
|
||||||
|
|
||||||
|
Three measurements stand behind the replacement.
|
||||||
|
|
||||||
|
**1. The composition did not change.** Composited back onto cream, the new icon
|
||||||
|
reproduces the old matted one to within **1 of 255 on every channel of every
|
||||||
|
pixel** at all three sizes (mean delta 0.01–0.02, 0 pixels over a delta of 8).
|
||||||
|
So the regeneration did not restyle, rescale or reposition the mark.
|
||||||
|
|
||||||
|
⚠️ **That is NOT a test for a halo, and an earlier draft of this file filed it as
|
||||||
|
one.** Composite-onto-cream returns ~0 whether the edge is correct *or* is a
|
||||||
|
cream-matted edge that has merely had its background knocked out — the second
|
||||||
|
case composites straight back to what it came from. Both branches pass, so the
|
||||||
|
test cannot discriminate. Found by `adversarial-reviewer`, 2026-09-02.
|
||||||
|
|
||||||
|
**2. No halo — the measurement that does discriminate.** Read the RGB the
|
||||||
|
partial-alpha pixels actually carry. A cream halo means that RGB is near cream; a
|
||||||
|
correct export means it is the ribbon's own colour. Of the partial-alpha pixels,
|
||||||
|
**0 of 94 / 300 / 603** are within 12 of `rgb(250,247,242)`; the nearest is 13
|
||||||
|
away, the mean distance is **174.0 / 178.3 / 177.0**, and the mean colour per
|
||||||
|
frame is about `rgb(118,86,74)` — maroon-brown, not cream and not black.
|
||||||
|
|
||||||
|
**3. Legible on dark and on light, and NOT at the same cost.** ⚠️ *The ground
|
||||||
|
colours below are external constants, not repository facts: `#202124` is
|
||||||
|
Chrome's dark tab strip and `#f1f3f4` its light one, both `[observed
|
||||||
|
2026-09-02]`.* All ratios are computed on **composited** pixels, and the ink
|
||||||
|
colours are **fully opaque** pixels only — a ratio taken from a raw channel value
|
||||||
|
is a claim about a colour that is never painted, which is how an earlier draft
|
||||||
|
came to quote `14.02:1` for a pixel whose alpha is 251.
|
||||||
|
|
||||||
|
| | darkest opaque ink | lightest opaque ink | vs `#202124` | vs `#f1f3f4` |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 16 px | `rgb(70,33,33)` | `rgb(216,188,143)` | champagne **8.82:1** | maroon **12.58:1** |
|
||||||
|
| 32 px | `rgb(60,29,29)` | `rgb(234,214,172)` | champagne **11.28:1** | maroon **13.62:1** |
|
||||||
|
| 48 px | `rgb(61,26,26)` | `rgb(238,219,175)` | champagne **11.80:1** | maroon **13.86:1** |
|
||||||
|
|
||||||
|
Whichever ground it sits on, one end of the ribbon carries the silhouette. **But
|
||||||
|
the other end does not merely dim — on dark it goes.** The maroon lobe
|
||||||
|
composites to **1.04–1.15:1**, and counting pixels that reach 3:1 against each
|
||||||
|
ground gives **23 / 87 / 189** on dark against **40 / 168 / 378** on light, at
|
||||||
|
16 / 32 / 48 px. **So about half as many pixels reach 3:1 on dark as on light.**
|
||||||
|
⚠️ *Those pixels are not absent — they are painted and fall below 3:1, and the
|
||||||
|
mark's full outline is still there, dim. An earlier draft said "roughly half the
|
||||||
|
mark's visible pixels are absent", which is neither what was counted (pixels at
|
||||||
|
`alpha > 0` are 109 / 388 / 805) nor what a render shows* — `adversarial-reviewer`,
|
||||||
|
round 2. The previous opaque-cream icon was still more legible on dark.
|
||||||
|
|
||||||
|
**That is the price of the change and it was accepted, not overlooked.** What was
|
||||||
|
bought is the removal of a cream rectangle from every dark tab strip and from
|
||||||
|
white. Raised by `adversarial-reviewer`, 2026-09-02, against a draft that
|
||||||
|
recorded the change as costless.
|
||||||
|
|
||||||
|
**The obvious alternative is an icon pair keyed on `prefers-color-scheme`, and it
|
||||||
|
was NOT ruled out on measurement.** ⚠️ *An earlier draft dismissed it on a
|
||||||
|
mechanism that is wrong: it said the query keys off the page's colour scheme. It
|
||||||
|
does not — it reports the user's system or browser preference, and this site
|
||||||
|
declares `color-scheme` nowhere (`git grep -n color-scheme -- src`, exit 1), so
|
||||||
|
it would resolve to the same preference that makes the tab strip dark.* The real
|
||||||
|
objections are **untested here** and are recorded as such: `media` on
|
||||||
|
`<link rel="icon">` is unevenly supported for raster icons, a browser theme can
|
||||||
|
be set independently of the system preference, and it doubles the artefact
|
||||||
|
`npm run icons` has to keep in sync. **If this cost is ever revisited, that is
|
||||||
|
the option to test** — do not re-dismiss it on the reason struck above.
|
||||||
|
|
||||||
## The colours are the artwork's, not the palette's
|
## The colours are the artwork's, not the palette's
|
||||||
|
|
||||||
`tokens.css` is not involved. The ribbon carries its own gradient and it is
|
`tokens.css` is not involved. The ribbon carries its own gradient and it is
|
||||||
|
|||||||
@@ -0,0 +1,629 @@
|
|||||||
|
# Canadian privacy and AI legislation status, and technology-dispute context
|
||||||
|
|
||||||
|
Committed under AGENTS.md R14 and the CLAUDE.md rule it encodes: **anything a
|
||||||
|
spec makes a claim about must be reachable from the repository.** Every fact
|
||||||
|
the six `/practice/*` pages state about the world is checkable here or it is
|
||||||
|
not published.
|
||||||
|
|
||||||
|
**Retrieved 2026-08-29.**
|
||||||
|
|
||||||
|
> ⚠️ **THIS FILE EXISTS BECAUSE A SPEC NAMED A STATUTE THAT DOES NOT EXIST.**
|
||||||
|
> `docs/03-content-spec.md` §Practice areas listed *"the 2026 privacy statute"*
|
||||||
|
> among the market context for `/practice/technology/`. **There is no 2026
|
||||||
|
> Canadian privacy statute, federal or Ontario.** Bill C-27 — which would have
|
||||||
|
> enacted the Consumer Privacy Protection Act and the Artificial Intelligence and
|
||||||
|
> Data Act — died without royal assent when the 44th Parliament's first session
|
||||||
|
> ended, and was never reinstated. **PIPEDA remains the operative federal
|
||||||
|
> private-sector statute.** Caught before a word of it reached a page, and only
|
||||||
|
> because the check was run rather than the phrase trusted.
|
||||||
|
|
||||||
|
> ⚠️ **A statute, a bill and a regulation all move.** Bill C-36 in particular was
|
||||||
|
> at second reading on the retrieval date and could be law, or dead, by the time
|
||||||
|
> anyone reads this. **Re-check before cutover.**
|
||||||
|
|
||||||
|
> ### R18 re-check — cutover pass, 2026-09-01
|
||||||
|
>
|
||||||
|
> The trigger fired. `AGENTS.md` §12 R18 holds the per-limb findings and
|
||||||
|
> the sources; this stamp does not restate them. **The quoted bytes below
|
||||||
|
> are still the original retrieval and were not re-fetched** — what was
|
||||||
|
> re-checked is whether the *facts* they support have moved.
|
||||||
|
>
|
||||||
|
> - **(a) Bill C-36 — RE-VERIFIED UNMOVED** `[re-checked 2026-09-01 — Pouya,
|
||||||
|
> <https://www.parl.ca/legisinfo/en/bill/45-1/c-36>]`. Still at second
|
||||||
|
> reading in the House of Commons; latest completed stage is first reading,
|
||||||
|
> 2026-06-15; no advance since. `/practice/technology/`'s sentence stands as
|
||||||
|
> written.
|
||||||
|
|
||||||
|
**Topic as researched:** Canadian technology / data / AI dispute context — privacy legislation status as at 2026-08-29, Ontario public-sector and health privacy statutes, data residency law, and Canadian arbitral-institution rules for technology/AI disputes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
| Kind | Source | URL |
|
||||||
|
|---|---|---|
|
||||||
|
| statute | LEGISinfo — Bill C-27 (44-1), Digital Charter Implementation Act, 2022 — Parliament of Canada | <https://www.parl.ca/legisinfo/en/bill/44-1/c-27> |
|
||||||
|
| statute | LEGISinfo bills data (JSON), 44th Parliament 1st Session — Parliament of Canada | <https://www.parl.ca/legisinfo/en/bills/json?parlsession=44-1> |
|
||||||
|
| institution | House of Commons Procedure and Practice, Fourth Edition (2025), Ch. 8 — Prorogation and Dissolution | <https://www.ourcommons.ca/procedure/procedure-and-practice-4/ch08-7-e.html> |
|
||||||
|
| statute | Personal Information Protection and Electronic Documents Act (S.C. 2000, c. 5) — Justice Laws Website | <https://laws-lois.justice.gc.ca/eng/acts/P-8.6/FullText.html> |
|
||||||
|
| statute | LEGISinfo bills data (JSON), 45th Parliament 1st Session — Parliament of Canada | <https://www.parl.ca/legisinfo/en/bills/json?parlsession=45-1> |
|
||||||
|
| statute | LEGISinfo — Bill C-36 (45-1), An Act to enact the Protecting Privacy and Consumer Data Act — Parliament of Canada | <https://www.parl.ca/legisinfo/en/bill/45-1/c-36> |
|
||||||
|
| statute | Bill C-36 (45-1), first reading text — Parliament of Canada | <https://www.parl.ca/DocumentViewer/en/45-1/bill/C-36/first-reading> |
|
||||||
|
| statute | Statutes of Canada 2026, c. 9 — An Act respecting cyber security … — Justice Laws Website | <https://laws-lois.justice.gc.ca/eng/AnnualStatutes/2026_9/> |
|
||||||
|
| statute | Bill C-8 (45-1), royal assent text — Parliament of Canada | <https://www.parl.ca/DocumentViewer/en/45-1/bill/C-8/royal-assent> |
|
||||||
|
| regulator | Summary of privacy laws in Canada — Office of the Privacy Commissioner of Canada | <https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/02_05_d_15/> |
|
||||||
|
| regulator | Guidelines for processing personal data across borders — Office of the Privacy Commissioner of Canada | <https://www.priv.gc.ca/en/privacy-topics/airports-and-borders/gl_dab_090127/> |
|
||||||
|
| regulator | Announcement: Commissioner concludes consultation on transfers for processing — Office of the Privacy Commissioner of Canada | <https://www.priv.gc.ca/en/opc-news/news-and-announcements/2019/an_190923/> |
|
||||||
|
| statute | Personal Health Information Protection Act, 2004 — Ontario e-Laws consolidated text (JSON endpoint behind https://www.ontario.ca/laws/statute/04p03) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/04p03> |
|
||||||
|
| regulation | O. Reg. 329/04 (GENERAL) under PHIPA — Ontario e-Laws consolidated text (JSON endpoint) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/040329> |
|
||||||
|
| statute | Freedom of Information and Protection of Privacy Act — Ontario e-Laws consolidated text (JSON endpoint behind https://www.ontario.ca/laws/statute/90f31) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90f31> |
|
||||||
|
| statute | Municipal Freedom of Information and Protection of Privacy Act — Ontario e-Laws (JSON endpoint) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90m56> |
|
||||||
|
| statute | Enhancing Digital Security and Trust Act, 2024 — Ontario e-Laws consolidated text (JSON endpoint behind https://www.ontario.ca/laws/statute/24e24) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/24e24> |
|
||||||
|
| regulation | Ontario e-Laws — regulations made under the Enhancing Digital Security and Trust Act, 2024 (JSON endpoint) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-reg/regulation?title=enhancing%20digital%20security%20and%20trust%20act%2C%202024&sort=citation> |
|
||||||
|
| regulation | O. Reg. 51/26 (CYBER SECURITY) under the Enhancing Digital Security and Trust Act, 2024 — Ontario e-Laws (JSON endpoint) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/260051> |
|
||||||
|
| regulation | O. Reg. 52/26 (DIGITAL TECHNOLOGY AFFECTING INDIVIDUALS UNDER AGE 18) under the Enhancing Digital Security and Trust Act, 2024 — Ontario e-Laws (JSON endpoint) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/260052> |
|
||||||
|
| institution | Bill 194, Strengthening Cyber Security and Building Trust in the Public Sector Act, 2024 — Legislative Assembly of Ontario | <https://www.ola.org/en/legislative-business/bills/parliament-43/session-1/bill-194> |
|
||||||
|
| institution | Bills — 44th Parliament, 1st Session — Legislative Assembly of Ontario | <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1> |
|
||||||
|
| institution | Bill 61, Ontario Artificial Intelligence, Talent and Innovation Strategy Act, 2025 — Legislative Assembly of Ontario | <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-61> |
|
||||||
|
| statute | Kids' Online Safety and Privacy Month Act, 2025 — Ontario e-Laws consolidated text (JSON endpoint) | <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/25k25> |
|
||||||
|
| regulator | Directive on Service and Digital — Treasury Board of Canada Secretariat | <https://www.tbs-sct.canada.ca/pol/doc-eng.aspx?id=32601> |
|
||||||
|
| regulator | Direction on the Secure Use of Commercial Cloud Services: Security Policy Implementation Notice (SPIN 2017-01) — Government of Canada | <https://www.canada.ca/en/government/system/digital-government/digital-government-innovations/cloud-services/direction-secure-use-commercial-cloud-services-spin.html> |
|
||||||
|
| institution | ADRIC Arbitration Rules, effective 01 March 2025 — ADR Institute of Canada, Inc. (PDF) | <https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf> |
|
||||||
|
| institution | Rules & Codes — ADR Institute of Canada | <https://adric.ca/rules-codes/> |
|
||||||
|
| institution | Artificial Intelligence and Arbitration: A Perfect Fit? — ADR Institute of Canada | <https://adric.ca/artificial-intelligence-and-arbitration-a-perfect-fit/> |
|
||||||
|
| institution | Who We Are — Canadian International Internet Dispute Resolution Centre (CIIDRC) | <https://ciidrc.org/about-ciidrc/> |
|
||||||
|
| institution | CIIDRC Supplemental Rules — Canadian International Internet Dispute Resolution Centre | <https://ciidrc.org/domain-name-disputes/ciidrc-supplemental-rules/> |
|
||||||
|
| institution | CIRA Domain Name Dispute Resolution Policy (reproduced by CIIDRC, a CIRA-approved provider) | <https://ciidrc.org/domain-name-disputes/cdrp-policy/> |
|
||||||
|
| institution | Rules of Procedure — VanIAC (Vancouver International Arbitration Centre) | <https://vaniac.org/arbitration/rules-of-procedure/> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verbatim quotations
|
||||||
|
|
||||||
|
### LEGISinfo — Bill C-27 (44-1), Digital Charter Implementation Act, 2022 — Parliament of Canada
|
||||||
|
|
||||||
|
<https://www.parl.ca/legisinfo/en/bill/44-1/c-27> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> An Act to enact the Consumer Privacy Protection Act, the Personal Information and Data Protection Tribunal Act and the Artificial Intelligence and Data Act and to make consequential and related amendments to other Acts
|
||||||
|
|
||||||
|
> Digital Charter Implementation Act, 2022
|
||||||
|
|
||||||
|
> At consideration in committee in the House of Commons
|
||||||
|
|
||||||
|
> Second reading and referral to committee on Monday, April 24, 2023
|
||||||
|
|
||||||
|
> 44th Parliament, 1st session (November 22, 2021 to January 6, 2025)
|
||||||
|
|
||||||
|
### LEGISinfo bills data (JSON), 44th Parliament 1st Session — Parliament of Canada
|
||||||
|
|
||||||
|
<https://www.parl.ca/legisinfo/en/bills/json?parlsession=44-1> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "NumberCode":"C-27" ... "StatusNameEn":"At consideration in committee in the House of Commons"
|
||||||
|
|
||||||
|
> "LatestCompletedMajorStageNameEn":"Second reading"
|
||||||
|
|
||||||
|
> "ReceivedRoyalAssent":false
|
||||||
|
|
||||||
|
> "ReceivedRoyalAssentDateTime":null
|
||||||
|
|
||||||
|
> "DidReinstateInNextSession":false
|
||||||
|
|
||||||
|
> "IsSessionOngoing":false
|
||||||
|
|
||||||
|
> "ParliamentNumber":44, "SessionNumber":1
|
||||||
|
|
||||||
|
### House of Commons Procedure and Practice, Fourth Edition (2025), Ch. 8 — Prorogation and Dissolution
|
||||||
|
|
||||||
|
<https://www.ourcommons.ca/procedure/procedure-and-practice-4/ch08-7-e.html> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Government bills which have not received royal assent before prorogation die and, in order to be proceeded with in the new session, must be reintroduced as if they had never existed.
|
||||||
|
|
||||||
|
> All items on the Order Paper including government and private members' bills die.
|
||||||
|
|
||||||
|
### Personal Information Protection and Electronic Documents Act (S.C. 2000, c. 5) — Justice Laws Website
|
||||||
|
|
||||||
|
<https://laws-lois.justice.gc.ca/eng/acts/P-8.6/FullText.html> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Personal Information Protection and Electronic Documents Act
|
||||||
|
|
||||||
|
> Act current to 2026-06-21 and last amended on 2025-03-04.
|
||||||
|
|
||||||
|
> 4 (1) This Part applies to every organization in respect of personal information that (a) the organization collects, uses or discloses in the course of commercial activities; or (b) is about an employee of, or an applicant for employment with, the organization and that the organization collects, uses or discloses in connection with the operation of a federal work, undertaking or business.
|
||||||
|
|
||||||
|
> 4.1.3 An organization is responsible for personal information in its possession or custody, including information that has been transferred to a third party for processing. The organization shall use contractual or other means to provide a comparable level of protection while the information is being processed by a third party.
|
||||||
|
|
||||||
|
> (grep over the full text for the phrases "outside Canada", "stored in Canada", "within Canada" and "localiz" returned exit status 1 and zero lines; instrument check on the same file returned 113 occurrences of "personal information")
|
||||||
|
|
||||||
|
### LEGISinfo bills data (JSON), 45th Parliament 1st Session — Parliament of Canada
|
||||||
|
|
||||||
|
<https://www.parl.ca/legisinfo/en/bills/json?parlsession=45-1> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> (185 bills in the session; a regex scan of every LongTitleEn and ShortTitleEn for /privacy|personal information|artificial intelligence|consumer privacy|data protection|cyber|digital charter/i returned exactly two: C-8 and C-36)
|
||||||
|
|
||||||
|
> "NumberCode":"C-8" ... "An Act respecting cyber security, amending the Telecommunications Act and making consequential amendments to other Acts" ... "StatusNameEn":"Royal assent received" ... "ReceivedRoyalAssentDateTime":"2026-06-15T06:15:00-04:00"
|
||||||
|
|
||||||
|
> "NumberCode":"C-36" ... "An Act to enact the Protecting Privacy and Consumer Data Act, to amend the Personal Information Protection and Electronic Documents Act and to make amendments to other Acts" ... "StatusNameEn":"At second reading in the House of Commons" ... "ReceivedRoyalAssent":false
|
||||||
|
|
||||||
|
> "PassedHouseFirstReadingDateTime":"2026-06-15T11:18:34.507-04:00"
|
||||||
|
|
||||||
|
> (a regex scan of every 45-1 bill title for /intellig/i returned 0 matches)
|
||||||
|
|
||||||
|
### LEGISinfo — Bill C-36 (45-1), An Act to enact the Protecting Privacy and Consumer Data Act — Parliament of Canada
|
||||||
|
|
||||||
|
<https://www.parl.ca/legisinfo/en/bill/45-1/c-36> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> An Act to enact the Protecting Privacy and Consumer Data Act, to amend the Personal Information Protection and Electronic Documents Act and to make amendments to other Acts
|
||||||
|
|
||||||
|
> Sponsor: Minister of Artificial Intelligence and Digital Innovation
|
||||||
|
|
||||||
|
> At second reading in the House of Commons
|
||||||
|
|
||||||
|
> First reading: Completed Monday, June 15, 2026
|
||||||
|
|
||||||
|
> Royal Assent: Not received
|
||||||
|
|
||||||
|
### Bill C-36 (45-1), first reading text — Parliament of Canada
|
||||||
|
|
||||||
|
<https://www.parl.ca/DocumentViewer/en/45-1/bill/C-36/first-reading> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> This enactment enacts the Protecting Privacy and Consumer Data Act to govern the protection of personal information of individuals while taking into account the need of organizations to collect, use or disclose personal information in the course of commercial activities.
|
||||||
|
|
||||||
|
> This Act may be cited as the Protecting Privacy and Consumer Data Act.
|
||||||
|
|
||||||
|
### Statutes of Canada 2026, c. 9 — An Act respecting cyber security … — Justice Laws Website
|
||||||
|
|
||||||
|
<https://laws-lois.justice.gc.ca/eng/AnnualStatutes/2026_9/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> An Act respecting cyber security, amending the Telecommunications Act and making consequential amendments to other Acts (S.C. 2026, c. 9)
|
||||||
|
|
||||||
|
> Assented to June 15, 2026
|
||||||
|
|
||||||
|
### Bill C-8 (45-1), royal assent text — Parliament of Canada
|
||||||
|
|
||||||
|
<https://www.parl.ca/DocumentViewer/en/45-1/bill/C-8/royal-assent> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> STATUTES OF CANADA 2026 CHAPTER 9
|
||||||
|
|
||||||
|
> ASSENTED TO June 15, 2026
|
||||||
|
|
||||||
|
> Part 2 enacts the Critical Cyber Systems Protection Act
|
||||||
|
|
||||||
|
### Summary of privacy laws in Canada — Office of the Privacy Commissioner of Canada
|
||||||
|
|
||||||
|
<https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/02_05_d_15/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Canada has two federal privacy laws that are enforced by the Office of the Privacy Commissioner of Canada: the Privacy Act, which covers how the federal government handles personal information; the Personal Information Protection and Electronic Documents Act (PIPEDA), which covers how businesses handle personal information.
|
||||||
|
|
||||||
|
> PIPEDA sets the ground rules for how private-sector organizations collect, use, and disclose personal information in the course of for-profit, commercial activities across Canada.
|
||||||
|
|
||||||
|
> PIPEDA generally applies to personal information held by private sector organizations that are not federally-regulated, and conduct business in: Manitoba New Brunswick Newfoundland and Labrador Northwest Territories Nova Scotia Nunavut Ontario Prince Edward Island Saskatchewan Yukon.
|
||||||
|
|
||||||
|
> Date modified: 2018-01-31
|
||||||
|
|
||||||
|
### Guidelines for processing personal data across borders — Office of the Privacy Commissioner of Canada
|
||||||
|
|
||||||
|
<https://www.priv.gc.ca/en/privacy-topics/airports-and-borders/gl_dab_090127/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> PIPEDA does not prohibit organizations in Canada from transferring personal information to an organization in another jurisdiction for processing. However, under PIPEDA, organizations are held accountable for the protection of personal information transfers under each individual outsourcing arrangement.
|
||||||
|
|
||||||
|
> Principle 4.1.3 of Schedule 1 of PIPEDA specifically recognizes that personal information may be transferred to third parties for processing. It also requires organizations to use contractual or other means to "provide a comparable level of protection while the information is being processed by the third party."
|
||||||
|
|
||||||
|
> In contrast to this state-to-state approach, Canada has, through PIPEDA, chosen an organization-to-organization approach that is not based on the concept of adequacy.
|
||||||
|
|
||||||
|
> Date modified: 2009-01-27
|
||||||
|
|
||||||
|
### Announcement: Commissioner concludes consultation on transfers for processing — Office of the Privacy Commissioner of Canada
|
||||||
|
|
||||||
|
<https://www.priv.gc.ca/en/opc-news/news-and-announcements/2019/an_190923/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Commissioner concludes consultation on transfers for processing (September 23, 2019)
|
||||||
|
|
||||||
|
> guidelines for processing personal data across borders
|
||||||
|
|
||||||
|
> remain unchanged under the current law
|
||||||
|
|
||||||
|
### Personal Health Information Protection Act, 2004 — Ontario e-Laws consolidated text (JSON endpoint behind https://www.ontario.ca/laws/statute/04p03)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/04p03> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": "Personal Health Information Protection Act, 2004, S.O. 2004, c. 3, Sched. A"
|
||||||
|
|
||||||
|
> "description": "Consolidation Period: From January 1, 2026 to the e-Laws currency date." / "comment": "Last amendment: 2025, c. 7, Sched. 6, s. 1-13"
|
||||||
|
|
||||||
|
> 12 (1) A health information custodian shall take steps that are reasonable in the circumstances to ensure that personal health information in the custodian's custody or control is protected against theft, loss and unauthorized use or disclosure and to ensure that the records containing the information are protected against unauthorized copying, modification or disposal.
|
||||||
|
|
||||||
|
> Place where records kept 14 (1) A health information custodian may keep a record of personal health information about an individual in the individual's home in any reasonable manner to which the individual consents, subject to any restrictions set out in a regulation, by-law or published guideline under the Regulated Health Professions Act, 1991 …
|
||||||
|
|
||||||
|
> Records kept in other places (2) A health care practitioner may keep a record of personal health information about an individual in a place other than the individual's home and other than a place in the control of the practitioner if, (a) the record is kept in a reasonable manner; (b) the individual consents; …
|
||||||
|
|
||||||
|
> Disclosure outside Ontario 50 (1) A health information custodian may disclose personal health information about an individual collected in Ontario to a person outside Ontario only if, (a) the individual consents to the disclosure; (b) this Act permits the disclosure; …
|
||||||
|
|
||||||
|
> (grep over the extracted plain text for "outside Canada" returned 0 matches; the only "outside Ontario" provisions are s. 44 research approval, and s. 50 disclosure)
|
||||||
|
|
||||||
|
### O. Reg. 329/04 (GENERAL) under PHIPA — Ontario e-Laws consolidated text (JSON endpoint)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/040329> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "actTitle": "Personal Health Information Protection Act, 2004, S.O. 2004, c. 3, Sched. A"
|
||||||
|
|
||||||
|
> "consolidationPeriod": "January 1, 2026"
|
||||||
|
|
||||||
|
> (7) Despite subsection 45 (6) of the Act, the Canadian Institute for Health Information may disclose personal health information about an individual to a person outside Ontario where,
|
||||||
|
|
||||||
|
> (10) Despite subsection 45 (6) of the Act, Ontario Health may disclose personal health information about an individual to a person outside Ontario where,
|
||||||
|
|
||||||
|
> (a grep over the extracted text for "outside canada", "in canada", "outside ontario" and "stored" returned 3 lines, all of them disclosure-permission or health-number provisions; none imposes a storage-location requirement)
|
||||||
|
|
||||||
|
### Freedom of Information and Protection of Privacy Act — Ontario e-Laws consolidated text (JSON endpoint behind https://www.ontario.ca/laws/statute/90f31)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90f31> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": "Freedom of Information and Protection of Privacy Act, R.S.O. 1990, c. F.31"
|
||||||
|
|
||||||
|
> "description": "Consolidation Period: From July 1, 2026 to the e-Laws currency date." / "comment": "Last amendment: 2026, c. 2, Sched. 7"
|
||||||
|
|
||||||
|
> Privacy safeguards (5) The head of an institution shall take steps that are reasonable in the circumstances to ensure that personal information in the custody or under the control of the institution is protected against theft, loss and unauthorized use or disclosure and to ensure that the records containing the personal information are protected against unauthorized copying, modification or disposal. 2024, c. 24, Sched. 2, s. 5. / Section Amendments with date in force (d/m/y) 2024, c. 24, Sched. 2, s. 5 - 01/07/2025
|
||||||
|
|
||||||
|
> Breach of privacy safeguards 40.1 (1) The head of an institution shall report to the Commissioner any theft, loss or unauthorized use or disclosure of personal information in the custody or under the control of the institution if it is reasonable in the circumstances to believe that there is real risk that a significant harm to an individual would result or if any other prescribed circumstances exist. 2024, c. 24, Sched. 2, s. 6.
|
||||||
|
|
||||||
|
> (grep for "outside Canada" returned exit status 1 and 0 lines; instrument check on the same file returned 248 occurrences of "personal information". "in Canada" appears only at 3 law-enforcement disclosure clauses)
|
||||||
|
|
||||||
|
### Municipal Freedom of Information and Protection of Privacy Act — Ontario e-Laws (JSON endpoint)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90m56> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": "Municipal Freedom of Information and Protection of Privacy Act, R.S.O. 1990, c. M.56"
|
||||||
|
|
||||||
|
> "description": "Consolidation Period: From July 1, 2026 to the e-Laws currency date."
|
||||||
|
|
||||||
|
### Enhancing Digital Security and Trust Act, 2024 — Ontario e-Laws consolidated text (JSON endpoint behind https://www.ontario.ca/laws/statute/24e24)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/24e24> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": "Enhancing Digital Security and Trust Act, 2024, S.O. 2024, c. 24, Sched. 1"
|
||||||
|
|
||||||
|
> "description": "Consolidation Period: From January 29, 2025 to the e-Laws currency date." / "comment": "No amendments."
|
||||||
|
|
||||||
|
> "artificial intelligence system" means, (a) a machine-based system that, for explicit or implicit objectives, infers from the input it receives in order to generate outputs such as predictions, content, recommendations or decisions that can influence physical or virtual environments, and (b) such other systems as may be prescribed;
|
||||||
|
|
||||||
|
> 5 (1) This section applies to such public sector entities as may be prescribed for the purposes of this section if they use or intend to use an artificial intelligence system in prescribed circumstances.
|
||||||
|
|
||||||
|
> No establishment of private law duty of care 12 Nothing in the Strengthening Cyber Security and Building Trust in the Public Sector Act, 2024 , this Act or any regulation made or directive issued under this Act establishes a private law duty of care owing to any person.
|
||||||
|
|
||||||
|
> Effect of failure to comply 13 Failure to comply with this Act or any regulation made or directive issued under this Act does not affect the validity of any policy, Act, regulation, directive, instrument or decision.
|
||||||
|
|
||||||
|
### Ontario e-Laws — regulations made under the Enhancing Digital Security and Trust Act, 2024 (JSON endpoint)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-reg/regulation?title=enhancing%20digital%20security%20and%20trust%20act%2C%202024&sort=citation> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> current: 2 results — "regulation/260052" DIGITAL TECHNOLOGY AFFECTING INDIVIDUALS UNDER AGE 18; "regulation/260051" CYBER SECURITY
|
||||||
|
|
||||||
|
> revoked: 0 results
|
||||||
|
|
||||||
|
### O. Reg. 51/26 (CYBER SECURITY) under the Enhancing Digital Security and Trust Act, 2024 — Ontario e-Laws (JSON endpoint)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/260051> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": "CYBER SECURITY" / "actTitle": "Enhancing Digital Security and Trust Act, 2024, S.O. 2024, c. 24, Sched. 1"
|
||||||
|
|
||||||
|
> "consolidationPeriod": "July 1, 2026" / "comment": "No amendments."
|
||||||
|
|
||||||
|
> CONTENTS 1. Interpretation 2. Prescribed public sector entities 3. Program 4. Primary point of contact and alternate 5. Cyber security maturity assessment 6. Cyber security maturity assessment summary 7. Critical cyber security incident, report
|
||||||
|
|
||||||
|
> (a case-insensitive count of "artificial intelligence" in the extracted text returned 0)
|
||||||
|
|
||||||
|
### O. Reg. 52/26 (DIGITAL TECHNOLOGY AFFECTING INDIVIDUALS UNDER AGE 18) under the Enhancing Digital Security and Trust Act, 2024 — Ontario e-Laws (JSON endpoint)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/260052> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": "DIGITAL TECHNOLOGY AFFECTING INDIVIDUALS UNDER AGE 18" / "actTitle": "Enhancing Digital Security and Trust Act, 2024, S.O. 2024, c. 24, Sched. 1"
|
||||||
|
|
||||||
|
> "consolidationPeriod": "July 1, 2026" / "comment": "No amendments."
|
||||||
|
|
||||||
|
> (a case-insensitive count of "artificial intelligence" in the extracted text returned 0)
|
||||||
|
|
||||||
|
### Bill 194, Strengthening Cyber Security and Building Trust in the Public Sector Act, 2024 — Legislative Assembly of Ontario
|
||||||
|
|
||||||
|
<https://www.ola.org/en/legislative-business/bills/parliament-43/session-1/bill-194> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Bill 194, Strengthening Cyber Security and Building Trust in the Public Sector Act, 2024
|
||||||
|
|
||||||
|
> Royal Assent received. Statutes of Ontario 2024, chapter 24
|
||||||
|
|
||||||
|
> cyber security and artificial intelligence systems at public sector entities
|
||||||
|
|
||||||
|
> public sector entities may be required to comply with requirements respecting the use of artificial intelligence, including requirements to provide information, to develop and implement accountability frameworks and to take steps respecting risk management
|
||||||
|
|
||||||
|
### Bills — 44th Parliament, 1st Session — Legislative Assembly of Ontario
|
||||||
|
|
||||||
|
<https://www.ola.org/en/legislative-business/bills/parliament-44/session-1> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> (139 numbered bills, 1 through 139, listed on a single unpaginated page; a keyword scan of all titles for /privacy|personal information|freedom of information|health information|artificial intelligence|data|digital|cyber|technolog|online/i returned exactly four)
|
||||||
|
|
||||||
|
> Bill 15: Kids' Online Safety and Privacy Month Act, 2025
|
||||||
|
|
||||||
|
> Bill 61: Ontario Artificial Intelligence, Talent and Innovation Strategy Act, 2025
|
||||||
|
|
||||||
|
> Bill 66: Kids' Online Safety and Privacy Month Act, 2025
|
||||||
|
|
||||||
|
> Bill 137: Keeping Our Kids Safe Online Act, 2026
|
||||||
|
|
||||||
|
### Bill 61, Ontario Artificial Intelligence, Talent and Innovation Strategy Act, 2025 — Legislative Assembly of Ontario
|
||||||
|
|
||||||
|
<https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-61> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Bill 61, Ontario Artificial Intelligence, Talent and Innovation Strategy Act, 2025
|
||||||
|
|
||||||
|
> Private member's bill
|
||||||
|
|
||||||
|
> November 24, 2025 — Second Reading — Lost on division
|
||||||
|
|
||||||
|
### Kids' Online Safety and Privacy Month Act, 2025 — Ontario e-Laws consolidated text (JSON endpoint)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/25k25> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": "Kids' Online Safety and Privacy Month Act, 2025, S.O. 2025, c. 25"
|
||||||
|
|
||||||
|
> "description": "Consolidation Period: From December 11, 2025 to the e-Laws currency date."
|
||||||
|
|
||||||
|
> Kids' Online Safety and Privacy Month 1 The month of October in each year is proclaimed as Kids' Online Safety and Privacy Month.
|
||||||
|
|
||||||
|
> 2 Omitted ( provides for coming into force of provisions of this Act ). 3 Omitted (enacts short title of this Act).
|
||||||
|
|
||||||
|
### Directive on Service and Digital — Treasury Board of Canada Secretariat
|
||||||
|
|
||||||
|
<https://www.tbs-sct.canada.ca/pol/doc-eng.aspx?id=32601> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Information and data residency
|
||||||
|
|
||||||
|
> 4.3.24 Ensuring that computing facilities located within the geographic boundaries of Canada or within the premises of a GC department located abroad, such as a diplomatic or consular mission, be identified and evaluated as a principal delivery option for all sensitive electronic information and data under government control that has been categorized as Protected B or Protected C or is classified;
|
||||||
|
|
||||||
|
> Date modified: 2025-08-29
|
||||||
|
|
||||||
|
### Direction on the Secure Use of Commercial Cloud Services: Security Policy Implementation Notice (SPIN 2017-01) — Government of Canada
|
||||||
|
|
||||||
|
<https://www.canada.ca/en/government/system/digital-government/digital-government-innovations/cloud-services/direction-secure-use-commercial-cloud-services-spin.html> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> SPIN No.: 2017-01 Date: November 1, 2017 Date modified: June 23, 2022
|
||||||
|
|
||||||
|
> 6.2.2 Data residency — Departments are expected to apply the Directive on Service and Digital when implementing safeguards for GC electronic data residency.
|
||||||
|
|
||||||
|
### ADRIC Arbitration Rules, effective 01 March 2025 — ADR Institute of Canada, Inc. (PDF)
|
||||||
|
|
||||||
|
<https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADRIC ARBITRATION RULES Effective 01 March 2025
|
||||||
|
|
||||||
|
> ADRIC intends these Rules for Canadian commercial disputes; however, parties can apply them to international or non-commercial disputes.
|
||||||
|
|
||||||
|
> Privacy and Security of Evidence 31. Each party and its counsel are responsible for ensuring that all relevant privacy and data security requirements prescribed by law or contract in relation to evidence put forward by that party are complied with, and that the Tribunal is made aware of any steps that the Tribunal needs to take in that regard.
|
||||||
|
|
||||||
|
> (a case-insensitive grep of the extracted 103,811-character text for "artificial intelligence", "machine learning" and the standalone token "AI" returned no matches; the only hits for "technolog|cyber|data|electronic" were three lines about electronic data as evidence, electronic delivery, and the clause quoted above)
|
||||||
|
|
||||||
|
### Rules & Codes — ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/rules-codes/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Rules & Codes — ADR Institute of Canada
|
||||||
|
|
||||||
|
> ADRIC By-laws / Federation MoU / ADRIC Arbitration Rules / National Mediation Rules / ADRIC Med-Arb Rules
|
||||||
|
|
||||||
|
> Ethics & Professional Practice — Code of Ethics / Code of Conduct / Conflict of Interest / Complaints & Discipline Policy / Privacy Policy / Online Dispute Resolution (ODR) Vision
|
||||||
|
|
||||||
|
### Artificial Intelligence and Arbitration: A Perfect Fit? — ADR Institute of Canada
|
||||||
|
|
||||||
|
<https://adric.ca/artificial-intelligence-and-arbitration-a-perfect-fit/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Artificial Intelligence and Arbitration: A Perfect Fit?
|
||||||
|
|
||||||
|
> March 2, 2023
|
||||||
|
|
||||||
|
> By Robin Dodokin, Sarah McEachern, Les Honywill
|
||||||
|
|
||||||
|
> Machine learning and AI have progressed so far that their integration into the arbitral process seems inevitable, with the only question being a matter of time and degree.
|
||||||
|
|
||||||
|
### Who We Are — Canadian International Internet Dispute Resolution Centre (CIIDRC)
|
||||||
|
|
||||||
|
<https://ciidrc.org/about-ciidrc/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The Canadian International Internet Dispute Resolution Centre ("CIIDRC", "the Centre") serves global Internet users by providing trusted and efficient resolution of domain name disputes under the Uniform Domain Name Dispute Resolution Policy (the UDRP) and the CIRA Domain Name Dispute Resolution Policy (the CDRP).
|
||||||
|
|
||||||
|
> CIIDRC is a division of the Vancouver International Arbitration Centre, formerly known as the British Columbia International Commercial Arbitration Centre ("the Centre").
|
||||||
|
|
||||||
|
> CIIDRC's parent organization, VanIAC (formerly BCICAC), has been a service provider for the Canadian Internet Registration Authority (CIRA) since 2002, successfully managing .ca (dot ca) domain name disputes.
|
||||||
|
|
||||||
|
### CIIDRC Supplemental Rules — Canadian International Internet Dispute Resolution Centre
|
||||||
|
|
||||||
|
<https://ciidrc.org/domain-name-disputes/ciidrc-supplemental-rules/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> CIIDRC Supplemental Rules OF THE CANADIAN INTERNATIONAL INTERNET DISPUTE RESOLUTION CENTRE (the "Centre" or the "CIIDRC" or the "Provider") FOR THE UNIFORM DOMAIN NAME DISPUTE RESOLUTION POLICY (the "Policy") AND THE RULES FOR THE UNIFORM DOMAIN NAME DISPUTE RESOLUTION POLICY (the "UDRP Rules")
|
||||||
|
|
||||||
|
> The Supplemental Rules (In effect as of May 9, 2018)
|
||||||
|
|
||||||
|
### CIRA Domain Name Dispute Resolution Policy (reproduced by CIIDRC, a CIRA-approved provider)
|
||||||
|
|
||||||
|
<https://ciidrc.org/domain-name-disputes/cdrp-policy/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> CIRA Domain Name Dispute Resolution Policy — Version 1.3 (August 22, 2011)
|
||||||
|
|
||||||
|
> 1.1 Purpose. The purpose of this CIRA Domain Name Dispute Resolution Policy (the "Policy") is to provide a forum in which cases of bad faith registration of domain names registered in the dot-ca country code top level domain name registry operated by CIRA (the "Registry") can be dealt with relatively inexpensively and quickly.
|
||||||
|
|
||||||
|
> 1.2 Scope. The Policy sets forth the terms and conditions for resolution by arbitration of a dispute between a person (the "Registrant") who has obtained the registration of a domain name in the Registry (the "Registration") and any other person …
|
||||||
|
|
||||||
|
> 1.5 Dispute Resolution Service Provider. All Proceedings will be administered by a dispute resolution service provider approved by CIRA (the "Provider").
|
||||||
|
|
||||||
|
### Rules of Procedure — VanIAC (Vancouver International Arbitration Centre)
|
||||||
|
|
||||||
|
<https://vaniac.org/arbitration/rules-of-procedure/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Rules of Procedure — Domestic Arbitration Rules (as amended Sept. 1, 2020)
|
||||||
|
|
||||||
|
> International Commercial Arbitration Rules of Procedure (as amended July 1, 2022)
|
||||||
|
|
||||||
|
> International Commercial Arbitration Rules of Procedure (as amended Jan. 1, 2000)
|
||||||
|
|
||||||
|
> (the page's full navigation lists arbitration, mediation and motor-vehicle rules, forms, fee schedules and an Arbitrator Code of Conduct; no rule set, guideline or note on artificial intelligence or technology disputes appears)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this establishes
|
||||||
|
|
||||||
|
- PIPEDA — the Personal Information Protection and Electronic Documents Act, S.C. 2000, c. 5 — is the federal private-sector privacy statute in force. The Justice Laws consolidation states "Act current to 2026-06-21 and last amended on 2025-03-04."
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/P-8.6/FullText.html>
|
||||||
|
- PIPEDA Part 1 applies to every organization in respect of personal information it "collects, uses or discloses in the course of commercial activities" (s. 4(1)(a)), and to employee information in connection with a federal work, undertaking or business (s. 4(1)(b)).
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/P-8.6/FullText.html>
|
||||||
|
- The Office of the Privacy Commissioner of Canada states that Canada has two federal privacy laws it enforces — the Privacy Act (federal government) and PIPEDA, which "sets the ground rules for how private-sector organizations collect, use, and disclose personal information in the course of for-profit, commercial activities across Canada." PIPEDA generally applies to non-federally-regulated private-sector organizations doing business in Ontario (among other provinces and territories).
|
||||||
|
*Source:* <https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/02_05_d_15/>
|
||||||
|
- Bill C-27 (44th Parliament, 1st Session) was the bill that would have enacted the Consumer Privacy Protection Act, the Personal Information and Data Protection Tribunal Act and the Artificial Intelligence and Data Act. Its short title was the Digital Charter Implementation Act, 2022.
|
||||||
|
*Source:* <https://www.parl.ca/legisinfo/en/bill/44-1/c-27>
|
||||||
|
- Bill C-27 never received royal assent. LEGISinfo records its last completed major stage as "Second reading", its status as "At consideration in committee in the House of Commons", ReceivedRoyalAssent = false, ReceivedRoyalAssentDateTime = null, IsSessionOngoing = false, and DidReinstateInNextSession = false.
|
||||||
|
*Source:* <https://www.parl.ca/legisinfo/en/bills/json?parlsession=44-1>
|
||||||
|
- The 44th Parliament's 1st session ran to January 6, 2025, and Bill C-27 had not advanced past committee when it ended.
|
||||||
|
*Source:* <https://www.parl.ca/legisinfo/en/bill/44-1/c-27>
|
||||||
|
- Under House of Commons Procedure and Practice (4th ed., 2025), "Government bills which have not received royal assent before prorogation die and, in order to be proceeded with in the new session, must be reintroduced as if they had never existed," and on dissolution "All items on the Order Paper including government and private members' bills die." Combined with the LEGISinfo record, this means the Consumer Privacy Protection Act and the Artificial Intelligence and Data Act were never enacted and do not exist as Canadian law.
|
||||||
|
*Source:* <https://www.ourcommons.ca/procedure/procedure-and-practice-4/ch08-7-e.html>
|
||||||
|
- In the 45th Parliament, 1st Session, a scan of all 185 bills found only two whose titles touch privacy, AI, cyber or data protection: C-8 and C-36. No bill in the session has "intellig" (i.e. "intelligence") anywhere in its title — there is no successor AI bill to AIDA before Parliament.
|
||||||
|
*Source:* <https://www.parl.ca/legisinfo/en/bills/json?parlsession=45-1>
|
||||||
|
- Bill C-36 (45-1), "An Act to enact the Protecting Privacy and Consumer Data Act, to amend the Personal Information Protection and Electronic Documents Act and to make amendments to other Acts", received first reading on June 15, 2026, is sponsored by the Minister of Artificial Intelligence and Digital Innovation, and its status is "At second reading in the House of Commons". Royal assent has NOT been received.
|
||||||
|
*Source:* <https://www.parl.ca/legisinfo/en/bill/45-1/c-36>
|
||||||
|
- Bill C-36 would enact the "Protecting Privacy and Consumer Data Act" to govern protection of personal information collected, used or disclosed in the course of commercial activities. It is a bill, not a statute — nothing in it is in force.
|
||||||
|
*Source:* <https://www.parl.ca/DocumentViewer/en/45-1/bill/C-36/first-reading>
|
||||||
|
- There is no "2026 privacy statute" in Canadian federal law. The only 2026 federal privacy instrument is Bill C-36, introduced 15 June 2026 and still at second reading with no royal assent, so PIPEDA remains the operative federal private-sector privacy statute as at 2026-08-29.
|
||||||
|
*Source:* <https://www.parl.ca/legisinfo/en/bills/json?parlsession=45-1>
|
||||||
|
- The one cyber/data-adjacent federal statute enacted in 2025–2026 is Bill C-8, "An Act respecting cyber security, amending the Telecommunications Act and making consequential amendments to other Acts", which received royal assent on June 15, 2026 and is S.C. 2026, c. 9. It enacts the Critical Cyber Systems Protection Act. It is a critical-infrastructure cyber security statute, not a privacy or AI statute.
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/AnnualStatutes/2026_9/>
|
||||||
|
- Ontario public-sector access/privacy statute: Freedom of Information and Protection of Privacy Act, R.S.O. 1990, c. F.31 (e-Laws consolidation period from July 1, 2026; last amendment 2026, c. 2, Sched. 7).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90f31>
|
||||||
|
- Ontario municipal-sector equivalent: Municipal Freedom of Information and Protection of Privacy Act, R.S.O. 1990, c. M.56 (e-Laws consolidation period from July 1, 2026).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90m56>
|
||||||
|
- Ontario health privacy statute: Personal Health Information Protection Act, 2004, S.O. 2004, c. 3, Sched. A (e-Laws consolidation period from January 1, 2026; last amendment 2025, c. 7, Sched. 6, ss. 1–13).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/04p03>
|
||||||
|
- FIPPA's privacy-safeguard duty (s. 40(5)) and mandatory breach reporting to the Commissioner and notification to affected individuals (s. 40.1) were enacted by S.O. 2024, c. 24, Sched. 2, ss. 5–6, and the e-Laws in-force note records both as in force 01/07/2025.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90f31>
|
||||||
|
- Ontario's AI-relevant statute is the Enhancing Digital Security and Trust Act, 2024, S.O. 2024, c. 24, Sched. 1 (enacted by Bill 194, royal assent giving Statutes of Ontario 2024, chapter 24), consolidated from January 29, 2025 with no amendments. It defines "artificial intelligence system" as "a machine-based system that, for explicit or implicit objectives, infers from the input it receives in order to generate outputs such as predictions, content, recommendations or decisions that can influence physical or virtual environments".
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/24e24>
|
||||||
|
- Every EDSTA AI obligation is conditional on regulations: s. 5(1) applies only "to such public sector entities as may be prescribed … if they use or intend to use an artificial intelligence system in prescribed circumstances." The Act also states at s. 12 that nothing in it "establishes a private law duty of care owing to any person", and at s. 13 that failure to comply "does not affect the validity of any policy, Act, regulation, directive, instrument or decision."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/24e24>
|
||||||
|
- Only two regulations have been made under EDSTA — O. Reg. 51/26 (Cyber Security) and O. Reg. 52/26 (Digital Technology Affecting Individuals Under Age 18); the e-Laws listing shows 2 current and 0 revoked. No AI regulation has been made, so EDSTA's artificial-intelligence sections have no prescribed entities or circumstances and impose no operative obligation as at 2026-08-29.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-reg/regulation?title=enhancing%20digital%20security%20and%20trust%20act%2C%202024&sort=citation>
|
||||||
|
- O. Reg. 51/26 (Cyber Security) under EDSTA has a consolidation period from July 1, 2026 and covers prescribed public sector entities, cyber security programs, a primary point of contact, cyber security maturity assessments and critical incident reporting. The phrase "artificial intelligence" does not appear in it.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/260051>
|
||||||
|
- O. Reg. 52/26 (Digital Technology Affecting Individuals Under Age 18) under EDSTA has a consolidation period from July 1, 2026 and deals with prescribed school boards and notice of disclosure of students' personal digital information. The phrase "artificial intelligence" does not appear in it.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/260052>
|
||||||
|
- No Ontario privacy or AI regulatory statute was enacted in 2025 or 2026. Of the 139 bills in the Ontario 44th Parliament 1st Session, only four have privacy/AI/online titles: Bill 61, the Ontario Artificial Intelligence, Talent and Innovation Strategy Act, 2025 (a private member's bill) was lost on division at second reading on November 24, 2025; Bill 137 is still at first reading; and Bills 15/66 are commemorative-month bills.
|
||||||
|
*Source:* <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1>
|
||||||
|
- Bill 61, the Ontario Artificial Intelligence, Talent and Innovation Strategy Act, 2025, was a private member's bill and was lost on division at second reading on November 24, 2025 — Ontario has no AI strategy statute.
|
||||||
|
*Source:* <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-61>
|
||||||
|
- The only Ontario statute with "Privacy" in its title enacted in this period is the Kids' Online Safety and Privacy Month Act, 2025, S.O. 2025, c. 25 (in force December 11, 2025). Its entire operative content is s. 1: "The month of October in each year is proclaimed as Kids' Online Safety and Privacy Month." It creates no privacy obligations.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/25k25>
|
||||||
|
- DATA RESIDENCY — PIPEDA contains no data-localization requirement. A grep of the full Justice Laws consolidation for "outside Canada", "stored in Canada", "within Canada" and "localiz" returned zero matches (grep exit status 1), against 113 occurrences of "personal information" in the same file as an instrument check.
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/P-8.6/FullText.html>
|
||||||
|
- The OPC states directly: "PIPEDA does not prohibit organizations in Canada from transferring personal information to an organization in another jurisdiction for processing. However, under PIPEDA, organizations are held accountable for the protection of personal information transfers under each individual outsourcing arrangement." Canada's approach is organization-to-organization accountability, not EU-style adequacy.
|
||||||
|
*Source:* <https://www.priv.gc.ca/en/privacy-topics/airports-and-borders/gl_dab_090127/>
|
||||||
|
- What PIPEDA requires instead of residency is accountability: Schedule 1, clause 4.1.3 — "An organization is responsible for personal information in its possession or custody, including information that has been transferred to a third party for processing. The organization shall use contractual or other means to provide a comparable level of protection while the information is being processed by a third party."
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/P-8.6/FullText.html>
|
||||||
|
- The OPC reopened and then closed this question: on September 23, 2019 the Commissioner concluded the consultation on transfers for processing, confirming that the guidelines for processing personal data across borders "remain unchanged under the current law."
|
||||||
|
*Source:* <https://www.priv.gc.ca/en/opc-news/news-and-announcements/2019/an_190923/>
|
||||||
|
- DATA RESIDENCY — Ontario PHIPA imposes no requirement that personal health information be stored in Ontario or in Canada. The section headed "Place where records kept" (s. 14) is about keeping records in the individual's home or a place other than the practitioner's control, not about jurisdiction. The phrase "outside Canada" does not appear anywhere in the Act.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/04p03>
|
||||||
|
- PHIPA s. 50 ("Disclosure outside Ontario") is a disclosure-permission rule, not a storage rule: it permits a custodian to disclose personal health information collected in Ontario to a person outside Ontario where, among other gateways, the individual consents, the Act permits the disclosure, or the disclosure is reasonably necessary for the provision of health care to the individual.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/04p03>
|
||||||
|
- PHIPA's security duty (s. 12(1)) is a reasonableness standard — "steps that are reasonable in the circumstances" to protect against theft, loss and unauthorized use or disclosure — with no location component.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/04p03>
|
||||||
|
- O. Reg. 329/04 (General) under PHIPA likewise imposes no storage-location requirement. Its only "outside Ontario" provisions permit the Canadian Institute for Health Information and Ontario Health to disclose to persons outside Ontario in defined circumstances.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/regulation/040329>
|
||||||
|
- DATA RESIDENCY — Ontario FIPPA contains no data-localization requirement either. A grep of the full consolidated text for "outside Canada" returned zero matches (grep exit status 1) against 248 occurrences of "personal information" as an instrument check; the only "in Canada" occurrences are law-enforcement disclosure clauses.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-content/statute/90f31>
|
||||||
|
- The closest thing to a Canadian residency rule is a federal internal-administration policy, not a law of general application, and it is not absolute. Treasury Board's Directive on Service and Digital, s. 4.3.24, requires only that Canadian computing facilities "be identified and evaluated as a principal delivery option" for Government of Canada data categorized Protected B, Protected C or classified. It binds federal departments, not private organizations.
|
||||||
|
*Source:* <https://www.tbs-sct.canada.ca/pol/doc-eng.aspx?id=32601>
|
||||||
|
- The cloud direction commonly cited for "data must stay in Canada" (SPIN 2017-01) does not itself set a residency rule: its s. 6.2.2 says only that "Departments are expected to apply the Directive on Service and Digital when implementing safeguards for GC electronic data residency."
|
||||||
|
*Source:* <https://www.canada.ca/en/government/system/digital-government/digital-government-innovations/cloud-services/direction-secure-use-commercial-cloud-services-spin.html>
|
||||||
|
- ARBITRAL INSTITUTIONS — The ADR Institute of Canada's current ADRIC Arbitration Rules (effective 01 March 2025) contain no provision on artificial intelligence, machine learning, or technology disputes. A case-insensitive grep of the full 103,811-character extracted text for "artificial intelligence", "machine learning" and the token "AI" returned no matches. The only data-related clause is a party-responsibility rule for privacy and data security of evidence.
|
||||||
|
*Source:* <https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf>
|
||||||
|
- ADRIC's published Rules & Codes are: ADRIC By-laws, Federation MoU, ADRIC Arbitration Rules, National Mediation Rules, ADRIC Med-Arb Rules, Code of Ethics, Code of Conduct, Conflict of Interest, Complaints & Discipline Policy, Privacy Policy, and an Online Dispute Resolution (ODR) Vision. None is specific to technology, data or AI disputes.
|
||||||
|
*Source:* <https://adric.ca/rules-codes/>
|
||||||
|
- ADRIC's only AI-related publication located is an article, not a rule or guideline: "Artificial Intelligence and Arbitration: A Perfect Fit?", dated March 2, 2023, by Robin Dodokin, Sarah McEachern and Les Honywill. It is commentary about AI's likely role in arbitration, not institutional guidance to arbitrators or parties.
|
||||||
|
*Source:* <https://adric.ca/artificial-intelligence-and-arbitration-a-perfect-fit/>
|
||||||
|
- There IS a Canadian arbitral institution with rules specific to one class of technology dispute: the Canadian International Internet Dispute Resolution Centre (CIIDRC), a division of the Vancouver International Arbitration Centre (VanIAC, formerly BCICAC), which resolves domain-name disputes under the UDRP and CIRA's CDRP and has been a CIRA service provider since 2002.
|
||||||
|
*Source:* <https://ciidrc.org/about-ciidrc/>
|
||||||
|
- CIIDRC publishes its own Supplemental Rules for the UDRP, in effect as of May 9, 2018, which govern communications, complaints and annexes, panelist appointment, fees, word limits and file format for domain-name proceedings.
|
||||||
|
*Source:* <https://ciidrc.org/domain-name-disputes/ciidrc-supplemental-rules/>
|
||||||
|
- The CIRA Domain Name Dispute Resolution Policy, Version 1.3 (August 22, 2011), provides for "resolution by arbitration" of disputes over bad-faith registration of .ca domain names, administered by a dispute resolution service provider approved by CIRA.
|
||||||
|
*Source:* <https://ciidrc.org/domain-name-disputes/cdrp-policy/>
|
||||||
|
- VanIAC's own Rules of Procedure page lists only its Domestic Arbitration Rules (as amended Sept. 1, 2020) and International Commercial Arbitration Rules of Procedure (as amended July 1, 2022 and Jan. 1, 2000), plus mediation and motor-vehicle rules. No AI or technology-dispute rule set or guidance note appears.
|
||||||
|
*Source:* <https://vaniac.org/arbitration/rules-of-procedure/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this does NOT establish
|
||||||
|
|
||||||
|
**Read this section before writing copy.**
|
||||||
|
|
||||||
|
- **Does "the 2026 privacy statute" referred to in docs/03-content-spec.md line 299 exist?**
|
||||||
|
- *Searched:* LEGISinfo bill records for the 44th Parliament 1st Session (all 412 bills) and 45th Parliament 1st Session (all 185 bills), fetched as JSON from parl.ca; the LEGISinfo bill pages for C-27, C-36 and C-8; the Justice Laws consolidation of PIPEDA and the 2026 annual statutes index; the Ontario e-Laws consolidated statute database; and the Legislative Assembly of Ontario's complete bill list for the 44th Parliament 1st Session (139 bills).
|
||||||
|
- *Outcome:* NO SUCH STATUTE EXISTS. Nothing enacted federally or in Ontario in 2025 or 2026 is a privacy statute. The nearest real things are (a) federal Bill C-36, introduced 15 June 2026, which WOULD enact the Protecting Privacy and Consumer Data Act — but it is at second reading with no royal assent; (b) federal Bill C-8 / S.C. 2026, c. 9, a cyber security statute, not privacy; and (c) Ontario O. Regs. 51/26 and 52/26 in force 1 July 2026, which are regulations under a 2024 Act, not a statute. Any public-page copy relying on "the 2026 privacy statute" as market context is asserting something that is not law. If the intent was "pending federal privacy reform", the accurate framing is Bill C-36 (45-1), first reading 15 June 2026, still before the House.
|
||||||
|
- **Did the Consumer Privacy Protection Act or the Artificial Intelligence and Data Act ever come into force in any form?**
|
||||||
|
- *Searched:* LEGISinfo C-27 page and JSON record (royal assent flags, reinstatement flags, session-ongoing flag); a scan of all 185 bills in the 45th Parliament 1st Session for any bill title containing "intellig", "artificial intelligence", "consumer privacy" or "data protection"; House of Commons Procedure and Practice 4th ed. on prorogation and dissolution.
|
||||||
|
- *Outcome:* No. C-27 died without royal assent and was not reinstated; no successor AI or CPPA bill has been introduced in the 45th Parliament. Canada has no federal AI statute as at 2026-08-29.
|
||||||
|
- **Is there any Canadian federal or Ontario legal requirement that personal data be stored in Canada?**
|
||||||
|
- *Searched:* Full-text greps of PIPEDA, Ontario FIPPA, PHIPA and O. Reg. 329/04 for "outside Canada", "within Canada", "stored in Canada" and "localiz"; the OPC's Guidelines for processing personal data across borders and its 2019 consultation conclusion; Treasury Board's SPIN 2017-01 and the Directive on Service and Digital.
|
||||||
|
- *Outcome:* No such requirement was found in any of them, and the OPC states the opposite for PIPEDA. NOT CHECKED, and outside the scope asked: the public-sector residency provisions in British Columbia's FIPPA and Nova Scotia's PIIDPA, which are the usual real source of the belief that "Canadian data must stay in Canada". Do not assert anything about those provinces from this artefact.
|
||||||
|
- **Does any Canadian arbitral institution publish formal guidance (as distinct from rules) on the use of AI in arbitration or mediation?**
|
||||||
|
- *Searched:* ADRIC's page sitemap (175 pages) grepped for ai/artificial/tech/rule/code/guideline/protocol; the ADRIC Rules & Codes index; the full text of the ADRIC Arbitration Rules effective 01 March 2025; VanIAC's Rules of Procedure page and site navigation.
|
||||||
|
- *Outcome:* None found. ADRIC's only AI material located is a 2023 commentary article and a 2026 conference session page ("The AI-Ready Neutral: Practical Essentials for Arbitrators and Mediators"), neither of which is institutional guidance. The conference page itself was NOT fetched — only its URL appeared in the sitemap — so nothing should be claimed about its content.
|
||||||
|
- **Do ICDR Canada or the Canadian Arbitration Association publish technology- or AI-specific rules?**
|
||||||
|
- *Searched:* Keyword web search naming ADRIC, VanIAC, CCAC and ICDR Canada together with AI guidance; their own sites were not individually fetched.
|
||||||
|
- *Outcome:* Not established either way. Neither icdr.org nor the Canadian Arbitration Association's site was retrieved, so no claim can be made about what they do or do not publish.
|
||||||
|
- **Coming-into-force status of the Critical Cyber Systems Protection Act (S.C. 2026, c. 9) — which of its provisions are actually operative.**
|
||||||
|
- *Searched:* The LEGISinfo C-8 page, the royal assent text summary, and the Justice Laws Annual Statutes 2026 c. 9 landing page.
|
||||||
|
- *Outcome:* Royal assent (15 June 2026) is confirmed, but the coming-into-force provisions were not read in full. Do not assert that the Critical Cyber Systems Protection Act is in force; assert only that it was enacted.
|
||||||
|
- **What S.O. 2026, c. 2, Sched. 7 (the most recent FIPPA amendment) actually changes.**
|
||||||
|
- *Searched:* Ontario e-Laws statute record for S.O. 2026, c. 2, identified as the Plan to Protect Ontario Act (Budget Measures), 2026 (Bill 97), assented to April 24, 2026; the schedule's text was not extracted.
|
||||||
|
- *Outcome:* Identified as a budget-measures omnibus amendment to FIPPA; its substance was not read and must not be characterised.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Searches run
|
||||||
|
|
||||||
|
- `WebSearch: Bill C-27 Digital Charter Implementation Act status LEGISinfo died on Order Paper prorogation`
|
||||||
|
- `WebSearch: PIPEDA Personal Information Protection and Electronic Documents Act S.C. 2000 c. 5 justice laws`
|
||||||
|
- `WebFetch: https://www.parl.ca/legisinfo/en/bill/44-1/c-27`
|
||||||
|
- `WebFetch: https://www.parl.ca/legisinfo/en/bill/44-1/c-27/json`
|
||||||
|
- `WebFetch: https://laws-lois.justice.gc.ca/eng/acts/P-8.6/`
|
||||||
|
- `curl: https://laws-lois.justice.gc.ca/eng/acts/P-8.6/FullText.html (then grep for residency terms; grep exit status read directly rather than through a pipe, after an initial `grep ... | head` gave a misleading exit code)`
|
||||||
|
- `curl: https://www.parl.ca/legisinfo/en/bills/json?parlsession=44-1 (412 bill records, keyword scan)`
|
||||||
|
- `curl: https://www.parl.ca/legisinfo/en/bills/json?parlsession=45-1 (185 bill records, keyword scan + 'intellig' scan + full royal-assent list)`
|
||||||
|
- `WebFetch: https://www.parl.ca/legisinfo/en/bill/45-1/c-36`
|
||||||
|
- `WebFetch: https://www.parl.ca/DocumentViewer/en/45-1/bill/C-36/first-reading`
|
||||||
|
- `WebFetch: https://www.parl.ca/legisinfo/en/bill/45-1/c-8`
|
||||||
|
- `WebFetch: https://www.parl.ca/DocumentViewer/en/45-1/bill/C-8/royal-assent`
|
||||||
|
- `WebFetch: https://laws-lois.justice.gc.ca/eng/AnnualStatutes/2026_9/`
|
||||||
|
- `WebSearch + WebFetch: https://www.ourcommons.ca/procedure/procedure-and-practice-4/ch08-7-e.html (prorogation and dissolution)`
|
||||||
|
- `Ontario e-Laws: discovered the JSON API behind the ontario.ca/laws SPA (the HTML pages return only a JS shell to any fetcher, and WebFetch got nothing) by reading /laws/static/js/main.dbd400db.js; base https://www.ontario.ca/laws/api/v2/legislation`
|
||||||
|
- `e-Laws API: /en/currency-date -> "August 26, 2026"`
|
||||||
|
- `e-Laws API: /en/act-content/statute/04p03 (PHIPA) + extraction of ss. 12, 13, 14, 50 and residency grep`
|
||||||
|
- `e-Laws API: /en/act-content/regulation/040329 (O. Reg. 329/04 under PHIPA) + residency grep`
|
||||||
|
- `e-Laws API: /en/act-content/statute/90f31 (FIPPA) + ss. 40, 40.1 + residency grep with exit status read`
|
||||||
|
- `e-Laws API: /en/act-content/statute/90m56 (MFIPPA)`
|
||||||
|
- `e-Laws API: /en/act-content/statute/24e24 (Enhancing Digital Security and Trust Act, 2024) + full text extraction`
|
||||||
|
- `e-Laws API: /en/act-reg/regulation?title=enhancing+digital+security+and+trust+act,+2024 (complete list of regulations made under EDSTA: 2 current, 0 revoked)`
|
||||||
|
- `e-Laws API: /en/act-content/regulation/260051 and /260052 (O. Reg. 51/26 and 52/26) + 'artificial intelligence' count`
|
||||||
|
- `e-Laws API: /en/act-content/statute/s26002 (S.O. 2026, c. 2 = Plan to Protect Ontario Act (Budget Measures), 2026, assented April 24, 2026)`
|
||||||
|
- `e-Laws API: /en/act-content/statute/25k25 and /s25025 (Kids' Online Safety and Privacy Month Act, 2025)`
|
||||||
|
- `WebSearch: Ontario Enhancing Digital Security and Trust Act 2024 in force FIPPA amendments Bill 194`
|
||||||
|
- `WebSearch: 'Enhancing Digital Security and Trust Act' Ontario regulation O. Reg. cyber security 2026 July 1 2026`
|
||||||
|
- `WebFetch: https://www.ola.org/en/legislative-business/bills/parliament-43/session-1/bill-194`
|
||||||
|
- `curl + scrape: https://www.ola.org/en/legislative-business/bills/parliament-44/session-1 (all 139 bills, unpaginated, keyword scan)`
|
||||||
|
- `WebFetch: ola.org bills 61, 66 and 137 (44-1)`
|
||||||
|
- `WebFetch: https://www.priv.gc.ca/en/privacy-topics/airports-and-borders/gl_dab_090127/ + curl to verify the page title and Date modified`
|
||||||
|
- `curl: https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/02_05_d_15/ (Summary of privacy laws in Canada)`
|
||||||
|
- `WebSearch (priv.gc.ca only) + WebFetch: https://www.priv.gc.ca/en/opc-news/news-and-announcements/2019/an_190923/`
|
||||||
|
- `WebSearch (canada.ca only) + curl: SPIN 2017-01 and https://www.tbs-sct.canada.ca/pol/doc-eng.aspx?id=32601 (Directive on Service and Digital, s. 4.3.24)`
|
||||||
|
- `WebSearch: ADRIC artificial intelligence arbitration guideline rules technology disputes`
|
||||||
|
- `curl + pdftotext: https://adric.ca/rules/ADRIC-Arbitration-Rules-2025.pdf (103,811 chars extracted) + AI/technology grep`
|
||||||
|
- `curl: https://adric.ca/sitemap_index.xml and /page-sitemap.xml (175 pages) + ai/artificial/tech/rule/guideline grep`
|
||||||
|
- `curl: https://adric.ca/rules-codes/ and https://adric.ca/artificial-intelligence-and-arbitration-a-perfect-fit/`
|
||||||
|
- `WebSearch: VanIAC Vancouver International Arbitration Centre artificial intelligence guidelines rules 2025 2026`
|
||||||
|
- `curl: https://vaniac.org/arbitration/rules-of-procedure/ and https://vaniac.org/`
|
||||||
|
- `curl: https://ciidrc.org/ , /about-ciidrc/ , /domain-name-disputes/cdrp-policy/ , /domain-name-disputes/ciidrc-supplemental-rules/`
|
||||||
|
- `BLOCKED, recorded so a later reader does not mistake silence for absence: canlii.org returned HTTP 403 to WebFetch; cira.ca returned a Cloudflare HTTP 403 to both WebFetch and curl (the CDRP policy was therefore sourced from CIIDRC, a CIRA-approved provider, not from CIRA itself); adric.ca/rules/ returned HTTP 403 to curl although the rules PDF on the same host returned 200; canada.ca returned 403 to WebFetch but 200 to curl with a browser user-agent.`
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
# Deploy credential — verification output
|
||||||
|
|
||||||
|
**What this is.** The tool output behind `AGENTS.md` §7's *Deploy credential —
|
||||||
|
PROVISIONED* row and the two risk downgrades in `AGENTS.md` §10. It exists because the row
|
||||||
|
was written on evidence that lived only in a terminal, which is the
|
||||||
|
**Q24 / Q32 shape** R14 exists for: a claim whose supporting artefact is
|
||||||
|
unreachable is unverifiable by construction, not merely unverified.
|
||||||
|
`AGENTS.md` **Q52**, ruled 2026-08-28: *"YES — commit the simulate results, the
|
||||||
|
inline policy, and the `NoSuchBucketPolicy` response, access key ID redacted."*
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Subject | IAM user `adr-sml-deploy`, AWS account `327082975128` |
|
||||||
|
| Retrieved | **2026-08-28**, re-run from the repository root |
|
||||||
|
| Calling identity | `arn:aws:iam::327082975128:user/pouya` — the broadly-permissioned personal user, **at an interactive keyboard**. `AGENTS.md` §7 and §10: acceptable interactively, **never** as a CI credential |
|
||||||
|
| Calls | 18 — all **read-only**. No `create`, `put`, `attach`, `delete`, or `update` |
|
||||||
|
| Redaction | The access key ID is replaced with `AKIA…REDACTED` throughout. Nothing else is redacted; `UserId` (`AIDA…`) is retained because it is the durable principal identifier that appears in CloudTrail and grants nothing. The **secret** access key was never requested and cannot be retrieved from the API at all |
|
||||||
|
| Method | `aws-cli/2.34.53`. Every command's exit status was read, and **no `2>/dev/null` anywhere** — see the `NoSuchBucketPolicy` section, where the error *is* the result |
|
||||||
|
|
||||||
|
**Re-run it.** Every command below is copy-pasteable. Nothing here is
|
||||||
|
transcribed from a session; this file was generated from the captured output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The user
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ aws iam get-user --user-name adr-sml-deploy
|
||||||
|
{
|
||||||
|
"User": {
|
||||||
|
"Path": "/",
|
||||||
|
"UserName": "adr-sml-deploy",
|
||||||
|
"UserId": "AIDAUYJ5KD6MHAAC4BOKD",
|
||||||
|
"Arn": "arn:aws:iam::327082975128:user/adr-sml-deploy",
|
||||||
|
"CreateDate": "2026-08-26T15:45:18+00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Created **2026-08-26T15:45:18+00:00**, path `/`, no tags.
|
||||||
|
|
||||||
|
## 2. Nothing attached, no groups, one inline policy
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ aws iam list-attached-user-policies --user-name adr-sml-deploy
|
||||||
|
{
|
||||||
|
"AttachedPolicies": []
|
||||||
|
}
|
||||||
|
|
||||||
|
$ aws iam list-groups-for-user --user-name adr-sml-deploy
|
||||||
|
{
|
||||||
|
"Groups": []
|
||||||
|
}
|
||||||
|
|
||||||
|
$ aws iam list-user-policies --user-name adr-sml-deploy
|
||||||
|
{
|
||||||
|
"PolicyNames": [
|
||||||
|
"adr-sml-deploy-minimal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**This is the part policy-reading alone gets wrong.** An empty
|
||||||
|
`AttachedPolicies` proves nothing on its own — permissions can arrive through a
|
||||||
|
group, or through a resource-based policy that is invisible from the IAM side.
|
||||||
|
Both are checked: `Groups` is empty here, and **§6 below** checks the resource
|
||||||
|
side — §5 is the identity-side simulation and does not reach a bucket policy.
|
||||||
|
|
||||||
|
## 3. The inline policy — `adr-sml-deploy-minimal`
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ aws iam get-user-policy --user-name adr-sml-deploy \
|
||||||
|
--policy-name adr-sml-deploy-minimal
|
||||||
|
{
|
||||||
|
"UserName": "adr-sml-deploy",
|
||||||
|
"PolicyName": "adr-sml-deploy-minimal",
|
||||||
|
"PolicyDocument": {
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "ListSiteBucket",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "s3:ListBucket",
|
||||||
|
"Resource": "arn:aws:s3:::adr-smlcompany-site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sid": "WriteSiteObjects",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:DeleteObject"
|
||||||
|
],
|
||||||
|
"Resource": "arn:aws:s3:::adr-smlcompany-site/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sid": "InvalidateOneDistribution",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "cloudfront:CreateInvalidation",
|
||||||
|
"Resource": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Four actions, one bucket, one distribution. **Never widen it** — `AGENTS.md` §10
|
||||||
|
records why
|
||||||
|
in terms: this is the control standing between a shared Gitea instance and an
|
||||||
|
AWS account that also holds `mlp-clientdb-prod-backups-327082975128`. Treat any
|
||||||
|
request to widen it as a security decision, not a convenience one.
|
||||||
|
|
||||||
|
## 4. The access key — issued, never used
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ aws iam list-access-keys --user-name adr-sml-deploy
|
||||||
|
{
|
||||||
|
"AccessKeyMetadata": [
|
||||||
|
{
|
||||||
|
"UserName": "adr-sml-deploy",
|
||||||
|
"AccessKeyId": "AKIA…REDACTED",
|
||||||
|
"Status": "Active",
|
||||||
|
"CreateDate": "2026-08-26T15:45:19+00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
$ aws iam get-access-key-last-used --access-key-id AKIA…REDACTED
|
||||||
|
{
|
||||||
|
"UserName": "adr-sml-deploy",
|
||||||
|
"AccessKeyLastUsed": {
|
||||||
|
"ServiceName": "N/A",
|
||||||
|
"Region": "N/A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
One key, `Active`, created **2026-08-26T15:45:19+00:00**.
|
||||||
|
|
||||||
|
⚠️ **A correction to `AGENTS.md` §7's wording, found by re-running the call.**
|
||||||
|
That row said
|
||||||
|
*"one active access key, `LastUsed` null"*. The API does not return null — it
|
||||||
|
returns `AccessKeyLastUsed` with **`ServiceName: "N/A"` and `Region: "N/A"`**,
|
||||||
|
and there is no `LastUsedDate` key at all. Same conclusion (the key has never
|
||||||
|
authenticated a request), different field shape, and a reader looking for a
|
||||||
|
literal `null` would not find one.
|
||||||
|
|
||||||
|
**Rotation is due 2026-11-26** — key created 2026-08-26, D3 commits to
|
||||||
|
quarterly. `AGENTS.md` §12 R17 carries the date; `docs/06-deployment.md` §Key rotation
|
||||||
|
carries the procedure. Create the second key, update the Gitea secrets, verify a
|
||||||
|
deploy, **then** delete the first.
|
||||||
|
|
||||||
|
## 5. `simulate-principal-policy` — nine checks, verified by execution
|
||||||
|
|
||||||
|
The point of this section is that it tests the **negative** half. A policy that
|
||||||
|
grants four actions is only a control if it can be shown not to grant the rest.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PRINCIPAL=arn:aws:iam::327082975128:user/adr-sml-deploy
|
||||||
|
sim() { aws iam simulate-principal-policy \
|
||||||
|
--policy-source-arn "$PRINCIPAL" --action-names "$1" --resource-arns "$2" ; }
|
||||||
|
```
|
||||||
|
|
||||||
|
| # | Action | Resource | `EvalDecision` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `s3:PutObject` | `arn:aws:s3:::adr-smlcompany-site/index.html` | **allowed** |
|
||||||
|
| 2 | `s3:DeleteObject` | `arn:aws:s3:::adr-smlcompany-site/old.html` | **allowed** |
|
||||||
|
| 3 | `s3:ListBucket` | `arn:aws:s3:::adr-smlcompany-site` | **allowed** |
|
||||||
|
| 4 | `cloudfront:CreateInvalidation` | `arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA` | **allowed** |
|
||||||
|
| 5 | `s3:ListBucket` | `arn:aws:s3:::mlp-clientdb-prod-backups-327082975128` | **implicitDeny** |
|
||||||
|
| 6 | `s3:GetObject` | `arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything` | **implicitDeny** |
|
||||||
|
| 7 | `s3:DeleteBucket` | `arn:aws:s3:::adr-smlcompany-site` | **implicitDeny** |
|
||||||
|
| 8 | `s3:PutObject` | `arn:aws:s3:::meshkinilaw.ca/anything` | **implicitDeny** |
|
||||||
|
| 9 | `iam:CreateUser` | `*` | **implicitDeny** |
|
||||||
|
|
||||||
|
Four `allowed`, five `implicitDeny`. `implicitDeny` rather than `explicitDeny`
|
||||||
|
is the correct and expected shape: the policy contains no `Deny` statement, so
|
||||||
|
everything outside its three `Allow` statements falls through to IAM's
|
||||||
|
default-deny. An `explicitDeny` here would mean a *different* policy was also in
|
||||||
|
play.
|
||||||
|
|
||||||
|
Raw output for each check, including the full `MatchedStatements` and
|
||||||
|
`MissingContextValues`, follows in **§7 of this file**. *(Bare `§n` here means a
|
||||||
|
section of this document; references to the working file are written
|
||||||
|
`AGENTS.md` §n.)*
|
||||||
|
|
||||||
|
## 6. The resource side — `NoSuchBucketPolicy`, and why the contrast call matters
|
||||||
|
|
||||||
|
IAM simulation covers identity-based policy. It does **not** cover a
|
||||||
|
resource-based grant on the backup bucket, which would be invisible from the
|
||||||
|
principal's side. So:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ aws s3api get-bucket-policy --bucket mlp-clientdb-prod-backups-327082975128
|
||||||
|
# exit status: 254
|
||||||
|
# stdout: 0 bytes
|
||||||
|
# stderr:
|
||||||
|
# aws: [ERROR]: An error occurred (NoSuchBucketPolicy) when calling the GetBucketPolicy operation: The bucket policy does not exist
|
||||||
|
```
|
||||||
|
|
||||||
|
**An error is the result here, and that is only legible because stderr was not
|
||||||
|
suppressed.** `CLAUDE.md`'s convention, from Pouya's own 2026-08-28 correction:
|
||||||
|
`2>/dev/null` converts *"it failed"* into *"it found nothing"*, and those are
|
||||||
|
opposite results. The exit status is **254** and stdout is **empty** — read the
|
||||||
|
status, not just stdout.
|
||||||
|
|
||||||
|
**And an empty result needs a working instrument.** The same command against the
|
||||||
|
site bucket, to prove the call itself resolves a policy when one exists:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ aws s3api get-bucket-policy --bucket adr-smlcompany-site
|
||||||
|
{
|
||||||
|
"Policy": "{\"Version\":\"2008-10-17\",\"Id\":\"PolicyForCloudFrontPrivateContent\",\"Statement\":[{\"Sid\":\"AllowCloudFrontServicePrincipal\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cloudfront.amazonaws.com\"},\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::adr-smlcompany-site/*\",\"Condition\":{\"StringEquals\":{\"AWS:SourceArn\":\"arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA\"}}}]}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit **0**, a real policy returned — the CloudFront OAC grant, scoped to
|
||||||
|
distribution `E1OK7G98KNKUTA`. So `NoSuchBucketPolicy` on the backup bucket is a
|
||||||
|
**genuine absence**, not a command that failed to run. That contrast is the
|
||||||
|
whole reason this call is in the file: without it, an empty result from a broken
|
||||||
|
command looks exactly like an empty result from a bucket with no policy.
|
||||||
|
|
||||||
|
*(This contrast call was **not** part of the original Q22 verification. It was
|
||||||
|
added on re-run, and it closes the gap that made the original evidence weaker
|
||||||
|
than it read.)*
|
||||||
|
|
||||||
|
## 7. Raw `simulate-principal-policy` responses
|
||||||
|
|
||||||
|
### Check 1
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "s3:PutObject",
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/index.html",
|
||||||
|
"EvalDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 6,
|
||||||
|
"Column": 55
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 9,
|
||||||
|
"Column": 57
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/index.html",
|
||||||
|
"EvalResourceDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 6,
|
||||||
|
"Column": 55
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 9,
|
||||||
|
"Column": 57
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 2
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "s3:DeleteObject",
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/old.html",
|
||||||
|
"EvalDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 6,
|
||||||
|
"Column": 55
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 9,
|
||||||
|
"Column": 57
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/old.html",
|
||||||
|
"EvalResourceDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 6,
|
||||||
|
"Column": 55
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 9,
|
||||||
|
"Column": 57
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 3
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "s3:ListBucket",
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||||
|
"EvalDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 3,
|
||||||
|
"Column": 17
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 6,
|
||||||
|
"Column": 55
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||||
|
"EvalResourceDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 3,
|
||||||
|
"Column": 17
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 6,
|
||||||
|
"Column": 55
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 4
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "cloudfront:CreateInvalidation",
|
||||||
|
"EvalResourceName": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA",
|
||||||
|
"EvalDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 9,
|
||||||
|
"Column": 57
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 12,
|
||||||
|
"Column": 83
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA",
|
||||||
|
"EvalResourceDecision": "allowed",
|
||||||
|
"MatchedStatements": [
|
||||||
|
{
|
||||||
|
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||||
|
"SourcePolicyType": "IAM Policy",
|
||||||
|
"StartPosition": {
|
||||||
|
"Line": 9,
|
||||||
|
"Column": 57
|
||||||
|
},
|
||||||
|
"EndPosition": {
|
||||||
|
"Line": 12,
|
||||||
|
"Column": 83
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 5
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "s3:ListBucket",
|
||||||
|
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128",
|
||||||
|
"EvalDecision": "implicitDeny",
|
||||||
|
"MatchedStatements": [],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128",
|
||||||
|
"EvalResourceDecision": "implicitDeny"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 6
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "s3:GetObject",
|
||||||
|
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything",
|
||||||
|
"EvalDecision": "implicitDeny",
|
||||||
|
"MatchedStatements": [],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything",
|
||||||
|
"EvalResourceDecision": "implicitDeny"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 7
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "s3:DeleteBucket",
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||||
|
"EvalDecision": "implicitDeny",
|
||||||
|
"MatchedStatements": [],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||||
|
"EvalResourceDecision": "implicitDeny"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 8
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "s3:PutObject",
|
||||||
|
"EvalResourceName": "arn:aws:s3:::meshkinilaw.ca/anything",
|
||||||
|
"EvalDecision": "implicitDeny",
|
||||||
|
"MatchedStatements": [],
|
||||||
|
"MissingContextValues": [],
|
||||||
|
"EvalDecisionDetails": {},
|
||||||
|
"ResourceSpecificResults": [
|
||||||
|
{
|
||||||
|
"EvalResourceName": "arn:aws:s3:::meshkinilaw.ca/anything",
|
||||||
|
"EvalResourceDecision": "implicitDeny"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check 9
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"EvaluationResults": [
|
||||||
|
{
|
||||||
|
"EvalActionName": "iam:CreateUser",
|
||||||
|
"EvalResourceName": "*",
|
||||||
|
"EvalDecision": "implicitDeny",
|
||||||
|
"MatchedStatements": [],
|
||||||
|
"MissingContextValues": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this does and does not establish
|
||||||
|
|
||||||
|
**Establishes.** The user exists with the creation date `AGENTS.md` §7 records; it holds one
|
||||||
|
inline policy and nothing else; the policy grants four actions on one bucket and
|
||||||
|
one distribution; the four intended actions are `allowed`; five representative
|
||||||
|
out-of-scope actions — including two against the client-database backup bucket —
|
||||||
|
are denied; the backup bucket carries no resource-based policy that could grant
|
||||||
|
around IAM; and one key exists that has never authenticated a request.
|
||||||
|
|
||||||
|
**Does not establish.** That the key works (it has never been used, and testing
|
||||||
|
it would mean putting a real object in the bucket). That Gitea holds it —
|
||||||
|
`AGENTS.md` §7 records **`GITEA ACTIONS SECRETS: UNSET`**, and Q23 is open on whether a runner
|
||||||
|
exists at all. That the policy is sufficient for a deploy in practice: the
|
||||||
|
`s3:AbortMultipartUpload` omission is covered by asset sizes rather than by a
|
||||||
|
lifecycle rule, and `docs/06-deployment.md` records that with a revisit trigger.
|
||||||
|
|
||||||
|
**Not in scope of this file, and deliberately so.** The secret access key. It
|
||||||
|
must never reach the repository — D3, and `AGENTS.md` §10 on the jointly administered Gitea
|
||||||
|
instance, where an instance administrator can generally reach repository secrets
|
||||||
|
or register a runner that receives them.
|
||||||
@@ -49,12 +49,20 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
env:
|
env:
|
||||||
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
||||||
PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
# SUPERSEDED 2026-08-31 — do not copy these two lines. Build step 8
|
||||||
PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
# moved the intake form to the same-origin path /api/intake, after
|
||||||
|
# which nothing in the build read either variable; both were removed
|
||||||
|
# from the live workflow and from scripts/deploy-local.sh. Kept visible
|
||||||
|
# rather than deleted because this whole file is a historical
|
||||||
|
# alternative, and a silent edit to it would make it disagree with the
|
||||||
|
# entry that recorded it.
|
||||||
|
# PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
||||||
|
# PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
||||||
|
|
||||||
# If adopting this: set AWS_DEPLOY_ROLE_ARN as a repository variable. The
|
# If adopting this: set AWS_DEPLOY_ROLE_ARN as a repository variable. The
|
||||||
# rest — AWS_REGION, S3_BUCKET, CLOUDFRONT_DISTRIBUTION_ID, INTAKE_ENDPOINT and
|
# rest — AWS_REGION, S3_BUCKET and CLOUDFRONT_DISTRIBUTION_ID — are recorded
|
||||||
# BOOKING_URL — are recorded in docs/06-deployment.md.
|
# in docs/06-deployment.md. (INTAKE_ENDPOINT and BOOKING_URL were listed here
|
||||||
|
# and are no longer required by either deploy path; see above.)
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
# Who can read `adr-intake-submissions` — verification extract
|
||||||
|
|
||||||
|
**Why this file exists.** `/legal/privacy/` makes a statement to the public about
|
||||||
|
who can see the contents of the intake table. `AGENTS.md` §4 admits no factual
|
||||||
|
claim that cannot be traced, and `CLAUDE.md`'s R14 rule is that anything a spec
|
||||||
|
claims about must be reachable from the repository — *"if the artefact lives only
|
||||||
|
in a console, no reviewer can compare the claim against it and the claim is
|
||||||
|
unverifiable by construction."* Until this file existed, that sentence was the
|
||||||
|
one claim on the site whose subject was entirely outside the repo.
|
||||||
|
|
||||||
|
Raised by `claims-auditor` in the D20 cutover audit, 2026-09-01, finding 8.
|
||||||
|
|
||||||
|
**Provenance.** Figures in the ORIGINAL sections were read from AWS on
|
||||||
|
**2026-09-01**; everything in the **2026-09-02 addendum** at the foot of this file
|
||||||
|
was read on 2026-09-02, and it supersedes the original role screen. Both were run
|
||||||
|
read-only as `arn:aws:iam::327082975128:user/pouya` with the commands listed at
|
||||||
|
the end.
|
||||||
|
No command in this file creates or changes anything. Re-run them rather than
|
||||||
|
trusting this file; it is dated for that reason.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ RULED AND APPLIED — 2026-09-02
|
||||||
|
|
||||||
|
**Pouya ruled `state the truth`, not `remove the access`** (§9 Q62, ruled
|
||||||
|
2026-09-01, applied 2026-09-02). Option 2 below is the one taken; option 1 was
|
||||||
|
declined. `lars`'s membership of `admins` is **unchanged**.
|
||||||
|
|
||||||
|
⚠️ **AND THEN RULED AGAIN THE SAME DAY — §9 Q63, and the second ruling is the
|
||||||
|
one this file most needs to carry, because THIS FILE SUPPLIED THE ERROR.** The
|
||||||
|
first shipped sentence was *"Two people can"*, and it was read straight off the
|
||||||
|
enumeration below. **Pouya's attestation, 2026-09-02:** *"two people is an
|
||||||
|
exaggeration… a handful is accurate — the simulation counts identities, not
|
||||||
|
humans, and the two are not the same claim."*
|
||||||
|
|
||||||
|
**The enumeration is exhaustive and the inference off it was not.** Every read
|
||||||
|
path terminates at `user/pouya` or `user/lars`; how many **people** can reach
|
||||||
|
those two credentials is not something `simulate-principal-policy` can see, so
|
||||||
|
the identity count is a **lower bound on people** and the page published it as an
|
||||||
|
exact count. **No numeric human headcount may ship.** The identity counts in this
|
||||||
|
file are unaffected and stay exactly as measured — this is a correction to what
|
||||||
|
may be *concluded* from them, not to any of them.
|
||||||
|
|
||||||
|
⚠️ **AND RULED A THIRD TIME, LATER THE SAME DAY: THE PAGE STATES WHO, AND THIS
|
||||||
|
FILE HOLDS THE METHOD.** Pouya, 2026-09-02: *"the page stays generic. It
|
||||||
|
over-explains technical mechanics that belong in the evidence file, not in front
|
||||||
|
of an inquirer."* §Who can see it is now **four short statements**. **Deleted from
|
||||||
|
the page:** the measurement paragraph, the root-credential sentence, the single-sign-on and federated-login enumeration, the resource-policy clause, the *"company that runs a database"* aside, the deploy-credential sentence and the three-copies summary. ⚠️ **THE SHARED-ACCOUNT CLAUSE WAS CUT WITH THEM AND THEN RESTORED — to §Where it is stored, where it belongs.** It is a storage disclosure rather than mechanics, the ruling did not name it, and without it no page told a reader their intake sits in an account that also runs unrelated systems (`adversarial-reviewer`, round 1). **These lists must stay identical — there were four of them and they named four different sets.** None of that was retracted and none of it is lost — it
|
||||||
|
is all still below, unchanged, and **that is now this file's job rather than a
|
||||||
|
supporting role.** ⚠️ **THE PAGE NO LONGER CITES THIS FILE'S CONTENT, SO THE
|
||||||
|
COMPARISON BELOW IS THE ONLY THING TYING THE TWO TOGETHER. Keep it in sync, and
|
||||||
|
do not restore a deleted sentence to the page on the strength of finding it
|
||||||
|
here** — the section comment in `src/pages/legal/privacy.astro` carries the same
|
||||||
|
bar.
|
||||||
|
|
||||||
|
**The shipped sentences as at 2026-09-02**, so this file can be compared against
|
||||||
|
the live page rather than against a struck one. All four, in order, complete:
|
||||||
|
|
||||||
|
> The record in the table: me, and the small number of people who administer the
|
||||||
|
> account it sits in with me.
|
||||||
|
|
||||||
|
> The system that receives what you send **can only add a record — it cannot read
|
||||||
|
> back what is stored.**
|
||||||
|
|
||||||
|
> The notification goes to the practice's mailbox, which is read by me and by
|
||||||
|
> administrative staff and is hosted on Google Workspace — so Google holds a copy
|
||||||
|
> of whatever you send me.
|
||||||
|
|
||||||
|
> The confirmation that went to you sits with whoever runs your email. That copy
|
||||||
|
> is in your hands rather than mine.
|
||||||
|
|
||||||
|
**What each rests on, because that mapping is the reason this file exists.**
|
||||||
|
Sentence 1: the enumeration below, plus Pouya's *"a handful"* attestation for the
|
||||||
|
human quantifier — **the measurement gives administrators, the attestation gives
|
||||||
|
the number, and neither gives the other.** Sentence 2: `adr-intake-lambda-role`
|
||||||
|
holds `PutItem` only, implicitDeny on all six read and modify actions.
|
||||||
|
Sentence 3: **an attestation, not a measurement** — `AGENTS.md` §7's
|
||||||
|
`info@smlcompany.ca` row; nothing in this repository or in AWS can check it.
|
||||||
|
Sentence 4: the handler's second `SendEmailCommand`.
|
||||||
|
|
||||||
|
**Three things the page deliberately does NOT say, and each was deleted by
|
||||||
|
ruling rather than being unsupported.** The **root credential** (§7 records it as
|
||||||
|
held by Pouya with no access key and MFA on — ⚠️ *held*, not *held only*, which
|
||||||
|
is why publishing it needed a question and why §9 Q64 is closed as **moot** and
|
||||||
|
not as answered). The **single-sign-on and resource-policy findings**. And the
|
||||||
|
**`33`** — deliberately withheld even while the paragraph stood, because a role
|
||||||
|
total moves when AWS creates a service-linked role by itself.
|
||||||
|
|
||||||
|
**The wording approval Pouya reserved is discharged by the read-through** — his
|
||||||
|
ruling, 2026-09-02: *"do not hold anything open waiting on a separate wording
|
||||||
|
approval; the read-through is the approval."* Q63(a) had approved a version, and
|
||||||
|
the version changed twice after it.
|
||||||
|
|
||||||
|
⚠️ **AND THE ENUMERATION BELOW WAS NOT ENOUGH TO SUPPORT THE COMPLETENESS CLAIM
|
||||||
|
— *"every user and every role in the account was simulated against this table"*,
|
||||||
|
which §7 still asserts and the page no longer carries. See the addendum at the
|
||||||
|
foot of this file**, which is what that claim actually rests on. Read it before
|
||||||
|
citing the five-row table. *(This pointer said "the second of those sentences"
|
||||||
|
until 2026-09-02: it indexed the quote block by POSITION, and the block changed
|
||||||
|
length under it. Name the claim, not its ordinal.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The claim being checked — ⚠️ STRUCK 2026-09-01, QUOTED HERE AS THE DEFECT
|
||||||
|
|
||||||
|
This is **no longer on the page.** It is kept because it is the string
|
||||||
|
`check-claims.mjs`'s `sole-administrator-q62` pattern permanently bars, and a
|
||||||
|
tripwire whose target is not recorded anywhere becomes unmaintainable.
|
||||||
|
|
||||||
|
`src/pages/legal/privacy.astro`, §Who can see it, **as it stood at `bd282aa`**:
|
||||||
|
|
||||||
|
> The table is reachable by the function that writes to it and by one
|
||||||
|
> administrative account, which is mine — nobody else has access to the table.
|
||||||
|
> There is no team, no assistant and no external administrator.
|
||||||
|
|
||||||
|
## The finding: it is false
|
||||||
|
|
||||||
|
The account has an IAM group **`admins`** carrying the AWS managed policy
|
||||||
|
**`AdministratorAccess`**, and it has **two members: `pouya` and `lars`.**
|
||||||
|
|
||||||
|
`iam simulate-principal-policy` for `dynamodb:GetItem`, `dynamodb:Query` and
|
||||||
|
`dynamodb:Scan` against
|
||||||
|
`arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions`, across
|
||||||
|
all five IAM users in the account:
|
||||||
|
|
||||||
|
| principal | GetItem | Query | Scan |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user/pouya` | **allowed** | **allowed** | **allowed** |
|
||||||
|
| `user/lars` | **allowed** | **allowed** | **allowed** |
|
||||||
|
| `user/adr-sml-deploy` | implicitDeny | implicitDeny | implicitDeny |
|
||||||
|
| `user/gitea-deploy-meshkinilaw` | implicitDeny | implicitDeny | implicitDeny |
|
||||||
|
| `user/meshkini-backend-deploy` | implicitDeny | implicitDeny | implicitDeny |
|
||||||
|
|
||||||
|
`lars` holds exactly the access `pouya` holds, by the same route: membership of
|
||||||
|
`admins`. The user's own attachments are only `IAMUserChangePassword`, so the
|
||||||
|
group is the whole of it.
|
||||||
|
|
||||||
|
So the published sentence is wrong on both of its halves — a second account has
|
||||||
|
access, and it belongs to a second administrator of a shared account.
|
||||||
|
|
||||||
|
## The rest of the surface, recorded so the check is complete rather than partial
|
||||||
|
|
||||||
|
- **5 IAM users**: `adr-sml-deploy`, `gitea-deploy-meshkinilaw`, `lars`,
|
||||||
|
`meshkini-backend-deploy`, `pouya`. The three deploy users are all
|
||||||
|
`implicitDeny` above. `adr-sml-deploy`'s scope is S3 + CloudFront and touches
|
||||||
|
no table (`docs/reference/deploy-credential-verification.md`).
|
||||||
|
- **1 IAM group**: `admins` — `AdministratorAccess` and `Billing`, two members.
|
||||||
|
- **33 IAM roles**, 26 of them not service-linked. Two carry
|
||||||
|
`AdministratorAccess`:
|
||||||
|
`cdk-hnb659fds-cfn-exec-role-327082975128-ca-central-1` and
|
||||||
|
`…-us-east-1`. These are **AWS CDK bootstrap CloudFormation execution roles**,
|
||||||
|
assumable by CloudFormation for stack deployment. They are a real path to the
|
||||||
|
table for anyone who can deploy a CDK stack in this account — which is the two
|
||||||
|
administrators above — rather than a third party.
|
||||||
|
- **`adr-intake-lambda-role`** is the writing principal: `dynamodb:PutItem` on
|
||||||
|
this table, `ses:SendEmail`/`SendRawEmail`, plus
|
||||||
|
`AWSLambdaBasicExecutionRole`. **`PutItem` only — it cannot read the table**,
|
||||||
|
which is worth stating because it is a stronger fact than the page currently
|
||||||
|
claims and it is the part of the sentence that is true.
|
||||||
|
- **No resource-based policy on the table.** DynamoDB supports one; this table
|
||||||
|
has none, so access is governed entirely by identity policies.
|
||||||
|
- The account is **not single-project** (`AGENTS.md` §10). `lars` and the two
|
||||||
|
`meshkini*`/`gitea*` users are evidence of that on the IAM surface, not just
|
||||||
|
in the S3 bucket listing §10 describes.
|
||||||
|
|
||||||
|
## What had to happen before `/legal/privacy/` went public — ✅ RESOLVED BY OPTION 2
|
||||||
|
|
||||||
|
Tracked as `AGENTS.md` §9 **Q62**, **closed 2026-09-02 on option 2.** Kept
|
||||||
|
unstruck because the reasoning is what makes the ruling re-readable, and because
|
||||||
|
option 1 remains live in one direction: if that access is ever actually removed,
|
||||||
|
the page and the tripwire both have to change, and `check-claims.mjs`'s `rule:`
|
||||||
|
line carries that instruction.
|
||||||
|
|
||||||
|
1. **Remove the access** — take `lars` out of `admins`, or replace that
|
||||||
|
membership with a policy that denies DynamoDB on this table — and then this
|
||||||
|
sentence becomes true. Note the likely collision: `AGENTS.md` Q23 records the
|
||||||
|
Gitea instance as *jointly administered* and blocked on *"its second
|
||||||
|
administrator"*, so this account is probably not the only thing that access
|
||||||
|
is for.
|
||||||
|
2. **Correct the sentence** to what is true. It is a privacy policy, so the
|
||||||
|
honest version is short and specific — the number of people with
|
||||||
|
administrative access, and that the function that writes cannot read.
|
||||||
|
|
||||||
|
**Do not resolve it by softening.** "Access is limited to authorised
|
||||||
|
administrators" is the shape §4 exists to bar: defensible, uninformative, and it
|
||||||
|
would replace a false specific with a true vacancy on the one page where a reader
|
||||||
|
is entitled to the specific.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Run as `user/pouya`, `ca-central-1`, all read-only. Exit status read on each; no
|
||||||
|
stderr suppressed anywhere.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws iam list-users --query 'Users[].UserName'
|
||||||
|
aws iam list-groups --query 'Groups[].GroupName'
|
||||||
|
aws iam get-group --group-name admins --query 'Users[].UserName'
|
||||||
|
aws iam list-attached-group-policies --group-name admins --query 'AttachedPolicies[].PolicyName'
|
||||||
|
aws iam list-group-policies --group-name admins --query 'PolicyNames'
|
||||||
|
aws iam list-groups-for-user --user-name lars --query 'Groups[].GroupName'
|
||||||
|
aws iam list-attached-user-policies --user-name lars --query 'AttachedPolicies[].PolicyName'
|
||||||
|
aws iam list-user-policies --user-name lars --query 'PolicyNames'
|
||||||
|
|
||||||
|
TARN="arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions"
|
||||||
|
for U in pouya lars adr-sml-deploy gitea-deploy-meshkinilaw meshkini-backend-deploy; do
|
||||||
|
aws iam simulate-principal-policy \
|
||||||
|
--policy-source-arn "arn:aws:iam::327082975128:user/${U}" \
|
||||||
|
--action-names dynamodb:GetItem dynamodb:Query dynamodb:Scan \
|
||||||
|
--resource-arns "$TARN" \
|
||||||
|
--query 'EvaluationResults[].{A:EvalActionName,D:EvalDecision}' --output text
|
||||||
|
done
|
||||||
|
|
||||||
|
# Roles: 33 total, 26 non-service-linked; screened for broad policies.
|
||||||
|
for R in $(aws iam list-roles --query 'Roles[].RoleName' --output text \
|
||||||
|
| tr '\t' '\n' | grep -v '^AWSServiceRole'); do
|
||||||
|
aws iam list-attached-role-policies --role-name "$R" \
|
||||||
|
--query 'AttachedPolicies[].PolicyName' --output text
|
||||||
|
done
|
||||||
|
|
||||||
|
aws iam get-role-policy --role-name adr-intake-lambda-role \
|
||||||
|
--policy-name adr-intake-lambda-inline --query PolicyDocument
|
||||||
|
aws iam list-attached-role-policies --role-name adr-intake-lambda-role
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **`simulate-principal-policy` was the tool that produced a false negative on
|
||||||
|
this project once already** — `AGENTS.md` Q22, where eight checks returned empty
|
||||||
|
because `2>/dev/null` was hiding an `InvalidInput` error caused by a zsh
|
||||||
|
parameter-expansion bug (`$ACCT:user/` parses `:u` as a history modifier). The
|
||||||
|
loop above brace-quotes `${U}` for that reason, prints one line per principal so
|
||||||
|
a silently-skipped iteration is visible as a missing row, and suppresses nothing.
|
||||||
|
**Five rows, or the run did not happen.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ ADDENDUM 2026-09-02 — THE ENUMERATION ABOVE WAS INCOMPLETE IN THREE WAYS, AND ITS CONCLUSION SURVIVES ANYWAY
|
||||||
|
|
||||||
|
**Why this addendum exists.** `/legal/privacy/` published a completeness claim
|
||||||
|
about who can read this table — *"every user and every role in the account was
|
||||||
|
simulated against this table"* — and, written as it stood, this file did not
|
||||||
|
support it. ⚠️ **THAT SENTENCE IS NO LONGER ON THE PAGE**: Pouya ruled later the
|
||||||
|
same day that the section states who and not how, and the measurement paragraph
|
||||||
|
was deleted (see **The shipped sentences** at the top of this file, which is now
|
||||||
|
four statements and does not include it). **This addendum is not thereby
|
||||||
|
obsolete — it is now load-bearing in a different place.** `AGENTS.md` §7's
|
||||||
|
`Intake table — who can read it` row still asserts the completeness, `docs/06`
|
||||||
|
and §12 **R21** still instruct an operator to re-run it before cutover, and the
|
||||||
|
page's first sentence still rests on its conclusion even though it no longer
|
||||||
|
recites the method. **A claim moved off a public page into a register is still a
|
||||||
|
claim.**
|
||||||
|
|
||||||
|
*(This paragraph quoted a different sentence until 2026-09-02 — round 1's
|
||||||
|
**pre-fix** wording, *"every account and role in this infrastructure…"*, which
|
||||||
|
the audit then struck. So this file briefly quoted two different sentences as the
|
||||||
|
live one, in the two halves of the same document, which defeats the comparison
|
||||||
|
R14 exists for. Fixed by pointing at the block above rather than re-quoting it:
|
||||||
|
one copy of a fact, in one place. `adversarial-reviewer`, round 2.)* Under R14 the artefact is what a reviewer compares the
|
||||||
|
claim against, so a claim stronger than its artefact is unverifiable by
|
||||||
|
construction even when it happens to be true.
|
||||||
|
|
||||||
|
**What was missing.**
|
||||||
|
|
||||||
|
1. **`aws iam list-role-policies` was never run.** The command block above lists
|
||||||
|
only `list-attached-role-policies`, which returns *managed* policies. **23 of
|
||||||
|
the 26 non-service-linked roles carry inline policies**, and none of them had
|
||||||
|
been read. The screen for "broad policies" could not have seen an inline grant.
|
||||||
|
2. **No role was ever simulated against the table.** Access was inferred from
|
||||||
|
policy *names* (`AdministratorAccess`) rather than measured as a decision.
|
||||||
|
3. **The five users were simulated for reads only** — `GetItem`, `Query`, `Scan`.
|
||||||
|
So the page's *(then-shipped; deleted by the mechanics ruling of 2026-09-02 and now §7's alone)* *"the credential that publishes this website has no access to the
|
||||||
|
table at all"* covered three read actions and said "at all".
|
||||||
|
|
||||||
|
**What the measurement found, and it changes the role count.** Simulating all 26
|
||||||
|
non-service-linked roles across **seven** actions — `GetItem`, `Query`, `Scan`,
|
||||||
|
`BatchGetItem`, `PutItem`, `UpdateItem`, `DeleteItem`:
|
||||||
|
|
||||||
|
| role | decision on the table | trust |
|
||||||
|
|---|---|---|
|
||||||
|
| `cdk-hnb659fds-cfn-exec-role-…-ca-central-1` | **allowed on all 7** | `cloudformation.amazonaws.com` only |
|
||||||
|
| `cdk-hnb659fds-cfn-exec-role-…-us-east-1` | **allowed on all 7** | `cloudformation.amazonaws.com` only |
|
||||||
|
| `cdk-hnb659fds-lookup-role-…-ca-central-1` | **allowed on the 4 READS**, denied on writes | `arn:aws:iam::327082975128:root` |
|
||||||
|
| `cdk-hnb659fds-lookup-role-…-us-east-1` | **allowed on the 4 READS**, denied on writes | `arn:aws:iam::327082975128:root` |
|
||||||
|
| `adr-intake-lambda-role` | `PutItem` **only**; implicitDeny on the other six | `lambda.amazonaws.com` |
|
||||||
|
| the other 21 | implicitDeny on all 7 | — |
|
||||||
|
|
||||||
|
**So FOUR roles can read the table, not the two this file recorded.** The two
|
||||||
|
`lookup` roles were missed by exactly the gap above: their grant is the inline
|
||||||
|
`LookupRolePolicy`, and `list-attached-role-policies` returns nothing for them.
|
||||||
|
|
||||||
|
**Why the published sentence is nevertheless correct.** The question is not how
|
||||||
|
many roles exist but which *people* they lead back to.
|
||||||
|
|
||||||
|
- The two **cfn-exec** roles trust `cloudformation.amazonaws.com` and nothing
|
||||||
|
else. No human can assume them; they are reachable only by deploying a
|
||||||
|
CloudFormation/CDK stack, which requires a principal who can deploy one.
|
||||||
|
- The two **lookup** roles trust the account root, which delegates the decision
|
||||||
|
to the caller's own identity policy. Simulated for `sts:AssumeRole` against
|
||||||
|
both role ARNs, for all five users, reading `ResourceSpecificResults` (ten
|
||||||
|
per-resource decisions, not five — `EvaluationResults` is one entry per
|
||||||
|
**action**, and an earlier pass here asserted the wrong count):
|
||||||
|
|
||||||
|
| principal | assume `lookup-…-ca-central-1` | assume `lookup-…-us-east-1` |
|
||||||
|
|---|---|---|
|
||||||
|
| `user/pouya` | **allowed** | **allowed** |
|
||||||
|
| `user/lars` | **allowed** | **allowed** |
|
||||||
|
| `user/adr-sml-deploy` | implicitDeny | implicitDeny |
|
||||||
|
| `user/gitea-deploy-meshkinilaw` | implicitDeny | implicitDeny |
|
||||||
|
| `user/meshkini-backend-deploy` | implicitDeny | implicitDeny |
|
||||||
|
|
||||||
|
- The **CloudFormation escalation path this file named and left untested** —
|
||||||
|
*"a real path to the table for anyone who can deploy a CDK stack"* — is now
|
||||||
|
measured. Simulated for `cloudformation:CreateStack`, `UpdateStack`,
|
||||||
|
`CreateChangeSet`, `ExecuteChangeSet`, `iam:PassRole` and `sts:AssumeRole`,
|
||||||
|
five users × six actions = **30 decisions**, count asserted:
|
||||||
|
|
||||||
|
| principal | the CDK / CloudFormation path |
|
||||||
|
|---|---|
|
||||||
|
| `user/pouya` | **allowed** on all six |
|
||||||
|
| `user/lars` | **allowed** on all six |
|
||||||
|
| `user/adr-sml-deploy` | implicitDeny on all six |
|
||||||
|
| `user/gitea-deploy-meshkinilaw` | implicitDeny on all six |
|
||||||
|
| `user/meshkini-backend-deploy` | implicitDeny on all six |
|
||||||
|
|
||||||
|
This is the finding that mattered most, because `meshkini-backend-deploy` is by
|
||||||
|
its name another project's backend-deploy credential and
|
||||||
|
`gitea-deploy-meshkinilaw` is held on a **jointly administered** Gitea instance
|
||||||
|
(Q23). Either one, had it been able to drive CloudFormation, would have read
|
||||||
|
the table without appearing in the five-row table above — and *"Two people
|
||||||
|
can"* would have been wrong. Neither can.
|
||||||
|
|
||||||
|
- **And the seven roles every sweep here had excluded BY CONSTRUCTION are now
|
||||||
|
measured too.** Every role loop in this file filters `grep -v
|
||||||
|
'^AWSServiceRole'`, and the assertion was written as *"twenty-six role rows"* —
|
||||||
|
so a service-linked role was outside the claim rather than inside it, which
|
||||||
|
matters because a service-linked role for a backup or migration service can
|
||||||
|
read table contents. All **7** (`APIGateway`, `CloudFrontLogger`,
|
||||||
|
`InternetMonitor`, `RDS`, `ResourceExplorer`, `Support`, `TrustedAdvisor`) are
|
||||||
|
**implicitDeny on all seven actions** — 49 decisions, count asserted
|
||||||
|
`[verified 2026-09-02]`. **So the enumeration is 33 of 33 roles, not 26 of
|
||||||
|
33**, and the page's *"every user and every role in the account"* is now
|
||||||
|
literally true. Raised by `adversarial-reviewer`, round 2.
|
||||||
|
|
||||||
|
- **There is no federated identity surface at all**: `list-saml-providers` **0**,
|
||||||
|
`list-open-id-connect-providers` **0**, `sso-admin list-instances` **0**
|
||||||
|
`[verified 2026-09-02]`. So "every user and every role" is not leaving out a
|
||||||
|
federated principal, because there is none to leave out.
|
||||||
|
|
||||||
|
- ⚠️ **AND THE `sso-admin` ZERO NEEDED A SECOND COMMAND TO MEAN ANYTHING, added
|
||||||
|
2026-09-02 (round 2).** `list-instances` answers about the account it is called
|
||||||
|
in, so a **member of an AWS Organization returns 0 while Identity Center runs in
|
||||||
|
the management account** — the zero would have been true and the conclusion
|
||||||
|
false. `aws organizations describe-organization` returns
|
||||||
|
**`AWSOrganizationsNotInUseException`: "Your account is not a member of an
|
||||||
|
organization"** `[verified 2026-09-02]`, so there is no management account above
|
||||||
|
this one and the zero is conclusive. Command 8.
|
||||||
|
|
||||||
|
- **The table carries NO RESOURCE-BASED POLICY OF ITS OWN**, and this is now a
|
||||||
|
command rather than an assertion. `aws dynamodb get-resource-policy` returns
|
||||||
|
**`PolicyNotFoundException`** `[verified 2026-09-02]` — exit **254**, the error
|
||||||
|
on stderr being the result. ⚠️ **A DynamoDB resource policy is invisible to
|
||||||
|
`describe-table`**, so no earlier command in this file could have seen one, and
|
||||||
|
`/legal/privacy/` publishes the claim (*"the table carries no policy of its own
|
||||||
|
granting access to anyone"*). It is the identity-policy enumeration's blind
|
||||||
|
spot: every simulation here asks what a **principal** may do, and a resource
|
||||||
|
policy grants from the other side. Found unbacked by `adversarial-reviewer`
|
||||||
|
round 2, on §10's own precedent — the client-backup bucket, where
|
||||||
|
`get-bucket-policy` returning `NoSuchBucketPolicy` was recorded because *"a
|
||||||
|
policy read alone could not have established the second half."* Command 7.
|
||||||
|
|
||||||
|
Every read path therefore terminates at `pouya` or `lars`. ⚠️ **AND THAT IS
|
||||||
|
WHERE THIS FILE WENT WRONG, SO THE CORRECTION SITS AT THE SENTENCE THAT CAUSED
|
||||||
|
IT.** This read *"The count of people is two, and it is now the result of an
|
||||||
|
enumeration rather than of a policy name"* until 2026-09-02, and `/legal/privacy/`
|
||||||
|
published that count. **It is a count of IDENTITIES.** Two credentials is a lower
|
||||||
|
bound on the number of people who can use them, and Pouya's attestation is that
|
||||||
|
*"a handful"* is the true figure (§9 Q63). The enumeration stands exactly as
|
||||||
|
measured; **an enumeration of principals is not a census.**
|
||||||
|
|
||||||
|
**The account root user, recorded because an enumeration that quietly omits it is
|
||||||
|
not an enumeration.** Root is not an IAM user and does not appear in
|
||||||
|
`list-users`, so it cannot be simulated and no policy constrains it — root can
|
||||||
|
always read the table. Two facts bound it: `get-account-summary` reports
|
||||||
|
`AccountAccessKeysPresent: 0`, so **there is no programmatic root credential**,
|
||||||
|
and `AccountMFAEnabled: 1`. Root access therefore requires the root password and
|
||||||
|
its MFA device.
|
||||||
|
|
||||||
|
⚠️ **THE PAGE SAYS NOTHING ABOUT ROOT, AND THIS PASSAGE HAS NOW BEEN THE REASON
|
||||||
|
FOR THAT TWICE ON OPPOSITE GROUNDS.** It first read *"The page does not mention
|
||||||
|
root and should not"*, reasoned from its own last clause — *"who holds the root
|
||||||
|
credentials is not established in this repository."* **Pouya then established it
|
||||||
|
(§9 Q63(c), 2026-09-02): he holds it** `[verified 2026-09-02 — Pouya]`, the page
|
||||||
|
published *"has no programmatic key, and I hold it"*, and the gap that opened
|
||||||
|
immediately was that *held by* is not *held only by* — a reader takes the
|
||||||
|
possessive as sole custody, which nothing measured or attested supports (§9
|
||||||
|
**Q64**). **His second ruling that day deleted the sentence** along with the rest
|
||||||
|
of the mechanics, so **Q64 is closed as MOOT rather than answered and the
|
||||||
|
underlying fact is exactly as unestablished as it was.**
|
||||||
|
|
||||||
|
**The consequence to carry, because it is not "nothing happened":** root custody
|
||||||
|
is now recorded in `AGENTS.md` §7 and nowhere public. ⚠️ **Nothing about it may
|
||||||
|
be published without asking him again**, and the question to ask is not *who
|
||||||
|
holds root* — that is answered — but *whether anyone else does*. The original
|
||||||
|
reasoning still holds and is why the page's first sentence is scoped as it is: an
|
||||||
|
account owner's own credential is inherent to every cloud account and is not a
|
||||||
|
third party who has been *granted* access, which is why the measured claim was
|
||||||
|
always scoped to *"every user and every role"* rather than to a bare "nobody else
|
||||||
|
can".
|
||||||
|
|
||||||
|
**Two claims are supported that were not before. One of them still ships; the
|
||||||
|
other was deleted from the page by ruling on 2026-09-02 and is kept here because
|
||||||
|
it remains true and remains §7's.**
|
||||||
|
|
||||||
|
- **SHIPS** — *"The system that receives what you send can only add a record — it
|
||||||
|
cannot read back what is stored"* — `adr-intake-lambda-role` returns `allowed`
|
||||||
|
for `PutItem` and `implicitDeny` for `GetItem`, `Query`, `Scan`,
|
||||||
|
`BatchGetItem`, `UpdateItem` and `DeleteItem`. Previously this rested on
|
||||||
|
reading the policy document; it is now the simulator's decision.
|
||||||
|
- **NO LONGER ON THE PAGE** — *"the credential that publishes this website has no
|
||||||
|
access to the table at all"* — `adr-sml-deploy` is `implicitDeny` on all
|
||||||
|
**seven**, so "at all" covers writes and deletes as well as reads. It went with
|
||||||
|
the mechanics cut, not because anything about it changed.
|
||||||
|
|
||||||
|
**And it corroborates §10 from the IAM surface.** Of the 26 non-service-linked
|
||||||
|
roles, **9 belong to CDK bootstrap** and **14 to four unrelated production
|
||||||
|
systems** in the same account. *(`/legal/privacy/` tells a reader this in as many words —
|
||||||
|
*"The table sits in an Amazon Web Services account that also runs systems
|
||||||
|
unrelated to this practice"* — in **§Where it is stored**, which is where the
|
||||||
|
sentence now lives: the mechanics cut removed it and it was restored there, as a
|
||||||
|
storage disclosure rather than a method. §10 is unaffected either way; it never
|
||||||
|
depended on the page saying so.)* *(Their role names were listed here until 2026-09-02 and
|
||||||
|
are not any more: this is a committed file, they are another project's IAM
|
||||||
|
surface, and the count carries the whole of the argument. `adversarial-reviewer`,
|
||||||
|
round 2.)*
|
||||||
|
|
||||||
|
⚠️ **THE INSTRUMENT FAILED FIRST, UNIFORMLY, AND IN THE DIRECTION THAT READS AS
|
||||||
|
CLEAN.** The role sweep was first run as `--action-names $ACTS` with the seven
|
||||||
|
actions in a shell variable. **zsh does not word-split parameter expansions**, so
|
||||||
|
`simulate-principal-policy` received **one** action name — the whole string — and
|
||||||
|
answered it: `implicitDeny` for 22 roles, and `allowed` for the four with a `*`
|
||||||
|
grant, because `*` matches a bogus action too. Twenty-two clean rows and a
|
||||||
|
plausible four. The tell was the shape of the output, not the verdict: one
|
||||||
|
decision per role where there should have been seven. **The fix is the assertion,
|
||||||
|
not the memory** — the loop now counts `EvaluationResults` per call and refuses a
|
||||||
|
row that does not carry exactly seven, and the users' assume check counts
|
||||||
|
`ResourceSpecificResults` and refuses a row that does not carry exactly two.
|
||||||
|
`CLAUDE.md` records this class five times over; this is the sixth, and it is the
|
||||||
|
"uniformly good" half.
|
||||||
|
|
||||||
|
### Commands — the ones this addendum rests on
|
||||||
|
|
||||||
|
Read-only, run as `user/pouya` in `ca-central-1`. No stderr suppressed, exit
|
||||||
|
status read on every call, and note the **literal** action lists: they are not in
|
||||||
|
a variable, which is the whole point above.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Inline policies — the command the original block never ran.
|
||||||
|
# ⚠️ THE `grep -v` IS THE ORIGINAL DEFECT AND IS KEPT ONLY TO SHOW IT. It
|
||||||
|
# excluded the 7 service-linked roles from a claim written as "every role", and a
|
||||||
|
# service-linked role for a backup or migration service CAN read table contents.
|
||||||
|
# For a full run, DELETE the grep -v — all 33 must be screened, which is what the
|
||||||
|
# 2026-09-02 addendum measured and what the page's "every user and every role"
|
||||||
|
# now rests on.
|
||||||
|
aws iam list-roles --query 'Roles[].RoleName' --output text \
|
||||||
|
| tr '\t' '\n' \
|
||||||
|
| while IFS= read -r R; do
|
||||||
|
aws iam list-role-policies --role-name "$R" --query 'PolicyNames' --output text
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. Every non-service-linked role, seven actions, decision asserted per row.
|
||||||
|
# The count check is what makes a broken call loud instead of clean.
|
||||||
|
aws iam simulate-principal-policy \
|
||||||
|
--policy-source-arn "arn:aws:iam::327082975128:role/<ROLE>" \
|
||||||
|
--action-names dynamodb:GetItem dynamodb:Query dynamodb:Scan \
|
||||||
|
dynamodb:BatchGetItem dynamodb:PutItem dynamodb:UpdateItem \
|
||||||
|
dynamodb:DeleteItem \
|
||||||
|
--resource-arns "arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions" \
|
||||||
|
--query 'length(EvaluationResults)' --output text # must print 7
|
||||||
|
|
||||||
|
# 3. Trust policies of the four roles that can read.
|
||||||
|
aws iam get-role --role-name <ROLE> --query 'Role.AssumeRolePolicyDocument'
|
||||||
|
|
||||||
|
# 4. Who can assume the two lookup roles — per RESOURCE, not per action.
|
||||||
|
aws iam simulate-principal-policy \
|
||||||
|
--policy-source-arn "arn:aws:iam::327082975128:user/<USER>" \
|
||||||
|
--action-names sts:AssumeRole \
|
||||||
|
--resource-arns "arn:aws:iam::327082975128:role/cdk-hnb659fds-lookup-role-327082975128-ca-central-1" \
|
||||||
|
"arn:aws:iam::327082975128:role/cdk-hnb659fds-lookup-role-327082975128-us-east-1" \
|
||||||
|
--query 'EvaluationResults[].ResourceSpecificResults[].{R:EvalResourceName,D:EvalResourceDecision}' \
|
||||||
|
--output text # must print 2 rows
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 5. The CloudFormation / CDK escalation path, per user. Six actions, literal.
|
||||||
|
aws iam simulate-principal-policy \
|
||||||
|
--policy-source-arn "arn:aws:iam::327082975128:user/<USER>" \
|
||||||
|
--action-names cloudformation:CreateStack cloudformation:UpdateStack \
|
||||||
|
cloudformation:CreateChangeSet cloudformation:ExecuteChangeSet \
|
||||||
|
iam:PassRole sts:AssumeRole \
|
||||||
|
--query 'EvaluationResults[].{A:EvalActionName,D:EvalDecision}' --output text
|
||||||
|
|
||||||
|
# 6. Federated identity surfaces, and root.
|
||||||
|
aws iam list-saml-providers --query 'length(SAMLProviderList)'
|
||||||
|
aws iam list-open-id-connect-providers --query 'length(OpenIDConnectProviderList)'
|
||||||
|
aws sso-admin list-instances --query 'length(Instances)'
|
||||||
|
aws iam get-account-summary \
|
||||||
|
--query 'SummaryMap.{AccessKeysPresentRoot:AccountAccessKeysPresent,MFA:AccountMFAEnabled}'
|
||||||
|
|
||||||
|
# 7. The table's OWN policy. ⚠️ NOT VISIBLE IN describe-table — a DynamoDB
|
||||||
|
# resource policy needs its own call, and the page publishes a claim about it.
|
||||||
|
# A "no policy" answer arrives as a NON-ZERO EXIT with PolicyNotFoundException
|
||||||
|
# on stderr, so do not suppress stderr and do not read exit 0 as the result.
|
||||||
|
aws dynamodb get-resource-policy \
|
||||||
|
--resource-arn 'arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions'
|
||||||
|
|
||||||
|
# 8. Is the account in an AWS Organization? ⚠️ THIS IS WHAT MAKES COMMAND 6's
|
||||||
|
# sso-admin ZERO CONCLUSIVE. `list-instances` answers about THIS account, so a
|
||||||
|
# member account returns 0 while Identity Center runs in the management
|
||||||
|
# account. Not-in-an-org means there is no such management account.
|
||||||
|
aws organizations describe-organization
|
||||||
|
```
|
||||||
|
|
||||||
|
**Five user rows and THIRTY-THREE role rows, or the run did not happen.** ⚠️
|
||||||
|
**This said "twenty-six" until 2026-09-02, which made an INCOMPLETE sweep pass
|
||||||
|
its own acceptance test** — the number matched command 1's `grep -v
|
||||||
|
'^AWSServiceRole'`, so an operator following §12 R21's instruction to re-run this
|
||||||
|
file would have reproduced the exclusion and got the pass line for it. The page's
|
||||||
|
*"every user and every role"* rests on 33 of 33. `adversarial-reviewer`, round 2.
|
||||||
|
And for every simulation: **seven decisions per role call, six per CDK-path call,
|
||||||
|
two per-resource decisions per assume call** — the counts are the assertion,
|
||||||
|
because a call that silently received one bogus action name answers
|
||||||
|
`implicitDeny` and reads exactly like a clean row. **Commands 7 and 8 are read by
|
||||||
|
their ERROR, not their output**: `PolicyNotFoundException` and
|
||||||
|
`AWSOrganizationsNotInUseException` are each the clean result, arriving on stderr
|
||||||
|
with a non-zero exit.
|
||||||
@@ -106,7 +106,15 @@ the dispute."*
|
|||||||
|
|
||||||
**Consequence:** the neutral in the LAT's pre-hearing step is a **Member /
|
**Consequence:** the neutral in the LAT's pre-hearing step is a **Member /
|
||||||
adjudicator of the Tribunal**. It is directed by the Tribunal, attendance is
|
adjudicator of the Tribunal**. It is directed by the Tribunal, attendance is
|
||||||
mandatory, and the Member is disqualified from the subsequent hearing panel. A
|
mandatory, and the Member does not sit on the subsequent hearing panel **except
|
||||||
|
with the consent of the parties** (Rule 14.3, quoted verbatim above). ⚠️ **This
|
||||||
|
line read "the Member is disqualified from the subsequent hearing panel" until
|
||||||
|
2026-09-01** — an absolute, thirty lines below the quotation that qualifies it,
|
||||||
|
in this repository's own voice rather than the Tribunal's. `/practice/insurance/`
|
||||||
|
took the absolute from here and published it. Corrected in both places on the
|
||||||
|
same day; the page was corrected first and this file is where the defect would
|
||||||
|
otherwise have re-seeded, which is `CLAUDE.md`'s point about commentary around a
|
||||||
|
quotation being this repository speaking. A
|
||||||
privately retained neutral is not appointed to it and cannot be.
|
privately retained neutral is not appointed to it and cannot be.
|
||||||
|
|
||||||
## Finding 2 — the LAT Rules never use the words "mediation", "mediator" or "arbitration"
|
## Finding 2 — the LAT Rules never use the words "mediation", "mediator" or "arbitration"
|
||||||
@@ -135,9 +143,36 @@ and it is the affirmative basis for the offering rather than a problem for it:
|
|||||||
> at all times, including before filing at the LAT‑AABS, and continuing
|
> at all times, including before filing at the LAT‑AABS, and continuing
|
||||||
> negotiation discussions after a claim has been filed.
|
> negotiation discussions after a claim has been filed.
|
||||||
|
|
||||||
The Tribunal itself points parties at private mediation, **before filing and
|
> ⚠️ **CORRECTED 2026-08-29. THE GLOSS THAT STOOD HERE WAS WRONG, AND IT WAS
|
||||||
continuing after filing.** That is exactly the space a privately retained
|
> WRONG ABOUT THE QUOTATION THREE LINES ABOVE IT.** It read: *"The Tribunal
|
||||||
mediator occupies, and it is the Tribunal's own words for it.
|
> itself points parties at private mediation, **before filing and continuing
|
||||||
|
> after filing.** That is exactly the space a privately retained mediator
|
||||||
|
> occupies, and it is the Tribunal's own words for it."*
|
||||||
|
>
|
||||||
|
> **Read the passage again.** Sentence 1 names **mediation**, and names it for
|
||||||
|
> one moment only — *"Before you apply."* Sentence 2 is about **negotiation**:
|
||||||
|
> *"Parties are encouraged to attempt to **negotiate** the claim at all times,
|
||||||
|
> including before filing at the LAT-AABS, and continuing **negotiation**
|
||||||
|
> discussions after a claim has been filed."* The word appears twice. **The
|
||||||
|
> "continuing after filing" frame belongs to negotiation, and the gloss carried
|
||||||
|
> it across onto mediation.** The word **"private"** is not the Tribunal's
|
||||||
|
> either — the sentence names no provider and draws no public/private
|
||||||
|
> distinction.
|
||||||
|
>
|
||||||
|
> **What the passage does support:** the Tribunal names mediation as something a
|
||||||
|
> party may consider **before applying**, and encourages negotiated settlement
|
||||||
|
> at every stage. That is an affirmative basis for the offering. It is not a
|
||||||
|
> Tribunal statement about mediating a claim that is already filed.
|
||||||
|
>
|
||||||
|
> **How this got past three checks.** The quotation was fetched, pasted
|
||||||
|
> verbatim, and is correct. The overreach is in the sentence *underneath* it —
|
||||||
|
> and that sentence, not the quote, is what propagated into `docs/01`, into
|
||||||
|
> `src/data/site.ts`, and from there into published copy on three pages. Two
|
||||||
|
> review rounds and a claims audit read this file and did not catch it, because
|
||||||
|
> the quote sat right there and appeared to say it. Caught 2026-08-29 by an
|
||||||
|
> independent re-fetch that read the sentence structure rather than the gloss.
|
||||||
|
> **Same shape as Q39's struck universal: the source was fine and the sentence
|
||||||
|
> drawn from it was wider than the source.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -149,8 +184,14 @@ mediator occupies, and it is the Tribunal's own words for it.
|
|||||||
Tribunal Member**. `LAT pre-hearing mediation` therefore describes a thing
|
Tribunal Member**. `LAT pre-hearing mediation` therefore describes a thing
|
||||||
that does not exist, and the half a reader would recognise — *pre-hearing* —
|
that does not exist, and the half a reader would recognise — *pre-hearing* —
|
||||||
is the Tribunal's own label for a step nobody outside the Tribunal conducts.
|
is the Tribunal's own label for a step nobody outside the Tribunal conducts.
|
||||||
2. Private mediation of accident-benefits and SABS disputes is **compatible with
|
2. The Tribunal names **mediation** as an option a party may consider **before
|
||||||
a LAT application, before filing or after**, and the Tribunal says so.
|
applying**, and encourages negotiated settlement at all stages including
|
||||||
|
after filing. *(Corrected 2026-08-29. This item read: "Private mediation of
|
||||||
|
accident-benefits and SABS disputes is **compatible with a LAT application,
|
||||||
|
before filing or after**, and the Tribunal says so." The last five words were
|
||||||
|
false — see the correction box above. Whether a filed claim can also be
|
||||||
|
privately mediated is not something this source addresses either way, and
|
||||||
|
nothing here should be cited for it.)*
|
||||||
|
|
||||||
**Does not establish:**
|
**Does not establish:**
|
||||||
|
|
||||||
@@ -170,7 +211,15 @@ proceeding appears to appoint or host the mediator.
|
|||||||
**Published instead** — `src/data/site.ts`, `PRACTICE_AREAS` → `insurance`:
|
**Published instead** — `src/data/site.ts`, `PRACTICE_AREAS` → `insurance`:
|
||||||
|
|
||||||
> Accident benefits and SABS entitlement, MIG disputes, and private mediation
|
> Accident benefits and SABS entitlement, MIG disputes, and private mediation
|
||||||
> alongside a LAT application, before filing or after.
|
> retained by the parties, not the Tribunal's case conference.
|
||||||
|
|
||||||
|
*(Corrected 2026-08-29. The blurb read "…and private mediation alongside a LAT
|
||||||
|
application, before filing or after", and the "before filing or after" half
|
||||||
|
rested entirely on the gloss corrected above. The replacement carries the
|
||||||
|
distinction `docs/01` actually requires — **private, retained by the parties,
|
||||||
|
not the Tribunal's case conference** — which is the high-value half and is fully
|
||||||
|
supported. The Tribunal's own sentence about mediation before applying is quoted
|
||||||
|
on the page rather than compressed into a card.)*
|
||||||
|
|
||||||
`docs/01` keeps `LAT pre-hearing mediation` as a **search intent** — people do
|
`docs/01` keeps `LAT pre-hearing mediation` as a **search intent** — people do
|
||||||
type it — with a note that it must never be lifted into copy. That lift is
|
type it — with a note that it must never be lifted into copy. That lift is
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
# Ontario Construction Act — adjudication, prompt payment, liens; and the two nuclear programmes named in docs/01
|
||||||
|
|
||||||
|
Committed under AGENTS.md R14 and the CLAUDE.md rule it encodes: **anything a
|
||||||
|
spec makes a claim about must be reachable from the repository.** Every fact
|
||||||
|
the six `/practice/*` pages state about the world is checkable here or it is
|
||||||
|
not published.
|
||||||
|
|
||||||
|
**Retrieved 2026-08-29.** Fetched from the primary sources listed below and
|
||||||
|
extracted with quotations pasted verbatim. This file is the artefact; the pages
|
||||||
|
cite it. Do not paraphrase a fact into a page that is not stated here.
|
||||||
|
|
||||||
|
> ⚠️ **A statute, a regulation and a tribunal page all move.** Every consolidation
|
||||||
|
> date and effective date below is as at the retrieval date and nothing more.
|
||||||
|
> Re-check before cutover, and before any republish that turns on one of them.
|
||||||
|
|
||||||
|
> ### R18 re-check — cutover pass, 2026-09-01
|
||||||
|
>
|
||||||
|
> ⚠️ **THIS FILE CARRIES THE STANDING RE-CHECK INSTRUCTION ABOVE AND WAS
|
||||||
|
> NOT ONE OF R18's SEVEN LIMBS, AND IT WENT UNSTAMPED IN THE FIRST PASS.**
|
||||||
|
> Five of seven extracts were stamped and two were not, so a reader could
|
||||||
|
> not tell whether this one was considered and found non-volatile or simply
|
||||||
|
> missed. Found by `adversarial-reviewer`, 2026-09-02 — the same
|
||||||
|
> control-fires-over-part-of-its-scope defect as R18 having no checklist
|
||||||
|
> item, one notch smaller. **Nothing here was re-retrieved.**
|
||||||
|
>
|
||||||
|
> - **No R18 limb, but ONE CANDIDATE LIMB, and it is flagged rather than
|
||||||
|
> silently adopted.** `/practice/construction/` publishes: *"Ontario Power
|
||||||
|
> Generation … applied in March 2026 for a licence to operate it."* That is a
|
||||||
|
> **pending application**, so it moves the way limb (a) moves. It is not
|
||||||
|
> false today — the application was made, and a completed past act stays
|
||||||
|
> true — which is why this is a note and not a blocker. But a reader takes it
|
||||||
|
> as current status, and unlike limb (a) the sentence is **not time-anchored**
|
||||||
|
> ("when this page was written"). **For Pouya at the next re-check: adopt it
|
||||||
|
> as R18 limb (h), or time-anchor the sentence and drop it.**
|
||||||
|
> - The rest is stable: Part II.1 in force since 2019-10-01, and ODACC's own
|
||||||
|
> statement that it is the Authorized Nominating Authority. The ODACC 2025
|
||||||
|
> Annual Report is cited for institutional facts rather than for figures, so
|
||||||
|
> it does not carry limb (d)'s exposure `[verified 2026-09-02 — swept
|
||||||
|
> `dist/practice/construction/index.html`]`.
|
||||||
|
|
||||||
|
**Topic as researched:** Ontario construction dispute resolution — the statutory machinery (Construction Act, R.S.O. 1990, c. C.30); plus verification of the OPG Darlington New Nuclear Project and Bruce Power's Bruce C Project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
| Kind | Source | URL |
|
||||||
|
|---|---|---|
|
||||||
|
| statute | Construction Act, R.S.O. 1990, c. C.30 — Ontario e-Laws (current consolidation). NOTE ON RETRIEVAL: the e-Laws page is a JavaScript single-page app; a plain fetch returns an empty shell. The statute text quoted here was retrieved from the JSON API that backs that page: https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30 (HTTP 200, 374,864 bytes). | <https://www.ontario.ca/laws/statute/90c30> |
|
||||||
|
| statute | Ontario e-Laws — version list for R.S.O. 1990, c. C.30 (JSON API backing the 'Versions' tab of https://www.ontario.ca/laws/statute/90c30). Shows the title carried by each historical consolidation of the same chapter. | <https://www.ontario.ca/laws/api/v2/legislation/en/act-versions/statute/90c30> |
|
||||||
|
| statute | Construction Lien Act, R.S.O. 1990, c. C.30 — historical e-Laws consolidation, version 8 (period 14 December 2017 to 30 June 2018). Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30/v8 | <https://www.ontario.ca/laws/statute/90c30/v8> |
|
||||||
|
| statute | Construction Act, R.S.O. 1990, c. C.30 — historical e-Laws consolidation, version 9 (period beginning 1 July 2018). Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30/v9 | <https://www.ontario.ca/laws/statute/90c30/v9> |
|
||||||
|
| statute | Construction Lien Amendment Act, 2017, S.O. 2017, c. 24 - Bill 142 — Ontario e-Laws. Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/S17024 | <https://www.ontario.ca/laws/statute/S17024> |
|
||||||
|
| regulation | O. Reg. 264/25 — ADJUDICATIONS UNDER PART II.1 OF THE ACT (made under the Construction Act). Ontario e-Laws; consolidation period from January 1, 2026; 'No amendments.' Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/regulation/250264 | <https://www.ontario.ca/laws/regulation/250264> |
|
||||||
|
| institution | Ontario Dispute Adjudication for Construction Contracts (ODACC) — home page | <https://odacc.ca/en/> |
|
||||||
|
| institution | ODACC — About Us | <https://odacc.ca/en/aboutus/> |
|
||||||
|
| institution | ODACC — Adjudication Process | <https://odacc.ca/en/adjudication-process/> |
|
||||||
|
| institution | ODACC 2025 Annual Report (PDF, 37 pages) — the annual report ODACC is required to publish as Authorized Nominating Authority | <https://odacc.ca/wp-content/uploads/2021/07/2025-ODACC-Annual-Report-Final.pdf> |
|
||||||
|
| proponent | Small modular reactors / Darlington SMR – Ontario Power Generation (proponent's own page). NOTE ON RETRIEVAL: opg.com returned HTTP 403 (Cloudflare bot block) to both direct curl and WebFetch on 2026-08-29; the page text quoted here was obtained through the r.jina.ai HTML-to-text reader proxy (https://r.jina.ai/https://www.opg.com/projects-services/projects/nuclear/smr/darlington-smr/, HTTP 200). Key facts are independently corroborated by the CNSC page listed below. A human should re-read the OPG page in a browser before any of this wording is published. | <https://www.opg.com/projects-services/projects/nuclear/smr/darlington-smr/> |
|
||||||
|
| regulator | Darlington New Nuclear Project — Canadian Nuclear Safety Commission (federal nuclear regulator) | <https://www.cnsc-ccsn.gc.ca/eng/reactors/new-reactor-power-plant-projects/new-reactor-power-plant-facilities/darlington-new-nuclear-project/> |
|
||||||
|
| proponent | The Bruce C Project — Bruce Power (proponent's own page) | <https://www.brucepower.com/the-bruce-c-project/> |
|
||||||
|
| proponent | Bruce C Project — Engage Bruce Power (Bruce Power's own engagement platform). NOTE: WebFetch returned 403; retrieved with curl sending a browser User-Agent (HTTP 200, 62,728 bytes). | <https://engage.brucepower.com/brucec> |
|
||||||
|
| proponent | Planning phase of Integrated Impact Assessment completed for Bruce Power's potential Bruce C project — Bruce Power newsroom, dated 2025-08-21 (proponent's own release) | <https://www.brucepower.com/2025/08/21/planning-phase-of-integrated-impact-assessment-completed-for-bruce-powers-potential-bruce-c-project/> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verbatim quotations
|
||||||
|
|
||||||
|
### Construction Act, R.S.O. 1990, c. C.30 — Ontario e-Laws (current consolidation). NOTE ON RETRIEVAL: the e-Laws page is a JavaScript single-page app; a plain fetch returns an empty shell. The statute text quoted here was retrieved from the JSON API that backs that page: https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30 (HTTP 200, 374,864 bytes).
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/90c30> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Construction Act, R.S.O. 1990, c. C.30
|
||||||
|
|
||||||
|
> R.S.O. 1990, Chapter C.30
|
||||||
|
|
||||||
|
> Consolidation Period: From January 1, 2026 to the e-Laws currency date.
|
||||||
|
|
||||||
|
> Last amendment: 2025, c. 14, Sched. 2.
|
||||||
|
|
||||||
|
> PART I.1 PROMPT PAYMENT
|
||||||
|
|
||||||
|
> PART II.1 CONSTRUCTION DISPUTE INTERIM ADJUDICATION
|
||||||
|
|
||||||
|
> PART V EXPIRY, PRESERVATION AND PERFECTION OF LIENS
|
||||||
|
|
||||||
|
> Expiry of liens 31 (1) Unless preserved under section 34, the liens arising from the supply of services or materials to an improvement expire as provided in this section. R.S.O. 1990, c. C.30, s. 31 (1); 2017, c. 24, s. 67.
|
||||||
|
|
||||||
|
> Contractor’s liens (2) Subject to subsection (4), the lien of a contractor, (a) for services or materials supplied to an improvement on or before the date certified or declared to be the date of the substantial performance of the contract, expires at the conclusion of the 60-day period next following the occurrence of the earlier of, (i) the date on which a copy of the certificate or declaration of the substantial performance of the contract is published as provided in section 32, and (ii) the date the contract is completed, abandoned or terminated; and (b) for services or materials supplied to the improvement where there is no certification or declaration of the substantial performance of the contract, or for services or materials supplied to the improvement after the date certified or declared to be the date of substantial performance, expires at the conclusion of the 60-day period next following the occurrence of the earlier of, (i) the date the contract is completed, and (ii) the date the contract is abandoned or terminated. R.S.O. 1990, c. C.30, s. 31 (2); 2017, c. 24, s. 26 (1-5), 66.
|
||||||
|
|
||||||
|
> Liens of other persons (3) Subject to subsection (4), the lien of any other person, (a) for services or materials supplied to an improvement on or before the date certified or declared to be the date of the substantial performance of the contract, expires at the conclusion of the 60-day period next following the occurrence of the earliest of, (i) the date on which a copy of the certificate or declaration of the substantial performance of the contract is published, as provided in section 32, (ii) the date on which the person last supplies services or materials to the improvement, (ii.1) the date the contract is completed, abandoned or terminated, and (iii) the date a subcontract is certified to be completed under section 33, where the services or materials were supplied under or in respect of that subcontract
|
||||||
|
|
||||||
|
> Notice of termination (6) No later than seven days after a contract is terminated, either the owner or the contractor or other person whose lien is subject to expiry shall publish a notice of the termination in the prescribed form and manner. 2025, c. 14, Sched. 2, s. 6.
|
||||||
|
|
||||||
|
> 34 (1) A lien may be preserved during the supplying of services or materials or at any time before it expires, (a) where the lien attaches to the premises, by the registration in the proper land registry office of a claim for lien on the title of the premises in accordance with this Part; and (b) where the lien does not attach to the premises, by giving to the owner a copy of the claim for lien. R.S.O. 1990, c. C.30, s. 34 (1); 2010, c. 16, Sched. 2, s. 2 (5); 2017, c. 24, s. 63, 64, 68, 70, 71.
|
||||||
|
|
||||||
|
> What liens may be perfected 36 (1) A lien may not be perfected unless it is preserved. R.S.O. 1990, c. C.30, s. 36 (1).
|
||||||
|
|
||||||
|
> Expiry of preserved lien (2) A lien that has been preserved expires unless it is perfected prior to the end of the 90-day period next following the last day, under section 31, on which the lien could have been preserved. R.S.O. 1990, c. C.30, s. 36 (2); 2017, c. 24, s. 31 (1).
|
||||||
|
|
||||||
|
> How lien perfected (3) A lien claimant perfects the lien claimant’s preserved lien, (a) where the lien attaches to the premises, when the lien claimant commences an action to enforce the lien and, except where an order to vacate the registration of the lien is made, the lien claimant registers a certificate of action in the prescribed form on the title of the premises; or (b) where the lien does not attach to the premises, when the lien claimant commences an action to enforce the lien.
|
||||||
|
|
||||||
|
> Expiry of perfected lien 37 (1) A perfected lien expires immediately after the second anniversary of the commencement of the action that perfected the lien, unless one of the following occurs on or before that anniversary: 1. An order is made for the trial of an action in which the lien may be enforced. 2. An action in which the lien may be enforced is set down for trial. 1994, c. 27, s. 42 (1).
|
||||||
|
|
||||||
|
> Giving of proper invoices 6.3 (1) Proper invoices shall be given to an owner on a monthly basis, unless the contract provides otherwise. 2017, c. 24, s. 7.
|
||||||
|
|
||||||
|
> Payment deadline, owner to contractor 6.4 (1) Subject to the giving of a notice of non-payment under subsection (2), an owner shall pay the amount payable under a proper invoice no later than 28 days after receiving the proper invoice from the contractor. 2017, c. 24, s. 7.
|
||||||
|
|
||||||
|
> Exception, notice of non-payment if dispute (2) An owner who disputes a proper invoice may refuse to pay all or any portion of the amount payable under the proper invoice within the time specified in subsection (1) if, no later than 14 days after receiving the proper invoice from the contractor, the owner gives to the contractor a notice of non-payment, in the prescribed form and manner, specifying the amount of the proper invoice that is not being paid and detailing all of the reasons for non-payment. 2017, c. 24, s. 7.
|
||||||
|
|
||||||
|
> Payment deadlines, contractor to subcontractor Full payment 6.5 (1) Subject to the giving of a notice of non-payment under subsection (6), a contractor who receives full payment of a proper invoice within the time specified in subsection 6.4 (1) shall, no later than seven days after receiving payment, pay each subcontractor who supplied services or materials under a subcontract with the contractor that were included in the proper invoice the amount payable to the subcontractor. 2017, c. 24, s. 7.
|
||||||
|
|
||||||
|
> Non or partial payment, unpaid amount (4) Subject to the giving of a notice of non-payment under subsection (5) or (6), if the owner does not pay some or all of a proper invoice within the time specified in subsection 6.4 (1), the contractor shall, no later than 35 days after giving the proper invoice to the owner, pay each subcontractor who supplied services or materials under a subcontract with the contractor that were included in the proper invoice the amount payable to the subcontractor, to the extent that he or she was not paid fully under subsection (2). 2017, c. 24, s. 7.
|
||||||
|
|
||||||
|
> (iii) providing an undertaking to refer the matter to adjudication under Part II.1 no later than 21 days after giving the notice to the subcontractor
|
||||||
|
|
||||||
|
> 6.6 (1) Subject to the giving of a notice of non-payment under subsection (7), a subcontractor who receives full payment from a contractor in respect of a proper invoice within the time specified in subsection 6.5 (1) shall, no later than seven days after receiving payment, pay each subcontractor who supplied services or materials under a subcontract between them that were included in the proper invoice the amount payable to the subcontractor. 2017, c. 24, s. 7.
|
||||||
|
|
||||||
|
> Section Amendments with date in force (d/m/y) 2017, c. 24, s. 7 - 01/10/2019
|
||||||
|
|
||||||
|
> Definitions 13.1 In this Part, “adjudication” means construction dispute interim adjudication under this Part; (“arbitrage intérimaire”) “adjudicator” means a registry adjudicator or a private adjudicator; (“arbitre intérimaire”) “Authority” means the Authorized Nominating Authority designated under section 13.2; (“Autorité”)
|
||||||
|
|
||||||
|
> Authorized Nominating Authority 13.2 (1) The Minister may designate an entity to act as Authorized Nominating Authority for the purposes of this Part. 2017, c. 24, s. 11 (1); 2025, c. 14, Sched. 2, s. 2.
|
||||||
|
|
||||||
|
> Duties and powers of Authority Duties 13.3 (1) The Authority shall, (a) develop and oversee programs for the training of persons as registry adjudicators and as private adjudicators; (b) qualify persons who meet the prescribed requirements as registry adjudicators and as private adjudicators; (c) establish and maintain a publicly available registry of registry adjudicators; (d) appoint registry adjudicators for the purposes of subsection 13.9 (5); and (e) perform any other duties of the Authority set out in this Part or that may be prescribed for the purposes of this Part.
|
||||||
|
|
||||||
|
> Availability of adjudication Contract 13.5 (1) Subject to subsection (3), a party to a contract may refer a dispute with the other party to the contract respecting any prescribed matter or any matter agreed to by the parties to adjudication. 2024, c. 20, Sched. 4, s. 12 (1).
|
||||||
|
|
||||||
|
> Subcontract (2) Subject to subsection (3.1), a party to a subcontract may refer a dispute with the other party to the subcontract respecting any prescribed matter or any matter agreed to by the parties to adjudication. 2024, c. 20, Sched. 4, s. 12 (1).
|
||||||
|
|
||||||
|
> Expiry of adjudication period, contract (3) An adjudication in respect of a contract may not be commenced if the notice of adjudication is given more than 90 days after the date on which the contract is completed, abandoned or terminated, unless the parties to the adjudication agree otherwise. 2024, c. 20, Sched. 4, s. 12 (1).
|
||||||
|
|
||||||
|
> Multiple disputes only on consent (4) An adjudication may only address a single dispute, unless the parties to the adjudication and the adjudicator agree otherwise.
|
||||||
|
|
||||||
|
> Application despite other proceeding (5) A party may refer a dispute to adjudication under this Part even if the dispute is the subject of a court action or of an arbitration under the Arbitration Act, 1991, unless the action or arbitration has been finally determined. 2017, c. 24, s. 11 (1); 2024, c. 20, Sched. 4, s. 12 (3).
|
||||||
|
|
||||||
|
> 13.11 No later than five days after an adjudicator agrees or is appointed to conduct the adjudication, the party who gave the notice of adjudication shall, (a) provide to the adjudicator a copy of the notice; and (b) provide to the adjudicator and to the other party a copy of the contract or subcontract and any documents the party intends to rely on during the adjudication. 2018, c. 17, Sched. 8, s. 6.
|
||||||
|
|
||||||
|
> Determination 13.13 (1) Subject to subsection (2), an adjudicator shall make a determination of the matter that is the subject of an adjudication no later than 30 days after receiving the documents required by section 13.11. 2017, c. 24, s. 11 (1).
|
||||||
|
|
||||||
|
> Extension (2) The deadline for an adjudicator’s determination may be extended, at any time before its expiry and after the provision of documents to the adjudicator under section 13.11, (a) on the adjudicator’s request, with the written consent of the parties to the adjudication, for a period of no more than 14 days; or (b) on the written agreement of the parties to the adjudication, subject to the adjudicator’s consent, for the period specified in the agreement. 2017, c. 24, s. 11 (1).
|
||||||
|
|
||||||
|
> Delayed determination (5) A determination made by an adjudicator after the date determined under subsection (1) or (2) is of no force or effect. 2017, c. 24, s. 11 (1).
|
||||||
|
|
||||||
|
> Written reasons (6) The adjudicator’s determination shall be in writing and shall include reasons for the determination. 2017, c. 24, s. 11 (1).
|
||||||
|
|
||||||
|
> Effect of determination 13.15 (1) The determination of a matter by an adjudicator is binding on the parties to the adjudication until a determination of the matter by a court, a determination of the matter by way of an arbitration conducted under the Arbitration Act, 1991, or a written agreement between the parties respecting the matter. 2017, c. 24, s. 11 (1).
|
||||||
|
|
||||||
|
> Authority of court, arbitrator (2) Subject to section 13.18, nothing in this Part restricts the authority of a court or of an arbitrator acting under the Arbitration Act, 1991 to consider the merits of a matter determined by an adjudicator. 2017, c. 24, s. 11 (1).
|
||||||
|
|
||||||
|
> 13.18 (1) An application for judicial review of a determination of an adjudicator may only be made with leave of the Divisional Court in accordance with this section and the rules of court. 2017, c. 24, s. 11 (1).
|
||||||
|
|
||||||
|
> Enforcement of amounts payable (2) A party who is required under the determination of an adjudicator to pay an amount to another person shall pay the amount no later than 15 days after the determination has been communicated to the parties to the adjudication. 2017, c. 24, s. 11 (1); 2024, c. 20, Sched. 4, s. 23 (1).
|
||||||
|
|
||||||
|
> Section Amendments with date in force (d/m/y) 2017, c. 24, s. 11 (1) - 01/10/2019
|
||||||
|
|
||||||
|
> Persons who may be let in (6) The court may allow any person with a perfected lien, (a) who was not served with a notice of trial; or (b) whose action was stayed by reason of an order under the Arbitration Act, 1991, to be let in to prove the claim at any time before the amount realized in the action for the satisfaction of the lien has been distributed
|
||||||
|
|
||||||
|
> Transition, Construction Lien Amendment Act, 2017 87.3 (1) This Act and the regulations, as they read on June 29, 2018, continue to apply with respect to an improvement if,
|
||||||
|
|
||||||
|
### Ontario e-Laws — version list for R.S.O. 1990, c. C.30 (JSON API backing the 'Versions' tab of https://www.ontario.ca/laws/statute/90c30). Shows the title carried by each historical consolidation of the same chapter.
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/api/v2/legislation/en/act-versions/statute/90c30> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> "title": {"en": "Construction Lien Act, R.S.O. 1990, c. C.30"}, "dateFrom": {"en": "2017-12-14T05:00:00.000Z"}, "dateTo": {"en": "2018-06-30T04:00:00.000Z"}, "alias": {"en": "statute/90c30/v8"}
|
||||||
|
|
||||||
|
> "title": {"en": "Construction Act, R.S.O. 1990, c. C.30"}, "dateFrom": {"en": "2018-07-01T04:00:00.000Z"}, "dateTo": {"en": "2018-12-05T05:00:00.000Z"}, "alias": {"en": "statute/90c30/v9"}
|
||||||
|
|
||||||
|
> "title": {"en": "Construction Act, R.S.O. 1990, c. C.30"}, "dateFrom": {"en": "2026-01-01T05:00:00.000Z"}, "state": {"en": "current"}, "alias": {"en": "statute/90c30"}
|
||||||
|
|
||||||
|
### Construction Lien Act, R.S.O. 1990, c. C.30 — historical e-Laws consolidation, version 8 (period 14 December 2017 to 30 June 2018). Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30/v8
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/90c30/v8> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> title: Construction Lien Act, R.S.O. 1990, c. C.30
|
||||||
|
|
||||||
|
> shortTitle: Construction Lien Act
|
||||||
|
|
||||||
|
> chapter: R.S.O. 1990, Chapter C.30
|
||||||
|
|
||||||
|
### Construction Act, R.S.O. 1990, c. C.30 — historical e-Laws consolidation, version 9 (period beginning 1 July 2018). Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30/v9
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/90c30/v9> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> title: Construction Act, R.S.O. 1990, c. C.30
|
||||||
|
|
||||||
|
> shortTitle: Construction Act
|
||||||
|
|
||||||
|
> chapter: R.S.O. 1990, Chapter C.30
|
||||||
|
|
||||||
|
### Construction Lien Amendment Act, 2017, S.O. 2017, c. 24 - Bill 142 — Ontario e-Laws. Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/S17024
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/S17024> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Construction Lien Amendment Act, 2017, S.O. 2017, c. 24 - Bill 142
|
||||||
|
|
||||||
|
> Assented to December 12, 2017
|
||||||
|
|
||||||
|
> 1 The short title of the Construction Lien Act is repealed and the following substituted: Construction Act
|
||||||
|
|
||||||
|
> Commencement 86 (1) Subject to subsections (2), (3), (4), (5) and (6), this Act comes into force on the day it receives Royal Assent. (2) The following provisions come into force on a day to be named by proclamation of the Lieutenant Governor: 1. Section 1.
|
||||||
|
|
||||||
|
> 75 Subsection 103 (3) of the Courts of Justice Act is amended by striking out “Construction Lien Act” at the end and substituting “Construction Act”.
|
||||||
|
|
||||||
|
> 77 (1) The Schedule to the Limitations Act, 2002 is amended by striking out “Construction Lien Act” in the column titled “Act” and substituting “Construction Act”.
|
||||||
|
|
||||||
|
> 87 The short title of this Act is the Construction Lien Amendment Act, 2017.
|
||||||
|
|
||||||
|
### O. Reg. 264/25 — ADJUDICATIONS UNDER PART II.1 OF THE ACT (made under the Construction Act). Ontario e-Laws; consolidation period from January 1, 2026; 'No amendments.' Retrieved via https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/regulation/250264
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/regulation/250264> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ADJUDICATIONS UNDER PART II.1 OF THE ACT
|
||||||
|
|
||||||
|
> Adjudication Availability 19. (1) The following matters are prescribed for the purposes of subsection 13.5 (1) of the Act: 1. The valuation of services or materials provided under the contract. 2. Payment under the contract, including in respect of a change order, whether approved or not, or a proposed change order. 3. A dispute that is the subject of a notice of non-payment under Part I.1 of the Act. 4. Amounts retained under section 12 of the Act (set-off by trustee) or under subsection 17 (3) of the Act (lien set-off). 5. Payment of a holdback under section 26 of the Act. 6. Any of the following matters, if it is reasonably necessary to resolve a dispute respecting the matter in order to make a determination on any other matter that may be adjudicated: i. The scope of work required to be performed under the contract. ii. A request for a change in the contract price. iii. A request for an extension of time in the completion of work required to be performed under the contract.
|
||||||
|
|
||||||
|
> (2) The matters listed in subsection (1), with necessary modifications, are prescribed for the purposes of subsection 13.5 (2) of the Act.
|
||||||
|
|
||||||
|
> Designation 2. (1) To be eligible to be designated to act as Authorized Nominating Authority, an entity must, (a) submit an application to the Minister in the time and manner specified by the Minister; and (b) agree in writing to abide by any conditions of designation specified by the Minister, including any conditions respecting the term or termination of any such designation.
|
||||||
|
|
||||||
|
> Notice of adjudication, copy to Authority 20. A party to a contract or subcontract who gives a notice of adjudication under subsection 13.7 (1) of the Act shall, on the same day, provide a copy of the notice in electronic format to the Authority.
|
||||||
|
|
||||||
|
### Ontario Dispute Adjudication for Construction Contracts (ODACC) — home page
|
||||||
|
|
||||||
|
<https://odacc.ca/en/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Ontario Dispute Adjudication for Construction Contracts (“ODACC”) is the Authorized Nominating Authority (“ANA”) under the Construction Act. As the ANA, ODACC is responsible for administering construction-related adjudications and for training and qualifying Adjudicators. ODACC derives its powers from the Construction Act and Ontario Regulation 264/25
|
||||||
|
|
||||||
|
> Adjudication and prompt payment provisions of the Construction Act came into force on October 1, 2019. Further amendments to the Construction Act came into force on January 1, 2026. ODACC is responsible for administering adjudications and for training and certifying adjudicators.
|
||||||
|
|
||||||
|
> Only Adjudicators listed in the Adjudicator Registry are permitted to conduct adjudications and make Determinations under the Construction Act.
|
||||||
|
|
||||||
|
> The adjudication process commences when the Claimant provides the Respondent with a Notice of Adjudication (and sends ODACC an electronic copy).
|
||||||
|
|
||||||
|
### ODACC — About Us
|
||||||
|
|
||||||
|
<https://odacc.ca/en/aboutus/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Ontario Dispute Adjudication for Construction Contracts (“ODACC”) is the Authorized Nominating Authority (“ANA”) under the Construction Act. As the ANA, ODACC is responsible for administering construction-related adjudications and for training and qualifying Adjudicators.
|
||||||
|
|
||||||
|
> Further duties and powers of ODACC are listed in sections 6 through 18 of Ontario Regulation 264/25.
|
||||||
|
|
||||||
|
### ODACC — Adjudication Process
|
||||||
|
|
||||||
|
<https://odacc.ca/en/adjudication-process/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Adjudication is a dispute resolution process that allows Parties to present their dispute to an independent third party for a decision.
|
||||||
|
|
||||||
|
> The Adjudicator will consider the evidence and make a decision (a “Determination”) within thirty days of the Claimant submitting its supporting documents. If the Adjudicator orders a Party to pay the other Party, the payment must be made within fifteen days of the issuing of the Determination.
|
||||||
|
|
||||||
|
> Adjudications are quick. After an Adjudicator receives the Claimant’s documents, the Adjudicator must render a Determination no later than 30 days after receiving the Claimant’s documents (unless the Determination due date is extended);
|
||||||
|
|
||||||
|
> Determinations are binding only until a decision is made in a subsequent proceeding. Either Party can commence a proceeding in court or through arbitration. The Determination is binding until a final decision is made in the subsequent proceeding;
|
||||||
|
|
||||||
|
> Adjudication is available as a right. A Party to a construction contract can commence an adjudication without the other Party’s consent
|
||||||
|
|
||||||
|
> An adjudication may only be conducted by a certified ODACC Adjudicator.
|
||||||
|
|
||||||
|
### ODACC 2025 Annual Report (PDF, 37 pages) — the annual report ODACC is required to publish as Authorized Nominating Authority
|
||||||
|
|
||||||
|
<https://odacc.ca/wp-content/uploads/2021/07/2025-ODACC-Annual-Report-Final.pdf> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> ODACC is the Authorized Nominating Authority (the “ANA”) under the Ontario Construction Act and is responsible for administering construction-related Adjudications and for training and qualifying Adjudicators.
|
||||||
|
|
||||||
|
> The Adjudication and Prompt Payment provisions of the Construction Act came into force on October 1, 2019.
|
||||||
|
|
||||||
|
> ODACC is pleased to provide this report on the sixth year of its operation for the fiscal year August 1, 2024, to July 31, 2025 (the “2025 Fiscal Year”).
|
||||||
|
|
||||||
|
### Small modular reactors | Darlington SMR – Ontario Power Generation (proponent's own page). NOTE ON RETRIEVAL: opg.com returned HTTP 403 (Cloudflare bot block) to both direct curl and WebFetch on 2026-08-29; the page text quoted here was obtained through the r.jina.ai HTML-to-text reader proxy (https://r.jina.ai/https://www.opg.com/projects-services/projects/nuclear/smr/darlington-smr/, HTTP 200). Key facts are independently corroborated by the CNSC page listed below. A human should re-read the OPG page in a browser before any of this wording is published.
|
||||||
|
|
||||||
|
<https://www.opg.com/projects-services/projects/nuclear/smr/darlington-smr/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The Darlington New Nuclear Project is leading the way in the advancement of Small Modular Reactor (SMR) technology in Canada – the future of nuclear power generation.
|
||||||
|
|
||||||
|
> On July 7, 2023, the Ontario government announced it will work with Ontario Power Generation (OPG) to commence planning and licensing for three additional SMRs, for a total of four SMRs at the Darlington new nuclear site.
|
||||||
|
|
||||||
|
> In March 2026, OPG applied to the Canadian Nuclear Safety Commission (CNSC) for a Licence to Operate (LTO) the first SMR. The LTO is the regulatory approval required to complete commissioning and safely operate the reactor, once construction is complete.
|
||||||
|
|
||||||
|
> OPG’s application for this licence is comprised of a number of packages, submitted to the CNSC over the course of several months. This application process will culminate in a public hearing where the regulator will hear comments from members of the public regarding our application.
|
||||||
|
|
||||||
|
> Site construction progress - Summer 2026
|
||||||
|
|
||||||
|
> The construction of the reactor building is now progressing upwards towards grade following the successful basemat (the foundation of the Unit 1 reactor building) installation earlier this year.
|
||||||
|
|
||||||
|
> Additional regulatory approvals will be required prior to construction and operation of additional units.
|
||||||
|
|
||||||
|
> Our planning goal is to complete construction of the first SMR by the end of this decade, and connect to the grid by the end of 2030.
|
||||||
|
|
||||||
|
### Darlington New Nuclear Project — Canadian Nuclear Safety Commission (federal nuclear regulator)
|
||||||
|
|
||||||
|
<https://www.cnsc-ccsn.gc.ca/eng/reactors/new-reactor-power-plant-projects/new-reactor-power-plant-facilities/darlington-new-nuclear-project/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The site is owned by Ontario Power Generation (OPG).
|
||||||
|
|
||||||
|
> The BWRX-300 is a 300 MWe water-cooled, natural circulation small modular reactor.
|
||||||
|
|
||||||
|
> OPG applied for a licence to construct 1 BWRX-300 reactor in October 2022 and was granted the licence in April 2025.
|
||||||
|
|
||||||
|
> In March 2026, OPG applied for a 20-year licence to operate 1 BWRX-300 reactor and an associated low- and intermediate-level waste storage structure.
|
||||||
|
|
||||||
|
> Current status: 1 unit under construction
|
||||||
|
|
||||||
|
> The CNSC has verified that the commitments for RHP-1 (for installation of the reactor building foundation) were met, and the hold point was removed on March 30, 2026.
|
||||||
|
|
||||||
|
### The Bruce C Project — Bruce Power (proponent's own page)
|
||||||
|
|
||||||
|
<https://www.brucepower.com/the-bruce-c-project/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> As Ontario prepares for the future through its Integrated Energy Plan, Bruce Power has initiated a federal Impact Assessment (IA) for the Bruce C Project. The project aims to create an option to build up to 4,800 megawatts of nuclear capacity at the Bruce Power site, located within the Territory of the Saugeen Ojibway Nation, in the Municipality of Kincardine, Ontario.
|
||||||
|
|
||||||
|
> Bruce Power is advancing the IA process in a proactive, open and transparent manner to engage Indigenous Peoples, local communities, interested parties and the public early on.
|
||||||
|
|
||||||
|
### Bruce C Project — Engage Bruce Power (Bruce Power's own engagement platform). NOTE: WebFetch returned 403; retrieved with curl sending a browser User-Agent (HTTP 200, 62,728 bytes).
|
||||||
|
|
||||||
|
<https://engage.brucepower.com/brucec> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The Bruce C Project is creating the option to build up to 4,800 MW of nuclear capacity on the existing Bruce Power site.
|
||||||
|
|
||||||
|
> Through the federal integrated Impact Assessment (IA) process led by the Impact Assessment Agency of Canada (IAAC) alongside the Canadian Nuclear Safety Commission (CNSC), Bruce Power is studying the potential environmental, economic, social and health impacts of a new nuclear build.
|
||||||
|
|
||||||
|
> IN PROGRESS — In the Impact Statement phase, the Bruce C Project team will prepare the Impact Statement
|
||||||
|
|
||||||
|
> UPCOMING — In the Impact Assessment Phase phase, the Review Panel will prepare hearing materials and public hearings will be held.
|
||||||
|
|
||||||
|
> COMPLETED — During the Planning phase, the Bruce C Project team: Engaged with Indigenous Nations and Communities, local municipalities and the public on project information.
|
||||||
|
|
||||||
|
> At the completion of the Planning Phase, Bruce Power received the Integrated Tailored Impact Statement Guidelines and Planning Phase documents from the IAAC and CNSC, which defines the requirements for Bruce Power to include in the Impact Statement and Licence to Prepare Site Application.
|
||||||
|
|
||||||
|
> Reactor technology has not been selected at this time, and the Impact Assessment for the Bruce C Project will be technology neutral. This approach considers multiple technologies to provide optionality to the province in long-term electricity system planning.
|
||||||
|
|
||||||
|
> Bruce Power has commenced a siting assessment to understand potential constraints and opportunities on the Bruce Power site, support conceptual layout development, and evaluate suitable areas for potential development.
|
||||||
|
|
||||||
|
### Planning phase of Integrated Impact Assessment completed for Bruce Power's potential Bruce C project — Bruce Power newsroom, dated 2025-08-21 (proponent's own release)
|
||||||
|
|
||||||
|
<https://www.brucepower.com/2025/08/21/planning-phase-of-integrated-impact-assessment-completed-for-bruce-powers-potential-bruce-c-project/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The planning phase of the federal Integrated Impact Assessment process has been completed for Bruce Power’s Bruce C project.
|
||||||
|
|
||||||
|
> On August 19, the Impact Assessment Agency of Canada (IAAC), in collaboration with the Canadian Nuclear Safety Commission (CNSC), issued the formal Notice of Commencement of Impact Assessment under the Impact Assessment Act. This notice initiates the next stage of the process, the development of the Impact Statement, and is accompanied by Tailored Impact Statement Guidelines (TISG) and documents that will shape the scope and depth of the assessment moving forward.
|
||||||
|
|
||||||
|
> The planning phase is the first of the five phases in Impact Assessment process.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this establishes
|
||||||
|
|
||||||
|
Each item names the source it rests on. An item here that no quotation above
|
||||||
|
supports is a defect in this file, not a fact.
|
||||||
|
|
||||||
|
- The statute's current name and citation is the Construction Act, R.S.O. 1990, c. C.30. The e-Laws consolidation retrieved on 2026-08-29 states 'Consolidation Period: From January 1, 2026 to the e-Laws currency date' and 'Last amendment: 2025, c. 14, Sched. 2.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- The Construction Act did not replace the Construction Lien Act with a new statute — it IS the same statute renamed. Section 1 of the Construction Lien Amendment Act, 2017, S.O. 2017, c. 24 (assented to December 12, 2017) reads: '1 The short title of the Construction Lien Act is repealed and the following substituted: Construction Act'. The chapter number is unchanged (R.S.O. 1990, c. C.30) across the rename.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/S17024>
|
||||||
|
- The rename took effect on 1 July 2018. e-Laws' own version list for R.S.O. 1990, c. C.30 shows version 8 titled 'Construction Lien Act, R.S.O. 1990, c. C.30' running to 2018-06-30, and version 9 titled 'Construction Act, R.S.O. 1990, c. C.30' beginning 2018-07-01.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/api/v2/legislation/en/act-versions/statute/90c30>
|
||||||
|
- The historical e-Laws consolidation at /laws/statute/90c30/v8 carries the short title 'Construction Lien Act' and the chapter 'R.S.O. 1990, Chapter C.30'; the next consolidation, /laws/statute/90c30/v9, carries the short title 'Construction Act' and the same chapter.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30/v9>
|
||||||
|
- LIEN PRESERVATION — the deadline is set by s. 31 (headed 'Expiry of liens'), read with s. 34 ('A lien may be preserved during the supplying of services or materials or at any time before it expires'). Under s. 31(2) a contractor's lien 'expires at the conclusion of the 60-day period next following' publication of the certificate/declaration of substantial performance or, where earlier, completion, abandonment or termination of the contract; where there is no certification or declaration, the 60-day period runs from the earlier of completion and abandonment or termination. Section 31(3) applies the same 60-day period to the lien of 'any other person' (running from the earliest of publication, last supply, completion/abandonment/termination, or certification of the subcontract as complete under s. 33). Section 31(2.1) applies the same 60-day period to a workers' trust fund lien.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- HOW A LIEN IS PRESERVED — s. 34(1): where the lien attaches to the premises, 'by the registration in the proper land registry office of a claim for lien on the title of the premises'; where it does not attach, 'by giving to the owner a copy of the claim for lien.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- LIEN PERFECTION — s. 36(1): 'A lien may not be perfected unless it is preserved.' Section 36(2): 'A lien that has been preserved expires unless it is perfected prior to the end of the 90-day period next following the last day, under section 31, on which the lien could have been preserved.' Section 36(3) defines perfection as commencing an action to enforce the lien and (where the lien attaches to the premises) registering a certificate of action on title.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- A perfected lien has a further limit: s. 37(1) provides that it 'expires immediately after the second anniversary of the commencement of the action that perfected the lien' unless an order is made for the trial of an action in which the lien may be enforced, or such an action is set down for trial, on or before that anniversary.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- As of the 1 January 2026 consolidation, s. 31(6) (added by 2025, c. 14, Sched. 2, s. 6) requires that 'No later than seven days after a contract is terminated, either the owner or the contractor or other person whose lien is subject to expiry shall publish a notice of the termination in the prescribed form and manner', and s. 31(7) fixes the publication date as the termination date for the purposes of s. 31.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- INTERIM ADJUDICATION — Ontario does have statutory construction adjudication. It sits in Part II.1 of the Construction Act, headed 'CONSTRUCTION DISPUTE INTERIM ADJUDICATION' (ss. 13.1–13.23). Section 13.1 defines 'adjudication' as 'construction dispute interim adjudication under this Part'.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- Section 13.2(1) provides that 'The Minister may designate an entity to act as Authorized Nominating Authority for the purposes of this Part', and s. 13.1 defines 'Authority' as 'the Authorized Nominating Authority designated under section 13.2'. The Act itself does not name the designated entity.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- The Authorized Nominating Authority is Ontario Dispute Adjudication for Construction Contracts (ODACC). ODACC states on its own site: 'Ontario Dispute Adjudication for Construction Contracts ("ODACC") is the Authorized Nominating Authority ("ANA") under the Construction Act. As the ANA, ODACC is responsible for administering construction-related adjudications and for training and qualifying Adjudicators.'
|
||||||
|
*Source:* <https://odacc.ca/en/aboutus/>
|
||||||
|
- ODACC repeats the same statement in its own 2025 Annual Report (the report it is required to publish as ANA), which describes the fiscal year 1 August 2024 to 31 July 2025 as 'the sixth year of its operation'.
|
||||||
|
*Source:* <https://odacc.ca/wp-content/uploads/2021/07/2025-ODACC-Annual-Report-Final.pdf>
|
||||||
|
- The Part II.1 (adjudication) and Part I.1 (prompt payment) provisions came into force on 1 October 2019. The Act's own in-force notes record '2017, c. 24, s. 11 (1) - 01/10/2019' against the Part II.1 sections and '2017, c. 24, s. 7 - 01/10/2019' against the Part I.1 sections. ODACC states the same date: 'Adjudication and prompt payment provisions of the Construction Act came into force on October 1, 2019.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- WHAT MAY BE ADJUDICATED — s. 13.5(1) (as amended effective 1 January 2026) permits a party to a contract to refer a dispute 'respecting any prescribed matter or any matter agreed to by the parties' to adjudication; s. 13.5(2) does the same for subcontracts. The prescribed matters are set out in O. Reg. 264/25, s. 19(1): the valuation of services or materials provided under the contract; payment under the contract, including in respect of a change order (approved or not) or a proposed change order; a dispute that is the subject of a notice of non-payment under Part I.1; amounts retained under s. 12 (set-off by trustee) or s. 17(3) (lien set-off); payment of a holdback under s. 26; and — only where reasonably necessary to resolve another adjudicable matter — the scope of work, a request for a change in the contract price, and a request for an extension of time.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/250264>
|
||||||
|
- Adjudication is time-limited at the front end: s. 13.5(3) bars an adjudication in respect of a contract 'if the notice of adjudication is given more than 90 days after the date on which the contract is completed, abandoned or terminated, unless the parties to the adjudication agree otherwise', with a parallel rule for subcontracts in s. 13.5(3.1). Section 13.5(4) limits an adjudication to 'a single dispute, unless the parties to the adjudication and the adjudicator agree otherwise.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- STATUTORY TIMELINE FOR A DETERMINATION — s. 13.13(1): 'an adjudicator shall make a determination of the matter that is the subject of an adjudication no later than 30 days after receiving the documents required by section 13.11.' Section 13.11 requires the party who gave the notice of adjudication to provide those documents 'No later than five days after an adjudicator agrees or is appointed to conduct the adjudication'. Section 13.13(2) permits extension — up to 14 days on the adjudicator's request with the parties' written consent, or for a period agreed in writing by the parties with the adjudicator's consent. Section 13.13(5): 'A determination made by an adjudicator after the date determined under subsection (1) or (2) is of no force or effect.' Section 13.13(6) requires the determination to be in writing with reasons.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- A determined amount must be paid 'no later than 15 days after the determination has been communicated to the parties to the adjudication' (s. 13.19(2)). Judicial review of a determination is available only with leave of the Divisional Court (s. 13.18(1)).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- PROMPT PAYMENT (Part I.1) — proper invoices 'shall be given to an owner on a monthly basis, unless the contract provides otherwise' (s. 6.3(1)). Owner to contractor: payment 'no later than 28 days after receiving the proper invoice from the contractor' (s. 6.4(1)), unless the owner gives a notice of non-payment 'no later than 14 days after receiving the proper invoice' (s. 6.4(2)).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- PROMPT PAYMENT, down the chain — contractor to subcontractor: where the contractor is paid in full within the s. 6.4(1) time, it must pay each subcontractor 'no later than seven days after receiving payment' (s. 6.5(1)), and the same seven days applies to partial payment received (s. 6.5(2)). Where the owner does not pay, the contractor must pay its subcontractors 'no later than 35 days after giving the proper invoice to the owner' (s. 6.5(4)) unless it gives a notice of non-payment; that notice must include 'an undertaking to refer the matter to adjudication under Part II.1 no later than 21 days after giving the notice to the subcontractor' (s. 6.5(5)(a)(iii)). Subcontractor to sub-subcontractor: seven days after receiving payment (s. 6.6(1)).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- ARBITRATION — the Act contemplates arbitration under the Arbitration Act, 1991 in four places, all of them treating arbitration as the forum that supersedes an interim adjudication rather than as a process the Act itself creates: s. 13.5(5) (a dispute may be referred to adjudication 'even if the dispute is the subject of a court action or of an arbitration under the Arbitration Act, 1991, unless the action or arbitration has been finally determined'); s. 13.15(1) (an adjudicator's determination 'is binding on the parties to the adjudication until a determination of the matter by a court, a determination of the matter by way of an arbitration conducted under the Arbitration Act, 1991, or a written agreement between the parties'); s. 13.15(2) (nothing in Part II.1 restricts 'the authority of a court or of an arbitrator acting under the Arbitration Act, 1991 to consider the merits of a matter determined by an adjudicator'); and s. 62(6)(b) (a lien claimant 'whose action was stayed by reason of an order under the Arbitration Act, 1991' may be let in to prove the claim).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90c30>
|
||||||
|
- DARLINGTON — the Darlington New Nuclear Project is real and is Ontario Power Generation's. OPG's own page describes it as 'leading the way in the advancement of Small Modular Reactor (SMR) technology in Canada' and states that on 7 July 2023 the Ontario government announced it would work with OPG 'to commence planning and licensing for three additional SMRs, for a total of four SMRs at the Darlington new nuclear site.'
|
||||||
|
*Source:* <https://www.opg.com/projects-services/projects/nuclear/smr/darlington-smr/>
|
||||||
|
- DARLINGTON, status in the proponent's own words as at 2026-08-29: OPG's page states 'In March 2026, OPG applied to the Canadian Nuclear Safety Commission (CNSC) for a Licence to Operate (LTO) the first SMR', that the application 'is comprised of a number of packages, submitted to the CNSC over the course of several months' and 'will culminate in a public hearing'. Under the heading 'Site construction progress - Summer 2026' it states 'The construction of the reactor building is now progressing upwards towards grade following the successful basemat (the foundation of the Unit 1 reactor building) installation earlier this year.' It adds that 'Additional regulatory approvals will be required prior to construction and operation of additional units.'
|
||||||
|
*Source:* <https://www.opg.com/projects-services/projects/nuclear/smr/darlington-smr/>
|
||||||
|
- DARLINGTON, corroborated by the regulator: the CNSC page states 'The site is owned by Ontario Power Generation (OPG)', 'The BWRX-300 is a 300 MWe water-cooled, natural circulation small modular reactor', 'OPG applied for a licence to construct 1 BWRX-300 reactor in October 2022 and was granted the licence in April 2025', 'In March 2026, OPG applied for a 20-year licence to operate 1 BWRX-300 reactor and an associated low- and intermediate-level waste storage structure', and 'Current status: 1 unit under construction'.
|
||||||
|
*Source:* <https://www.cnsc-ccsn.gc.ca/eng/reactors/new-reactor-power-plant-projects/new-reactor-power-plant-facilities/darlington-new-nuclear-project/>
|
||||||
|
- BRUCE C — the project is real and is Bruce Power's. Bruce Power's own page states 'Bruce Power has initiated a federal Impact Assessment (IA) for the Bruce C Project. The project aims to create an option to build up to 4,800 megawatts of nuclear capacity at the Bruce Power site, located within the Territory of the Saugeen Ojibway Nation, in the Municipality of Kincardine, Ontario.'
|
||||||
|
*Source:* <https://www.brucepower.com/the-bruce-c-project/>
|
||||||
|
- BRUCE C, status in the proponent's own words as at 2026-08-29: Bruce Power's engagement site marks the Planning phase 'COMPLETED', the Impact Statement phase 'IN PROGRESS' ('In the Impact Statement phase, the Bruce C Project team will prepare the Impact Statement'), and the Impact Assessment phase 'UPCOMING' (Review Panel hearing materials and public hearings). It states 'At the completion of the Planning Phase, Bruce Power received the Integrated Tailored Impact Statement Guidelines and Planning Phase documents from the IAAC and CNSC'. Bruce Power's newsroom release of 2025-08-21 states 'The planning phase of the federal Integrated Impact Assessment process has been completed for Bruce Power's Bruce C project' and that on 19 August the IAAC with the CNSC 'issued the formal Notice of Commencement of Impact Assessment under the Impact Assessment Act'.
|
||||||
|
*Source:* <https://engage.brucepower.com/brucec>
|
||||||
|
- BRUCE C — no reactor technology has been chosen. Bruce Power states: 'Reactor technology has not been selected at this time, and the Impact Assessment for the Bruce C Project will be technology neutral.' It also states it 'has commenced a siting assessment' on the existing site. Bruce C is therefore at an assessment/optioning stage, not a construction stage — unlike Darlington, no construction licence exists.
|
||||||
|
*Source:* <https://engage.brucepower.com/brucec>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this does NOT establish
|
||||||
|
|
||||||
|
**Read this section before writing copy.** It is the half that keeps a page
|
||||||
|
honest, and on this project it is the half that has twice been skipped.
|
||||||
|
|
||||||
|
- **Does the Construction Act contemplate MEDIATION of construction disputes, and where?**
|
||||||
|
- *Searched:* Case-insensitive regex sweep for 'mediat' over the complete fetched text of the current consolidation (the full JSON payload from https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30, 374,864 bytes, tags stripped), plus a read of the Part headings list and of Part VIII (Jurisdiction and Procedure).
|
||||||
|
- *Outcome:* NO MEDIATION PROVISION FOUND. The sweep returned exactly 2 matches and both were false positives — 'in the immediate vicinity of the premises' (s. 1 definition of 'improvement') and 'the year immediately preceding the anniversary' (s. 26.1(4), annual holdback release). Both matches were printed and read. On the text retrieved, the Construction Act contains no mediation provisions at all: no mediation Part, no mandatory or court-annexed mediation step, no mediator role. There is nothing to quote because there is nothing there. This is a confirmed negative, not an unsearched gap — but it is stated as an absence in the statute only. Whether mediation of construction disputes is required or available by some OTHER instrument (for example the Rules of Civil Procedure, O. Reg. 194/90, or a standard-form contract such as CCDC 2) was NOT searched and must not be inferred from this.
|
||||||
|
- **On what date, and by what instrument, was ODACC designated as the Authorized Nominating Authority?**
|
||||||
|
- *Searched:* O. Reg. 264/25 in full (searched for 'ODACC' and 'Ontario Dispute Adjudication' — zero matches; s. 2 sets designation eligibility criteria but names no entity); ODACC's home page, About Us page and 2025 Annual Report PDF (searched for 'designat', 'named', '2019'); s. 13.2 of the Act.
|
||||||
|
- *Outcome:* NOT ESTABLISHED. ODACC's designation is made by the Minister under s. 13.2(1), not by regulation, and no primary or institutional source retrieved states the designation date. ODACC's own pages assert only that it IS the ANA, with no date. A third-party law-firm/news source surfaced in search asserts 18 July 2019, but that was not fetched or verified and is NOT relied on here. Do not publish a designation date.
|
||||||
|
- **Verbatim confirmation of OPG's Darlington page by direct fetch of opg.com.**
|
||||||
|
- *Searched:* curl with a browser User-Agent and full navigation headers (HTTP 403, Cloudflare block, on both www.opg.com and opg.com); WebFetch on the same URL (HTTP 403). Text was then obtained through the r.jina.ai reader proxy (HTTP 200).
|
||||||
|
- *Outcome:* PARTIAL. The OPG content quoted above came through a third-party reader proxy rather than a direct fetch, so the exact wording carries one hop of risk. Every load-bearing fact in it (proponent, BWRX-300 technology, construction licence April 2025, operating licence application March 2026, one unit under construction) is independently confirmed by the CNSC regulator page, which WAS fetched directly. Before any OPG wording is quoted on a public page, open the OPG URL in a browser and confirm the sentence.
|
||||||
|
- **Whether the Darlington and Bruce C projects have any adjudication, lien, or prompt-payment activity under the Construction Act.**
|
||||||
|
- *Searched:* Not searched — outside the scope of the sources fetched.
|
||||||
|
- *Outcome:* NOT ESTABLISHED, and nothing above supports connecting these two projects to the Construction Act machinery. The projects were verified as real and their status recorded; no source retrieved links either project to any dispute, adjudication, lien or payment proceeding. Do not use them as examples of ADR work, of the practitioner's involvement, or of anything else beyond 'these projects exist and are at these stages'.
|
||||||
|
- **Whether the Construction Act's Part II.1 provisions quoted are affected by any not-yet-in-force amendment.**
|
||||||
|
- *Searched:* Searched the raw e-Laws payload for not-in-force markers ('not in force', 'ynote', NYIF classes). The consolidation's metadata flags "Contains 'not yet in force' provision sections".
|
||||||
|
- *Outcome:* MOSTLY CLEARED, with one caveat. Two not-in-force items were found, both amendments from 2017, c. 33, Sched. 2, s. 76 (1) and (2), attached to s. 1 and s. 34 — neither touches the periods or the Part II.1 text quoted above. However, the full NYIF set was not exhaustively mapped section by section. Anything published from this artefact should be re-checked against the live e-Laws page on the day of publication, since this consolidation is current only 'to the e-Laws currency date'.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Searches run
|
||||||
|
|
||||||
|
- `WebFetch https://www.ontario.ca/laws/statute/90c30 — returned an empty SPA shell ('e-Laws | Ontario.ca'), no statute text; recorded as a failed instrument rather than a null result`
|
||||||
|
- `curl https://www.ontario.ca/laws/statute/90c30 — HTTP 200, 54,243 bytes, but 0 matches for 'Construction'; confirmed the page is a React SPA`
|
||||||
|
- `curl https://www.canlii.org/en/on/laws/stat/rso-1990-c-c30/latest/rso-1990-c-c30.html — HTTP 403, blocked; CanLII not used`
|
||||||
|
- `Read the e-Laws JS bundle (/laws/static/js/main.dbd400db.js) to recover the API route pattern: legislation/{lang}/doc-search/{type}/{code}[/{version}]`
|
||||||
|
- `Probed five candidate e-Laws API shapes (all 404) before locating the correct route`
|
||||||
|
- `curl https://www.ontario.ca/laws/api/v2/legislation/en/doc-search/statute/90c30 — HTTP 200, 374,864 bytes, full current Construction Act`
|
||||||
|
- `curl .../doc-search/statute/S17024 — Construction Lien Amendment Act, 2017`
|
||||||
|
- `curl .../doc-search/statute/90c30/v8 and /v9 — the last 'Construction Lien Act' consolidation and the first 'Construction Act' consolidation`
|
||||||
|
- `curl .../act-versions/statute/90c30 — all 22 historical versions with titles and date ranges`
|
||||||
|
- `curl .../doc-search/regulation/250264 (O. Reg. 264/25) and /250384 (O. Reg. 384/25)`
|
||||||
|
- `grep of the Act text for sections 31, 34, 36, 37, 6.1–6.6, 13.1–13.19, and for all Part headings`
|
||||||
|
- `Case-insensitive sweep of the full Act payload for 'mediat' (2 hits, both false positives, both printed and read) and for 'arbitrat' (4 hits, all printed and read)`
|
||||||
|
- `Sweep of the raw Act payload for not-yet-in-force markers`
|
||||||
|
- `WebFetch https://odacc.ca/en/ — ODACC home page`
|
||||||
|
- `curl https://odacc.ca/en/ , /en/aboutus/ , /en/adjudication-process/ , /en/annual-report/`
|
||||||
|
- `curl + pdftotext https://odacc.ca/wp-content/uploads/2021/07/2025-ODACC-Annual-Report-Final.pdf (37 pages)`
|
||||||
|
- `WebFetch https://odacc.ca/en/about-us/ — HTTP 404 (wrong slug; correct slug is /en/aboutus/)`
|
||||||
|
- `WebSearch 'Ontario Ministry of the Attorney General designated ODACC Authorized Nominating Authority Construction Act' — only third-party sources for the designation date; not relied on`
|
||||||
|
- `WebSearch 'ontario.ca Construction Act renamed Construction Lien Act July 1 2018' — no primary source returned; the rename date was instead established from the e-Laws version list`
|
||||||
|
- `WebSearch 'OPG Darlington New Nuclear Project small modular reactor opg.com official page'`
|
||||||
|
- `curl and WebFetch https://www.opg.com/projects-services/projects/nuclear/smr/darlington-smr/ — HTTP 403 both, Cloudflare; retried via curl on bare opg.com (403) before falling back to r.jina.ai (HTTP 200)`
|
||||||
|
- `WebFetch https://www.cnsc-ccsn.gc.ca/.../darlington-new-nuclear-project/ — regulator corroboration`
|
||||||
|
- `WebSearch 'Bruce Power Bruce C project site brucepower.com'`
|
||||||
|
- `curl https://www.brucepower.com/the-bruce-c-project/`
|
||||||
|
- `WebFetch https://engage.brucepower.com/brucec — HTTP 403; refetched with curl and a browser User-Agent, HTTP 200`
|
||||||
|
- `curl https://www.brucepower.com/2025/08/21/planning-phase-of-integrated-impact-assessment-completed-for-bruce-powers-potential-bruce-c-project/`
|
||||||
@@ -0,0 +1,528 @@
|
|||||||
|
# Ontario energy — OEB leave to construct, IESO connection assessment, Bill 40 and the data-centre regime
|
||||||
|
|
||||||
|
Committed under AGENTS.md R14 and the CLAUDE.md rule it encodes: **anything a
|
||||||
|
spec makes a claim about must be reachable from the repository.** Every fact
|
||||||
|
the six `/practice/*` pages state about the world is checkable here or it is
|
||||||
|
not published.
|
||||||
|
|
||||||
|
**Retrieved 2026-08-29.** Fetched from the primary sources listed below and
|
||||||
|
extracted with quotations pasted verbatim. This file is the artefact; the pages
|
||||||
|
cite it. Do not paraphrase a fact into a page that is not stated here.
|
||||||
|
|
||||||
|
> ⚠️ **A statute, a regulation and a tribunal page all move.** Every consolidation
|
||||||
|
> date and effective date below is as at the retrieval date and nothing more.
|
||||||
|
> Re-check before cutover, and before any republish that turns on one of them.
|
||||||
|
|
||||||
|
> ### R18 re-check — cutover pass, 2026-09-01
|
||||||
|
>
|
||||||
|
> The trigger fired. `AGENTS.md` §12 R18 holds the per-limb findings and
|
||||||
|
> the sources; this stamp does not restate them. **The quoted bytes below
|
||||||
|
> are still the original retrieval and were not re-fetched** — what was
|
||||||
|
> re-checked is whether the *facts* they support have moved.
|
||||||
|
>
|
||||||
|
> - **(c) ERO 026-0853 — RE-VERIFIED UNMOVED** `[re-checked 2026-09-01 —
|
||||||
|
> Pouya]`. The comment period to 2026-09-12 is still open.
|
||||||
|
> - **(b) the regulation under `Electricity Act` s. 28.1 — NOT RE-RETRIEVED.**
|
||||||
|
> Held unchanged on a cadence judgement rather than a fresh retrieval
|
||||||
|
> `[assumed 2026-09-01 — Pouya: unchanged by its nature at this cadence]`.
|
||||||
|
> That is a weaker stamp than (c) and is written weaker on purpose.
|
||||||
|
|
||||||
|
**Topic as researched:** Ontario electricity/energy regulatory processes that generate disputes: OEB leave to construct (OEB Act, 1998 ss. 90/92/95/96), the IESO and market participation, the IESO connection assessment process (SIA/CIA), large-load and data-centre grid connection (Electricity Act, 1998 s. 28.1), and Ontario Bill 40
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
| Kind | Source | URL |
|
||||||
|
|---|---|---|
|
||||||
|
| statute | Ontario Energy Board Act, 1998, S.O. 1998, c. 15, Sched. B — e-Laws (ontario.ca), Government of Ontario | <https://www.ontario.ca/laws/statute/98o15> |
|
||||||
|
| statute | Electricity Act, 1998, S.O. 1998, c. 15, Sched. A — e-Laws (ontario.ca), Government of Ontario | <https://www.ontario.ca/laws/statute/98e15> |
|
||||||
|
| regulation | O. Reg. 161/99: DEFINITIONS AND EXEMPTIONS, under the Ontario Energy Board Act, 1998 — e-Laws (ontario.ca) | <https://www.ontario.ca/laws/regulation/990161> |
|
||||||
|
| regulator | Leave to construct applications for priority transmission projects — Ontario Energy Board | <https://www.oeb.ca/applications/applications-oeb/leave-construct-applications-priority-transmission-projects> |
|
||||||
|
| regulator | Natural Gas Facilities Handbook — Ontario Energy Board | <https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/natural-gas-facilities-handbook> |
|
||||||
|
| institution | Managing the Power System — IESO (Independent Electricity System Operator). Requested as https://www.ieso.ca/en/Learn/About-the-IESO/What-We-Do, which redirects here. | <https://ieso.ca/Learn/Ontario-Electricity-Grid/Managing-the-Power-System> |
|
||||||
|
| institution | Overview of the Connection Process — IESO | <https://www.ieso.ca/Sector-Participants/Connection-Process/Overview> |
|
||||||
|
| institution | Stage 2 – Obtain conditional approval to connect — IESO | <https://www.ieso.ca/Sector-Participants/Connection-Process/Obtain-Approval> |
|
||||||
|
| institution | Stage 4 – Authorize market and program participation — IESO | <https://www.ieso.ca/en/Sector-Participants/Connection-Process/Authorize-Market-and-Program-Participation> |
|
||||||
|
| institution | Frequently Asked Questions - Connection Process — IESO | <https://www.ieso.ca/Sector-Participants/Connection-Process/Frequently-Asked-Questions> |
|
||||||
|
| statute | Bill 40, Protect Ontario by Securing Affordable Energy for Generations Act, 2025 — Legislative Assembly of Ontario | <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40> |
|
||||||
|
| statute | Bill 40 (44th Parliament, 1st Session) — Status — Legislative Assembly of Ontario | <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40/status> |
|
||||||
|
| statute | Bill 40, Moving Ontarians Safely Act, 2023 — Legislative Assembly of Ontario | <https://www.ola.org/en/legislative-business/bills/parliament-43/session-1/bill-40> |
|
||||||
|
| statute | Bill 40, Support for Adults in Need of Assistance Act, 2021 — Legislative Assembly of Ontario | <https://www.ola.org/en/legislative-business/bills/parliament-42/session-2/bill-40> |
|
||||||
|
| regulator | Economic and Strategic Assessment Framework for New Data Centres — Environmental Registry of Ontario (ERO 026-0853), Ministry of Energy and Mines | <https://ero.ontario.ca/notice/026-0853> |
|
||||||
|
| regulator | New Requirements for Data Centres Seeking to Connect to the Electricity Grid in Ontario — Environmental Registry of Ontario (ERO 025-1001), Ministry of Energy and Mines | <https://ero.ontario.ca/notice/025-1001> |
|
||||||
|
| regulator | Distribution System Code (DSC) — Ontario Energy Board | <https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/distribution-system-code-dsc> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verbatim quotations
|
||||||
|
|
||||||
|
### Ontario Energy Board Act, 1998, S.O. 1998, c. 15, Sched. B — e-Laws (ontario.ca), Government of Ontario
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/98o15> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Ontario Energy Board Act, 1998 / S.O. 1998, CHAPTER 15 / Schedule B
|
||||||
|
|
||||||
|
> Consolidation Period: From December 11, 2025 to the e-Laws currency date.
|
||||||
|
|
||||||
|
> Last amendment: 2025, c. 22, Sched. 3.
|
||||||
|
|
||||||
|
> PART VI — TRANSMISSION AND DISTRIBUTION LINES
|
||||||
|
|
||||||
|
> Definitions, Part VI — 89 In this Part, "electricity distribution line" means a line, transformers, plant or equipment used for conveying electricity at voltages of 50 kilovolts or less; ("ligne de distribution d'électricité")
|
||||||
|
|
||||||
|
> "electricity transmission line" means a line, transformers, plant or equipment used for conveying electricity at voltages higher than 50 kilovolts; ("ligne de transport d'électricité")
|
||||||
|
|
||||||
|
> "hydrocarbon line" means a pipe line carrying any hydrocarbon, other than a pipe line within an oil refinery, oil or petroleum storage depot, chemical processing plant or pipe line terminal or station; ("ligne pour hydrocarbures")
|
||||||
|
|
||||||
|
> "work" means a hydrocarbon line, electricity distribution line, electricity transmission line, interconnection or station. ("ouvrage") 1998, c. 15, Sched. B, s. 89; 2003, c. 3, s. 62.
|
||||||
|
|
||||||
|
> Leave to construct hydrocarbon line — 90 (1) No person shall construct a hydrocarbon line without first obtaining from the Board an order granting leave to construct the hydrocarbon line if, (a) the proposed hydrocarbon line is more than 20 kilometres in length; (b) the proposed hydrocarbon line is projected to cost more than the amount prescribed by the regulations; (c) any part of the proposed hydrocarbon line, (i) uses pipe that has a nominal pipe size of 12 inches or more, and (ii) has an operating pressure of 2,000 kilopascals or more; or (d) criteria prescribed by the regulations are met. 2003, c. 3, s. 63 (1).
|
||||||
|
|
||||||
|
> Exception — 90 (2) Subsection (1) applies to the relocation or reconstruction of a hydrocarbon line only if the conditions prescribed by the regulations are met. 2024, c. 16, Sched. 11, s. 1.
|
||||||
|
|
||||||
|
> Application for leave to construct hydrocarbon line or station — 91 (1) Any person may, before constructing a hydrocarbon line to which subsection 90 (1) does not apply or a station, apply to the Board for an order granting leave to construct the hydrocarbon line or station. 2024, c. 10, s. 6.
|
||||||
|
|
||||||
|
> Leave to construct, etc., electricity transmission or distribution line — 92 (1) No person shall construct, expand or reinforce an electricity transmission line or an electricity distribution line or make an interconnection without first obtaining from the Board an order granting leave to construct, expand or reinforce such line or interconnection. 1998, c. 15, Sched. B, s. 92 (1).
|
||||||
|
|
||||||
|
> Exception — 92 (2) Subsection (1) applies to the relocation or reconstruction of an existing electricity transmission line or electricity distribution line or interconnection where no expansion or reinforcement is involved only if the acquisition of additional land or authority to use additional land is necessary. 2024, c. 16, Sched. 11, s. 2.
|
||||||
|
|
||||||
|
> Route map — 94 An applicant for an order granting leave under this Part shall file with the application a map showing the general location of the proposed work and the municipalities, highways, railways, utility lines and navigable waters through, under, over, upon or across which the proposed work is to pass. 1998, c. 15, Sched. B, s. 94.
|
||||||
|
|
||||||
|
> Exemption, subs. 90 (1) or 92 (1) — 95 (1) The Board may, if in its opinion special circumstances of a particular case so require, make an order exempting any person from the requirements of subsection 90 (1) or 92 (1) without a hearing. 2024, c. 10, s. 7.
|
||||||
|
|
||||||
|
> Same, prescribed circumstances — 95 (2) The Board shall, with or without a hearing, make an order exempting a person from the requirements of subsection 90 (1) or 92 (1) if the Board is satisfied that the circumstances prescribed by the regulations have been met. 2024, c. 10, s. 7.
|
||||||
|
|
||||||
|
> Order allowing work to be carried out — 96 (1) If, after considering an application under section 90, 91 or 92 the Board is of the opinion that the construction, expansion or reinforcement of the proposed work is in the public interest, it shall make an order granting leave to carry out the work. 1998, c. 15, Sched. B, s. 96.
|
||||||
|
|
||||||
|
> Applications under s. 92 — 96 (2) In an application under section 92, the Board shall only consider the following when, under subsection (1), it considers whether the construction, expansion or reinforcement of the electricity transmission line or electricity distribution line, or the making of the interconnection, is in the public interest: 1. The interests of consumers with respect to prices and the reliability and quality of electricity service. 2. Supporting economic growth in a manner consistent with the policies of the Government of Ontario. 2009, c. 12, Sched. D, s. 16; 2021, c. 25, Sched. 19, s. 2; 2025, c. 22, Sched. 3, s. 9 (1).
|
||||||
|
|
||||||
|
> Same — 96 (3) In an application under section 92, the Board shall consider such reports, documents or other information as may be prescribed by the regulations. 2025, c. 22, Sched. 3, s. 9 (2).
|
||||||
|
|
||||||
|
> Section Amendments with date in force (d/m/y) — 2025, c. 22, Sched. 3, s. 9 (1, 2) - 11/12/2025
|
||||||
|
|
||||||
|
> Lieutenant Governor in Council, order re electricity transmission line — 96.1 (1) The Lieutenant Governor in Council may make an order declaring that the construction, expansion or reinforcement of an electricity transmission line specified in the order is needed as a priority project. 2015, c. 29, s. 16.
|
||||||
|
|
||||||
|
### Electricity Act, 1998, S.O. 1998, c. 15, Sched. A — e-Laws (ontario.ca), Government of Ontario
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/98e15> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Electricity Act, 1998 / S.O. 1998, CHAPTER 15 / Schedule A
|
||||||
|
|
||||||
|
> Consolidation Period: From April 24, 2026 to the e-Laws currency date.
|
||||||
|
|
||||||
|
> Last amendment: 2026, c. 2, Sched. 5, s. 9.
|
||||||
|
|
||||||
|
> "market participant" means a person who is authorized by the market rules to participate in the IESO-administered markets or to cause or permit electricity to be conveyed into, through or out of the IESO-controlled grid; ("intervenant du marché")
|
||||||
|
|
||||||
|
> "market rules" means the rules made under section 32; ("règles du marché")
|
||||||
|
|
||||||
|
> Objects — 6 (1) The objects of the IESO are, (a) to exercise the powers and perform the duties assigned to it under this Act, the regulations, directions, the market rules and its licence;
|
||||||
|
|
||||||
|
> (c) to direct the operation and maintain the reliability of the IESO-controlled grid to promote the purposes of this Act;
|
||||||
|
|
||||||
|
> (g) to operate the IESO-administered markets to promote the purposes of this Act;
|
||||||
|
|
||||||
|
> (l) to conduct independent planning for electricity generation, demand management, conservation and transmission;
|
||||||
|
|
||||||
|
> (l.1) to support economic growth in a manner that protects the interests of consumers;
|
||||||
|
|
||||||
|
> Prohibition — (5) The IESO shall not conduct the operations of the IESO-administered markets in any manner that, (a) unjustly advantages or disadvantages any market participant or class of market participants; or (b) is inconsistent with this Act. 2014, c. 7, Sched. 7, s. 3 (1).
|
||||||
|
|
||||||
|
> Exceptions for specified load facilities — 28.1 (1) In this section, "specified connection requirements" means, in relation to a specified load facility, (a) any conditions, requirements or limitations that the regulations specify must be met for the specified load facility to be connected or reconnected to a transmission system or distribution system, as applicable, (b) any approvals that the regulations specify must be obtained for the specified load facility to be connected or reconnected to a transmission system or distribution system, as applicable, and (c) any conditions, requirements or limitations of an approval described in clause (b); ("exigences précisées en matière de raccordement")
|
||||||
|
|
||||||
|
> "specified load facility" means a facility or class of facilities, (a) that is a data centre and that meets any criteria that may be set out in the regulations, or (b) that, (i) withdraws or would, if it were connected to a transmission system or distribution system, be expected to withdraw electricity from the IESO-controlled grid or from the distribution system of a distributor who is licensed under Part V of the Ontario Energy Board Act, 1998, (ii) has or would, if it were connected to a transmission system or distribution system, be expected to have a demand for electricity at the point of connection to the transmission system or distribution system, as applicable, that exceeds the amount prescribed by the regulations, and (iii) meets any other criteria that may be set out in the regulations. ("installation de charge précisée") 2025, c. 22, Sched. 1, s. 7.
|
||||||
|
|
||||||
|
> Connection or reconnection of specified load facilities — (2) Unless a transmitter or distributor is satisfied that the specified connection requirements have been complied with, the transmitter or distributor shall not, (a) connect a specified load facility to its transmission system or distribution system, as applicable; or (b) reconnect a specified load facility to the transmission or distribution system after the specified load facility was disconnected, in accordance with the regulations, as a result of the nonfulfillment or breach of any of the specified connection requirements. 2025, c. 22, Sched. 1, s. 7.
|
||||||
|
|
||||||
|
> Transition — (6) This section does not apply to a specified load facility in respect of which a connection request made in accordance with the Transmission System Code or the Distribution System Code issued under the Ontario Energy Board Act, 1998 was submitted to a transmitter or distributor, as applicable, before June 3, 2025. 2025, c. 22, Sched. 1, s. 7.
|
||||||
|
|
||||||
|
> Section Amendments with date in force (d/m/y) — 2025, c. 22, Sched. 1, s. 7 - 11/12/2025
|
||||||
|
|
||||||
|
> (f.0.1) defining "data centre" for the purposes of section 28.1;
|
||||||
|
|
||||||
|
> (f.0.2) prescribing criteria for the purposes of the definition of "specified load facility" in section 28.1, which may include, but are not limited to, criteria related to, (i) the geographic area in which the specified load facility is or may be located, (ii) the maximum volume of electricity that the specified load facility may withdraw at the point of connection to the transmission system or have distributed to it at the point of connection to the distribution system, as applicable, or (iii) the maximum demand for electricity that the specified load facility may have at the point of connection to the transmission system or distribution system, as applicable;
|
||||||
|
|
||||||
|
### O. Reg. 161/99: DEFINITIONS AND EXEMPTIONS, under the Ontario Energy Board Act, 1998 — e-Laws (ontario.ca)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/regulation/990161> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> O. Reg. 161/99: DEFINITIONS AND EXEMPTIONS, Under: Ontario Energy Board Act, 1998, S.O. 1998, c. 15, Sched. B
|
||||||
|
|
||||||
|
> ONTARIO REGULATION 161/99 — DEFINITIONS AND EXEMPTIONS — Consolidation Period: From January 1, 2024 to the e-Laws currency date. Last amendment: 376/23.
|
||||||
|
|
||||||
|
> 6.2 (1) Subsection 92 (1) of the Act does not apply to, (a) a person that constructs, expands or reinforces an electricity distribution line; (b) a person that makes an interconnection linking a distribution system with an adjacent distribution system; (c) a person that constructs or reinforces an electricity transmission line that is two kilometres or less in length; (d) a person that expands an electricity transmission line in length by two kilometres or less;
|
||||||
|
|
||||||
|
> (e) a person, other than a licensed transmitter or licensed distributor, that constructs, expands or reinforces an electricity transmission line, if the cost of the construction, expansion or reinforcement of the line is to be exclusively paid for by the person;
|
||||||
|
|
||||||
|
> (f) a person that makes an interconnection linking a transmission system with an adjacent transmission system in Ontario; or (g) a person that makes an interconnection linking a distribution system with an adjacent transmission system. O. Reg. 365/00, s. 4; O. Reg. 72/02, s. 6; O. Reg. 511/22, s. 1 (1).
|
||||||
|
|
||||||
|
### Leave to construct applications for priority transmission projects — Ontario Energy Board
|
||||||
|
|
||||||
|
<https://www.oeb.ca/applications/applications-oeb/leave-construct-applications-priority-transmission-projects> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Section 92 of the Ontario Energy Board Act, 1998 (Act) requires transmitters and distributors to obtain approval from the OEB for the construction, expansion, or reinforcement of electricity transmission and distribution lines or interconnections.
|
||||||
|
|
||||||
|
> Section 96 (1) of the Act sets out that after the OEB considers an application under section 92, if it is of the opinion that the construction, expansion or reinforcement of the proposed work is in the public interest, it shall make an order granting leave to carry out the work.
|
||||||
|
|
||||||
|
> Under section 96.1 (1) of the Act, the Lieutenant Governor in Council may make an order declaring that the construction, expansion or reinforcement of an electricity transmission line specified in the order is needed as a priority project. Even if a transmission line is declared to be a priority project, OEB approval to build the line under section 92 of the Act is still required. However, in these cases the OEB must accept that the project is needed when forming its opinion under section 96 of the Act.
|
||||||
|
|
||||||
|
> This page identifies any leave to construct applications for priority transmission projects currently in process at the OEB.
|
||||||
|
|
||||||
|
> There are currently no leave to construct applications before the OEB for approval to build a transmission line that has been declared to be a priority project under section 96.1 of the Act.
|
||||||
|
|
||||||
|
### Natural Gas Facilities Handbook — Ontario Energy Board
|
||||||
|
|
||||||
|
<https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/natural-gas-facilities-handbook> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The Natural Gas Facilities Handbook provides guidance related to the following application types:
|
||||||
|
|
||||||
|
> 2. Under the Ontario Energy Board Act — Section 38, Designated Storage Area Applications; Section 40, DSA Well Drilling Licence Application Referrals; Section 90 and 91, Leave to Construct Applications; Section 95, Exemption from the Requirements of Section 90 Applications; Section 99, Expropriation Applications related to Leave to Construct Approvals
|
||||||
|
|
||||||
|
> On September 24, 2024, the OEB issued new Filing Requirements applicable to applications for exemption from leave to construct applications contemplated in sections 90(2) and 95(2) of the Ontario Energy Board Act, 1998.
|
||||||
|
|
||||||
|
> On April 3, 2024, the OEB issued a letter to natural gas distributors regarding minor revisions that it has made to the standard conditions of approval typically attached to the OEB's decisions approving natural gas leave to construct applications.
|
||||||
|
|
||||||
|
### Managing the Power System — IESO (Independent Electricity System Operator). Requested as https://www.ieso.ca/en/Learn/About-the-IESO/What-We-Do, which redirects here.
|
||||||
|
|
||||||
|
<https://ieso.ca/Learn/Ontario-Electricity-Grid/Managing-the-Power-System> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The IESO is the coordinator and integrator of Ontario's electricity system. Our system operators monitor the energy needs of the province in real time – 24 hours a day, 7 days a week – balancing supply and demand and directing the flow of electricity across Ontario's transmission lines.
|
||||||
|
|
||||||
|
> The IESO works with our partners in the electricity sector, as well as our Canadian and U.S. neighbours, to ensure that Ontario's grid operates reliably and that everyone in Ontario has access to the energy they need, when and where they need it.
|
||||||
|
|
||||||
|
> The IESO is responsible for ensuring these five pillars of electricity system reliability are met: Capacity: The ability to produce enough electricity to meet demand; Energy: The ability to produce electricity consistently over time; Transmission: The ability to distribute and deliver electricity; Operability: The ability to respond to changes in demand; Ancillary Services: Technical support that fine-tunes the electricity grid to adapt to second-by-second changes in demand.
|
||||||
|
|
||||||
|
> Because the IESO controls all dispatch instructions for the province, our system operators can ensure that Ontario's electricity suppliers are not over or under producing at any given time and that the system is running as efficiently and cost-effectively as possible.
|
||||||
|
|
||||||
|
> Copyright © 2026 Independent Electricity System Operator
|
||||||
|
|
||||||
|
### Overview of the Connection Process — IESO
|
||||||
|
|
||||||
|
<https://www.ieso.ca/Sector-Participants/Connection-Process/Overview> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The process to connect a new facility or to modify an existing facility involves up to six stages as described below.
|
||||||
|
|
||||||
|
> New or modified connections to a transmitter's system are generally subject to all six stages, while new or modified connections to a distributor's system may only be subject to the first three.
|
||||||
|
|
||||||
|
> You are required to successfully complete all applicable stages to receive final approval to connect your new or modified facility to the electricity system and begin commercial operation.
|
||||||
|
|
||||||
|
> The entire process can take anywhere from a few months for small modifications to existing facilities, to more than three years for major modifications or to connect new facilities.
|
||||||
|
|
||||||
|
> 1. Prepare application — Planned connection of new facilities and modifications to existing facilities must be assessed to identify and mitigate any potential adverse effect on the reliability of the electricity grid and its existing customers. To ensure that the applicable processes are initiated, as a first step, please contact your transmitter or distributor.
|
||||||
|
|
||||||
|
> 2. Obtain conditional approval to connect — Your organization is required to obtain conditional approval for new or modified connections through the appropriate processes. Successful completion of the connection assessment process results in a conditional approval to connect. This stage typically takes one year.
|
||||||
|
|
||||||
|
> Transmission system connections — New connections or modifications to facilities connected to a transmitter's system are subject to the IESO's system impact assessment (SIA) and the transmitter's customer impact assessment (CIA).
|
||||||
|
|
||||||
|
> Distribution system connections — New connections or modifications to facilities connected to a distributor's system must participate in the distributor's connection assessment process. As part of this process, distributors may need to participate in the IESO's and transmitter's connection assessment processes to obtain their conditional approval to connect on your behalf.
|
||||||
|
|
||||||
|
> 4. Authorize market and program participation — To participate in the IESO-controlled grid, IESO-administered markets or programs, you must register your organization with the IESO to authorize it as a market or program participant. This stage typically takes about one month.
|
||||||
|
|
||||||
|
> 5. Register equipment — The equipment, telemetry, and metering installed at your facility must be registered and tested with the IESO. The IESO uses the data provided by market participants for the systems and models that are necessary to maintain the reliability of the IESO-controlled grid and to operate the IESO-administered markets. This stage takes at least three months.
|
||||||
|
|
||||||
|
> Successful completion of all six stages, to the satisfaction of the IESO, is required to obtain final approval to connect to the IESO-controlled grid, and start commercial operation in the IESO-administered markets.
|
||||||
|
|
||||||
|
> If you're exploring connection options and aren't ready to submit a formal request, start with our Major Projects Identification Committees (MPIC) process.
|
||||||
|
|
||||||
|
> Whether your organization is an existing or potential market participant, you can explore options for a new or modified connection with the IESO's technical feasibility study (TFS). This optional, confidential service is provided on a cost-recovery basis to identify and mitigate potential issues with various connection options, and help participants select a final connection option.
|
||||||
|
|
||||||
|
### Stage 2 – Obtain conditional approval to connect — IESO
|
||||||
|
|
||||||
|
<https://www.ieso.ca/Sector-Participants/Connection-Process/Obtain-Approval> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> If you determined during the previous stage that your organization is required to participate in the IESO's and transmitter's connection assessment and approval (CAA) process, please contact the IESO for a pre-application meeting.
|
||||||
|
|
||||||
|
> Upon receipt, the IESO will determine if the application qualifies for a system impact assessment (SIA) or an expedited system impact assessment (ESIA) and will assign a unique CAA ID.
|
||||||
|
|
||||||
|
> Step 2 – Execute the system impact assessment (SIA) agreement — After receiving your application and associated deposit for a connection assessment, the IESO will prepare an SIA agreement in accordance with section 6.1.15.3 of chapter 0.4 of the Market Rules for execution by your authorized representative.
|
||||||
|
|
||||||
|
> Step 3 – IESO completes SIA studies and produces draft report — Once you have provided all of the required information, the IESO will assess the impact of your proposed new or modified connection on the reliability of the integrated power system and issue a draft SIA report to your organization and the transmitter for review and comments.
|
||||||
|
|
||||||
|
> The transmitter generally initiates the customer impact assessment (CIA) after the draft SIA report from the IESO. A CIA agreement between the connection applicant and transmitter is also needed as part of the transmitter's CIA process.
|
||||||
|
|
||||||
|
> Step 4 – IESO issues final SIA report — After addressing comments received from your organization and the transmitter on the draft or revised draft SIA report, the IESO sends the final SIA report and one of the following to both parties: Notification of conditional approval (NoCA); Notification of disapproval with reasons (NoDR)
|
||||||
|
|
||||||
|
> The final SIA report will be published on the IESO website in the Application Status table at the end of the month in which it was finalized.
|
||||||
|
|
||||||
|
### Stage 4 – Authorize market and program participation — IESO
|
||||||
|
|
||||||
|
<https://www.ieso.ca/en/Sector-Participants/Connection-Process/Authorize-Market-and-Program-Participation> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Your organization must be authorized by the IESO to participate in the IESO-administered markets or programs, or to connect a physical facility to the IESO-controlled grid.
|
||||||
|
|
||||||
|
> Step 1 – Register organization — If your organization is new to the IESO, you must register prior to receiving authorization. To register, your organization must submit company information and appoint mandatory contact roles using the IESO's online application form... It may take up to three weeks to complete the registration process.
|
||||||
|
|
||||||
|
> Step 2 - Authorize as a market participant — The authorized representative appoints other roles, including the applicant representative, who will be responsible for authorizing your newly registered organization as a market participant using Online IESO (the IESO's web-based registration system).
|
||||||
|
|
||||||
|
> Paying the market registration application fee $1,130 ($1,000 + 13% HST)
|
||||||
|
|
||||||
|
> Providing the following: Ontario Energy Board (OEB) licence; National Energy Permit (for energy trader – exporter only); Prudential support if your organization plans to participate in the real-time IESO-administered markets
|
||||||
|
|
||||||
|
> Step 4 – IESO issues registration approval notification — Once the above authorization tasks are completed, the IESO will issue a registration approval notification (RAN) confirming that your organization has been approved by the IESO to participate in the IESO-administered markets and programs.
|
||||||
|
|
||||||
|
### Frequently Asked Questions - Connection Process — IESO
|
||||||
|
|
||||||
|
<https://www.ieso.ca/Sector-Participants/Connection-Process/Frequently-Asked-Questions> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> FAQ - Connection Assessments — What tools and models do the IESO use for System Impact Assessments (SIAs)? The IESO uses DSA and PSSE tools to conduct SIA studies.
|
||||||
|
|
||||||
|
> Is there an expedited process available in Ontario to skip the interconnection queue, similar to the US market? The IESO is not using an "interconnection queue", instead it adopted the concept of "committed projects" that is defined in Section 3.3 of Market Manual 1.4: Connection Assessment and Approval. As such, there is no option to "skip the interconnection queue", each assessment follows the timelines described in Section 5.8 of Market Manual 1.4: Connection Assessment and Approval.
|
||||||
|
|
||||||
|
> The treatment of new renewable generation facilities is no different than any other new facility, the normal System Impact Assessment (SIA) process applies to the connection of all generation facilities, renewable or non-renewable, equally.
|
||||||
|
|
||||||
|
> Where can I find the required application forms and data requirements for submitting a connection application to the IESO? You'll need to complete Form 128 and send it via email to connection.assessments@ieso.ca to initiate the SIA process.
|
||||||
|
|
||||||
|
### Bill 40, Protect Ontario by Securing Affordable Energy for Generations Act, 2025 — Legislative Assembly of Ontario
|
||||||
|
|
||||||
|
<https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Bill 40, Protect Ontario by Securing Affordable Energy for Generations Act, 2025
|
||||||
|
|
||||||
|
> Lecce, Hon. Stephen (Minister of Energy and Mines)
|
||||||
|
|
||||||
|
> Current status: Royal Assent received. Statutes of Ontario 2025, chapter 22
|
||||||
|
|
||||||
|
> Bill 40 has been enacted as Chapter 22 of the Statutes of Ontario, 2025.
|
||||||
|
|
||||||
|
> Bill 40 2025 — An Act to amend various statutes with respect to energy, the electrical sector and public utilities
|
||||||
|
|
||||||
|
> CONTENTS — Preamble; 1. Contents of this Act; 2. Commencement; 3. Short title; Schedule 1 Electricity Act, 1998; Schedule 2 Municipal Franchises Act; Schedule 3 Ontario Energy Board Act, 1998
|
||||||
|
|
||||||
|
> Preamble — The Government of Ontario is committed to: Building an affordable, secure, reliable and clean energy system to power the strongest economy in the G7. Prioritizing economic growth, job creation and strong governance as objectives of Ontario's energy system. Supporting the responsible growth of energy-intensive industries like data centres that align with Ontario's economic priorities and benefit local communities. Keeping our energy supply secure by supporting the sector with the implementation of limitations on foreign participation in Ontario's energy sector. Delivering affordable and clean energy for generations to power our economy and peoples' lives.
|
||||||
|
|
||||||
|
> Short title — 3 The short title of this Act is the Protect Ontario by Securing Affordable Energy for Generations Act, 2025.
|
||||||
|
|
||||||
|
> SCHEDULE 1 ELECTRICITY ACT, 1998 ... 4. New section 28.1 is added to the Act. It provides that transmitters or distributors shall not connect or reconnect certain load facilities from its transmission system or distribution system unless connection requirements that are specified in the regulations are met. Complementary amendments are made to the regulation-making authority in section 114.
|
||||||
|
|
||||||
|
> SCHEDULE 3 ONTARIO ENERGY BOARD ACT, 1998 ... 5. Section 96 of the Act is amended in respect of applications under section 92 of the Act (leave to construct, etc., electricity transmission or distribution line). Economic growth is added to the list of matters the Board is permitted to consider when deciding whether granting leave is in the public interest. As well, the Board is required to consider reports, documents or other information that may be specified by the regulations made under the Act in considering an application under section 92.
|
||||||
|
|
||||||
|
> SCHEDULE 3 ... 1. Subsection 1 (1) of the Act is amended to add a new objective of the Ontario Energy Board respecting economic growth in relation to the regulation of the electricity sector. 2. A new section 13.1 of the Act authorizes the Board's chief executive officer to issue internal policies respecting various procedural matters in relation to hearings and determinations.
|
||||||
|
|
||||||
|
> SCHEDULE 2 MUNICIPAL FRANCHISES ACT — ... Section 3 is re-enacted to remove the requirement for the municipal electors to assent to such a by-law and instead to require that a municipality pass a by-law setting out the terms and conditions.
|
||||||
|
|
||||||
|
### Bill 40 (44th Parliament, 1st Session) — Status — Legislative Assembly of Ontario
|
||||||
|
|
||||||
|
<https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40/status> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Date | Bill stage | Event | Outcome | Committee
|
||||||
|
|
||||||
|
> December 11, 2025 | Royal Assent | Royal Assent received | - | -
|
||||||
|
|
||||||
|
> December 8, 2025 | Third Reading | Vote | Carried on division | -
|
||||||
|
|
||||||
|
> December 3, 2025 | Second Reading | Reported as amended | - | Standing Committee on the Interior
|
||||||
|
|
||||||
|
> November 17, 2025 | Second Reading | Ordered referred to Standing Committee pursuant to the Order of the House | - | Standing Committee on the Interior
|
||||||
|
|
||||||
|
> June 3, 2025 | First Reading | Ordered for Second Reading | - | -
|
||||||
|
|
||||||
|
> June 3, 2025 | First Reading | Vote | Carried | -
|
||||||
|
|
||||||
|
### Bill 40, Moving Ontarians Safely Act, 2023 — Legislative Assembly of Ontario
|
||||||
|
|
||||||
|
<https://www.ola.org/en/legislative-business/bills/parliament-43/session-1/bill-40> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Parliament 43, Session 1 — Bill 40, Moving Ontarians Safely Act, 2023
|
||||||
|
|
||||||
|
> The Bill amends the Highway Traffic Act. It is about the legal consequences of a collision that seriously injures or kills a pedestrian, a cyclist, a mobility device user, a roadway worker, an emergency responder outside their motor vehicle or another individual listed in the Bill.
|
||||||
|
|
||||||
|
### Bill 40, Support for Adults in Need of Assistance Act, 2021 — Legislative Assembly of Ontario
|
||||||
|
|
||||||
|
<https://www.ola.org/en/legislative-business/bills/parliament-42/session-2/bill-40> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Parliament 42, Session 2 — Bill 40, Support for Adults in Need of Assistance Act, 2021
|
||||||
|
|
||||||
|
> The Bill enacts the Support for Adults in Need of Assistance Act, 2021. The Act requires regulated health professionals to report to a board of health if they have reasonable suspicion that an individual who is 16 years of age or older is being abused or neglected.
|
||||||
|
|
||||||
|
### Economic and Strategic Assessment Framework for New Data Centres — Environmental Registry of Ontario (ERO 026-0853), Ministry of Energy and Mines
|
||||||
|
|
||||||
|
<https://ero.ontario.ca/notice/026-0853> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Economic and Strategic Assessment Framework for New Data Centres — ERO number 026-0853 — Notice type: Regulation — Posted by: Ministry of Energy and Mines — Notice stage: Proposal — Proposal posted: August 13, 2026 — Comment period: August 13, 2026 - September 12, 2026 (30 days) Open — Last updated: August 13, 2026
|
||||||
|
|
||||||
|
> This consultation closes at 11:59 p.m. on: September 12, 2026
|
||||||
|
|
||||||
|
> To ensure responsible data centre growth, the government introduced legislative amendments to the Electricity Act, 1998 that provide the Lieutenant Governor in Council the authority to set out in regulation requirements that data centres covered by the regulation must meet before connecting or re-connecting to the electricity grid.
|
||||||
|
|
||||||
|
> Data centres that do not meet these requirements would not be able to connect (or re-connect) to the electricity grid. The province is considering drafting a proposed regulation that, if approved, would require new large data centres to obtain the approval of the government to connect or reconnect to the electricity grid.
|
||||||
|
|
||||||
|
> To guide such approvals and attract the best data centre investments that drive economic growth, ensure Canadians' data remains in Canada and deliver significant and meaningful benefits to local communities, Ontario is proposing a Data Centre Playbook.
|
||||||
|
|
||||||
|
> Assessing proposed data centres against these pillars could form part of the broader proposed regulatory data centre connection process requiring connection approval by the government, alongside other considerations related to electricity system reliability, technical feasibility and cost responsibility.
|
||||||
|
|
||||||
|
> Pillar 1: Advance Economic Development ... Pillar 2: Protect Data Security and Digital Sovereignty ... Pillar 3: Invest in Communities and Earn Public Trust
|
||||||
|
|
||||||
|
> The government is therefore exploring amendments to Ontario Regulation 429/04 under the Electricity Act, 1998 that would establish a new separate rate Class (e.g., Class C) for new data centres above a specific demand threshold (e.g., 1 MW). Facilities in this rate class would not be eligible to participate in the Industrial Conservation Initiative (ICI), a demand response program for large electricity customers.
|
||||||
|
|
||||||
|
> In addition, there are a significant number of data centres that have applied for connections. MEM estimates these proposals could total more than 10,000 MW cumulatively.
|
||||||
|
|
||||||
|
### New Requirements for Data Centres Seeking to Connect to the Electricity Grid in Ontario — Environmental Registry of Ontario (ERO 025-1001), Ministry of Energy and Mines
|
||||||
|
|
||||||
|
<https://ero.ontario.ca/notice/025-1001> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> New Requirements for Data Centres Seeking to Connect to the Electricity Grid in Ontario — ERO number 025-1001 — Notice type: Regulation — Posted by: Ministry of Energy and Mines — Notice stage: Proposal Updated — Proposal posted: September 5, 2025 — Comment period: September 5, 2025 - November 4, 2025 (60 days) Closed — Last updated: September 8, 2025
|
||||||
|
|
||||||
|
> The Ministry of Energy and Mines (MEM) introduced amendments to the Electricity Act, 1998 as part of Bill 40. If passed, the government is proposing to set out in regulation a process that will allow the Minister of Energy and Mines to prioritize and approve connection requests from data centre projects that serve the province's economic interests.
|
||||||
|
|
||||||
|
> The proposal contemplates implementing an approval process for connection requests of data centres covered by the proposed regulation.
|
||||||
|
|
||||||
|
> If the legislation passes, the proposed regulation will require covered data centre projects to receive approval from the Minister of Energy and Mines to connect to the provincial electricity grid.
|
||||||
|
|
||||||
|
> The data centre sector is forecast to represent about 13 per cent of new electricity demand in the province by 2035.
|
||||||
|
|
||||||
|
> The government is seeking input on the types of data centres that would be subject to the proposed requirement, including the electrica[l]
|
||||||
|
|
||||||
|
### Distribution System Code (DSC) — Ontario Energy Board
|
||||||
|
|
||||||
|
<https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/distribution-system-code-dsc> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Distribution System Code (DSC) — Sets out the minimum obligations that a licensed electricity distributor must meet in carrying out its obligations to distribute electricity within its service area under its licence.
|
||||||
|
|
||||||
|
> Last revised August 1, 2026
|
||||||
|
|
||||||
|
> Appendix I - Methodology for Implementing a Capacity Allocation Model (pdf)
|
||||||
|
|
||||||
|
> 40. Amendment to introduce Capacity Allocation Model (CAM) to facilitate housing development connections and to create Appendix I to set out the CAM methodology. This change came into force on September 16, 2025 (EB- 2024-0092).
|
||||||
|
|
||||||
|
> DER Connection Procedures (DERCP) - Version 3.0 (issued December 18, 2025, came into force on May 1, 2026)
|
||||||
|
|
||||||
|
> Electric Vehicle Charging Connection Procedures (EVCCP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this establishes
|
||||||
|
|
||||||
|
Each item names the source it rests on. An item here that no quotation above
|
||||||
|
supports is a defect in this file, not a fact.
|
||||||
|
|
||||||
|
- LEAVE TO CONSTRUCT — electricity: s. 92 (1) of the Ontario Energy Board Act, 1998 is the leave-to-construct provision for electricity lines. Operative words: "No person shall construct, expand or reinforce an electricity transmission line or an electricity distribution line or make an interconnection without first obtaining from the Board an order granting leave to construct, expand or reinforce such line or interconnection."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98o15>
|
||||||
|
- LEAVE TO CONSTRUCT — hydrocarbon pipeline: s. 90 (1) is the separate leave-to-construct provision for hydrocarbon lines. Operative words: "No person shall construct a hydrocarbon line without first obtaining from the Board an order granting leave to construct the hydrocarbon line if," followed by four triggers — more than 20 km in length; projected cost above the prescribed amount; any part using pipe of nominal size 12 inches or more AND operating at 2,000 kilopascals or more; or criteria prescribed by the regulations. So s. 90 is threshold-based while s. 92 is not.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98o15>
|
||||||
|
- Both provisions sit in Part VI of the Act ("TRANSMISSION AND DISTRIBUTION LINES"). Section 89 defines "electricity transmission line" as conveying electricity above 50 kilovolts, "electricity distribution line" as 50 kilovolts or less, and "hydrocarbon line" as a pipe line carrying any hydrocarbon other than one within a refinery, storage depot, chemical processing plant or pipe line terminal or station.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98o15>
|
||||||
|
- The public-interest test is in s. 96 (1): if the Board "is of the opinion that the construction, expansion or reinforcement of the proposed work is in the public interest, it shall make an order granting leave to carry out the work." For s. 92 applications, s. 96 (2) confines what "public interest" may mean to two enumerated matters: "1. The interests of consumers with respect to prices and the reliability and quality of electricity service. 2. Supporting economic growth in a manner consistent with the policies of the Government of Ontario."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98o15>
|
||||||
|
- The second branch of the s. 96 (2) test — "Supporting economic growth in a manner consistent with the policies of the Government of Ontario" — and the new s. 96 (3) duty to consider prescribed reports were added by 2025, c. 22, Sched. 3, s. 9, in force 11/12/2025. The e-Laws consolidation period for the Act begins December 11, 2025 and its last amendment is 2025, c. 22, Sched. 3.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98o15>
|
||||||
|
- Exemptions from leave to construct are available under s. 95: the Board "may, if in its opinion special circumstances of a particular case so require, make an order exempting any person from the requirements of subsection 90 (1) or 92 (1) without a hearing" (s. 95 (1)), and "shall" exempt where prescribed circumstances are met (s. 95 (2)). Section 94 requires a route map to be filed with the application.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98o15>
|
||||||
|
- Section 96.1 (1) lets the Lieutenant Governor in Council declare a transmission line a "priority project". The OEB's own page states that even then, "OEB approval to build the line under section 92 of the Act is still required. However, in these cases the OEB must accept that the project is needed when forming its opinion under section 96 of the Act."
|
||||||
|
*Source:* <https://www.oeb.ca/applications/applications-oeb/leave-construct-applications-priority-transmission-projects>
|
||||||
|
- OEB's own description of the s. 92 process: "Section 92 of the Ontario Energy Board Act, 1998 (Act) requires transmitters and distributors to obtain approval from the OEB for the construction, expansion, or reinforcement of electricity transmission and distribution lines or interconnections."
|
||||||
|
*Source:* <https://www.oeb.ca/applications/applications-oeb/leave-construct-applications-priority-transmission-projects>
|
||||||
|
- As of retrieval on 2026-08-29, the OEB's priority-transmission page states: "There are currently no leave to construct applications before the OEB for approval to build a transmission line that has been declared to be a priority project under section 96.1 of the Act." (This is a point-in-time statement and will change.)
|
||||||
|
*Source:* <https://www.oeb.ca/applications/applications-oeb/leave-construct-applications-priority-transmission-projects>
|
||||||
|
- On the natural-gas side, the OEB publishes a Natural Gas Facilities Handbook giving guidance on, among others, "Section 90 and 91, Leave to Construct Applications", "Section 95, Exemption from the Requirements of Section 90 Applications", and "Section 99, Expropriation Applications related to Leave to Construct Approvals". On September 24, 2024 the OEB issued new filing requirements for exemption applications under ss. 90(2) and 95(2).
|
||||||
|
*Source:* <https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/natural-gas-facilities-handbook>
|
||||||
|
- O. Reg. 161/99 (Definitions and Exemptions) under the OEB Act, s. 6.2 (1), exempts several categories from s. 92 (1) — including "a person that constructs, expands or reinforces an electricity distribution line" and "a person that constructs or reinforces an electricity transmission line that is two kilometres or less in length". This is the source of the commonly cited 2 km transmission threshold; distribution lines are exempt outright by regulation notwithstanding their inclusion in s. 92 (1).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/990161>
|
||||||
|
- THE IESO, in its own words: "The IESO is the coordinator and integrator of Ontario's electricity system. Our system operators monitor the energy needs of the province in real time – 24 hours a day, 7 days a week – balancing supply and demand and directing the flow of electricity across Ontario's transmission lines." It names five reliability pillars: Capacity, Energy, Transmission, Operability and Ancillary Services.
|
||||||
|
*Source:* <https://ieso.ca/Learn/Ontario-Electricity-Grid/Managing-the-Power-System>
|
||||||
|
- The IESO's statutory objects are in s. 6 (1) of the Electricity Act, 1998 and include directing the operation and maintaining the reliability of the IESO-controlled grid (cl. c), operating the IESO-administered markets (cl. g), conducting independent planning (cl. l), and — added by Bill 40 — "to support economic growth in a manner that protects the interests of consumers" (cl. l.1).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98e15>
|
||||||
|
- MARKET PARTICIPATION — statutory definition: "market participant" means "a person who is authorized by the market rules to participate in the IESO-administered markets or to cause or permit electricity to be conveyed into, through or out of the IESO-controlled grid" (Electricity Act, 1998, s. 2 definitions). Section 6 (5) forbids the IESO from operating the markets in a way that "unjustly advantages or disadvantages any market participant or class of market participants".
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98e15>
|
||||||
|
- MARKET PARTICIPATION — operationally, per the IESO: "To participate in the IESO-controlled grid, IESO-administered markets or programs, you must register your organization with the IESO to authorize it as a market or program participant." Registration runs through Online IESO, requires an OEB licence, prudential support for real-time market participation, and a market registration application fee of $1,130; it ends with the IESO issuing a "registration approval notification (RAN)".
|
||||||
|
*Source:* <https://www.ieso.ca/en/Sector-Participants/Connection-Process/Authorize-Market-and-Program-Participation>
|
||||||
|
- CONNECTION PROCESS — the IESO's published connection process runs to **up to six** stages (⚠️ this read *"the IESO runs a six-stage connection process"* until 2026-09-03 — it gave the process to the IESO alone and stated the count unscoped, which are the two things the note below corrects; the quotation it rests on is the Overview's *"involves up to six stages"*): (1) Prepare application; (2) Obtain conditional approval to connect; (3) Design and build; (4) Authorize market and program participation; (5) Register equipment; (6) Commission equipment and validate performance. "New or modified connections to a transmitter's system are generally subject to all six stages, while new or modified connections to a distributor's system may only be subject to the first three."
|
||||||
|
*Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Overview>
|
||||||
|
- "System Impact Assessment" IS the IESO's real term, confirmed on multiple IESO pages. The IESO: "New connections or modifications to facilities connected to a transmitter's system are subject to the IESO's system impact assessment (SIA) and the transmitter's customer impact assessment (CIA)." The IESO conducts the SIA; the transmitter conducts the CIA.
|
||||||
|
*Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Overview>
|
||||||
|
- ⚠️ **CORRECTED 2026-09-03 — THIS LINE IS COMMENTARY AND IT MISATTRIBUTED THE PROCESS.** It read *"The umbrella name for the process is the 'connection assessment and approval (CAA)' process"*, and the pages took that from here: `/practice/energy/` published *"The IESO operates a six-stage connection process and calls it connection assessment and approval"* and `docs/01` directed *"CAA is the umbrella"*. **The IESO's own words, quoted above at the Stage 2 heading, are "the IESO's **and transmitter's** connection assessment and approval (CAA) process"**, and the Overview says the process *"involves **up to** six stages"*, scoped by connection type. Both were corrected on the pages the same day. This is `CLAUDE.md`'s point exactly — the quotations here are evidence, the prose around them is this repository's voice, and it is where a corrected page re-seeds if the commentary is left standing. The original line follows. The umbrella name for the process is the "connection assessment and approval (CAA)" process. On application the IESO "will determine if the application qualifies for a system impact assessment (SIA) or an expedited system impact assessment (ESIA) and will assign a unique CAA ID". The SIA agreement is prepared "in accordance with section 6.1.15.3 of chapter 0.4 of the Market Rules". The IESO then "will assess the impact of your proposed new or modified connection on the reliability of the integrated power system" and issues a draft, then final, SIA report accompanied by either a "Notification of conditional approval (NoCA)" or a "Notification of disapproval with reasons (NoDR)".
|
||||||
|
*Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Obtain-Approval>
|
||||||
|
- The transmitter "generally initiates the customer impact assessment (CIA) after the draft SIA report from the IESO", and a CIA agreement between the connection applicant and the transmitter is required as part of the transmitter's CIA process.
|
||||||
|
*Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Obtain-Approval>
|
||||||
|
- Ontario does NOT operate an "interconnection queue". The IESO: "The IESO is not using an 'interconnection queue', instead it adopted the concept of 'committed projects' that is defined in Section 3.3 of Market Manual 1.4: Connection Assessment and Approval. As such, there is no option to 'skip the interconnection queue'."
|
||||||
|
*Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Frequently-Asked-Questions>
|
||||||
|
- Connection timelines, per the IESO: stage 2 (conditional approval) "typically takes one year"; stage 4 "typically takes about one month"; stage 5 "takes at least three months"; and the whole process "can take anywhere from a few months for small modifications to existing facilities, to more than three years for major modifications or to connect new facilities."
|
||||||
|
*Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Overview>
|
||||||
|
- LARGE LOADS / DATA CENTRES — there IS a real, in-force Ontario statutory process. Section 28.1 of the Electricity Act, 1998 ("Exceptions for specified load facilities") came into force 11/12/2025 (added by 2025, c. 22, Sched. 1, s. 7). It provides: "Unless a transmitter or distributor is satisfied that the specified connection requirements have been complied with, the transmitter or distributor shall not, (a) connect a specified load facility to its transmission system or distribution system... or (b) reconnect a specified load facility..."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98e15>
|
||||||
|
- "Specified load facility" is defined in s. 28.1 (1) as a facility or class of facilities "(a) that is a data centre and that meets any criteria that may be set out in the regulations", OR (b) one that withdraws electricity from the IESO-controlled grid or a licensed distributor's system, has demand at the point of connection "that exceeds the amount prescribed by the regulations", and meets any other prescribed criteria. So the section is expressly aimed at data centres and at large loads defined by a regulated demand threshold.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98e15>
|
||||||
|
- Section 28.1 (6) is a transition rule: the section "does not apply to a specified load facility in respect of which a connection request made in accordance with the Transmission System Code or the Distribution System Code issued under the Ontario Energy Board Act, 1998 was submitted to a transmitter or distributor, as applicable, before June 3, 2025" — i.e. the date Bill 40 received First Reading.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/98e15>
|
||||||
|
- ⚠️ **CORRECTED 2026-08-29 — "two LIVE consultations" was wrong, and the quoted notice data three sections above says so.** ERO 025-1001's comment period ran 5 September – **4 November 2025** and the notice reads **"Closed"**; only ERO 026-0853 (13 August – 12 September 2026) was open on the retrieval date. The overreach reached `src/data/practice-pages.ts` and was caught on a self-audit against this file before it shipped. The original wording of this item follows.
|
||||||
|
- ~~There are two live Ontario government consultations specifically about connecting data centres to the grid.~~ There are two Ontario government consultations specifically about connecting data centres to the grid, **one of them closed.** ERO 025-1001, "New Requirements for Data Centres Seeking to Connect to the Electricity Grid in Ontario" (Ministry of Energy and Mines, Regulation notice, Proposal, posted September 5, 2025, comment period closed November 4, 2025): "the proposed regulation will require covered data centre projects to receive approval from the Minister of Energy and Mines to connect to the provincial electricity grid."
|
||||||
|
*Source:* <https://ero.ontario.ca/notice/025-1001>
|
||||||
|
- ERO 026-0853, "Economic and Strategic Assessment Framework for New Data Centres" (Ministry of Energy and Mines, Regulation notice, Proposal, posted August 13, 2026, comment period August 13 – September 12, 2026, OPEN as at 2026-08-29). It proposes a "Data Centre Playbook" and a three-pillar assessment (Advance Economic Development; Protect Data Security and Digital Sovereignty; Invest in Communities and Earn Public Trust), and separately proposes "amendments to Ontario Regulation 429/04 under the Electricity Act, 1998 that would establish a new separate rate Class (e.g., Class C) for new data centres above a specific demand threshold (e.g., 1 MW)", whose facilities "would not be eligible to participate in the Industrial Conservation Initiative (ICI)".
|
||||||
|
*Source:* <https://ero.ontario.ca/notice/026-0853>
|
||||||
|
- As at August 13, 2026 the connection-approval regulation under s. 28.1 was still prospective in the Ministry's own words: "The province is considering drafting a proposed regulation that, if approved, would require new large data centres to obtain the approval of the government to connect or reconnect to the electricity grid."
|
||||||
|
*Source:* <https://ero.ontario.ca/notice/026-0853>
|
||||||
|
- BILL 40 — the energy Bill 40 is Bill 40 of the 44th Parliament, 1st Session: "Protect Ontario by Securing Affordable Energy for Generations Act, 2025", sponsored by Hon. Stephen Lecce (Minister of Energy and Mines). Long title: "An Act to amend various statutes with respect to energy, the electrical sector and public utilities". Status: "Royal Assent received. Statutes of Ontario 2025, chapter 22". It has three schedules: Electricity Act, 1998; Municipal Franchises Act; Ontario Energy Board Act, 1998.
|
||||||
|
*Source:* <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40>
|
||||||
|
- Bill 40 (44-1) timeline: First Reading June 3, 2025; referred to the Standing Committee on the Interior November 17, 2025; reported as amended December 3, 2025; Third Reading vote carried on division December 8, 2025; Royal Assent December 11, 2025.
|
||||||
|
*Source:* <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40/status>
|
||||||
|
- Bill 40's preamble names data centres expressly: the Government of Ontario is committed to "Supporting the responsible growth of energy-intensive industries like data centres that align with Ontario's economic priorities and benefit local communities."
|
||||||
|
*Source:* <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40>
|
||||||
|
- Bill 40 is the direct link between the two topics above: its Schedule 1 added Electricity Act s. 28.1 (data centre / large load connection requirements) and its Schedule 3 amended OEB Act s. 96 so that "Economic growth is added to the list of matters the Board is permitted to consider when deciding whether granting leave is in the public interest" on a s. 92 leave-to-construct application, and required the Board to consider prescribed reports and documents.
|
||||||
|
*Source:* <https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40>
|
||||||
|
- THE TRAP CONFIRMED — "Bill 40" is reused every parliament and most Bill 40s are unrelated to energy. Two verified non-energy examples: Bill 40 of the 43rd Parliament, 1st Session is the "Moving Ontarians Safely Act, 2023", amending the Highway Traffic Act; and Bill 40 of the 42nd Parliament, 2nd Session is the "Support for Adults in Need of Assistance Act, 2021", on reporting abuse or neglect of adults. Only the 44-1 Bill 40 is the energy bill. Any reference to "Bill 40" in an Ontario energy context must be cited as Bill 40, 44th Parliament, 1st Session / S.O. 2025, c. 22.
|
||||||
|
*Source:* <https://www.ola.org/en/legislative-business/bills/parliament-43/session-1/bill-40>
|
||||||
|
- A real Ontario term adjacent to "connection allocation" is the OEB's "Capacity Allocation Model (CAM)" in the Distribution System Code: DSC amendment 40 "introduce[d] Capacity Allocation Model (CAM) to facilitate housing development connections and to create Appendix I to set out the CAM methodology. This change came into force on September 16, 2025 (EB- 2024-0092)." The DSC also carries DER Connection Procedures (DERCP) and Electric Vehicle Charging Connection Procedures (EVCCP).
|
||||||
|
*Source:* <https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/distribution-system-code-dsc>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this does NOT establish
|
||||||
|
|
||||||
|
**Read this section before writing copy.** It is the half that keeps a page
|
||||||
|
honest, and on this project it is the half that has twice been skipped.
|
||||||
|
|
||||||
|
- **Is "connection allocation" an actual Ontario term of art for the process by which a generator or large load gets grid access?**
|
||||||
|
- *Searched:* Full-text grep for "allocation" and "connection allocation" across all four IESO pages fetched (Connection Process Overview, Stage 2 Obtain Approval, Stage 4 Authorize Participation, Connection Process FAQ); plus WebSearch for '"connection allocation" Ontario electricity IESO OEB'.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — zero hits for "allocation" of any kind in the IESO connection-process pages. Do not use "connection allocation" as an Ontario term. The correct terms, all verified above, are: connection assessment and approval (CAA), System Impact Assessment (SIA, by the IESO), Customer Impact Assessment (CIA, by the transmitter), and Notification of Conditional Approval (NoCA). A distinct and real OEB term is "Capacity Allocation Model (CAM)" in the Distribution System Code, but it is about distribution capacity for housing developments, not transmission connection generally.
|
||||||
|
- **Has the implementing regulation under Electricity Act s. 28.1 (defining "data centre", the demand threshold for a "specified load facility", and the "specified connection requirements") actually been made?**
|
||||||
|
- *Searched:* e-Laws act-reg API listing of current regulations under the Electricity Act, 1998 (returned 50 current regulations, none titled for data centres or specified load facilities); ERO notices 025-1001 and 026-0853 fetched in full; WebSearch for '"O. Reg." Ontario "data centre" Electricity Act 1998 connection regulation'.
|
||||||
|
- *Outcome:* NOT ESTABLISHED either way, and DO NOT ASSERT ITS ABSENCE. What IS established is that as at August 13, 2026 the Ministry of Energy and Mines described the connection-approval regulation as something "the province is considering drafting". The 50-item regulation list may have been truncated by a page cap, and criteria could in principle be added to an existing regulation rather than a new one. Safe wording: "the enabling section is in force since 11 December 2025; the Ministry's August 2026 consultation still described the connection-approval regulation as under consideration." Re-verify before publishing anything about the regulation's status.
|
||||||
|
- **Is there a dedicated IESO connection process, page, or queue specifically for large loads or data centres (as distinct from the general six-stage process)?**
|
||||||
|
- *Searched:* WebSearch restricted to ieso.ca for '"data centre" OR "large load" connection IESO Ontario'; plus review of the four IESO connection-process pages fetched.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — no IESO page describing a data-centre- or large-load-specific connection process was found. The general six-stage CAA/SIA/CIA process appears to apply to load facilities as it does to generation (the IESO's CAA application form has a "Load Facilities" variant). An IESO planning paper titled "Demand & Conservation Planning Technical Paper: Large Step Loads" appeared in search results but was NOT fetched and is a planning/forecasting document, not a connection process. Note that several search results for "large load connection process" were about the AESO in Alberta, not Ontario — do not confuse the two.
|
||||||
|
- **Does the OEB publish a general (non-priority-project) explainer page for the electricity leave-to-construct process?**
|
||||||
|
- *Searched:* WebSearch on oeb.ca for leave-to-construct application/filing-requirements pages; fetched the OEB priority-transmission leave-to-construct page and the Natural Gas Facilities Handbook page.
|
||||||
|
- *Outcome:* PARTIALLY ESTABLISHED. The OEB page fetched and quoted is specifically the priority-transmission-projects page, though its text describes ss. 92, 96(1) and 96.1(1) generally. An 'OEB-Electricity-Leave-to-Construct-Filing-Requirements-20230316.pdf' and an 'issues-list-LTC-electricity.pdf' appeared in search results but were NOT fetched, and the 2023 filing requirements would predate the 2024 and 2025 statutory amendments verified above. Do not cite them.
|
||||||
|
- **Typical duration or cost of an OEB section 92 leave-to-construct hearing.**
|
||||||
|
- *Searched:* WebSearch for the OEB section 92 application process (a snippet asserted 'several months'); no primary OEB page fetched that states a timeline.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — the 'several months' figure came only from a search-result snippet, not from a fetched OEB source. Do not publish any duration or cost figure for a leave-to-construct proceeding. The only timeline figures verified in this research are the IESO connection-process stage timings, which are a different process.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Searches run
|
||||||
|
|
||||||
|
- `WebSearch: IESO "System Impact Assessment" connection assessment procedure ieso.ca`
|
||||||
|
- `WebSearch: Ontario Energy Board "leave to construct" section 92 application process oeb.ca`
|
||||||
|
- `WebSearch: Ontario Bill 40 site:ola.org`
|
||||||
|
- `WebSearch: Ontario regulation "specified load facility" data centre connection Electricity Act 28.1 consultation`
|
||||||
|
- `WebSearch: "O. Reg." Ontario "data centre" Electricity Act 1998 connection regulation ontario.ca/laws 2026`
|
||||||
|
- `WebSearch: IESO large load data centre connection process ieso.ca`
|
||||||
|
- `WebSearch: oeb.ca "leave to construct" natural gas pipeline application filing requirements process page`
|
||||||
|
- `WebSearch: IESO "market registration" become a market participant registration process ieso.ca overview`
|
||||||
|
- `WebSearch (allowed_domains restricted to ieso.ca): "data centre" OR "large load" connection IESO Ontario`
|
||||||
|
- `WebSearch: "connection allocation" Ontario electricity IESO OEB`
|
||||||
|
- `WebFetch: https://www.ontario.ca/laws/statute/98e15 — FAILED, returned only the SPA shell with no statute text`
|
||||||
|
- `WebFetch: https://www.canlii.org/en/on/laws/stat/so-1998-c-15-sch-b/latest/so-1998-c-15-sch-b.html — FAILED, HTTP 403 (CanLII blocks both WebFetch and curl)`
|
||||||
|
- `e-Laws API: GET https://www.ontario.ca/laws/api/v2/laws/autocomplete?term=Ontario%20Energy%20Board%20Act — used to establish the correct e-Laws alias`
|
||||||
|
- `e-Laws API: GET https://www.ontario.ca/laws/api/v2/laws/autocomplete?term=Definitions%20and%20Exemptions — used to find the alias for O. Reg. 161/99`
|
||||||
|
- `e-Laws API: GET https://www.ontario.ca/laws/api/v2/legislation/en/act-reg/statute?title=electricity%20act,%201998&sort=citation — listed 50 current regulations under the Electricity Act, 1998`
|
||||||
|
- `curl (Googlebot UA, required for e-Laws prerender): https://www.ontario.ca/laws/statute/98o15 — Ontario Energy Board Act, 1998`
|
||||||
|
- `curl (Googlebot UA): https://www.ontario.ca/laws/statute/98e15 — Electricity Act, 1998`
|
||||||
|
- `curl (Googlebot UA): https://www.ontario.ca/laws/regulation/990161 — O. Reg. 161/99`
|
||||||
|
- `curl: https://www.oeb.ca/applications/applications-oeb/leave-construct-applications-priority-transmission-projects`
|
||||||
|
- `curl: https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/natural-gas-facilities-handbook`
|
||||||
|
- `curl: https://www.oeb.ca/regulatory-rules-and-documents/rules-codes-and-requirements/distribution-system-code-dsc`
|
||||||
|
- `curl: https://www.ieso.ca/en/Learn/About-the-IESO/What-We-Do (redirects to https://ieso.ca/Learn/Ontario-Electricity-Grid/Managing-the-Power-System)`
|
||||||
|
- `curl: https://www.ieso.ca/Sector-Participants/Connection-Process/Overview`
|
||||||
|
- `curl: https://www.ieso.ca/Sector-Participants/Connection-Process/Obtain-Approval`
|
||||||
|
- `curl: https://www.ieso.ca/en/Sector-Participants/Connection-Process/Authorize-Market-and-Program-Participation`
|
||||||
|
- `curl: https://www.ieso.ca/Sector-Participants/Connection-Process/Frequently-Asked-Questions`
|
||||||
|
- `curl: https://www.ola.org/en/legislative-business/bills/parliament-44/session-1/bill-40 and /status`
|
||||||
|
- `curl: https://www.ola.org/en/legislative-business/bills/parliament-43/session-1/bill-40`
|
||||||
|
- `curl: https://www.ola.org/en/legislative-business/bills/parliament-42/session-2/bill-40`
|
||||||
|
- `curl: https://ero.ontario.ca/notice/026-0853`
|
||||||
|
- `curl: https://ero.ontario.ca/notice/025-1001`
|
||||||
|
- `METHOD NOTE 1 — e-Laws (ontario.ca/laws) is a React SPA. A normal fetch returns a 54 KB shell containing ZERO statute text and no error. Fetching with a Googlebot user-agent returns the full prerendered HTML (672 KB for the OEB Act, 909 KB for the Electricity Act). Anyone re-verifying these quotes must use the bot UA or the quotes will appear unverifiable.`
|
||||||
|
- `METHOD NOTE 2 — the e-Laws alias for the Ontario Energy Board Act, 1998 is 98o15 (S.O. 1998, c. 15, Sched. B). 98e15 is the Electricity Act, 1998 (Sched. A). My first fetch used 98e15 for the OEB Act and would have sourced the wrong statute; the autocomplete API caught it. Both statutes are needed here and they are easy to transpose.`
|
||||||
|
- `ARTEFACTS — all fetched HTML and extracted text saved under /private/tmp/claude-501/-Users-pouya-Dev-Websites-adr-sml/2e628a52-3cc2-46e4-a4c9-dc1e5273a175/scratchpad/ : g.html + g.txt (OEB Act), ea.html + ea.txt (Electricity Act), r161.html + r161.txt (O. Reg. 161/99), oeb_ltc.*, oeb_ngfh.*, oeb_dsc.*, ieso_Overview.*, ieso_Obtain-Approval.*, ieso_stage4.*, ieso_faq.*, ieso_What-We-Do.*, bill40.*, b40status.*, b40_43.html, b40_42.html, ero_026-0853.*, ero_025-1001.*. These are in a scratchpad, NOT in the repo — under the project's own rule that a claim's supporting artefact must be reachable from the repository, they must be committed (or a faithful extract with provenance committed) before any of these facts goes on a public page.`
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
# Ontario SABS and the Licence Appeal Tribunal — the regulation, the MIG, the forum, and published caseload
|
||||||
|
|
||||||
|
Committed under AGENTS.md R14 and the CLAUDE.md rule it encodes: **anything a
|
||||||
|
spec makes a claim about must be reachable from the repository.** Every fact
|
||||||
|
the six `/practice/*` pages state about the world is checkable here or it is
|
||||||
|
not published.
|
||||||
|
|
||||||
|
**Retrieved 2026-08-29.** Fetched from the primary sources listed below and
|
||||||
|
extracted with quotations pasted verbatim. This file is the artefact; the pages
|
||||||
|
cite it. Do not paraphrase a fact into a page that is not stated here.
|
||||||
|
|
||||||
|
> ⚠️ **A statute, a regulation and a tribunal page all move.** Every consolidation
|
||||||
|
> date and effective date below is as at the retrieval date and nothing more.
|
||||||
|
> Re-check before cutover, and before any republish that turns on one of them.
|
||||||
|
|
||||||
|
> ### R18 re-check — cutover pass, 2026-09-01
|
||||||
|
>
|
||||||
|
> The trigger fired. `AGENTS.md` §12 R18 holds the per-limb findings and
|
||||||
|
> the sources; this stamp does not restate them. **The quoted bytes below
|
||||||
|
> are still the original retrieval and were not re-fetched** — what was
|
||||||
|
> re-checked is whether the *facts* they support have moved.
|
||||||
|
>
|
||||||
|
> - **(d) the Tribunals Ontario annual report — RE-VERIFIED UNMOVED**
|
||||||
|
> `[re-checked 2026-09-01 — Pouya,
|
||||||
|
> <https://tribunalsontario.ca/en/about/governance-and-accountability/>]`.
|
||||||
|
> **No 2025-26 report is published; FY2024-25 remains current**, so the
|
||||||
|
> figures `/practice/insurance/` publishes are still the latest. This closes
|
||||||
|
> the open item at the foot of this file, which asked exactly that question.
|
||||||
|
> - **(e) the SABS, amended with effect 2026-07-01 — NOT RE-RETRIEVED.** Held
|
||||||
|
> unchanged on a cadence judgement `[assumed 2026-09-01 — Pouya]`.
|
||||||
|
|
||||||
|
**Topic as researched:** Ontario accident benefits (SABS) disputes — regulation, Minor Injury Guideline, the LAT-AABS dispute route, tribunal caseload volume, mediation references in tribunal materials, and FSRA's role
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
| Kind | Source | URL |
|
||||||
|
|---|---|---|
|
||||||
|
| regulation | O. Reg. 34/10: STATUTORY ACCIDENT BENEFITS SCHEDULE - EFFECTIVE SEPTEMBER 1, 2010 — Ontario e-Laws | <https://www.ontario.ca/laws/regulation/100034> |
|
||||||
|
| statute | Insurance Act, R.S.O. 1990, c. I.8 — Ontario e-Laws | <https://www.ontario.ca/laws/statute/90i08> |
|
||||||
|
| statute | Licence Appeal Tribunal Act, 1999, S.O. 1999, c. 12, Sched. G — Ontario e-Laws | <https://www.ontario.ca/laws/statute/99l12> |
|
||||||
|
| tribunal | Licence Appeal Tribunal - Automobile Accident Benefits Service (LAT-AABS) — Tribunals Ontario | <https://tribunalsontario.ca/lat-aabs/> |
|
||||||
|
| tribunal | Laws, rules and decisions — LAT-AABS, Tribunals Ontario | <https://tribunalsontario.ca/lat-aabs/laws-rules-and-decisions/> |
|
||||||
|
| tribunal | Application and hearing process — LAT-AABS, Tribunals Ontario | <https://tribunalsontario.ca/lat-aabs/application-and-hearing-process/> |
|
||||||
|
| tribunal | Tribunals Ontario 2024-25 Annual Report | <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html> |
|
||||||
|
| tribunal | Licence Appeal Tribunal Rules (2025 consolidation) — Tribunals Ontario | <https://tribunalsontario.ca/documents/lat/LAT-Rules_2025.html> |
|
||||||
|
| statute | Financial Services Regulatory Authority of Ontario Act, 2016, S.O. 2016, c. 37, Sched. 8 — Ontario e-Laws | <https://www.ontario.ca/laws/statute/16f37> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verbatim quotations
|
||||||
|
|
||||||
|
### O. Reg. 34/10: STATUTORY ACCIDENT BENEFITS SCHEDULE - EFFECTIVE SEPTEMBER 1, 2010 — Ontario e-Laws
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/regulation/100034> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> O. Reg. 34/10: STATUTORY ACCIDENT BENEFITS SCHEDULE - EFFECTIVE SEPTEMBER 1, 2010
|
||||||
|
|
||||||
|
> <meta property="og:actTitle" content="Insurance Act" />
|
||||||
|
|
||||||
|
> ONTARIO REGULATION 34/10
|
||||||
|
|
||||||
|
> Consolidation Period: From July 1, 2026 to the e-Laws currency date.
|
||||||
|
|
||||||
|
> Last amendment: 58/25.
|
||||||
|
|
||||||
|
> Definitions and interpretation
|
||||||
|
|
||||||
|
> 3. (1) In this Regulation,
|
||||||
|
|
||||||
|
> “minor injury” means one or more of a sprain, strain, whiplash associated disorder, contusion, abrasion, laceration or subluxation and includes any clinically associated sequelae to such an injury; (“blessure légère”)
|
||||||
|
|
||||||
|
> “Minor Injury Guideline” means a guideline, (a) that is issued by the Chief Executive Officer under subsection 268.3 (1.1) of the Act and published in The Ontario Gazette, and (b) that establishes a treatment framework in respect of one or more minor injuries; (“Directive sur les blessures légères”)
|
||||||
|
|
||||||
|
> “Guideline” means, (a) a guideline, including the Minor Injury Guideline, issued by the Chief Executive Officer under subsection 268.3 (1) of the Act and published in The Ontario Gazette,
|
||||||
|
|
||||||
|
> Monetary limits re medical and rehabilitation benefits
|
||||||
|
|
||||||
|
> 18. (1) The sum of the medical and rehabilitation benefits payable in respect of an insured person who sustains an impairment that is predominantly a minor injury shall not exceed $3,500 plus the amount of any applicable harmonized sales tax payable under Part IX of the Excise Tax Act (Canada) for accidents that occur on or after June 3, 2019 for any one accident, less the sum of all amounts paid in respect of the insured person in accordance with the Minor Injury Guideline. O. Reg. 34/10, s. 18 (1); O. Reg. 123/19, s. 2 (1).
|
||||||
|
|
||||||
|
> (2) Despite subsection (1), the limit in that subsection does not apply to an insured person if his or her health practitioner determines and provides compelling evidence that the insured person has a pre-existing medical condition that was documented by a health practitioner before the accident and that will prevent the insured person from achieving maximal recovery from the minor injury if the insured person is subject to the limit or is limited to the goods and services authorized under the Minor Injury Guideline. O. Reg. 34/10, s. 18 (2); O. Reg. 347/13, s. 1; O. Reg. 123/19, s. 2 (2).
|
||||||
|
|
||||||
|
> (3) The sum of the medical, rehabilitation and attendant care benefits paid in respect of an insured person who is not subject to the financial limit in subsection (1) shall not exceed, for any one accident, (a) $65,000 plus the amount of any applicable harmonized sales tax payable under Part IX of the Excise Tax Act (Canada) for accidents that occur on or after June 3, 2019; or (b) if the insured person sustained a catastrophic impairment as a result of the accident, $1,000,000
|
||||||
|
|
||||||
|
> “Old Regulation” means Ontario Regulation 403/96 (Statutory Accident Benefits Schedule — Accidents on or After November 1, 1996), made under the Act; (“ancien règlement”)
|
||||||
|
|
||||||
|
> (a) that are authorized by, and calculated by applying the rates set out in, the most recent transportation expense guideline published by the Financial Services Regulatory Authority of Ontario, and
|
||||||
|
|
||||||
|
> An application under subsection 280 (2) of the Act in respect of a benefit shall be commenced within two years after the insurer’s refusal to pay the amount claimed. O. Reg. 44/16, s. 6.
|
||||||
|
|
||||||
|
### Insurance Act, R.S.O. 1990, c. I.8 — Ontario e-Laws
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/90i08> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Dispute Resolution — Statutory Accident Benefits
|
||||||
|
|
||||||
|
> 279 For the purposes of sections 280 to 283, “insured person” includes a person who is claiming funeral expenses or a death benefit under the Statutory Accident Benefits Schedule; (“personne assurée”)
|
||||||
|
|
||||||
|
> “Licence Appeal Tribunal” means the Licence Appeal Tribunal established under the Licence Appeal Tribunal Act, 1999. (“Tribunal d’appel en matière de permis”) 2014, c. 9, Sched. 3, s. 14.
|
||||||
|
|
||||||
|
> Resolution of disputes
|
||||||
|
|
||||||
|
> 280 (1) This section applies with respect to the resolution of disputes in respect of an insured person’s entitlement to statutory accident benefits or in respect of the amount of statutory accident benefits to which an insured person is entitled. 2014, c. 9, Sched. 3, s. 14.
|
||||||
|
|
||||||
|
> Application to Tribunal (2) The insured person or the insurer may apply to the Licence Appeal Tribunal to resolve a dispute described in subsection (1). 2014, c. 9, Sched. 3, s. 14.
|
||||||
|
|
||||||
|
> Limit on court proceedings (3) No person may bring a proceeding in any court with respect to a dispute described in subsection (1), other than an appeal from a decision of the Licence Appeal Tribunal or an application for judicial review. 2014, c. 9, Sched. 3, s. 14.
|
||||||
|
|
||||||
|
> Resolution in accordance with Schedule (4) The dispute shall be resolved in accordance with the Statutory Accident Benefits Schedule. 2014, c. 9, Sched. 3, s. 14.
|
||||||
|
|
||||||
|
> 2014, c. 9, Sched. 3, s. 14 - 01/04/2016
|
||||||
|
|
||||||
|
> “Chief Executive Officer” means the Chief Executive Officer appointed under subsection 10 (2) of the Financial Services Regulatory Authority of Ontario Act, 2016; (“directeur général de l’Autorité”)
|
||||||
|
|
||||||
|
> 268.3 (1) The Chief Executive Officer may issue guidelines on the interpretation and operation of the Statutory Accident Benefits Schedule or any provision of that Schedule. 1993, c. 10, s. 27; 1997, c. 28, s. 116; 2018, c. 8, Sched. 13, s. 22.
|
||||||
|
|
||||||
|
> Same (1.1) The Chief Executive Officer may issue guidelines setting out the treatment, services, measures or goods applicable in respect of types of impairments for the purposes of payment of a medical or rehabilitation benefit provided under the Statutory Accident Benefits Schedule, and such guidelines may include conditions, restrictions and limits with respect to such treatment, services, measures or goods. 2002, c. 22, s. 125; 2018, c. 8, Sched. 13, s. 22.
|
||||||
|
|
||||||
|
> Effect of guideline (2) Subject to section 268.2, a guideline shall be considered in any determination involving the interpretation of the Statutory Accident Benefits Schedule. 1993, c. 10, s. 27.
|
||||||
|
|
||||||
|
### Licence Appeal Tribunal Act, 1999, S.O. 1999, c. 12, Sched. G — Ontario e-Laws
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/99l12> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Licence Appeal Tribunal Act, 1999, S.O. 1999, c. 12, Sched. G
|
||||||
|
|
||||||
|
> S.O. 1999, CHAPTER 12 Schedule G
|
||||||
|
|
||||||
|
> Consolidation Period: From January 1, 2025 to the e-Laws currency date.
|
||||||
|
|
||||||
|
> Tribunal established 2 (1) There is hereby established a tribunal to be known in English as the Licence Appeal Tribunal and in French as Tribunal d’appel en matière de permis. 1999, c. 12, Sched. G, s. 2 (1).
|
||||||
|
|
||||||
|
> Members (2) The Tribunal shall consist of not fewer than three members. 1999, c. 12, Sched. G, s. 2 (2).
|
||||||
|
|
||||||
|
### Licence Appeal Tribunal - Automobile Accident Benefits Service (LAT-AABS) — Tribunals Ontario
|
||||||
|
|
||||||
|
<https://tribunalsontario.ca/lat-aabs/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Resolve a dispute about an insured person’s entitlement to, or amount of, statutory motor vehicle accident benefits
|
||||||
|
|
||||||
|
> A person who is injured in an automobile accident can apply to LAT-AABS if there is a disagreement about their entitlement to accident benefits or the amount of benefits that should be paid.
|
||||||
|
|
||||||
|
> If an insurance company believes an individual has been paid too much under their policy, the company can file a LAT-AABS application to have the money returned.
|
||||||
|
|
||||||
|
> The LAT has two divisions – LAT-AABS and LAT-GS (General Service). LAT-GS resolves a variety of appeals including vehicle impoundments, licence suspensions, monetary orders, licensing, consumer compensation claims, and compliance orders.
|
||||||
|
|
||||||
|
### Laws, rules and decisions — LAT-AABS, Tribunals Ontario
|
||||||
|
|
||||||
|
<https://tribunalsontario.ca/lat-aabs/laws-rules-and-decisions/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> The Insurance Act and the Statutory Accident Benefits Schedule (SABS) make the LAT-AABS responsible for resolving disputes over automobile accident benefits:
|
||||||
|
|
||||||
|
> Section 280 of the Insurance Act says that a person or insurance company may apply to the LAT-AABS to resolve a dispute about an insured person’s entitlement to, or amount of, a statutory motor vehicle accident benefit.
|
||||||
|
|
||||||
|
> Section 268 of the Insurance Act says that every motor vehicle liability policy provides specified benefits which are set out in the Statutory Accident Benefits Schedule (SABS), a regulation made under the Insurance Act.
|
||||||
|
|
||||||
|
> Sections 279-288 of the Insurance Act deal with disputes about motor vehicle accident insurance benefits.
|
||||||
|
|
||||||
|
> Related laws Licence Appeal Tribunal Act Insurance Act Statutory Powers Procedure Act Compulsory Automobile Insurance Act Motor Vehicle Accident Claims Act Auto Insurance Rate Stabilization Act
|
||||||
|
|
||||||
|
> Related regulations Statutory Accident Benefits Schedule, O. Reg. 34/10 (Effective September 1, 2010)
|
||||||
|
|
||||||
|
> Rules of Practice Licence Appeal Tribunal Rules, 2023 (effective November 17, 2025)
|
||||||
|
|
||||||
|
### Application and hearing process — LAT-AABS, Tribunals Ontario
|
||||||
|
|
||||||
|
<https://tribunalsontario.ca/lat-aabs/application-and-hearing-process/> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> 4. Consider other ways to resolve your dispute
|
||||||
|
|
||||||
|
> Before you apply to the LAT-AABS, you may want to consider negotiation or mediation services. Parties are encouraged to attempt to negotiate the claim at all times, including before filing at the LAT-AABS, and continuing negotiation discussions after a claim has been filed.
|
||||||
|
|
||||||
|
> The SABS is the framework that the LAT-AABS uses to help you and your motor vehicle insurance company reach a settlement. The SABS is a regulation under the Insurance Act that sets out the benefits and compensation that may be available to you as a driver, passenger, or pedestrian if you have been hurt in a motor vehicle accident.
|
||||||
|
|
||||||
|
> You are encouraged to talk to your insurance company and try to settle your dispute before starting the LAT-AABS process. Settling your dispute with your insurance company is the fastest and least costly way to resolve your claim.
|
||||||
|
|
||||||
|
> You must file an application within two years after receiving this notice from the insurance company.
|
||||||
|
|
||||||
|
> A case conference is an opportunity for parties to attempt to settle their cases and, if a settlement is not reached, an adjudicator from the
|
||||||
|
|
||||||
|
> The goals of the case conference are to: help the parties reach a settlement;
|
||||||
|
|
||||||
|
> A case conference is led by an adjudicator whose role is to guide and support the parties in working to resolve the dispute. The adjudicator is trained to understand accident benefits disputes and will provide his or her view on what could happen if the case went to a hearing.
|
||||||
|
|
||||||
|
> the LAT-AABS will schedule a two-hour case conference for the parties to meet with an adjudicator. Case conferences will usually take place within 45-60 days of the response being received. The case conference will usually take place by telephone.
|
||||||
|
|
||||||
|
> Changes to the Statutory Accident Benefits Schedule effective July 1, 2026 may impact your insurance benefits and the rules that apply to them.
|
||||||
|
|
||||||
|
### Tribunals Ontario 2024-25 Annual Report
|
||||||
|
|
||||||
|
<https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Tribunals Ontario 2024-25 Annual Report
|
||||||
|
|
||||||
|
> This report reflects the agency's accomplishments for the fiscal year ending March 31, 2025.
|
||||||
|
|
||||||
|
> ISBN 978-1-4868-8848-1 © King's Printer for Ontario, 2025
|
||||||
|
|
||||||
|
> Table 2: LAT-AABS Caseload Overview
|
||||||
|
|
||||||
|
> Caseload 2024-2025 2023-2024 2022-2023 2021-2022
|
||||||
|
|
||||||
|
> Appeals received 16,002 16,142 13,983 15,800
|
||||||
|
|
||||||
|
> Appeals closed 18,884 18,016 16,257 11,668
|
||||||
|
|
||||||
|
> Active appeals at fiscal year end 9,191 12,016 13,903 16,204
|
||||||
|
|
||||||
|
> Case conferences held 12,081 11,556 11,411 7,752
|
||||||
|
|
||||||
|
> Decisions rendered 1,104 1,088 587 555
|
||||||
|
|
||||||
|
> Applications settled/withdrawn 17,603 16,941 15,337 11,116
|
||||||
|
|
||||||
|
> In 2024–2025, the LAT maintained strong service delivery while managing 16,776 new matters—the second-highest annual intake on record. This included 4,559 matters in Q4 alone, marking the highest quarterly appeal and application volume ever recorded at the LAT.
|
||||||
|
|
||||||
|
> In the last fiscal year, the LAT continued to reduce its active caseload, resolving a record 19,627 files despite the increased intake volume. As of March 31, 2025, the active caseload stood at 9,367 files down from a peak caseload of 17,465 in August 2022.
|
||||||
|
|
||||||
|
> To sustain service improvements, the LAT-AABS significantly reduced the time between application intake and the first case conference. In 2024-2025, the average timeline dropped to three months and three weeks, down from six months the previous year.
|
||||||
|
|
||||||
|
> The LAT-AABS also improved scheduling efficiency for merits hearings. The average time from application to oral hearing decreased from 437 to 332 days, while written hearing timelines decreased from 528 to 421 days.
|
||||||
|
|
||||||
|
> The LAT adjudicates applications and resolves disputes concerning compensation claims and licensing activities regulated by the provincial government, including the activities of delegated administrative authorities. The LAT is comprised of two main divisions: General Service (LAT-GS) and Automobile Accident Benefits Service (LAT-AABS).
|
||||||
|
|
||||||
|
> Table 1: LAT-GS Caseload Overview ... Appeals received 774 645 625 595
|
||||||
|
|
||||||
|
### Licence Appeal Tribunal Rules (2025 consolidation) — Tribunals Ontario
|
||||||
|
|
||||||
|
<https://tribunalsontario.ca/documents/lat/LAT-Rules_2025.html> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> 2.4 “CASE CONFERENCE” “Case Conference” has the same meaning as “Pre-Hearing Conference” as defined in the
|
||||||
|
|
||||||
|
> 14. Case Conferences 14.1 Directions And Orders At Case Conferences 14.2 Scope Of Case Conferences 14.4 Settlement Discussions 14.5 Case Conference Not Public 14.6 Party Attendance At Case Conferences
|
||||||
|
|
||||||
|
> 20.5 Settlement At Case Conferences
|
||||||
|
|
||||||
|
> Rule 20.4 provides that at least 10 days before a scheduled case conference, each party must file a case conference summary in such form as required by the Tribunal.
|
||||||
|
|
||||||
|
### Financial Services Regulatory Authority of Ontario Act, 2016, S.O. 2016, c. 37, Sched. 8 — Ontario e-Laws
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/16f37> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Financial Services Regulatory Authority of Ontario Act, 2016, S.O. 2016, c. 37, Sched. 8
|
||||||
|
|
||||||
|
> “Authority” means the Financial Services Regulatory Authority of Ontario continued under subsection 2 (1); (“Autorité”)
|
||||||
|
|
||||||
|
> “regulated sector” means a sector that is subject to, (a) the Credit Unions and Caisses Populaires Act, 2020, (a.1) the Financial Professionals Title Protection Act, 2019, (b) the Insurance Act, (c) the Loan and Trust Corporations Act, (d) the Mortgage Brokerages, Lenders and Administrators Act, 2006, (e) the Pension Benefits Act, (f) the Pooled Registered Pension Plans Act, 2015, or (g) such other legislation as may be prescribed; (“secteur réglementé”)
|
||||||
|
|
||||||
|
> 2 (1) The predecessor Authority and DICO are amalgamated and shall continue as one corporation without share capital under the name Financial Services Regulatory Authority of Ontario in English and Autorité ontarienne de réglementation des services financiers in French. 2018, c. 17, Sched. 17, s. 2.
|
||||||
|
|
||||||
|
> Crown agency (3) The Authority is an agent of the Crown in right of Ontario.
|
||||||
|
|
||||||
|
> Objects of the Authority 3 (1) The objects of the Authority are, (a) to regulate and generally supervise the regulated sectors; (b) to contribute to public confidence in the regulated sectors; (c) to monitor and evaluate developments and trends in the regulated sectors; (d) to cooperate and collaborate with other regulators where appropriate; (e) to promote public education and knowledge about the regulated sectors; (f) to promote transparency and disclosure of information by the regulated sectors; (g) to deter deceptive or fraudulent conduct, practices and activities by the regulated sectors; and (h) to carry out such other objects as may be prescribed. 2017, c. 34, Sched. 16, s. 2.
|
||||||
|
|
||||||
|
> Same, financial services sectors (2) In addition to the objects set out in subsection (1), the objects of the Authority in respect of the financial services sectors are, (a) to promote high standards of business conduct; (b) to protect the rights and interests of consumers; and (c) to foster strong, sustainable, competitive and innovative financial services sectors. 2017, c. 34, Sched. 16, s. 2.
|
||||||
|
|
||||||
|
> “FSCO” means the former Financial Services Commission of Ontario that was established under the repealed Financial Services Commission of Ontario Act, 1997; (“CSFO”)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this establishes
|
||||||
|
|
||||||
|
Each item names the source it rests on. An item here that no quotation above
|
||||||
|
supports is a defect in this file, not a fact.
|
||||||
|
|
||||||
|
- The SABS is Ontario Regulation 34/10, titled "Statutory Accident Benefits Schedule — Effective September 1, 2010". Ontario e-Laws publishes it as "O. Reg. 34/10: STATUTORY ACCIDENT BENEFITS SCHEDULE - EFFECTIVE SEPTEMBER 1, 2010". (Note: the e-Laws body heading renders in small-caps markup, which flattens to the text string "Statutory Accident bEnefits Schedule" — this is a rendering artefact, not the regulation's title.)
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- O. Reg. 34/10 is made under the Insurance Act. The e-Laws page carries the machine-readable field og:actTitle = "Insurance Act", and the regulation's own text refers to "the Act" throughout while defining its predecessor O. Reg. 403/96 as "made under the Act".
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- The tribunal independently confirms the enabling statute: "Section 268 of the Insurance Act says that every motor vehicle liability policy provides specified benefits which are set out in the Statutory Accident Benefits Schedule (SABS), a regulation made under the Insurance Act."
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/laws-rules-and-decisions/>
|
||||||
|
- The version of O. Reg. 34/10 retrieved on 2026-08-29 carries "Consolidation Period: From July 1, 2026 to the e-Laws currency date" and "Last amendment: 58/25". A SABS amendment took effect 1 July 2026; the tribunal notes "Changes to the Statutory Accident Benefits Schedule effective July 1, 2026 may impact your insurance benefits and the rules that apply to them." Any SABS copy predating that date is stale.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- "Minor Injury Guideline" is defined in section 3(1) of O. Reg. 34/10 (heading: "Definitions and interpretation") as "a guideline, (a) that is issued by the Chief Executive Officer under subsection 268.3 (1.1) of the Act and published in The Ontario Gazette, and (b) that establishes a treatment framework in respect of one or more minor injuries".
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- "minor injury" is separately defined in section 3(1) of O. Reg. 34/10 as "one or more of a sprain, strain, whiplash associated disorder, contusion, abrasion, laceration or subluxation and includes any clinically associated sequelae to such an injury".
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- The $3,500 monetary limit is set by section 18(1) of the SABS itself — NOT by the Minor Injury Guideline. Section 18 is headed "Monetary limits re medical and rehabilitation benefits" and s. 18(1) reads: "The sum of the medical and rehabilitation benefits payable in respect of an insured person who sustains an impairment that is predominantly a minor injury shall not exceed $3,500 plus the amount of any applicable harmonized sales tax payable under Part IX of the Excise Tax Act (Canada) for accidents that occur on or after June 3, 2019 for any one accident, less the sum of all amounts paid in respect of the insured person in accordance with the Minor Injury Guideline."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- Section 18(2) of the SABS creates an exception to the $3,500 limit: it "does not apply to an insured person if his or her health practitioner determines and provides compelling evidence that the insured person has a pre-existing medical condition that was documented by a health practitioner before the accident and that will prevent the insured person from achieving maximal recovery from the minor injury" if subject to the limit or to the MIG's authorized goods and services.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- For an insured person NOT subject to the s. 18(1) minor-injury limit, s. 18(3) caps medical, rehabilitation and attendant care benefits at $65,000 per accident (plus applicable HST, for accidents on or after June 3, 2019), or $1,000,000 where the person sustained a catastrophic impairment.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- The Minor Injury Guideline is issued by the Chief Executive Officer under s. 268.3(1.1) of the Insurance Act, which empowers the CEO to "issue guidelines setting out the treatment, services, measures or goods applicable in respect of types of impairments for the purposes of payment of a medical or rehabilitation benefit provided under the Statutory Accident Benefits Schedule". Under s. 268.3(2), "a guideline shall be considered in any determination involving the interpretation of the Statutory Accident Benefits Schedule."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90i08>
|
||||||
|
- SABS disputes are heard by the Licence Appeal Tribunal. Insurance Act s. 280(1) applies "with respect to the resolution of disputes in respect of an insured person's entitlement to statutory accident benefits or in respect of the amount of statutory accident benefits to which an insured person is entitled", and s. 280(2) provides: "The insured person or the insurer may apply to the Licence Appeal Tribunal to resolve a dispute described in subsection (1)."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90i08>
|
||||||
|
- Insurance Act s. 280(3) bars the courts: "No person may bring a proceeding in any court with respect to a dispute described in subsection (1), other than an appeal from a decision of the Licence Appeal Tribunal or an application for judicial review." Section 280(4) adds that "The dispute shall be resolved in accordance with the Statutory Accident Benefits Schedule."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90i08>
|
||||||
|
- Insurance Act s. 279 defines "Licence Appeal Tribunal" as "the Licence Appeal Tribunal established under the Licence Appeal Tribunal Act, 1999". The e-Laws amendment history records the s. 279–280 scheme (2014, c. 9, Sched. 3, s. 14) as in force 01/04/2016.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90i08>
|
||||||
|
- The Licence Appeal Tribunal is established by s. 2(1) of the Licence Appeal Tribunal Act, 1999, S.O. 1999, c. 12, Sched. G: "There is hereby established a tribunal to be known in English as the Licence Appeal Tribunal and in French as Tribunal d'appel en matière de permis."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/99l12>
|
||||||
|
- The LAT's accident-benefits division is the Automobile Accident Benefits Service (LAT-AABS), which exists to "Resolve a dispute about an insured person's entitlement to, or amount of, statutory motor vehicle accident benefits". The LAT has two divisions, LAT-AABS and LAT-GS (General Service).
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/>
|
||||||
|
- Either side may apply: an injured person may apply "if there is a disagreement about their entitlement to accident benefits or the amount of benefits that should be paid", and "If an insurance company believes an individual has been paid too much under their policy, the company can file a LAT-AABS application to have the money returned."
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/>
|
||||||
|
- The tribunal lists the governing instruments as the Licence Appeal Tribunal Act, the Insurance Act, the Statutory Powers Procedure Act, the Compulsory Automobile Insurance Act, the Motor Vehicle Accident Claims Act and the Auto Insurance Rate Stabilization Act, with the Statutory Accident Benefits Schedule, O. Reg. 34/10 among the related regulations. Procedure is governed by the Licence Appeal Tribunal Rules, 2023 (effective November 17, 2025).
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/laws-rules-and-decisions/>
|
||||||
|
- A SABS application to the LAT must be commenced within two years: SABS s. 56 provides that "An application under subsection 280 (2) of the Act in respect of a benefit shall be commenced within two years after the insurer's refusal to pay the amount claimed." The tribunal states the same rule as "You must file an application within two years after receiving this notice from the insurance company."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
- YES — the LAT publishes accident-benefit caseload volume figures. The Tribunals Ontario 2024-25 Annual Report contains "Table 2: LAT-AABS Caseload Overview", reporting for the fiscal year ending March 31, 2025 and three prior years.
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- LAT-AABS appeals RECEIVED, by fiscal year: 16,002 (2024-2025); 16,142 (2023-2024); 13,983 (2022-2023); 15,800 (2021-2022). Reporting period for the most recent column is the fiscal year ending March 31, 2025.
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- LAT-AABS appeals CLOSED, by fiscal year: 18,884 (2024-2025); 18,016 (2023-2024); 16,257 (2022-2023); 11,668 (2021-2022).
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- LAT-AABS active appeals at fiscal year end: 9,191 (2024-2025); 12,016 (2023-2024); 13,903 (2022-2023); 16,204 (2021-2022).
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- LAT-AABS case conferences held: 12,081 (2024-2025); 11,556 (2023-2024); 11,411 (2022-2023); 7,752 (2021-2022). Decisions rendered: 1,104 (2024-2025); 1,088 (2023-2024); 587 (2022-2023); 555 (2021-2022).
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- LAT-AABS "Applications settled/withdrawn" (a single combined row — settlements are NOT reported separately from withdrawals): 17,603 (2024-2025); 16,941 (2023-2024); 15,337 (2022-2023); 11,116 (2021-2022).
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- Tribunal-wide LAT context for 2024-2025: the LAT managed "16,776 new matters—the second-highest annual intake on record", including "4,559 matters in Q4 alone, marking the highest quarterly appeal and application volume ever recorded at the LAT", and resolved "a record 19,627 files". Active caseload as at March 31, 2025 was 9,367, "down from a peak caseload of 17,465 in August 2022". (These totals cross-check the AABS table: 16,002 + 774 GS = 16,776; 18,884 + 743 GS = 19,627; 9,191 + 176 GS = 9,367.)
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- LAT-AABS timelines for 2024-2025: average time from application intake to first case conference "dropped to three months and three weeks, down from six months the previous year"; average time from application to oral hearing "decreased from 437 to 332 days, while written hearing timelines decreased from 528 to 421 days".
|
||||||
|
*Source:* <https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html>
|
||||||
|
- YES — the tribunal's own materials expressly point parties to mediation before applying. Under the heading "4. Consider other ways to resolve your dispute", the LAT-AABS Application and hearing process page states verbatim: "Before you apply to the LAT-AABS, you may want to consider negotiation or mediation services. Parties are encouraged to attempt to negotiate the claim at all times, including before filing at the LAT-AABS, and continuing negotiation discussions after a claim has been filed."
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/application-and-hearing-process/>
|
||||||
|
- PRECISION LIMIT on the mediation passage: the sentence does not use the word "private", does not name any provider, and does not say mediation may be used DURING a pending application. Mediation is mentioned only in the "before you apply" clause; the clause about continuing after filing refers specifically to "negotiation discussions", not to mediation. Any public copy should not extend it beyond that.
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/application-and-hearing-process/>
|
||||||
|
- The tribunal generally encourages pre-application settlement: "You are encouraged to talk to your insurance company and try to settle your dispute before starting the LAT-AABS process. Settling your dispute with your insurance company is the fastest and least costly way to resolve your claim."
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/application-and-hearing-process/>
|
||||||
|
- The LAT's own in-process settlement mechanism is the adjudicator-led case conference, not mediation. "A case conference is an opportunity for parties to attempt to settle their cases"; its goals include "help the parties reach a settlement"; and it is "led by an adjudicator whose role is to guide and support the parties in working to resolve the dispute", who "will provide his or her view on what could happen if the case went to a hearing". Case conferences are scheduled for two hours, usually within 45-60 days of the response, usually by telephone.
|
||||||
|
*Source:* <https://tribunalsontario.ca/lat-aabs/application-and-hearing-process/>
|
||||||
|
- FSRA is the Financial Services Regulatory Authority of Ontario, a corporation without share capital continued under s. 2(1) of the Financial Services Regulatory Authority of Ontario Act, 2016, S.O. 2016, c. 37, Sched. 8, and "an agent of the Crown in right of Ontario" (s. 2(3)). It succeeded the former Financial Services Commission of Ontario (FSCO).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/16f37>
|
||||||
|
- FSRA's statutory objects (s. 3(1)) are "(a) to regulate and generally supervise the regulated sectors; (b) to contribute to public confidence in the regulated sectors; (c) to monitor and evaluate developments and trends in the regulated sectors; (d) to cooperate and collaborate with other regulators where appropriate; (e) to promote public education and knowledge about the regulated sectors; (f) to promote transparency and disclosure of information by the regulated sectors; (g) to deter deceptive or fraudulent conduct, practices and activities by the regulated sectors". For financial services sectors, s. 3(2) adds "(a) to promote high standards of business conduct; (b) to protect the rights and interests of consumers; and (c) to foster strong, sustainable, competitive and innovative financial services sectors."
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/16f37>
|
||||||
|
- Insurance — and therefore auto insurance — falls inside FSRA's mandate because "regulated sector" is defined in s. 1(1) of the FSRA Act to mean a sector subject to, among other statutes, "(b) the Insurance Act".
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/16f37>
|
||||||
|
- FSRA's specific operative role in the accident-benefits scheme is exercised through its Chief Executive Officer: the Insurance Act defines "Chief Executive Officer" as "the Chief Executive Officer appointed under subsection 10 (2) of the Financial Services Regulatory Authority of Ontario Act, 2016", and s. 268.3(1) and (1.1) empower that officer to issue the SABS guidelines — including the Minor Injury Guideline.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90i08>
|
||||||
|
- The SABS itself names FSRA as the publisher of operative guidelines, e.g. the definition of "authorized transportation expense" turns on "the most recent transportation expense guideline published by the Financial Services Regulatory Authority of Ontario".
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/100034>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this does NOT establish
|
||||||
|
|
||||||
|
**Read this section before writing copy.** It is the half that keeps a page
|
||||||
|
honest, and on this project it is the half that has twice been skipped.
|
||||||
|
|
||||||
|
- **FSRA's description of itself and its auto-insurance role IN ITS OWN WORDS (from fsrao.ca).**
|
||||||
|
- *Searched:* WebSearch for FSRA/fsrao.ca auto insurance role; then direct retrieval of https://www.fsrao.ca/about-fsra/who-we-are and https://www.fsrao.ca/consumers/auto-insurance via WebFetch and via curl — plain, then with a Chrome UA, then with a full Safari UA plus Accept/Accept-Language/Sec-Fetch-* headers; also tried the media endpoint https://www.fsrao.ca/media/26151/download.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — fsrao.ca is behind a Cloudflare interstitial. Every attempt returned HTTP 403 with a body whose title is "Just a moment..." (verified by reading the returned bytes, not by assuming an empty result). No FSRA-authored sentence was retrieved, so none is quoted. FSRA's identity and role are instead established above from primary statute (FSRA Act, 2016 s. 1(1), 2, 3; Insurance Act s. 268.3 and the "Chief Executive Officer" definition), which is stronger sourcing than a marketing page — but it is NOT FSRA speaking in its own voice. If a public page needs FSRA's own wording, someone must open fsrao.ca in a browser and commit the extract.
|
||||||
|
- **The text and contents of the Minor Injury Guideline document itself — what treatments it authorizes, its version number, and its effective date.**
|
||||||
|
- *Searched:* The MIG is issued by FSRA's CEO and published on fsrao.ca / in The Ontario Gazette. fsrao.ca was unreachable (Cloudflare 403, as above). The SABS text on e-Laws defines the MIG and incorporates it by reference but does not reproduce it.
|
||||||
|
- *Outcome:* NOT ESTABLISHED. What IS established is (a) the MIG's legal definition (SABS s. 3(1)), (b) its enabling power (Insurance Act s. 268.3(1.1)), and (c) the $3,500 limit, which sits in SABS s. 18(1) rather than in the Guideline. Do not describe the MIG's clinical contents, its treatment blocks, or its dollar figures as coming from the Guideline itself on the strength of this research.
|
||||||
|
- **Whether the Licence Appeal Tribunal Rules contain any mediation provision, or whether the LAT offers a mediation service of its own.**
|
||||||
|
- *Searched:* Case-insensitive grep for 'mediat' across four fetched tribunal artefacts: the LAT Rules (tribunalsontario.ca/documents/lat/LAT-Rules_2025.html), the LAT-AABS landing page, the Laws/rules/decisions page, and the Application and hearing process page. Matches were printed with context and read, not counted.
|
||||||
|
- *Outcome:* RESULT — NEGATIVE, and this is a finding rather than a gap. 'mediat' occurs exactly ONCE across all four documents: the single sentence on the Application and hearing process page quoted above. The LAT Rules contain no mediation rule; their settlement machinery is the case conference (Rules 14, 14.4 'Settlement Discussions', 20.5 'Settlement At Case Conferences'). The LAT does not appear to offer mediation itself. Note this was a sweep whose output was read; it covers only those four documents, not every page on tribunalsontario.ca.
|
||||||
|
- **Whether the LAT publishes a breakdown of how many AABS applications settle (as opposed to being withdrawn).**
|
||||||
|
- *Searched:* Read Table 2 (LAT-AABS Caseload Overview) in the Tribunals Ontario 2024-25 Annual Report in full.
|
||||||
|
- *Outcome:* NOT ESTABLISHED — the report publishes a single combined row, 'Applications settled/withdrawn' (17,603 in 2024-2025). Settlements are not separated from withdrawals. Do NOT characterise that figure as a settlement rate or as a number of mediated/settled cases.
|
||||||
|
- **Whether a more recent annual report than 2024-25 exists (i.e. a 2025-26 report covering the year ending March 31, 2026).**
|
||||||
|
- *Searched:* WebSearch for Tribunals Ontario annual report LAT AABS caseload; retrieved the 2024-25 report, which is dated June 30, 2025 and is the most recent surfaced.
|
||||||
|
- *Outcome:* NOT CONFIRMED either way. The 2024-25 report (fiscal year ending March 31, 2025) is the latest located as of 2026-08-29, but no search was run specifically to rule out a 2025-26 edition. Given today's date, one may well have been published. Re-check before publishing any 'most recent' or 'latest available' framing around these figures.
|
||||||
|
- *Outcome, R18 re-check:* **CONFIRMED — no 2025-26 edition is published, and FY2024-25 remains current** `[re-checked 2026-09-01 — Pouya, <https://tribunalsontario.ca/en/about/governance-and-accountability/>]`. This closes the item as originally posed. The caution in the line above is retained rather than struck, because it is about a FRAMING and not about this fact: nothing on `/practice/insurance/` calls these figures the most recent or the latest available, and nothing should start to — a 2025-26 edition will appear eventually and a bare year label goes stale gracefully where a superlative does not.
|
||||||
|
- **What the SABS amendments effective July 1, 2026 actually changed.**
|
||||||
|
- *Searched:* Noted the consolidation banner on the e-Laws SABS page ('From July 1, 2026') and the tribunal's warning that 'Changes to the Statutory Accident Benefits Schedule effective July 1, 2026 may impact your insurance benefits'. Did not fetch O. Reg. 383/24 or the amending instruments.
|
||||||
|
- *Outcome:* NOT ESTABLISHED. Search-result snippets suggested that certain benefits became optional, but no primary amending regulation was fetched, so nothing about the substance of the July 2026 changes is asserted here. The s. 3(1) definitions and the s. 18(1) $3,500 limit quoted above ARE from the post-July-2026 consolidation and are current as retrieved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adversarial check on this extract
|
||||||
|
|
||||||
|
An independent pass was run over the items above with one instruction: decide
|
||||||
|
whether the pasted quotations actually support each one, and flag anything
|
||||||
|
broader than its quote. `PARTLY` means the wording overreaches the source.
|
||||||
|
|
||||||
|
**Overreach found:** YES — see below
|
||||||
|
|
||||||
|
| Verdict | Claim | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| **PARTLY** | 1. SABS is O. Reg. 34/10, titled as e-Laws publishes it — including the parenthetical note that the body heading is small-caps markup flattening to "Statutory Accident bEnefits Sch | The title strings are verbatim ("O. Reg. 34/10: STATUTORY ACCIDENT BENEFITS SCHEDULE - EFFECTIVE SEPTEMBER 1, 2010", "ONTARIO REGULATION 34/10"), and S5 independently quotes "Statutory Accident Benefits Schedule, O. Reg. 34/10 (Effective September 1, 2010)". OVERREACH: the entire parenthetical — "the e-Laws body heading renders in small-caps markup, which flattens to the text string 'Statutory Acc |
|
||||||
|
| **PARTLY** | 4. Retrieved version carries the July 1 2026 consolidation banner and "Last amendment: 58/25"; a SABS amendment took effect 1 July 2026; any SABS copy predating that date is stale | The banner, the last-amendment line, and the tribunal's "Changes to the Statutory Accident Benefits Schedule effective July 1, 2026" warning are all verbatim. OVERREACH: "Any SABS copy predating that date is stale" is a general editorial rule no quote states. "A SABS amendment took effect 1 July 2026" is also inferred — from a consolidation-period start date plus the tribunal's caution — with no a |
|
||||||
|
| **PARTLY** | 7. The $3,500 limit is set by SABS s. 18(1) — NOT by the Minor Injury Guideline | The s. 18 heading and the whole of s. 18(1) are verbatim, so "the limit sits in the regulation" is fully carried. OVERREACH: the words "NOT by the Minor Injury Guideline" are a statement about the Guideline's contents, and the extract's own notEstablished entry records that the MIG text was never retrieved. The quotes support only the positive form — the limit appears in the regulation's own text, |
|
||||||
|
| **PARTLY** | 10. MIG issued by the CEO under Insurance Act s. 268.3(1.1); under s. 268.3(2) "a guideline shall be considered in any determination..." | The 268.3(1.1) power and the SABS's own "issued by the Chief Executive Officer under subsection 268.3 (1.1) of the Act" are verbatim. OVERREACH: the s. 268.3(2) quotation silently drops the opening qualifier that is present in the pasted source quote — "Subject to section 268.2, a guideline shall be considered ..." — and presents an unconditional rule. Section 268.2 is not in evidence, so the scop |
|
||||||
|
| **PARTLY** | 13. s. 279 defines "Licence Appeal Tribunal"; the amendment history records the s. 279–280 scheme (2014, c. 9, Sched. 3, s. 14) as in force 01/04/2016 | The s. 279 definition is verbatim. OVERREACH: "records the s. 279–280 scheme ... as in force 01/04/2016" reads a bare e-Laws amendment-table line — "2014, c. 9, Sched. 3, s. 14 - 01/04/2016" — as a commencement statement. No quoted words say "in force", and the line names no sections. Minor, but it is an inference from page formatting rather than from text. |
|
||||||
|
| **PARTLY** | 17. The tribunal lists the governing instruments (LAT Act, Insurance Act, SPPA, Compulsory Automobile Insurance Act, MVACA, AIRSA; O. Reg. 34/10); procedure governed by the LAT Rul | The lists and the Rules line are verbatim. OVERREACH: the source heading is "Related laws", and calling those items "the governing instruments" upgrades a related-reading list into a claim that each governs LAT-AABS proceedings — the Auto Insurance Rate Stabilization Act being the clearest example the quote will not carry. "Procedure is governed by" for the quoted heading "Rules of Practice" is an |
|
||||||
|
| **PARTLY** | 18. Two-year limitation: "SABS s. 56" provides ...; the tribunal states the same rule | The limitation sentence is verbatim. Two overreaches. (a) The section number "s. 56" appears in NO pasted quote — the quoted line carries only "O. Reg. 44/16, s. 6", which is an amending citation, not the section of the SABS. Drop the number or re-verify it. (b) "The tribunal states the same rule" equates "two years after the insurer's refusal to pay the amount claimed" with "two years after recei |
|
||||||
|
| **PARTLY** | 24. "Applications settled/withdrawn" is a single combined row — settlements are NOT reported separately from withdrawals | The combined row and its four figures are verbatim, so "Table 2 reports one combined row" is fully carried. OVERREACH: "settlements are NOT reported separately from withdrawals" is a negative about the report as a whole, while the pasted quotes cover only Table 2's rows. Keep it to the table: nothing pasted shows what the rest of the report does or does not break out. |
|
||||||
|
| **PARTLY** | 25. Tribunal-wide 2024-25 context (16,776 new matters; 4,559 in Q4; 19,627 resolved; 9,367 active; peak 17,465 Aug 2022) with an arithmetic cross-check against the AABS table | Every narrative figure is verbatim. OVERREACH is in the parenthetical cross-check: two of its three reconciliations use LAT-GS numbers that appear in NO pasted quote — GS closed 743 and GS active 176. The only GS quote is "Appeals received 774 645 625 595". So 16,002 + 774 = 16,776 is checkable from the quotes; 18,884 + 743 and 9,191 + 176 are not, and "all three reconcile" cannot be verified from |
|
||||||
|
| **PARTLY** | 27. YES — the tribunal's own materials expressly point parties to mediation before applying | The heading and the sentence are verbatim and do mention mediation. OVERREACH in the framing, not the quote: "expressly point parties to mediation" overstates "you may want to consider negotiation or mediation services" — permissive, unranked, and paired with negotiation, with no referral or direction. "the tribunal's own materials" (plural, a class) rests on ONE sentence on ONE page; the extract' |
|
||||||
|
| **PARTLY** | 30. The LAT's own in-process settlement mechanism is the adjudicator-led case conference, not mediation | Every descriptive element — settlement opportunity, "help the parties reach a settlement", adjudicator-led, the adjudicator's view of a hearing outcome, two hours, 45-60 days of the response, usually by telephone — is verbatim. OVERREACH: "is ... not mediation" and the exclusivity of "THE LAT's own in-process settlement mechanism" rest on an absence found by a grep over four documents (recorded in |
|
||||||
|
| **PARTLY** | 31. FSRA continued under s. 2(1) of the FSRA Act, 2016 as a corporation without share capital, an agent of the Crown (s. 2(3)); it succeeded the former FSCO | "Corporation without share capital", the s. 2(1) continuation and "an agent of the Crown in right of Ontario" are verbatim. OVERREACH: "It succeeded the former Financial Services Commission of Ontario (FSCO)". The pasted s. 2(1) says FSRA continues from "the predecessor Authority and DICO" — neither term defined in any pasted quote — and the FSCO quote only defines FSCO as "the former ... Commissi |
|
||||||
|
| **PARTLY** | 32. FSRA's statutory objects, s. 3(1)(a)–(g) and s. 3(2)(a)–(c) | Everything quoted is verbatim. OVERREACH by omission: the pasted quote continues "(h) to carry out such other objects as may be prescribed", and the fact presents (a)–(g) as what the objects "are", with no ellipsis and no mention of (h). An enumerated list presented as complete when the source shows one more item. Add (h) or mark the truncation. |
|
||||||
|
| **PARTLY** | 33. Insurance — and therefore auto insurance — falls inside FSRA's mandate because "regulated sector" is defined in s. 1(1) to include "(b) the Insurance Act" | The "regulated sector" definition including "(b) the Insurance Act" is verbatim. Three things go beyond it: the section number "s. 1(1)" appears in no quote (the definition is pasted with no section reference); "and therefore auto insurance" is an inferential step absent from every FSRA Act quote, borrowed from the tribunal's separate s. 268 sentence; and "falls inside FSRA's mandate" is a conclus |
|
||||||
|
| **PARTLY** | 34. FSRA's specific operative role in the accident-benefits scheme is exercised through its CEO, who issues the SABS guidelines including the MIG | The CEO definition, s. 268.3(1) and (1.1), and the SABS's own "issued by the Chief Executive Officer under subsection 268.3 (1.1)" are all verbatim, so "the CEO issues the SABS guidelines, including the MIG" is fully carried. OVERREACH: "FSRA's specific operative role in the accident-benefits scheme IS exercised through its Chief Executive Officer" reads as exclusive. No quote surveys FSRA's role |
|
||||||
|
| **PARTLY** | 35. The SABS names FSRA as the publisher of operative guidelines, e.g. the definition of "authorized transportation expense" turns on the FSRA-published transportation expense guid | THIS IS THE CLASS-FROM-ONE-INSTANCE SHAPE the audit was looking for. The only evidence is a mid-sentence fragment: "(a) that are authorized by, and calculated by applying the rates set out in, the most recent transportation expense guideline published by the Financial Services Regulatory Authority of Ontario, and". Two overreaches. (a) "publisher of operative guidelines" — plural, a class — is dra |
|
||||||
|
|
||||||
|
*19 of 35 items were found fully supported; only the
|
||||||
|
others are tabled above.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Searches run
|
||||||
|
|
||||||
|
- `WebSearch: Statutory Accident Benefits Schedule O. Reg. 34/10 ontario.ca laws`
|
||||||
|
- `WebSearch: Licence Appeal Tribunal Automobile Accident Benefits Service tribunalsontario.ca`
|
||||||
|
- `WebSearch: Tribunals Ontario annual report Licence Appeal Tribunal AABS caseload applications received`
|
||||||
|
- `WebSearch: FSRA Financial Services Regulatory Authority of Ontario auto insurance role "about us" fsrao.ca`
|
||||||
|
- `WebFetch: https://www.ontario.ca/laws/regulation/100034 (returned page shell only — superseded by curl)`
|
||||||
|
- `WebFetch: https://www.canlii.org/en/on/laws/regu/o-reg-34-10/latest/o-reg-34-10.html (HTTP 403)`
|
||||||
|
- `WebFetch: https://tribunalsontario.ca/lat-aabs/`
|
||||||
|
- `WebFetch: https://tribunalsontario.ca/lat-aabs/laws-rules-and-decisions/`
|
||||||
|
- `WebFetch: https://www.fsrao.ca/about-fsra/who-we-are (HTTP 403)`
|
||||||
|
- `WebFetch: https://www.fsrao.ca/consumers/auto-insurance (HTTP 403)`
|
||||||
|
- `curl: https://www.ontario.ca/laws/regulation/100034 (HTTP 200, 260,313 bytes — full SABS text)`
|
||||||
|
- `curl: https://www.ontario.ca/laws/statute/90i08 (HTTP 200, 1,310,429 bytes — full Insurance Act)`
|
||||||
|
- `curl: https://www.ontario.ca/laws/statute/99l12 (HTTP 200 — Licence Appeal Tribunal Act, 1999)`
|
||||||
|
- `curl: https://www.ontario.ca/laws/statute/16f37 (HTTP 200 — FSRA Act, 2016)`
|
||||||
|
- `curl: https://tribunalsontario.ca/lat-aabs/ (HTTP 200)`
|
||||||
|
- `curl: https://tribunalsontario.ca/lat-aabs/laws-rules-and-decisions/ (HTTP 200)`
|
||||||
|
- `curl: https://tribunalsontario.ca/lat-aabs/application-and-hearing-process/ (HTTP 200)`
|
||||||
|
- `curl: https://tribunalsontario.ca/documents/lat/LAT-Rules_2025.html (HTTP 200)`
|
||||||
|
- `curl: https://tribunalsontario.ca/documents/TO/Tribunals_Ontario_2024-2025_Annual_Report.html (HTTP 200, 506,740 bytes)`
|
||||||
|
- `curl: https://www.fsrao.ca/about-fsra/who-we-are — HTTP 403 plain, 403 with Chrome UA, 403 with full Safari UA + Sec-Fetch headers (Cloudflare 'Just a moment...' interstitial)`
|
||||||
|
- `curl: https://www.fsrao.ca/media/26151/download (HTTP 403, same Cloudflare interstitial)`
|
||||||
|
- `grep (read with context): 'Minor Injury Guideline' x28 and '3,500' x1 in the SABS text`
|
||||||
|
- `grep (read with context): case-insensitive 'mediat|settle|alternative dispute|case conference' across LAT-AABS landing, laws, process pages and the LAT Rules`
|
||||||
|
- `grep (read with context): 'Chief Executive Officer means' and '268.3' in the Insurance Act`
|
||||||
|
- `grep (read with context): 'regulated sector' and 'Objects of the Authority' in the FSRA Act`
|
||||||
|
- `Arithmetic cross-check of the annual-report table alignment against the report's own narrative totals (16,002+774=16,776; 18,884+743=19,627; 9,191+176=9,367 — all three reconcile)`
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
# Ontario and federal shareholder, partnership and closely-held-business remedies; family-arbitration training re-checked
|
||||||
|
|
||||||
|
Committed under AGENTS.md R14 and the CLAUDE.md rule it encodes: **anything a
|
||||||
|
spec makes a claim about must be reachable from the repository.** Every fact
|
||||||
|
the six `/practice/*` pages state about the world is checkable here or it is
|
||||||
|
not published.
|
||||||
|
|
||||||
|
**Retrieved 2026-08-29.** Fetched from the primary sources listed below and
|
||||||
|
extracted with quotations pasted verbatim. This file is the artefact; the pages
|
||||||
|
cite it. Do not paraphrase a fact into a page that is not stated here.
|
||||||
|
|
||||||
|
> ⚠️ **A statute, a regulation and a tribunal page all move.** Every consolidation
|
||||||
|
> date and effective date below is as at the retrieval date and nothing more.
|
||||||
|
> Re-check before cutover, and before any republish that turns on one of them.
|
||||||
|
|
||||||
|
> ### R18 re-check — cutover pass, 2026-09-01
|
||||||
|
>
|
||||||
|
> ⚠️ **THIS FILE CARRIES THE STANDING RE-CHECK INSTRUCTION ABOVE AND WAS
|
||||||
|
> NOT ONE OF R18's SEVEN LIMBS, AND IT WENT UNSTAMPED IN THE FIRST PASS.**
|
||||||
|
> Five of seven extracts were stamped and two were not, so a reader could
|
||||||
|
> not tell whether this one was considered and found non-volatile or simply
|
||||||
|
> missed. Found by `adversarial-reviewer`, 2026-09-02 — the same
|
||||||
|
> control-fires-over-part-of-its-scope defect as R18 having no checklist
|
||||||
|
> item, one notch smaller. **Nothing here was re-retrieved.**
|
||||||
|
>
|
||||||
|
> - **No volatile limb at this cutover.** The facts this file supports on
|
||||||
|
> `/practice/shareholder/` are OBCA sections and case law, which do not move
|
||||||
|
> at this cadence, and a sweep of the built page for dates, statuses and
|
||||||
|
> "as of" framings returned nothing time-anchored `[verified 2026-09-02 —
|
||||||
|
> swept `dist/practice/shareholder/index.html`]`.
|
||||||
|
|
||||||
|
**Topic as researched:** Ontario/Canada shareholder, partnership and closely-held business disputes — statutory remedies (oppression, dissent/appraisal, winding up), the Partnerships Act, arbitration references in the corporations statutes; plus an independent re-check of Ontario family arbitration training requirements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
| Kind | Source | URL |
|
||||||
|
|---|---|---|
|
||||||
|
| statute | Business Corporations Act, R.S.O. 1990, c. B.16 (Ontario e-Laws, full text) | <https://www.ontario.ca/laws/statute/90b16> |
|
||||||
|
| statute | Canada Business Corporations Act, R.S.C., 1985, c. C-44 (Justice Laws, full text) | <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html> |
|
||||||
|
| statute | Partnerships Act, R.S.O. 1990, c. P.5 (Ontario e-Laws, full text) | <https://www.ontario.ca/laws/statute/90p05> |
|
||||||
|
| regulation | O. Reg. 134/07: Family Arbitration, under the Arbitration Act, 1991 (Ontario e-Laws, current consolidation) | <https://www.ontario.ca/laws/regulation/070134> |
|
||||||
|
| regulation | O. Reg. 134/07: Family Arbitration — original (v1) version, Ontario e-Laws source law | <https://www.ontario.ca/laws/regulation/070134/v1> |
|
||||||
|
| regulator | Training for family arbitrators — Government of Ontario (ontario.ca) | <https://www.ontario.ca/page/training-family-arbitrators> |
|
||||||
|
| statute | Family Law Act, R.S.O. 1990, c. F.3 (Ontario e-Laws, full text) | <https://www.ontario.ca/laws/statute/90f03> |
|
||||||
|
| statute | Arbitration Act, 1991, S.O. 1991, c. 17 (Ontario e-Laws, full text) | <https://www.ontario.ca/laws/statute/91a17> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verbatim quotations
|
||||||
|
|
||||||
|
### Business Corporations Act, R.S.O. 1990, c. B.16 (Ontario e-Laws, full text)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/90b16> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Business Corporations Act
|
||||||
|
> R.S.O. 1990, CHAPTER B.16
|
||||||
|
> Consolidation Period: From October 1, 2025 to the e-Laws currency date .
|
||||||
|
> Last amendment: 2024, c. 15, Sched. 1, s. 111 .
|
||||||
|
|
||||||
|
> Oppression remedy
|
||||||
|
> 248 (1) A complainant and, in the case of an offering corporation, the Commission may apply to the court for an order under this section. 1994, c. 27, s. 71 (33).
|
||||||
|
|
||||||
|
> Idem
|
||||||
|
> (2) Where, upon an application under subsection (1), the court is satisfied that in respect of a corporation or any of its affiliates,
|
||||||
|
> (a) any act or omission of the corporation or any of its affiliates effects or threatens to effect a result;
|
||||||
|
> (b) the business or affairs of the corporation or any of its affiliates are, have been or are threatened to be carried on or conducted in a manner; or
|
||||||
|
> (c) the powers of the directors of the corporation or any of its affiliates are, have been or are threatened to be exercised in a manner,
|
||||||
|
> that is oppressive or unfairly prejudicial to or that unfairly disregards the interests of any security holder, creditor, director or officer of the corporation, the court may make an order to rectify the matters complained of. R.S.O. 1990, c. B.16, s. 248 (2).
|
||||||
|
|
||||||
|
> Court order
|
||||||
|
> (3) In connection with an application under this section, the court may make any interim or final order it thinks fit including, without limiting the generality of the foregoing,
|
||||||
|
> (a) an order restraining the conduct complained of;
|
||||||
|
> (b) an order appointing a receiver or receiver-manager;
|
||||||
|
> (c) an order to regulate a corporation’s affairs by amending the articles or by-laws or creating or amending a unanimous shareholder agreement;
|
||||||
|
> ... (f) an order directing a corporation, subject to subsection (6), or any other person, to purchase securities of a security holder;
|
||||||
|
> ... (l) an order winding up the corporation under section 207;
|
||||||
|
|
||||||
|
> 245 In this Part,
|
||||||
|
> “action” means an action under this Act; (“action”)
|
||||||
|
> “complainant” means,
|
||||||
|
> (a) a registered holder or beneficial owner, and a former registered holder or beneficial owner, of a security of a corporation or any of its affiliates,
|
||||||
|
> (b) a director or an officer or a former director or officer of a corporation or of any of its affiliates,
|
||||||
|
> (c) any other person who, in the discretion of the court, is a proper person to make an application under this Part. (“plaignant”) R.S.O. 1990, c. B.16, s. 245.
|
||||||
|
|
||||||
|
> Rights of dissenting shareholders
|
||||||
|
> 185 (1) Subject to subsection (3) and to sections 186 and 248, if a corporation resolves to,
|
||||||
|
> (a) amend its articles under section 168 to add, remove or change restrictions on the issue, transfer or ownership of shares of a class or series of the shares of the corporation;
|
||||||
|
> ... (c) amalgamate with another corporation under sections 175 and 176;
|
||||||
|
> (d) be continued under the laws of another jurisdiction under section 181;
|
||||||
|
> ... (e) sell, lease or exchange all or substantially all its property under subsection 184 (3),
|
||||||
|
> a holder of shares of any class or series entitled to vote on the resolution may dissent. R.S.O. 1990, c. B.16, s. 185 (1); 2017, c. 20, Sched. 6, s. 24.
|
||||||
|
|
||||||
|
> Winding up by court
|
||||||
|
> 207 (1) A corporation may be wound up by order of the court,
|
||||||
|
> (a) where the court is satisfied that in respect of the corporation or any of its affiliates,
|
||||||
|
> (i) any act or omission of the corporation or any of its affiliates effects a result,
|
||||||
|
> (ii) the business or affairs of the corporation or any of its affiliates are or have been carried on or conducted in a manner, or
|
||||||
|
> (iii) the powers of the directors of the corporation or any of its affiliates are or have been exercised in a manner,
|
||||||
|
> that is oppressive or unfairly prejudicial to or that unfairly disregards the interests of any security holder, creditor, director or officer; or
|
||||||
|
|
||||||
|
> (b) where the court is satisfied that,
|
||||||
|
> (i) a unanimous shareholder agreement entitled a complaining shareholder to demand dissolution of the corporation after the occurrence of a specified event and that event has occurred,
|
||||||
|
> ... (iv) it is just and equitable for some reason, other than the bankruptcy or insolvency of the corporation, that it should be wound up; or
|
||||||
|
> (c) where the shareholders by special resolution authorize an application to be made to the court to wind up the corporation. R.S.O. 1990, c. B.16, s. 207 (1).
|
||||||
|
|
||||||
|
> Matter that a unanimous shareholder agreement may provide
|
||||||
|
> (6) A unanimous shareholder agreement may, without restricting the generality of subsection (2), provide that,
|
||||||
|
> (a) any amendment of the unanimous shareholder agreement may be effected in the manner specified therein; and
|
||||||
|
> (b) in the event that shareholders who are parties to the unanimous shareholder agreement are unable to agree on or resolve any matter pertaining to the agreement, the matter may be referred to arbitration under such procedures and conditions as are specified in the unanimous shareholder agreement. R.S.O. 1990, c. B.16, s. 108 (6).
|
||||||
|
|
||||||
|
### Canada Business Corporations Act, R.S.C., 1985, c. C-44 (Justice Laws, full text)
|
||||||
|
|
||||||
|
<https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Canada Business Corporations Act ( R.S.C. , 1985, c. C-44)
|
||||||
|
|
||||||
|
> Marginal note: Application to court re oppression
|
||||||
|
> 241 (1) A complainant may apply to a court for an order under this section.
|
||||||
|
|
||||||
|
> Marginal note: Grounds
|
||||||
|
> (2) If, on an application under subsection (1), the court is satisfied that in respect of a corporation or any of its affiliates
|
||||||
|
> (a) any act or omission of the corporation or any of its affiliates effects a result,
|
||||||
|
> (b) the business or affairs of the corporation or any of its affiliates are or have been carried on or conducted in a manner, or
|
||||||
|
> (c) the powers of the directors of the corporation or any of its affiliates are or have been exercised in a manner
|
||||||
|
> that is oppressive or unfairly prejudicial to or that unfairly disregards the interests of any security holder, creditor, director or officer, the court may make an order to rectify the matters complained of.
|
||||||
|
|
||||||
|
> Marginal note: Powers of court
|
||||||
|
> (3) In connection with an application under this section, the court may make any interim or final order it thinks fit including, without limiting the generality of the foregoing,
|
||||||
|
> (a) an order restraining the conduct complained of;
|
||||||
|
> (b) an order appointing a receiver or receiver-manager;
|
||||||
|
> (c) an order to regulate a corporation’s affairs by amending the articles or by-laws or creating or amending a unanimous shareholder agreement;
|
||||||
|
> ... (f) an order directing a corporation, subject to subsection (6), or any other person, to purchase securities of a security holder;
|
||||||
|
> ... (l) an order liquidating and dissolving the corporation;
|
||||||
|
|
||||||
|
> Marginal note: Alternative order
|
||||||
|
> (7) An applicant under this section may apply in the alternative for an order under section 214.
|
||||||
|
|
||||||
|
> 238 In this Part,
|
||||||
|
> complainant means
|
||||||
|
> (a) a registered holder or beneficial owner, and a former registered holder or beneficial owner, of a security of a corporation or any of its affiliates,
|
||||||
|
> (b) a director or an officer or a former director or officer of a corporation or any of its affiliates,
|
||||||
|
> (c) the Director, or
|
||||||
|
> (d) any other person who, in the discretion of a court, is a proper person to make an application under this Part. ( plaignant )
|
||||||
|
|
||||||
|
> Marginal note: Right to dissent
|
||||||
|
> 190 (1) Subject to sections 191 and 241, a holder of shares of any class of a corporation may dissent if the corporation is subject to an order under paragraph 192(4)(d) that affects the holder or if the corporation resolves to
|
||||||
|
> (a) amend its articles under section 173 or 174 to add, change or remove any provisions restricting or constraining the issue, transfer or ownership of shares of that class;
|
||||||
|
> ... (c) amalgamate otherwise than under section 184;
|
||||||
|
> (d) be continued under section 188;
|
||||||
|
> (e) sell, lease or exchange all or substantially all its property under subsection 189(3); or
|
||||||
|
> (f) carry out a going-private transaction or a squeeze-out transaction.
|
||||||
|
|
||||||
|
> Marginal note: Payment for shares
|
||||||
|
> (3) In addition to any other right the shareholder may have, but subject to subsection (26), a shareholder who complies with this section is entitled, when the action approved by the resolution from which the shareholder dissents or an order made under subsection 192(4) becomes effective, to be paid by the corporation the fair value of the shares in respect of which the shareholder dissents, determined as of the close of business on the day before the resolution was adopted or the order was made.
|
||||||
|
|
||||||
|
> Marginal note: Further grounds
|
||||||
|
> 214 (1) A court may order the liquidation and dissolution of a corporation or any of its affiliated corporations on the application of a shareholder,
|
||||||
|
> (a) if the court is satisfied that in respect of a corporation or any of its affiliates
|
||||||
|
> (i) any act or omission of the corporation or any of its affiliates effects a result,
|
||||||
|
> (ii) the business or affairs of the corporation or any of its affiliates are or have been carried on or conducted in a manner, or
|
||||||
|
> (iii) the powers of the directors of the corporation or any of its affiliates are or have been exercised in a manner
|
||||||
|
> that is oppressive or unfairly prejudicial to or that unfairly disregards the interests of any security holder, creditor, director or officer; or
|
||||||
|
> (b) if the court is satisfied that
|
||||||
|
> (i) a unanimous shareholder agreement entitles a complaining shareholder to demand dissolution of the corporation after the occurrence of a specified event and that event has occurred, or
|
||||||
|
> (ii) it is just and equitable that the corporation should be liquidated and dissolved.
|
||||||
|
|
||||||
|
> 146 (1) An otherwise lawful written agreement among all the shareholders of a corporation, or among all the shareholders and one or more persons who are not shareholders, that restricts, in whole or in part, the powers of the directors to manage, or supervise the management of, the business and affairs of the corporation is valid.
|
||||||
|
|
||||||
|
### Partnerships Act, R.S.O. 1990, c. P.5 (Ontario e-Laws, full text)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/90p05> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Partnerships Act
|
||||||
|
> R.S.O. 1990, Chapter P.5
|
||||||
|
> Consolidation Period: From October 1, 2023 to the e-Laws currency date .
|
||||||
|
> Last amendment: 2023, c. 9, Sched. 26 .
|
||||||
|
|
||||||
|
> Dissolution by expiry of term or notice
|
||||||
|
> 32 Subject to any agreement between the partners, a partnership is dissolved,
|
||||||
|
> (a) if entered into for a fixed term, by the expiration of that term;
|
||||||
|
> (b) if entered into for a single adventure or undertaking, by the termination of that adventure or undertaking; or
|
||||||
|
> (c) if entered into for an undefined time, by a partner giving notice to the other or others of his or her intention to dissolve the partnership, in which case the partnership is dissolved as from the date mentioned in the notice as the date of dissolution, or, if no date is so mentioned, as from the date of the communication of the notice. R.S.O. 1990, c. P.5, s. 32.
|
||||||
|
|
||||||
|
> Dissolution by death or insolvency of partner
|
||||||
|
> 33 (1) Subject to any agreement between the partners, every partnership is dissolved as regards all the partners by the death or insolvency of a partner. R.S.O. 1990, c. P.5, s. 33 (1).
|
||||||
|
> Where partner’s share charged for separate debt
|
||||||
|
> (2) A partnership may, at the option of the other partners, be dissolved if any partner suffers that partner’s share of the partnership property to be charged under this Act for that partner’s separate debt. R.S.O. 1990, c. P.5, s. 33 (2).
|
||||||
|
|
||||||
|
> By illegality of business
|
||||||
|
> 34 A partnership is in every case dissolved by the happening of any event that makes it unlawful for the business of the firm to be carried on or for the members of the firm to carry it on in partnership. R.S.O. 1990, c. P.5, s. 34.
|
||||||
|
|
||||||
|
> By the court
|
||||||
|
> 35 (1) On application by a partner, the court may order a dissolution of the partnership,
|
||||||
|
> (a) when a partner is found to be incapable as defined in the Substitute Decisions Act, 1992 ;
|
||||||
|
> (b) when a partner, other than the partner suing, becomes in any other way permanently incapable of performing the partner’s part of the partnership contract;
|
||||||
|
> (c) when a partner, other than the partner suing, has been guilty of such conduct as, in the opinion of the court, regard being had to the nature of the business, is calculated to prejudicially affect the carrying on of the business;
|
||||||
|
> (d) when a partner, other than the partner suing, wilfully or persistently commits a breach of the partnership agreement, or otherwise so conducts himself or herself in matters relating to the partnership business that it is not reasonably practicable for the other partner or partners to carry on the business in partnership with the partner;
|
||||||
|
> (e) when the business of the partnership can only be carried on at a loss; or
|
||||||
|
> (f) when in any case circumstances have arisen that in the opinion of the court render it just and equitable that the partnership be dissolved. R.S.O. 1990, c. P.5, s. 35; 2009, c. 33, Sched. 2, s. 57 (1).
|
||||||
|
|
||||||
|
> 39 On the dissolution of a partnership every partner is entitled, as against the other partners in the firm and all persons claiming through them in respect of their interests as partners, to have the property of the partnership applied in payment of the debts and liabilities of the firm and to have the surplus assets after such payment applied in payment of what may be due to the partners respectively after deducting what may be due from them as partners to the firm, and for that purpose any partner or the partner’s representative may, on the termination of the partnership, apply to the court to wind up the business and affairs of the firm. R.S.O. 1990, c. P.5, s. 39.
|
||||||
|
|
||||||
|
### O. Reg. 134/07: Family Arbitration, under the Arbitration Act, 1991 (Ontario e-Laws, current consolidation)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/regulation/070134> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> O. Reg. 134/07: FAMILY ARBITRATION, Under: Arbitration Act, 1991, S.O. 1991, c. 17
|
||||||
|
> ONTARIO REGULATION 134/07
|
||||||
|
> family arbitration
|
||||||
|
> Consolidation Period: From June 4, 2021 to the e-Laws currency date .
|
||||||
|
> Last amendment: 411/21 .
|
||||||
|
|
||||||
|
> Qualifications of arbitrator
|
||||||
|
> 3. Every arbitrator who conducts a family arbitration shall have received the training approved by the Attorney General for the arbitrator or class of arbitrators, as set out on a Government of Ontario website. O. Reg. 134/07, s. 3; O. Reg. 411/21, s. 1.
|
||||||
|
|
||||||
|
> 5. I, ......................... (print name of arbitrator ) , confirm the following matters:
|
||||||
|
> i. I will treat the parties equally and fairly in the arbitration, as subsection 19 (1) of the Arbitration Act, 1991 requires.
|
||||||
|
> ii. I have received the appropriate training approved by the Attorney General.
|
||||||
|
> iii. The parties were separately screened for power imbalances and domestic violence and I have considered the results of the screening and will do so throughout the arbitration, if I conduct one.
|
||||||
|
> iv. The parties were separately screened for power imbalances and domestic violence by someone other than me and I have considered his or her report on the results of the screening and will do so throughout the arbitration.
|
||||||
|
|
||||||
|
> 1. In this Regulation,
|
||||||
|
> “mediation-arbitration agreement” means a family arbitration agreement that provides that,
|
||||||
|
> (a) a mediation between the parties is to be conducted before any arbitration is conducted, and
|
||||||
|
> (b) if the mediation fails, the mediator shall arbitrate the dispute and make a binding resolution of it; (“convention de médiation-arbitrage”)
|
||||||
|
|
||||||
|
> (3) The arbitrator shall keep the record for at least 10 years after the date of the award. O. Reg. 134/07, s. 4 (3).
|
||||||
|
|
||||||
|
### O. Reg. 134/07: Family Arbitration — original (v1) version, Ontario e-Laws source law
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/regulation/070134/v1> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Qualifications of arbitrator 3. Every arbitrator who conducts a family arbitration shall have received the training approved by the Attorney General for the arbitrator or class of arbitrators, as set out on the Ministry’s website. O. Reg. 134/07, s. 3.
|
||||||
|
|
||||||
|
### Training for family arbitrators — Government of Ontario (ontario.ca)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/page/training-family-arbitrators> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> In Ontario, family arbitrators must complete training approved by the Attorney General. Arbitration awards made by family arbitrators who have not completed the required training are not enforceable by the court.
|
||||||
|
|
||||||
|
> You need to complete a training program of at least 14 hours (within one week) to learn about screening parties for domestic violence and power imbalances. You should ensure your training covers most of or all the following elements:
|
||||||
|
|
||||||
|
> Your training must follow the principles outlined in the Ontario Association for Family Mediation’s Policy on Domestic Violence and Power Imbalances , adjusted for arbitration.
|
||||||
|
|
||||||
|
> All family law arbitrators who are not a part of the Ontario Bar, or another Canadian bar, must complete 30 hours of training about Ontario family law. You do not need to complete this training all at once, but there are certain areas of family law that would be best to learn together.
|
||||||
|
|
||||||
|
> Your training must have been done within five years of an arbitration where you certified that you were trained, unless you have done on average at least two family arbitrations per year, during those five years.
|
||||||
|
|
||||||
|
> Ongoing training
|
||||||
|
> As a family arbitrator, you will also need to take ongoing training of 10 hours over any two-year period. Five of these hours must be related to domestic violence or power imbalance issues.
|
||||||
|
|
||||||
|
> Updated: August 12, 2021
|
||||||
|
> Published: May 04, 2021
|
||||||
|
|
||||||
|
### Family Law Act, R.S.O. 1990, c. F.3 (Ontario e-Laws, full text)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/90f03> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> “family arbitration” means an arbitration that,
|
||||||
|
> (a) deals with matters that could be dealt with in a marriage contract, separation agreement, cohabitation agreement or paternity agreement under this Part, and
|
||||||
|
> (b) is conducted exclusively in accordance with the law of Ontario or of another Canadian jurisdiction; (“arbitrage familial”)
|
||||||
|
|
||||||
|
> 59.1 (1) Family arbitrations, family arbitration agreements and family arbitration awards are governed by this Act and by the Arbitration Act, 1991 . 2006, c. 1, s. 5 (10).
|
||||||
|
|
||||||
|
> Conditions for enforceability
|
||||||
|
> 59.6 (1) A family arbitration award is enforceable only if,
|
||||||
|
> (a) the family arbitration agreement under which the award is made is made in writing and complies with any regulations made under the Arbitration Act, 1991 ;
|
||||||
|
> (b) each of the parties to the agreement receives independent legal advice before making the agreement;
|
||||||
|
> (c) the requirements of section 38 of the Arbitration Act, 1991 are met (formal requirements, writing, reasons, delivery to parties); and
|
||||||
|
> (d) the arbitrator complies with any regulations made under the Arbitration Act, 1991. 2006, c. 1, s. 5 (10).
|
||||||
|
|
||||||
|
> 59.4 A family arbitration agreement and an award made under it are unenforceable unless the family arbitration agreement is entered into after the dispute to be arbitrated has arisen. 2006, c. 1, s. 5 (10).
|
||||||
|
|
||||||
|
### Arbitration Act, 1991, S.O. 1991, c. 17 (Ontario e-Laws, full text)
|
||||||
|
|
||||||
|
<https://www.ontario.ca/laws/statute/91a17> — retrieved 2026-08-29
|
||||||
|
|
||||||
|
> Arbitration Act, 1991
|
||||||
|
> S.O. 1991, Chapter 17
|
||||||
|
> Consolidation Period: From March 22, 2017 to the e-Laws currency date .
|
||||||
|
> Last amendment: 2017, c. 2, Sched. 5, s. 13 .
|
||||||
|
|
||||||
|
> Family arbitrations, agreements and awards
|
||||||
|
> 2.1 (1) Family arbitrations, family arbitration agreements and family arbitration awards are governed by this Act and by the Family Law Act . 2006, c. 1, s. 1 (2).
|
||||||
|
> Conflict
|
||||||
|
> (2) In the event of conflict between this Act and the Family Law Act, the Family Law Act prevails. 2006, c. 1, s. 1 (2).
|
||||||
|
|
||||||
|
> Other third-party decision-making processes in family matters
|
||||||
|
> 2.2 (1) When a decision about a matter described in clause (a) of the definition of “family arbitration” in section 1 is made by a third person in a process that is not conducted exclusively in accordance with the law of Ontario or of another Canadian jurisdiction,
|
||||||
|
> (a) the process is not a family arbitration; and
|
||||||
|
> (b) the decision is not a family arbitration award and has no legal effect. 2006, c. 1, s. 1 (2).
|
||||||
|
|
||||||
|
> Family arbitration awards
|
||||||
|
> 50.1 Family arbitration awards are enforceable only under the Family Law Act . 2006, c. 1, s. 1 (10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this establishes
|
||||||
|
|
||||||
|
Each item names the source it rests on. An item here that no quotation above
|
||||||
|
supports is a defect in this file, not a fact.
|
||||||
|
|
||||||
|
- OPPRESSION REMEDY (Ontario): the oppression remedy is s. 248 of the Business Corporations Act, R.S.O. 1990, c. B.16. Section 248(1) gives a 'complainant' (and, for an offering corporation, the Commission) standing to apply to the court. The operative test in s. 248(2) is conduct 'that is oppressive or unfairly prejudicial to or that unfairly disregards the interests of any security holder, creditor, director or officer of the corporation', on which 'the court may make an order to rectify the matters complained of.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90b16>
|
||||||
|
- The OBCA oppression provision is broader than its federal counterpart in one respect visible on the face of the text: s. 248(2) reaches conduct that 'effects or threatens to effect a result' and affairs 'are, have been or are threatened to be carried on', i.e. it expressly captures threatened conduct. The CBCA equivalent (s. 241(2)) uses only 'effects a result' and 'are or have been carried on', with no 'threatened' language.
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html>
|
||||||
|
- OBCA s. 248(3) lists the remedial orders available, including '(f) an order directing a corporation, subject to subsection (6), or any other person, to purchase securities of a security holder' (the buy-out order) and '(l) an order winding up the corporation under section 207'. Payment under (3)(f) or (g) is barred by s. 248(6) where the corporation is or would become unable to pay its liabilities as they become due.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90b16>
|
||||||
|
- 'Complainant' is defined in OBCA s. 245 to include a registered holder or beneficial owner (and former holder/owner) of a security, a director or officer (or former director or officer), and '(c) any other person who, in the discretion of the court, is a proper person to make an application under this Part.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90b16>
|
||||||
|
- OPPRESSION REMEDY (federal): the equivalent is s. 241 of the Canada Business Corporations Act, R.S.C., 1985, c. C-44, marginal note 'Application to court re oppression'. Section 241(1) provides 'A complainant may apply to a court for an order under this section', and the s. 241(2) test is conduct 'that is oppressive or unfairly prejudicial to or that unfairly disregards the interests of any security holder, creditor, director or officer', on which 'the court may make an order to rectify the matters complained of.'
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html>
|
||||||
|
- CBCA s. 241(3) lists the same remedial menu as the OBCA, including a buy-out order at (3)(f) and, at (3)(l), 'an order liquidating and dissolving the corporation'. CBCA s. 241(7) expressly permits an oppression applicant to 'apply in the alternative for an order under section 214'.
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html>
|
||||||
|
- 'Complainant' is defined in CBCA s. 238 and, unlike the Ontario definition, expressly includes '(c) the Director' alongside security holders, former security holders, directors, officers, and any other person a court in its discretion finds a proper person.
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html>
|
||||||
|
- APPRAISAL / DISSENT RIGHTS (Ontario): OBCA s. 185, headed 'Rights of dissenting shareholders'. Section 185(1) is expressed 'Subject to subsection (3) and to sections 186 and 248' and lists the triggering resolutions (certain article amendments, amalgamation, continuance out of the jurisdiction, and sale/lease/exchange of all or substantially all property under s. 184(3)).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90b16>
|
||||||
|
- APPRAISAL / DISSENT RIGHTS (federal): CBCA s. 190, marginal note 'Right to dissent'. Section 190(3) states the remedy: a complying shareholder 'is entitled ... to be paid by the corporation the fair value of the shares in respect of which the shareholder dissents, determined as of the close of business on the day before the resolution was adopted or the order was made.' The CBCA list of triggers includes, at s. 190(1)(f), 'carry out a going-private transaction or a squeeze-out transaction'.
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html>
|
||||||
|
- A shareholder cannot use both remedies on the same amendment: OBCA s. 248(5) and CBCA s. 241(5) each provide that a shareholder is not entitled to dissent (under s. 185 / s. 190 respectively) if an amendment to the articles is effected under the oppression section.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90b16>
|
||||||
|
- WINDING UP / JUST AND EQUITABLE (Ontario): OBCA s. 207, headed 'Winding up by court'. Section 207(1)(a) reproduces the oppression grounds; s. 207(1)(b)(iv) is the just-and-equitable ground — 'it is just and equitable for some reason, other than the bankruptcy or insolvency of the corporation, that it should be wound up'. Section 207(1)(b)(i) separately covers a unanimous shareholder agreement that entitled a shareholder to demand dissolution on a specified event that has occurred. By s. 207(2), on such an application 'the court may make such order under this section or section 248 as it thinks fit.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90b16>
|
||||||
|
- LIQUIDATION AND DISSOLUTION / JUST AND EQUITABLE (federal): CBCA s. 214, marginal note 'Further grounds'. A court may order liquidation and dissolution 'on the application of a shareholder' on the oppression grounds in s. 214(1)(a) or, under s. 214(1)(b)(ii), where 'it is just and equitable that the corporation should be liquidated and dissolved.' Section 214(2) mirrors the Ontario cross-reference: 'a court may make such order under this section or section 241 as it thinks fit.'
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html>
|
||||||
|
- PARTNERSHIPS ACT (Ontario) citation: Partnerships Act, R.S.O. 1990, Chapter P.5. The e-Laws consolidation retrieved was 'From October 1, 2023 to the e-Laws currency date', last amendment 2023, c. 9, Sched. 26.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90p05>
|
||||||
|
- Partnerships Act dissolution provisions run from s. 32 under the heading 'Dissolution of Partnership': s. 32 (expiry of term, completion of the adventure, or notice by a partner where the partnership is for an undefined time); s. 33 (death or insolvency of a partner; and, at the option of the other partners, where a partner's share is charged for a separate debt); s. 34 (illegality of the business); and s. 35 (dissolution by the court).
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90p05>
|
||||||
|
- Sections 32 and 33 of the Partnerships Act are each expressed 'Subject to any agreement between the partners' — so a partnership agreement can displace them. Section 34 (illegality) is not so qualified: 'A partnership is in every case dissolved by the happening of any event that makes it unlawful for the business of the firm to be carried on'.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90p05>
|
||||||
|
- Partnerships Act s. 35(1) sets out six grounds on which, 'On application by a partner, the court may order a dissolution of the partnership', including (d) wilful or persistent breach of the partnership agreement or conduct such 'that it is not reasonably practicable for the other partner or partners to carry on the business in partnership with the partner'; (e) 'when the business of the partnership can only be carried on at a loss'; and (f) the just-and-equitable ground — 'when in any case circumstances have arisen that in the opinion of the court render it just and equitable that the partnership be dissolved.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90p05>
|
||||||
|
- Partnerships Act s. 39 provides that on dissolution every partner is entitled to have partnership property applied to the firm's debts and the surplus distributed, and that 'for that purpose any partner or the partner's representative may, on the termination of the partnership, apply to the court to wind up the business and affairs of the firm.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90p05>
|
||||||
|
- ARBITRATION IN THE CORPORATIONS STATUTES — Ontario: YES, in exactly one place. OBCA s. 108(6)(b) provides that a unanimous shareholder agreement may provide that 'in the event that shareholders who are parties to the unanimous shareholder agreement are unable to agree on or resolve any matter pertaining to the agreement, the matter may be referred to arbitration under such procedures and conditions as are specified in the unanimous shareholder agreement.' This is the only occurrence of the string 'arbitrat' in the whole Act — a case-insensitive search returned exactly 1 hit in both the converted text and the raw HTML, and the hit was read in full context.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90b16>
|
||||||
|
- ARBITRATION IN THE CORPORATIONS STATUTES — federal: NO. A case-insensitive search of the complete CBCA full text for 'arbitrat' returned zero occurrences, in both the converted text and the raw HTML. The instrument was validated on the same file in the same run ('oppressive' returned 3 hits, 'unanimous shareholder agreement' 26), and the retrieved document was confirmed complete (it runs through s. 267, the Schedule of Offences, and the Related Provisions). The CBCA does validate unanimous shareholder agreements at s. 146(1) but, unlike OBCA s. 108(6)(b), says nothing about referring a dispute under one to arbitration.
|
||||||
|
*Source:* <https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html>
|
||||||
|
- Neither corporations statute mentions mediation. A search for 'mediat' returned 23 hits in the CBCA and 25 in the OBCA; every one was read and all are 'immediately' or 'immediate'. The Ontario Partnerships Act likewise contains zero occurrences of 'arbitrat', and its only 'mediat' hits (4) are 'immediately'.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90p05>
|
||||||
|
- FAMILY ARBITRATION — the 14-hour screening requirement is CONFIRMED. The Government of Ontario page states: 'You need to complete a training program of at least 14 hours (within one week) to learn about screening parties for domestic violence and power imbalances.' This independently reproduces the figure in the repo's existing extract at docs/reference/ontario-family-arbitration-training.md.
|
||||||
|
*Source:* <https://www.ontario.ca/page/training-family-arbitrators>
|
||||||
|
- FAMILY ARBITRATION — the 30-hour Ontario family law requirement is CONFIRMED, and it applies only to non-lawyers: 'All family law arbitrators who are not a part of the Ontario Bar, or another Canadian bar, must complete 30 hours of training about Ontario family law.'
|
||||||
|
*Source:* <https://www.ontario.ca/page/training-family-arbitrators>
|
||||||
|
- FAMILY ARBITRATION — the ongoing-training requirement is CONFIRMED: 'As a family arbitrator, you will also need to take ongoing training of 10 hours over any two-year period. Five of these hours must be related to domestic violence or power imbalance issues.'
|
||||||
|
*Source:* <https://www.ontario.ca/page/training-family-arbitrators>
|
||||||
|
- FAMILY ARBITRATION — a currency requirement the repo's existing extract does NOT record: 'Your training must have been done within five years of an arbitration where you certified that you were trained, unless you have done on average at least two family arbitrations per year, during those five years.' This is a fourth requirement alongside the three hour figures.
|
||||||
|
*Source:* <https://www.ontario.ca/page/training-family-arbitrators>
|
||||||
|
- FAMILY ARBITRATION — the enforcement consequence, stated by the government page itself: 'In Ontario, family arbitrators must complete training approved by the Attorney General. Arbitration awards made by family arbitrators who have not completed the required training are not enforceable by the court.'
|
||||||
|
*Source:* <https://www.ontario.ca/page/training-family-arbitrators>
|
||||||
|
- IMPORTANT QUALIFICATION on the word 'prescribed': the hour figures are NOT set out in the regulation. O. Reg. 134/07, s. 3 ('Qualifications of arbitrator') requires only that 'Every arbitrator who conducts a family arbitration shall have received the training approved by the Attorney General for the arbitrator or class of arbitrators, as set out on a Government of Ontario website.' The regulation makes the training mandatory by reference; the content and the hours live on the ontario.ca page, which the Attorney General can change without amending the regulation.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/070134>
|
||||||
|
- The hour figures have NEVER appeared in O. Reg. 134/07. The original (v1) version of s. 3 reads identically except that it referred to 'the Ministry's website'; O. Reg. 411/21, s. 1 changed only that phrase to 'a Government of Ontario website'. So the delegation to a web page has been the mechanism since the regulation was made.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/070134/v1>
|
||||||
|
- The screening requirement has a second, harder anchor than the government web page: the arbitrator's certificate prescribed by O. Reg. 134/07, s. 2(4) para. 5 requires the arbitrator to confirm in the agreement itself both 'ii. I have received the appropriate training approved by the Attorney General' and that 'The parties were separately screened for power imbalances and domestic violence and I have considered the results of the screening and will do so throughout the arbitration'. This text IS in the regulation.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/070134>
|
||||||
|
- The statutory chain for family arbitration: Family Law Act, R.S.O. 1990, c. F.3, s. 51 defines 'family arbitration'; s. 59.1(1) provides that family arbitrations 'are governed by this Act and by the Arbitration Act, 1991'; and s. 59.6(1) makes an award 'enforceable only if' the agreement complies with the regulations, each party 'receives independent legal advice before making the agreement', s. 38 of the Arbitration Act, 1991 is met, and '(d) the arbitrator complies with any regulations made under the Arbitration Act, 1991' — which is the provision that makes O. Reg. 134/07 s. 3 training bite on enforceability.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90f03>
|
||||||
|
- Family Law Act s. 59.4 requires that a family arbitration agreement be entered into after the dispute has arisen: 'A family arbitration agreement and an award made under it are unenforceable unless the family arbitration agreement is entered into after the dispute to be arbitrated has arisen.' This is a structural difference from commercial arbitration, where pre-dispute clauses are the norm.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/90f03>
|
||||||
|
- Arbitration Act, 1991, S.O. 1991, c. 17, s. 2.1(1) states that family arbitrations 'are governed by this Act and by the Family Law Act', and s. 2.1(2) that 'In the event of conflict between this Act and the Family Law Act, the Family Law Act prevails.' Section 2.2(1) provides that a family-matter decision made in a process 'not conducted exclusively in accordance with the law of Ontario or of another Canadian jurisdiction' is not a family arbitration and 'has no legal effect'. Section 50.1: 'Family arbitration awards are enforceable only under the Family Law Act.'
|
||||||
|
*Source:* <https://www.ontario.ca/laws/statute/91a17>
|
||||||
|
- O. Reg. 134/07 defines 'mediation-arbitration agreement' for family matters as a family arbitration agreement providing that '(a) a mediation between the parties is to be conducted before any arbitration is conducted, and (b) if the mediation fails, the mediator shall arbitrate the dispute and make a binding resolution of it' — a definition of med-arb that exists in Ontario law only in the family context.
|
||||||
|
*Source:* <https://www.ontario.ca/laws/regulation/070134>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this does NOT establish
|
||||||
|
|
||||||
|
**Read this section before writing copy.** It is the half that keeps a page
|
||||||
|
honest, and on this project it is the half that has twice been skipped.
|
||||||
|
|
||||||
|
- **Does the word 'prescribed' accurately describe Ontario's family arbitrator training hours, as AGENTS.md §9 Q39 and line 1952 currently put it?**
|
||||||
|
- *Searched:* Fetched the current consolidation of O. Reg. 134/07 and its original (v1) version from ontario.ca, and read s. 3 in full in both.
|
||||||
|
- *Outcome:* NOT CONFIRMED — and this looks like a wording defect in the repo, not a sourcing gap. No hour figure appears anywhere in the regulation, in any version. The regulation requires 'the training approved by the Attorney General ... as set out on a Government of Ontario website'; the 14/30/10 figures are administratively approved training published on a web page the Attorney General can revise without amending the regulation. The requirement is mandatory and enforceability-bearing, so 'required' or 'approved by the Attorney General' is accurate; 'prescribed' implies the numbers sit in the instrument, and they do not. Flagging for Pouya rather than fixing — AGENTS.md is his record.
|
||||||
|
- **Is the ontario.ca training page still the operative statement of approved training as at 2026-08-29, given it carries 'Updated: August 12, 2021'?**
|
||||||
|
- *Searched:* Fetched the live page today (HTTP 200) and read its own date stamps; also fetched the current e-Laws consolidation of O. Reg. 134/07 to confirm s. 3 still points to a Government of Ontario website.
|
||||||
|
- *Outcome:* PARTLY. The page is live today and the regulation still delegates to it, so it is the operative source by the regulation's own terms. But its content stamp is 'Updated: August 12, 2021 / Published: May 04, 2021'. I found no separate published register of Attorney General training approvals against which to cross-check, so I cannot independently confirm the figures have not been superseded by an approval not reflected on the page. Treat the hours as 'what the government page states as at 2026-08-29'.
|
||||||
|
- **Do the regulations made under the CBCA (as distinct from the Act) say anything about arbitration?**
|
||||||
|
- *Searched:* Only the CBCA Act full text at laws-lois.justice.gc.ca was retrieved and searched. The Canada Business Corporations Regulations, 2001 (SOR/2001-512) were not fetched.
|
||||||
|
- *Outcome:* NOT SEARCHED. The negative finding above is scoped to the Act only. If the site is going to say anything about federal corporate arbitration, the regulations should be checked too before that claim is written.
|
||||||
|
- **Do any of these sources support a claim about what qualifications a COMMERCIAL arbitrator or mediator in Ontario needs?**
|
||||||
|
- *Searched:* All eight sources above were searched for arbitration/mediation references; the family arbitration chain (FLA ss. 51, 59.1-59.7; Arbitration Act ss. 2.1, 2.2, 50.1; O. Reg. 134/07) was read in full.
|
||||||
|
- *Outcome:* NOT ESTABLISHED, and this confirms the honest limit the repo's own extract already records. Every training and qualification requirement found is expressly tied to 'family arbitration' as defined in FLA s. 51. Nothing retrieved states, either way, what a commercial arbitrator or mediator requires. The absence of a commercial requirement in family-specific instruments is not authority that none exists — AGENTS.md §4 is right to carry the commercial half as Pouya's stated position rather than as a sourced fact, and nothing in this pass changes that.
|
||||||
|
- **Is there a statutory buy-out or shotgun mechanism for closely-held Ontario corporations outside the oppression remedy?**
|
||||||
|
- *Searched:* Read OBCA ss. 108, 185, 207, 245-250 and CBCA ss. 146, 190, 214, 238-241 in full text.
|
||||||
|
- *Outcome:* NOT FOUND as a freestanding mechanism. The buy-out order exists only as a discretionary remedy the court may grant under OBCA s. 248(3)(f) / CBCA s. 241(3)(f), subject to the solvency limit in s. 248(6) / s. 241(6). Shotgun and buy-sell clauses are creatures of the shareholders' agreement, not of either statute. Do not let site copy imply a statutory buy-out right.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adversarial check on this extract
|
||||||
|
|
||||||
|
An independent pass was run over the items above with one instruction: decide
|
||||||
|
whether the pasted quotations actually support each one, and flag anything
|
||||||
|
broader than its quote. `PARTLY` means the wording overreaches the source.
|
||||||
|
|
||||||
|
**Overreach found:** YES — see below
|
||||||
|
|
||||||
|
| Verdict | Claim | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| **PARTLY** | [3] OBCA s. 248(3) remedial orders incl. (3)(f) buy-out and (3)(l) winding up; plus 'Payment under (3)(f) or (g) is barred by s. 248(6) where the corporation is or would become una | The first sentence is verbatim in the quote. The second sentence overreaches: no quote of s. 248(6) is pasted anywhere, and the words 'is or would become unable to pay its liabilities as they become due' appear in no source. The quote's own words go only as far as 'subject to subsection (6)' — it does not disclose what subsection (6) says. Paragraph '(g)' also appears in no pasted quote at all. Ov |
|
||||||
|
| **PARTLY** | [6] 'CBCA s. 241(3) lists the same remedial menu as the OBCA', incl. (3)(f) buy-out and (3)(l) liquidation; s. 241(7) alternative application under s. 214 | The (3)(f), (3)(l) and 241(7) elements are verbatim in the quotes. 'lists the same remedial menu as the OBCA' overreaches: both pasted s. 248(3) and s. 241(3) quotes are elided (each contains '...'), so the full menus were never put side by side, and the one paragraph the fact itself compares differs textually — OBCA (l) is 'an order winding up the corporation under section 207', CBCA (l) is 'an o |
|
||||||
|
| **PARTLY** | [8] OBCA s. 185 dissent rights — 'Subject to subsection (3) and to sections 186 and 248' and the triggering resolutions | The heading, the 'Subject to' clause and the four named triggers are in the quote. But the pasted s. 185(1) is elided ('... (c) amalgamate') so at least paragraph (b) is missing from the source text, while the fact presents its parenthetical as 'the triggering resolutions' — a closed list a copywriter would reproduce as complete. Overreaching words: 'lists the triggering resolutions (…)' read as e |
|
||||||
|
| **NOT** | [10] OBCA s. 248(5) and CBCA s. 241(5) each bar dissent where an article amendment is effected under the oppression section | No quote of s. 248(5) or s. 241(5) appears anywhere in the source set — the OBCA and CBCA quote blocks jump from (3) to other sections. The proposition may well be true in the world, but nothing pasted supports any part of it, including the characterisation 'A shareholder cannot use both remedies on the same amendment'. |
|
||||||
|
| **PARTLY** | [11] OBCA s. 207 winding up — 207(1)(a) oppression grounds, (b)(iv) just and equitable, (b)(i) USA dissolution trigger; and 'By s. 207(2) … the court may make such order under this | Everything up to and including s. 207(1)(b)(iv) and (b)(i) is verbatim in the two pasted 207(1) quotes. The final sentence is not: no quote of s. 207(2) exists in the source set, yet the fact renders it inside quotation marks as source text. Overreaching words: "By s. 207(2), on such an application 'the court may make such order under this section or section 248 as it thinks fit.'" — presented as |
|
||||||
|
| **PARTLY** | [12] CBCA s. 214 liquidation/dissolution — 214(1)(a) and (b)(ii); and 'Section 214(2) mirrors the Ontario cross-reference: a court may make such order under this section or section | The marginal note 'Further grounds', 'on the application of a shareholder', the oppression grounds and the just-and-equitable ground are verbatim in the quote. The s. 214(2) sentence is not supported by any pasted quote and is again presented in quotation marks; the word 'mirrors' additionally rests on the equally unquoted OBCA s. 207(2). Overreaching words: the whole final sentence 'Section 214(2 |
|
||||||
|
| **PARTLY** | [14] Partnerships Act dissolution provisions 'run from s. 32 under the heading Dissolution of Partnership'; content of ss. 32, 33, 34, 35 | The substance of ss. 32, 33(1), 33(2), 34 and 35 is verbatim in the quotes. The structural claim is not: the string 'Dissolution of Partnership' appears in no pasted quote — the quoted headings are 'Dissolution by expiry of term or notice', 'Dissolution by death or insolvency of partner', 'By illegality of business' and 'By the court'. Nor does anything quoted show where the dissolution provisions |
|
||||||
|
| **PARTLY** | [15] 'Sections 32 and 33 are each expressed Subject to any agreement between the partners — so a partnership agreement can displace them'; s. 34 not so qualified | s. 32 and s. 33(1) do carry the phrase in the quotes, and the s. 34 quote demonstrably lacks it. But the quoted s. 33(2) — 'A partnership may, at the option of the other partners, be dissolved if any partner suffers that partner's share … to be charged' — carries no 'subject to any agreement' qualifier, so a section-level statement about 's. 33' is broader than the pasted text. 'so a partnership a |
|
||||||
|
| **PARTLY** | [18] Arbitration in the OBCA: 'YES, in exactly one place' — s. 108(6)(b), 'the only occurrence of the string arbitrat in the whole Act' | The text of s. 108(6)(b) is pasted verbatim and fully supports the affirmative half. The exhaustive half cannot be supported by any quote: a pasted excerpt cannot establish that a string occurs nowhere else in a 732 KB statute — that claim rests on a grep reported in searchesRun, not on quoted source text, and it is the kind of statement that would appear on a public page as a bare assertion about |
|
||||||
|
| **PARTLY** | [19] Arbitration in the CBCA: 'NO' — zero occurrences of 'arbitrat'; document confirmed complete; CBCA validates USAs at s. 146(1) but says nothing about arbitration | Only the last clause is quote-backed: s. 146(1) is pasted and does validate unanimous shareholder agreements without mentioning arbitration. Everything else is an absence claim about a whole statute that no pasted quote can carry — the zero-hit counts, the control-hit validation, and the completeness assertion ('runs through s. 267, the Schedule of Offences, and the Related Provisions') appear in |
|
||||||
|
| **NOT** | [20] 'Neither corporations statute mentions mediation' (23 CBCA / 25 OBCA hits, all 'immediately'); Partnerships Act has zero 'arbitrat' and 4 'mediat' hits, all 'immediately' | No pasted quote supports any part of this. It is entirely a report of grep output listed in searchesRun, and the sources contain no quoted text in which the word does or does not appear. The scope also runs past the cited source: the item is filed against the Partnerships Act URL while asserting negatives about both the OBCA and the CBCA. This is a three-statute class statement with zero quoted in |
|
||||||
|
| **PARTLY** | [22] The 30-hour Ontario family law requirement 'applies only to non-lawyers' | The 30-hour sentence is verbatim, but the class it describes is 'family law arbitrators who are not a part of the Ontario Bar, or another Canadian bar' — membership of a Canadian bar, not lawyer status. A foreign-qualified lawyer is a lawyer and is caught by the quoted words, so 'non-lawyers' widens (and misdescribes) the quoted class. 'only' also converts a positive requirement on one class into |
|
||||||
|
| **PARTLY** | [26] 'Prescribed' is inaccurate: the hour figures are NOT in O. Reg. 134/07; s. 3 delegates to a Government of Ontario website the AG can change without amending the regulation | The s. 3 quote fully supports the delegation mechanism and the exact words 'as set out on a Government of Ontario website'. The absence half is not quote-provable: the pasted regulation excerpts are four fragments (ss. 1, 2(4) para 5, 3, 4(3)), which cannot show that no hour figure appears anywhere in the instrument. 'which the Attorney General can change without amending the regulation' is also a |
|
||||||
|
| **PARTLY** | [27] 'The hour figures have NEVER appeared in O. Reg. 134/07'; v1 s. 3 identical but for 'the Ministry's website'; O. Reg. 411/21 s. 1 changed only that phrase | Comparing the two pasted texts of s. 3 supports the wording change and its amending citation. It does not support an all-versions, whole-regulation negative: only s. 3 is quoted from v1, and nothing at all is quoted from v2 or v3 despite searchesRun saying they were fetched. Overreaching words: 'The hour figures have NEVER appeared in O. Reg. 134/07' and 'changed only that phrase' as a statement a |
|
||||||
|
| **PARTLY** | [28] The certificate 'prescribed by O. Reg. 134/07, s. 2(4) para. 5' requires the arbitrator to confirm 'in the agreement itself' both item ii (training) and item iii (screening) | The certificate text is pasted and does contain items ii and iii verbatim. Three things exceed it. The pinpoint: the quote begins bare, at '5.', with nothing showing it sits in s. 2(4). The location: nothing quoted says the certificate goes 'in the agreement itself'. And the conjunction: the quote sets out iii (screened by me) and iv (screened by someone other than me) as apparent alternatives, so |
|
||||||
|
| **PARTLY** | [29] Statutory chain: FLA s. 51 defines 'family arbitration'; s. 59.1(1) governance; s. 59.6(1) enforceability conditions, (d) making O. Reg. 134/07 s. 3 bite | The definition, s. 59.1(1) and all four limbs of s. 59.6(1) are pasted verbatim, and the closing synthesis follows directly from quoted 59.6(1)(d) plus the quoted regulation heading ('Under: Arbitration Act, 1991'). The one thing outside the quotes is the pinpoint: the definition quote carries no section number, so 's. 51' is supplied from outside the source. Overreaching words: 's. 51' as a pinpo |
|
||||||
|
| **PARTLY** | [30] FLA s. 59.4 requires a post-dispute family arbitration agreement; 'This is a structural difference from commercial arbitration, where pre-dispute clauses are the norm.' | The s. 59.4 quote is verbatim and supports the first sentence entirely. The second sentence is supported by nothing: no source in the set says anything about commercial arbitration practice, and the extract's own notEstablished section concedes that nothing retrieved speaks to commercial arbitration. Overreaching words: 'This is a structural difference from commercial arbitration, where pre-disput |
|
||||||
|
| **PARTLY** | [32] O. Reg. 134/07 definition of 'mediation-arbitration agreement' — 'a definition of med-arb that exists in Ontario law only in the family context' | The definition itself is pasted verbatim and fully supports the first half. The trailing clause is a negative about the whole of Ontario law drawn from a family-specific regulation — precisely the class statement from a single instance this audit is looking for. The greps reported in searchesRun covered three corporations/partnership statutes and did not search for 'med-arb' or for the definition |
|
||||||
|
|
||||||
|
*14 of 32 items were found fully supported; only the
|
||||||
|
others are tabled above.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Searches run
|
||||||
|
|
||||||
|
- `curl https://www.ontario.ca/laws/statute/90b16 (OBCA full text, HTTP 200, 732,644 bytes)`
|
||||||
|
- `grep -c -i 'arbitrat' obca.txt / obca.html — 1 hit each, read in full context (s. 108(6)(b))`
|
||||||
|
- `grep -o -i '[a-z]*mediat[a-z]*' obca.txt | sort | uniq -c — 25 hits, all 'immediately'`
|
||||||
|
- `curl https://laws-lois.justice.gc.ca/eng/acts/C-44/FullText.html (CBCA full text, HTTP 200, 883,143 bytes)`
|
||||||
|
- `grep -c -i 'arbitrat' cbca.txt / cbca.html — 0 hits in both; instrument validated with controls 'oppressive' (3), 'unanimous shareholder agreement' (26); document completeness confirmed to s. 267 + Schedule + Related Provisions`
|
||||||
|
- `grep -o -i '[a-z]*mediat[a-z]*' cbca.txt | sort | uniq -c — 23 hits, all 'immediately'/'immediate'`
|
||||||
|
- `curl https://www.ontario.ca/laws/statute/90p05 (Partnerships Act, HTTP 200); grep 'arbitrat' — 0 hits in text and raw HTML, control 'partnership' 127 hits`
|
||||||
|
- `curl https://www.ontario.ca/laws/regulation/070134 (O. Reg. 134/07 current consolidation, HTTP 200)`
|
||||||
|
- `curl https://www.ontario.ca/laws/regulation/070134/v1, /v2, /v3 — historical versions of s. 3, to test whether hour figures were ever in the regulation`
|
||||||
|
- `WebSearch (allowed_domains: ontario.ca): 'Ontario family arbitrator training approved by the Attorney General hours screening domestic violence' — used only to locate the URL; snippets not relied on`
|
||||||
|
- `curl https://www.ontario.ca/page/training-family-arbitrators (HTTP 200) — primary source fetched and quoted directly rather than via search snippet`
|
||||||
|
- `curl https://www.ontario.ca/laws/statute/90f03 (Family Law Act, HTTP 200) — ss. 51, 59.1, 59.4, 59.6, 59.7`
|
||||||
|
- `curl https://www.ontario.ca/laws/statute/91a17 (Arbitration Act, 1991, HTTP 200) — ss. 2.1, 2.2, 50.1`
|
||||||
|
- `git grep -n -i '14 hours|30 hours|10 hours|screening' — located the repo's existing extract at docs/reference/ontario-family-arbitration-training.md and the AGENTS.md Q39 entry, for independent comparison`
|
||||||
@@ -64,4 +64,53 @@ export default [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// `scripts/` ARE CLI TOOLS, AND PRINTING IS THEIR OUTPUT. The `no-console`
|
||||||
|
// rule above is justified in this config as "a stray console.log in a static
|
||||||
|
// build is dead weight shipped to nobody" — which is a statement about the
|
||||||
|
// shipped bundle, and nothing in `scripts/` reaches it. `check-claims.mjs`
|
||||||
|
// exists to print what it matched: CLAUDE.md's rule is that a grep is not a
|
||||||
|
// finding until you read what it matched, so suppressing its output would
|
||||||
|
// defeat the tool. Scoped to this directory rather than disabled globally.
|
||||||
|
//
|
||||||
|
// ⚠️ IT MUST SIT AFTER THE BLOCK IT OVERRIDES. Flat config applies matching
|
||||||
|
// blocks in order, last one wins — placed above, this had no effect at all
|
||||||
|
// and `npm run lint` still reported all six warnings. Measured, not assumed.
|
||||||
|
{
|
||||||
|
files: ['scripts/**/*.{js,mjs}'],
|
||||||
|
rules: { 'no-console': 'off' },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* `infra/cloudfront/` IS NOT A NODE MODULE AND NOT A BROWSER SCRIPT. A
|
||||||
|
CloudFront Function's entry point is a bare `function handler(event)` that
|
||||||
|
the runtime calls **by name** — it has no `export` (the runtime rejects
|
||||||
|
module syntax) and nothing in the file references it, so
|
||||||
|
`no-unused-vars` fires on the one declaration that is the whole point of
|
||||||
|
the file. `argsIgnorePattern` cannot reach a function declaration, so the
|
||||||
|
rule is scoped off here rather than silenced with a comment at the
|
||||||
|
declaration, which would read as though the name were incidental.
|
||||||
|
The test beside it is a CLI tool and prints, exactly as `scripts/` does.
|
||||||
|
|
||||||
|
⚠️ LIKE THE BLOCK ABOVE, THIS MUST STAY LAST. Flat config applies matching
|
||||||
|
blocks in order and the last one wins. */
|
||||||
|
{
|
||||||
|
files: ['infra/cloudfront/**/*.{js,mjs}'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/* THE BACKEND TEST FILE ONLY — NOT `backend/intake/**`. `handler.mjs` runs in
|
||||||
|
Lambda, where `console.log` is a line in CloudWatch that nobody reads and
|
||||||
|
`console.warn`/`console.error` are the two that signal, so the rule stays on
|
||||||
|
for it deliberately. The test beside it is a CLI tool and prints its verdict,
|
||||||
|
exactly as `scripts/` and the router test do.
|
||||||
|
|
||||||
|
⚠️ LAST, LIKE THE TWO ABOVE. Flat config applies matching blocks in order
|
||||||
|
and the last one wins. */
|
||||||
|
{
|
||||||
|
files: ['backend/**/*.test.mjs'],
|
||||||
|
rules: { 'no-console': 'off' },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* Shape helpers for the CloudFront policy configs `configure.mjs` builds.
|
||||||
|
*
|
||||||
|
* ⚠️ **A POLICY AWS HANDS BACK IS NOT A POLICY AWS WILL ACCEPT.**
|
||||||
|
* `get-response-headers-policy` returns `{}` for a member the source does not
|
||||||
|
* define — `Managed-SecurityHeadersPolicy` does it for `ContentSecurityPolicy`
|
||||||
|
* — and sending that back fails `create-response-headers-policy` on
|
||||||
|
* ParamValidation before the call leaves the machine. `docs/09` Part 3 carries
|
||||||
|
* the incident and the exact error.
|
||||||
|
*
|
||||||
|
* **Dropping an empty member is safe at every depth, and that is a measurement
|
||||||
|
* rather than a hope.** Of the 16 structures reachable from
|
||||||
|
* `ResponseHeadersPolicyConfig` in the CLI's own service model, **15 declare at
|
||||||
|
* least one required field** — so `{}` is not a legal value there and can only
|
||||||
|
* be the placeholder. The single exception is `SecurityHeadersConfig` itself,
|
||||||
|
* and `configure.mjs` skips before it can build one of those empty, because a
|
||||||
|
* PDF policy cloning no security headers is the thing that section exists to
|
||||||
|
* avoid.
|
||||||
|
*
|
||||||
|
* They live in their own module so they can be tested: `configure.mjs` reads
|
||||||
|
* argv and calls AWS at import time, so importing THAT to reach two pure
|
||||||
|
* functions is not possible. Same reason `fields.mjs` sits beside
|
||||||
|
* `handler.mjs`. See `policy-shapes.test.mjs`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every empty-object member removed, at every depth, bottom-up — so a member
|
||||||
|
* left empty by stripping its own children is removed in turn.
|
||||||
|
*
|
||||||
|
* Arrays are recursed into but never have elements removed: an element index is
|
||||||
|
* load-bearing against its `Quantity` sibling, and an empty object inside one
|
||||||
|
* would be this script's own construction rather than an AWS placeholder. That
|
||||||
|
* case is left for `emptyObjectPaths` to report.
|
||||||
|
*/
|
||||||
|
export const withoutEmptyMembers = (value) => {
|
||||||
|
if (Array.isArray(value)) return value.map(withoutEmptyMembers);
|
||||||
|
if (!value || typeof value !== 'object') return value;
|
||||||
|
const out = {};
|
||||||
|
for (const [k, v] of Object.entries(value)) {
|
||||||
|
const cleaned = withoutEmptyMembers(v);
|
||||||
|
const isEmptyObject =
|
||||||
|
cleaned &&
|
||||||
|
typeof cleaned === 'object' &&
|
||||||
|
!Array.isArray(cleaned) &&
|
||||||
|
Object.keys(cleaned).length === 0;
|
||||||
|
if (!isEmptyObject) out[k] = cleaned;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** True for `{}` — the value AWS accepts nowhere in these configs. */
|
||||||
|
export const isEmptyObject = (v) =>
|
||||||
|
Boolean(v) &&
|
||||||
|
typeof v === 'object' &&
|
||||||
|
!Array.isArray(v) &&
|
||||||
|
Object.keys(v).length === 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dotted path of every empty object left in a config. A post-condition on
|
||||||
|
* the strip above, not a filter: if this returns anything, the strip did not do
|
||||||
|
* what this module claims it does.
|
||||||
|
*
|
||||||
|
* Empty ARRAYS are not reported — `{Quantity: 0, Items: []}` is valid and
|
||||||
|
* common, while an empty object is valid nowhere.
|
||||||
|
*/
|
||||||
|
export function emptyObjectPaths(value, path = '') {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((v, i) => emptyObjectPaths(v, `${path}[${i}]`));
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
if (Object.keys(value).length === 0) return [path || '(root)'];
|
||||||
|
return Object.entries(value).flatMap(([k, v]) =>
|
||||||
|
emptyObjectPaths(v, path ? `${path}.${k}` : k),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ **NO VALIDATOR I COULD READ ENFORCES ANY OF THESE, WHICH IS WHY THIS TABLE
|
||||||
|
* EXISTS.** Measured 2026-09-04 against **`aws-cli/2.34.53`'s bundled
|
||||||
|
* `botocore/validate.py`**: it checks **neither `max` nor `pattern`** —
|
||||||
|
* `range_check()` reads only `min`, and the word `pattern` does not appear in
|
||||||
|
* the file. And the caps that matter are not modelled as constraints anyway: on
|
||||||
|
* both policy configs `Comment` is a bare `string`, and the 128 lives in the
|
||||||
|
* shape's **`documentation` prose**.
|
||||||
|
*
|
||||||
|
* ⚠️ **THAT IS ONE INSTRUMENT, NAMED, NOT A CLAIM ABOUT EVERY MECHANISM.** This
|
||||||
|
* machine also carries `aws-cli/2.11.15`, whose install is PyInstaller-frozen
|
||||||
|
* and whose `validate.py` cannot be read, so it is **unchecked rather than
|
||||||
|
* confirmed**. `CLAUDE.md`: *"no mechanism can X" is a claim about every
|
||||||
|
* mechanism, including the ones you did not enumerate* — so the honest form is
|
||||||
|
* this one. What is **direct evidence** either way: the 182-character `Comment`
|
||||||
|
* reached the API and came back `InvalidArgument`, so nothing stopped it on the
|
||||||
|
* CLI that actually ran. `docs/09` Part 3 carries both attempts.
|
||||||
|
*
|
||||||
|
* Every entry below **with a cited AWS source** is therefore enforced by the
|
||||||
|
* service and by nothing local. The two `[assumed]` entries are not known to be
|
||||||
|
* enforced at all.
|
||||||
|
*
|
||||||
|
* ⚠️ **THE ENTRIES THAT MATTER MOST GUARD *CLONED* VALUES, NOT LITERALS THIS
|
||||||
|
* FILE AUTHORS.** A literal we write is reviewed when it is written; a value
|
||||||
|
* copied out of the default behaviour's policy changes without anyone here
|
||||||
|
* touching it, and `docs/05` already specifies a Content-Security-Policy that
|
||||||
|
* would land there. `CreateResponseHeadersPolicy` declares a dedicated error
|
||||||
|
* for exactly that — `TooLongCSPInResponseHeadersPolicy`.
|
||||||
|
*
|
||||||
|
* ⚠️ **KNOWN GAP, RECORDED RATHER THAN GUESSED: `RemoveHeadersConfig` is cloned
|
||||||
|
* too and its count cap is not published.** The operation declares
|
||||||
|
* `TooManyRemoveHeadersInResponseHeadersPolicy`, so a cap exists; the quotas
|
||||||
|
* page states no number and inventing one would be worse than the gap. A breach
|
||||||
|
* there surfaces as that error at the write, not as a pre-flight skip.
|
||||||
|
*
|
||||||
|
* ⚠️ **ABSENCE FROM A SOURCE IS NOT ABSENCE OF A LIMIT.** Entries marked
|
||||||
|
* `[assumed]` have no AWS source at all; they are kept because they cost nothing
|
||||||
|
* and constrain nothing this script sends.
|
||||||
|
*/
|
||||||
|
export const PAYLOAD_LIMITS = {
|
||||||
|
'response-headers-policy': [
|
||||||
|
{
|
||||||
|
path: 'Name',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 128,
|
||||||
|
source:
|
||||||
|
'[assumed] — no AWS source states a policy name length; the documented Name rule is uniqueness. Pouya, 2026-09-04',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Comment',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 128,
|
||||||
|
source:
|
||||||
|
'service model, ResponseHeadersPolicyConfig.Comment documentation: "The comment cannot be longer than 128 characters"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
/* CLONED, not authored here — see the header. */
|
||||||
|
path: 'SecurityHeadersConfig.ContentSecurityPolicy.ContentSecurityPolicy',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 1783,
|
||||||
|
source:
|
||||||
|
'CloudFront quotas, Quotas on headers: "Maximum length of the Content-Security-Policy header value | 1,783 characters"; error shape TooLongCSPInResponseHeadersPolicy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'CustomHeadersConfig.Items[].Header',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 256,
|
||||||
|
source:
|
||||||
|
'CloudFront quotas, Quotas on headers: "Custom headers: maximum length of a header name | 256 characters"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'CustomHeadersConfig.Items[].Value',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 1783,
|
||||||
|
source:
|
||||||
|
'CloudFront quotas, Quotas on headers: "Custom headers: maximum length of a header value | 1,783 characters"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'CustomHeadersConfig.Items[]',
|
||||||
|
rule: 'maxCount',
|
||||||
|
limit: 10,
|
||||||
|
source:
|
||||||
|
'CloudFront quotas: "maximum number of custom headers that you can add to a response headers policy | 10" (adjustable); error shape TooManyCustomHeadersInResponseHeadersPolicy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
'CustomHeadersConfig.Items[].Header',
|
||||||
|
'CustomHeadersConfig.Items[].Value',
|
||||||
|
],
|
||||||
|
rule: 'maxCombinedLength',
|
||||||
|
limit: 10240,
|
||||||
|
source:
|
||||||
|
'CloudFront quotas: "Custom headers: maximum length of all header values and names combined | 10,240 characters"',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'origin-request-policy': [
|
||||||
|
{
|
||||||
|
path: 'Name',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 128,
|
||||||
|
source: '[assumed] — see the response-headers-policy Name entry',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Comment',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 128,
|
||||||
|
source:
|
||||||
|
'service model, OriginRequestPolicyConfig.Comment documentation: "The comment cannot be longer than 128 characters". This is the one that failed on 2026-09-04 at 182',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'HeadersConfig.Headers.Items[]',
|
||||||
|
rule: 'maxCount',
|
||||||
|
limit: 10,
|
||||||
|
source:
|
||||||
|
'CloudFront quotas: "Headers per origin request policy | 10" (adjustable); error shape TooManyHeadersInOriginRequestPolicy. We send 5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
paths: ['HeadersConfig.Headers.Items[]'],
|
||||||
|
rule: 'maxCombinedLength',
|
||||||
|
limit: 1024,
|
||||||
|
source:
|
||||||
|
'CloudFront quotas: "Total combined length of all query string, header, and cookie names in an origin request policy | 1024". We contribute header names only',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
/* Checked as a flag before any AWS call, because by the time a distribution
|
||||||
|
payload exists sections 4 and 5 may already have created policies.
|
||||||
|
|
||||||
|
⚠️ NOT DECORATION. `aws cloudfront list-functions --output text` returns the
|
||||||
|
ARN twice, tab-joined, because the function exists in a DEVELOPMENT and a
|
||||||
|
LIVE stage — 113 characters, and it fails the pattern too. Staging that
|
||||||
|
replaces a working `router.js` association with a value CloudFront will not
|
||||||
|
accept, and `router.js` keeps 22 of 23 pages off S3's AccessDenied.
|
||||||
|
`docs/09` Part 2 derives it correctly with `describe-function --stage LIVE`. */
|
||||||
|
'function-association': [
|
||||||
|
{
|
||||||
|
path: 'FunctionARN',
|
||||||
|
rule: 'maxLength',
|
||||||
|
limit: 108,
|
||||||
|
source: "service model, shape FunctionARN: {'max': 108}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'FunctionARN',
|
||||||
|
rule: 'pattern',
|
||||||
|
limit: 'arn:aws:cloudfront::[0-9]{12}:function\\/[a-zA-Z0-9-_]{1,64}',
|
||||||
|
source: 'service model, shape FunctionARN: pattern',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a dotted path, where `[]` means "every element of this array". Always
|
||||||
|
* returns `{path, value}` pairs with the index substituted, so a violation
|
||||||
|
* names the element rather than the collection.
|
||||||
|
*/
|
||||||
|
function resolvePath(root, path) {
|
||||||
|
let frontier = [{ path: '', value: root }];
|
||||||
|
for (const segment of path.split('.')) {
|
||||||
|
const next = [];
|
||||||
|
const isArray = segment.endsWith('[]');
|
||||||
|
const key = isArray ? segment.slice(0, -2) : segment;
|
||||||
|
for (const { path: p, value } of frontier) {
|
||||||
|
const child = value?.[key];
|
||||||
|
const here = p ? `${p}.${key}` : key;
|
||||||
|
if (child === undefined || child === null) continue;
|
||||||
|
if (isArray) {
|
||||||
|
if (!Array.isArray(child)) continue;
|
||||||
|
child.forEach((v, i) => next.push({ path: `${here}[${i}]`, value: v }));
|
||||||
|
} else {
|
||||||
|
next.push({ path: here, value: child });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frontier = next;
|
||||||
|
}
|
||||||
|
return frontier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every limit the given payload breaches. Empty means it is safe to send as far
|
||||||
|
* as this table knows — which is a claim about the table, not about AWS.
|
||||||
|
*/
|
||||||
|
export function limitViolations(kind, payload) {
|
||||||
|
const rules = PAYLOAD_LIMITS[kind];
|
||||||
|
if (!rules) throw new Error(`no limit table for payload kind '${kind}'`);
|
||||||
|
const out = [];
|
||||||
|
const add = (v) => out.push(v);
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
/* `paths` (plural) is for the aggregate rules, where AWS caps a total
|
||||||
|
across more than one field — header names AND values combined. */
|
||||||
|
const paths = rule.paths ?? [rule.path];
|
||||||
|
const label = paths.join(' + ');
|
||||||
|
const resolved = paths.flatMap((one) => resolvePath(payload, one));
|
||||||
|
|
||||||
|
if (rule.rule === 'maxCount') {
|
||||||
|
if (resolved.length > rule.limit) {
|
||||||
|
add({
|
||||||
|
path: label,
|
||||||
|
rule: 'maxCount',
|
||||||
|
actual: resolved.length,
|
||||||
|
limit: rule.limit,
|
||||||
|
message: `${label} has ${resolved.length} entries; the limit is ${rule.limit} (${rule.source})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.rule === 'maxCombinedLength') {
|
||||||
|
const total = resolved.reduce(
|
||||||
|
(n, { value }) => n + (typeof value === 'string' ? value.length : 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if (total > rule.limit) {
|
||||||
|
add({
|
||||||
|
path: label,
|
||||||
|
rule: 'maxCombinedLength',
|
||||||
|
actual: total,
|
||||||
|
limit: rule.limit,
|
||||||
|
message: `${label} totals ${total} characters; the limit is ${rule.limit} (${rule.source})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { path, value } of resolved) {
|
||||||
|
if (typeof value !== 'string') continue;
|
||||||
|
if (rule.rule === 'maxLength' && value.length > rule.limit) {
|
||||||
|
add({
|
||||||
|
path,
|
||||||
|
rule: 'maxLength',
|
||||||
|
actual: value.length,
|
||||||
|
limit: rule.limit,
|
||||||
|
message: `${path} is ${value.length} characters; the limit is ${rule.limit} (${rule.source})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rule.rule === 'pattern' &&
|
||||||
|
!new RegExp(`^(?:${rule.limit})$`).test(value)
|
||||||
|
) {
|
||||||
|
add({
|
||||||
|
path,
|
||||||
|
rule: 'pattern',
|
||||||
|
actual: JSON.stringify(value),
|
||||||
|
limit: rule.limit,
|
||||||
|
message: `${path} does not match ${rule.limit} (${rule.source})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
/**
|
||||||
|
* Tests for `policy-shapes.mjs` — the two functions that answer the 2026-09-04
|
||||||
|
* `--apply` failure recorded in `docs/09` Part 3.
|
||||||
|
*
|
||||||
|
* The first case is that failure verbatim: the `SecurityHeadersConfig` the live
|
||||||
|
* `Managed-SecurityHeadersPolicy` returns, empty `ContentSecurityPolicy` and
|
||||||
|
* all, which is what `create-response-headers-policy` rejected.
|
||||||
|
*
|
||||||
|
* node infra/cloudfront/policy-shapes.test.mjs
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
withoutEmptyMembers,
|
||||||
|
emptyObjectPaths,
|
||||||
|
isEmptyObject,
|
||||||
|
limitViolations,
|
||||||
|
PAYLOAD_LIMITS,
|
||||||
|
} from './policy-shapes.mjs';
|
||||||
|
|
||||||
|
let pass = 0;
|
||||||
|
const failures = [];
|
||||||
|
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
const t = (name, got, want) => {
|
||||||
|
if (eq(got, want)) pass += 1;
|
||||||
|
else
|
||||||
|
failures.push(
|
||||||
|
`${name}\n got ${JSON.stringify(got)}\n want ${JSON.stringify(want)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* The live source policy, copied from `get-response-headers-policy` on
|
||||||
|
67f7725c-6f97-4210-82d7-5512b31e9d03 [verified 2026-09-04]. */
|
||||||
|
const LIVE_SECURITY_HEADERS = {
|
||||||
|
XSSProtection: { Override: false, Protection: true, ModeBlock: true },
|
||||||
|
FrameOptions: { Override: false, FrameOption: 'SAMEORIGIN' },
|
||||||
|
ReferrerPolicy: {
|
||||||
|
Override: false,
|
||||||
|
ReferrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
|
},
|
||||||
|
ContentSecurityPolicy: {},
|
||||||
|
ContentTypeOptions: { Override: true },
|
||||||
|
StrictTransportSecurity: {
|
||||||
|
Override: false,
|
||||||
|
AccessControlMaxAgeSec: 31536000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---- the incident itself ------------------------------------------------ */
|
||||||
|
const stripped = withoutEmptyMembers(LIVE_SECURITY_HEADERS);
|
||||||
|
t(
|
||||||
|
'the 2026-09-04 breach: ContentSecurityPolicy is dropped',
|
||||||
|
Object.keys(stripped).sort(),
|
||||||
|
[
|
||||||
|
'ContentTypeOptions',
|
||||||
|
'FrameOptions',
|
||||||
|
'ReferrerPolicy',
|
||||||
|
'StrictTransportSecurity',
|
||||||
|
'XSSProtection',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'and five survive — the count docs/09 Part 3 tells the operator to read',
|
||||||
|
Object.keys(stripped).length,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'the surviving members are untouched',
|
||||||
|
stripped.StrictTransportSecurity,
|
||||||
|
LIVE_SECURITY_HEADERS.StrictTransportSecurity,
|
||||||
|
);
|
||||||
|
t('nothing empty is left behind', emptyObjectPaths(stripped), []);
|
||||||
|
|
||||||
|
/* ---- the placeholder one level up, which a SecurityHeadersConfig-only strip
|
||||||
|
turned into a hard abort (adversarial-reviewer, round 1) ------------- */
|
||||||
|
t(
|
||||||
|
'a top-level policy-config member is dropped',
|
||||||
|
withoutEmptyMembers({
|
||||||
|
Name: 'p',
|
||||||
|
CorsConfig: {},
|
||||||
|
SecurityHeadersConfig: stripped,
|
||||||
|
}),
|
||||||
|
{ Name: 'p', SecurityHeadersConfig: stripped },
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- and the one BELOW that, which the first repair still aborted on
|
||||||
|
(adversarial-reviewer, round 2) -------------------------------------- */
|
||||||
|
t(
|
||||||
|
'a CorsConfig member is dropped, and the emptied CorsConfig with it',
|
||||||
|
withoutEmptyMembers({
|
||||||
|
Name: 'p',
|
||||||
|
CorsConfig: { AccessControlExposeHeaders: {} },
|
||||||
|
}),
|
||||||
|
{ Name: 'p' },
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'but a CorsConfig that still has content survives',
|
||||||
|
withoutEmptyMembers({
|
||||||
|
CorsConfig: { AccessControlExposeHeaders: {}, OriginOverride: false },
|
||||||
|
}),
|
||||||
|
{ CorsConfig: { OriginOverride: false } },
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- things that must NOT be discarded ---------------------------------- */
|
||||||
|
t(
|
||||||
|
'an empty ARRAY is kept — {Quantity: 0, Items: []} is valid and common',
|
||||||
|
withoutEmptyMembers({ RemoveHeadersConfig: { Quantity: 0, Items: [] } }),
|
||||||
|
{ RemoveHeadersConfig: { Quantity: 0, Items: [] } },
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'false, 0, null and empty string are kept',
|
||||||
|
withoutEmptyMembers({ a: false, b: 0, c: null, d: '' }),
|
||||||
|
{ a: false, b: 0, c: null, d: '' },
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'array elements are recursed into but never removed',
|
||||||
|
withoutEmptyMembers({ Items: [{ Header: 'X', Sub: {} }, {}] }),
|
||||||
|
{ Items: [{ Header: 'X' }, {}] },
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'the custom-headers list the script builds is untouched',
|
||||||
|
withoutEmptyMembers({
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [{ Header: 'X-Robots-Tag', Value: 'noindex', Override: true }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [{ Header: 'X-Robots-Tag', Value: 'noindex', Override: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
t('stripping is idempotent', withoutEmptyMembers(stripped), stripped);
|
||||||
|
|
||||||
|
/* ---- the drift comparison: {} and absent must normalise alike ------------
|
||||||
|
Round 1's repair stripped children but left `norm({})` as "{}" against
|
||||||
|
`norm(undefined)` as "null", which reported permanent, unrepairable drift on
|
||||||
|
the intake form's own path. */
|
||||||
|
const norm = (o) => {
|
||||||
|
const v = withoutEmptyMembers(o);
|
||||||
|
return JSON.stringify(isEmptyObject(v) ? null : (v ?? null));
|
||||||
|
};
|
||||||
|
t('norm({}) equals norm(undefined)', norm({}), norm(undefined));
|
||||||
|
t('norm({CorsConfig:{}}) equals norm({})', norm({ CorsConfig: {} }), norm({}));
|
||||||
|
t(
|
||||||
|
'but a real difference still differs',
|
||||||
|
norm({ a: 1 }) === norm({ a: 2 }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- emptyObjectPaths, the post-condition ------------------------------- */
|
||||||
|
t(
|
||||||
|
'reports the incident path',
|
||||||
|
emptyObjectPaths({ SecurityHeadersConfig: LIVE_SECURITY_HEADERS }),
|
||||||
|
['SecurityHeadersConfig.ContentSecurityPolicy'],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'reports round 2s deeper path',
|
||||||
|
emptyObjectPaths({ CorsConfig: { AccessControlExposeHeaders: {} } }),
|
||||||
|
['CorsConfig.AccessControlExposeHeaders'],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'reports an empty object inside an array, with its index',
|
||||||
|
emptyObjectPaths({ Items: [{ Header: 'X' }, {}] }),
|
||||||
|
['Items[1]'],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'reports every one, not just the first',
|
||||||
|
emptyObjectPaths({ a: {}, b: { c: {} } }),
|
||||||
|
['a', 'b.c'],
|
||||||
|
);
|
||||||
|
t('silent on an empty array', emptyObjectPaths({ a: [] }), []);
|
||||||
|
t(
|
||||||
|
'silent on null, undefined and primitives',
|
||||||
|
emptyObjectPaths({ a: null, b: undefined, c: 1, d: 'x', e: true }),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t('names the root when the whole config is empty', emptyObjectPaths({}), [
|
||||||
|
'(root)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* ---- the invariant the two functions exist to hold together ------------- */
|
||||||
|
t(
|
||||||
|
'THE INVARIANT: nothing survives the strip that the assertion would report',
|
||||||
|
emptyObjectPaths(
|
||||||
|
withoutEmptyMembers({
|
||||||
|
Name: 'adr-sml-pdf-noindex',
|
||||||
|
SecurityHeadersConfig: LIVE_SECURITY_HEADERS,
|
||||||
|
CorsConfig: { AccessControlExposeHeaders: {} },
|
||||||
|
ServerTimingHeadersConfig: {},
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [{ Header: 'X-Robots-Tag', Value: 'noindex', Override: true }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- PAYLOAD_LIMITS: the 2026-09-04 second failure -----------------------
|
||||||
|
InvalidArgument, "The parameter Comment is too big", from
|
||||||
|
create-origin-request-policy. The model types Comment as a bare `string`, so
|
||||||
|
ParamValidation could not see it and the dry run was the only place it could
|
||||||
|
have been caught. */
|
||||||
|
const ORP = (comment) => ({
|
||||||
|
Name: 'adr-sml-api-viewer-address',
|
||||||
|
Comment: comment,
|
||||||
|
HeadersConfig: {
|
||||||
|
HeaderBehavior: 'whitelist',
|
||||||
|
Headers: { Quantity: 1, Items: ['Origin'] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
t(
|
||||||
|
'the 182-character Comment that failed is reported',
|
||||||
|
limitViolations('origin-request-policy', ORP('x'.repeat(182))).map((v) => [
|
||||||
|
v.path,
|
||||||
|
v.actual,
|
||||||
|
v.limit,
|
||||||
|
]),
|
||||||
|
[['Comment', 182, 128]],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'the shipped Comment passes',
|
||||||
|
limitViolations(
|
||||||
|
'origin-request-policy',
|
||||||
|
ORP(
|
||||||
|
'Forwards CloudFront-Viewer-Address on /api/*. See configure.mjs section 5.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'128 exactly is allowed — the cap is inclusive',
|
||||||
|
limitViolations('origin-request-policy', ORP('x'.repeat(128))),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'129 is not',
|
||||||
|
limitViolations('origin-request-policy', ORP('x'.repeat(129))).length,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'the 118-character Comment AWS accepted on 2026-09-04 passes',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'adr-sml-pdf-noindex',
|
||||||
|
Comment:
|
||||||
|
'Cloned from the default behaviour, plus X-Robots-Tag: noindex for *.pdf. See infra/cloudfront/configure.mjs section 4.',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Section 4's payload needs its OWN over-cap cases: asserting only that the
|
||||||
|
accepted 118 passes leaves the cap free to be wrong in the loose direction,
|
||||||
|
which a mutation raising it to 1280 proved by surviving. */
|
||||||
|
t(
|
||||||
|
'a 129-character response-headers Comment is caught',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'adr-sml-pdf-noindex',
|
||||||
|
Comment: 'x'.repeat(129),
|
||||||
|
}).map((v) => [v.path, v.actual, v.limit]),
|
||||||
|
[['Comment', 129, 128]],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'and 128 exactly is allowed',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'adr-sml-pdf-noindex',
|
||||||
|
Comment: 'x'.repeat(128),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'an over-long policy Name is caught on both policy kinds',
|
||||||
|
[
|
||||||
|
limitViolations('response-headers-policy', { Name: 'n'.repeat(129) })
|
||||||
|
.length,
|
||||||
|
limitViolations('origin-request-policy', { Name: 'n'.repeat(129) }).length,
|
||||||
|
],
|
||||||
|
[1, 1],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'and both shipped names pass',
|
||||||
|
[
|
||||||
|
limitViolations('response-headers-policy', { Name: 'adr-sml-pdf-noindex' })
|
||||||
|
.length,
|
||||||
|
limitViolations('origin-request-policy', {
|
||||||
|
Name: 'adr-sml-api-viewer-address',
|
||||||
|
}).length,
|
||||||
|
],
|
||||||
|
[0, 0],
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- the function ARN, the one limit the service model does give us ------ */
|
||||||
|
const GOOD_ARN = 'arn:aws:cloudfront::327082975128:function/adr-sml-router';
|
||||||
|
t(
|
||||||
|
'a correctly derived function ARN passes',
|
||||||
|
limitViolations('function-association', { FunctionARN: GOOD_ARN }),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'the tab-doubled ARN that `list-functions --output text` returns breaks both rules',
|
||||||
|
limitViolations('function-association', {
|
||||||
|
FunctionARN: `${GOOD_ARN}\t${GOOD_ARN}`,
|
||||||
|
})
|
||||||
|
.map((v) => v.rule)
|
||||||
|
.sort(),
|
||||||
|
['maxLength', 'pattern'],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'a Lambda@Edge ARN is not a CloudFront function ARN',
|
||||||
|
limitViolations('function-association', {
|
||||||
|
FunctionARN: 'arn:aws:lambda:us-east-1:327082975128:function:edge',
|
||||||
|
}).some((v) => v.rule === 'pattern'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- the aggregate rules, both documented on the CloudFront quotas page -- */
|
||||||
|
const HDRS = (items) => ({
|
||||||
|
Name: 'adr-sml-api-viewer-address',
|
||||||
|
Comment: 'c',
|
||||||
|
HeadersConfig: {
|
||||||
|
HeaderBehavior: 'whitelist',
|
||||||
|
Headers: { Quantity: items.length, Items: items },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const SHIPPED_HEADERS = [
|
||||||
|
'CloudFront-Viewer-Address',
|
||||||
|
'Content-Type',
|
||||||
|
'Origin',
|
||||||
|
'Referer',
|
||||||
|
'User-Agent',
|
||||||
|
];
|
||||||
|
t(
|
||||||
|
'the five headers we actually whitelist pass every rule',
|
||||||
|
limitViolations('origin-request-policy', HDRS(SHIPPED_HEADERS)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'an 11th whitelisted header breaches "Headers per origin request policy | 10"',
|
||||||
|
limitViolations(
|
||||||
|
'origin-request-policy',
|
||||||
|
HDRS(Array.from({ length: 11 }, (_, i) => `X-H${i}`)),
|
||||||
|
).map((v) => [v.rule, v.actual, v.limit]),
|
||||||
|
[['maxCount', 11, 10]],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'ten is allowed',
|
||||||
|
limitViolations(
|
||||||
|
'origin-request-policy',
|
||||||
|
HDRS(Array.from({ length: 10 }, (_, i) => `X-H${i}`)),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'header names totalling over 1024 breach the combined-length quota',
|
||||||
|
limitViolations(
|
||||||
|
'origin-request-policy',
|
||||||
|
HDRS(Array.from({ length: 9 }, () => 'X'.repeat(120))),
|
||||||
|
)
|
||||||
|
.map((v) => v.rule)
|
||||||
|
.sort(),
|
||||||
|
['maxCombinedLength'],
|
||||||
|
);
|
||||||
|
|
||||||
|
t(
|
||||||
|
'an 11th custom response header breaches its own count quota',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'n',
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 11,
|
||||||
|
Items: Array.from({ length: 11 }, (_, i) => ({
|
||||||
|
Header: `X-${i}`,
|
||||||
|
Value: 'v',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}).map((v) => v.rule),
|
||||||
|
['maxCount'],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'the single X-Robots-Tag header we add passes',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'adr-sml-pdf-noindex',
|
||||||
|
Comment:
|
||||||
|
'X-Robots-Tag: noindex on *.pdf, cloned headers. See configure.mjs section 4.',
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [{ Header: 'X-Robots-Tag', Value: 'noindex', Override: true }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'names and values combined over 10,240 are caught',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'n',
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 8,
|
||||||
|
Items: Array.from({ length: 8 }, (_, i) => ({
|
||||||
|
Header: `X-${i}`,
|
||||||
|
Value: 'v'.repeat(1500),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}).some((v) => v.rule === 'maxCombinedLength'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- the walker -------------------------------------------------------- */
|
||||||
|
t(
|
||||||
|
'[] resolves every element and the violation names the index',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'n',
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 2,
|
||||||
|
Items: [
|
||||||
|
{ Header: 'X-Robots-Tag', Value: 'noindex' },
|
||||||
|
{ Header: 'X'.repeat(300), Value: 'v' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).map((v) => v.path),
|
||||||
|
['CustomHeadersConfig.Items[1].Header'],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'an absent field is not a violation',
|
||||||
|
limitViolations('response-headers-policy', { Name: 'n' }),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'a non-string value is skipped rather than crashing',
|
||||||
|
limitViolations('response-headers-policy', { Name: 'n', Comment: 12345 }),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'a null along the path is skipped',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'n',
|
||||||
|
CustomHeadersConfig: null,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'an unknown payload kind throws rather than passing silently',
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
limitViolations('nope', {});
|
||||||
|
return 'no throw';
|
||||||
|
} catch (e) {
|
||||||
|
return e.message.includes('nope');
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'every limit entry carries a source',
|
||||||
|
Object.values(PAYLOAD_LIMITS)
|
||||||
|
.flat()
|
||||||
|
.every((l) => typeof l.source === 'string' && l.source.length > 0),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'every entry addresses exactly one of path / paths',
|
||||||
|
Object.values(PAYLOAD_LIMITS)
|
||||||
|
.flat()
|
||||||
|
.every((l) => (l.path === undefined) !== (l.paths === undefined)),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---- the CLONED values, which change without this file being touched.
|
||||||
|
`docs/05` specifies a Content-Security-Policy that would land on the default
|
||||||
|
behaviour's policy and be copied straight into ours; the API declares
|
||||||
|
TooLongCSPInResponseHeadersPolicy for exactly that. */
|
||||||
|
const withCsp = (csp) => ({
|
||||||
|
Name: 'adr-sml-pdf-noindex',
|
||||||
|
Comment: 'c',
|
||||||
|
SecurityHeadersConfig: {
|
||||||
|
ContentTypeOptions: { Override: true },
|
||||||
|
ContentSecurityPolicy: { Override: false, ContentSecurityPolicy: csp },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
t(
|
||||||
|
'a cloned CSP over 1,783 characters is caught before the create',
|
||||||
|
limitViolations('response-headers-policy', withCsp('x'.repeat(1784))).map(
|
||||||
|
(v) => [v.path, v.actual, v.limit],
|
||||||
|
),
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'SecurityHeadersConfig.ContentSecurityPolicy.ContentSecurityPolicy',
|
||||||
|
1784,
|
||||||
|
1783,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'1,783 exactly is allowed',
|
||||||
|
limitViolations('response-headers-policy', withCsp('x'.repeat(1783))),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'a realistic CSP passes',
|
||||||
|
limitViolations(
|
||||||
|
'response-headers-policy',
|
||||||
|
withCsp("default-src 'self'; img-src 'self' data:; style-src 'self'"),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
t(
|
||||||
|
'the live source policy, which defines no CSP at all, passes whole',
|
||||||
|
limitViolations('response-headers-policy', {
|
||||||
|
Name: 'adr-sml-pdf-noindex',
|
||||||
|
Comment:
|
||||||
|
'X-Robots-Tag: noindex on *.pdf, cloned headers. See configure.mjs section 4.',
|
||||||
|
SecurityHeadersConfig: stripped,
|
||||||
|
CustomHeadersConfig: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [{ Header: 'X-Robots-Tag', Value: 'noindex', Override: true }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error(
|
||||||
|
`policy-shapes: ${failures.length} FAILED\n - ${failures.join('\n - ')}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`policy-shapes: ${pass} of ${pass} cases pass`);
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* CloudFront Function, VIEWER REQUEST, on the DEFAULT behaviour and on `*.pdf`.
|
||||||
|
* Not on `/api/*` — see the rule below, which is the one that matters.
|
||||||
|
*
|
||||||
|
* `*.pdf` has it because this function is NOT a no-op on file paths: it
|
||||||
|
* normalises `\` to `/` and collapses a leading `//` run BEFORE the extension
|
||||||
|
* test, and 301s when that changed anything. Measured live 2026-09-03:
|
||||||
|
* `//pouya-lajevardi-bio.pdf` returns 301. Dropping the association there hands
|
||||||
|
* S3 the doubled key and returns 404 instead.
|
||||||
|
*
|
||||||
|
* ⚠️ THE SITE DOES NOT WORK WITHOUT THIS. `astro.config.mjs` sets
|
||||||
|
* `trailingSlash: 'always'` and `build.format: 'directory'`, so every route is
|
||||||
|
* `<dir>/index.html`. CloudFront forwards the viewer path to the S3 REST origin
|
||||||
|
* unchanged, S3 has no key `about/`, and the request fails. Measured on the live
|
||||||
|
* distribution 2026-09-01, before this function existed: `/about/` and
|
||||||
|
* `/definitely-not-a-page/` both returned **403 with an 111-byte
|
||||||
|
* `application/xml` body** — S3's AccessDenied, served raw to the reader. Only
|
||||||
|
* `/` worked, via the distribution's default root object. That is 22 of the 23
|
||||||
|
* pages.
|
||||||
|
*
|
||||||
|
* ⚠️ DO NOT ASSOCIATE IT WITH THE `/api/*` BEHAVIOUR. The intake path
|
||||||
|
* `/api/intake` has no extension and no trailing slash, so the redirect branch
|
||||||
|
* below would answer a form POST with a 301 — and a 301 turns a POST into a GET,
|
||||||
|
* which would lose the submission body silently. The association is per
|
||||||
|
* behaviour and `/api/*` gets none.
|
||||||
|
*
|
||||||
|
* Two rules, and the second is a `docs/04` requirement rather than a nicety:
|
||||||
|
*
|
||||||
|
* /about/ -> rewrite to /about/index.html (the origin has that key)
|
||||||
|
* /about -> 301 to /about/ (one canonical URL per page)
|
||||||
|
*
|
||||||
|
* Anything with a file extension in its last segment is left alone —
|
||||||
|
* `robots.txt`, `sitemap-0.xml`, `/_astro/*`, `/fonts/*`, `/og/*.jpg`,
|
||||||
|
* `favicon.ico`, `pouya-lajevardi-bio.pdf`, and `404.html` itself.
|
||||||
|
*
|
||||||
|
* Written to the `cloudfront-js-2.0` runtime and deliberately conservative: no
|
||||||
|
* arrow functions, no `String.prototype.endsWith`, no template literals. The
|
||||||
|
* runtime supports more than this; a viewer-request function runs on every
|
||||||
|
* request to the site and is the wrong place to be clever.
|
||||||
|
*/
|
||||||
|
/* The header-injection surface, and nothing else: C0 controls, DEL, space, and
|
||||||
|
WHATWG's query percent-encode set (`"`, `#`, `<`, `>`). `#` is in because it
|
||||||
|
changes the STRUCTURE of the Location — left in, `?a=x#&b=y` drops `&b=y` into
|
||||||
|
a fragment. `| ^ ` { }` are NOT in, and must not be added: browsers send them
|
||||||
|
raw and `|` is routine in tracking values. Strip rather than encode — these
|
||||||
|
values arrive percent-encoded, so encoding again makes `%20` into `%2520`. */
|
||||||
|
function safe(part) {
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
return String(part).replace(/[\u0000-\u0020\u007f"<>#]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(event) {
|
||||||
|
var request = event.request;
|
||||||
|
|
||||||
|
/* ⚠️ NORMALISE, THEN REDIRECT IF ANYTHING CHANGED. Leading `//` and `\` are
|
||||||
|
collapsed because CloudFront forwards duplicate slashes verbatim (it resolves
|
||||||
|
dot-segments; it does not collapse `//`) and `Location: //host/x` is a
|
||||||
|
network-path reference that REPLACES THE AUTHORITY — RFC 3986 s4.2. `\` does
|
||||||
|
the same, because the URL Standard maps it to `/` in special schemes.
|
||||||
|
Redirect rather than rewrite, or `//about/` serves the About page at a second
|
||||||
|
URL with a 200. Only the leading run: an interior `//` is a key that does not
|
||||||
|
exist. */
|
||||||
|
var uri = request.uri.replace(/\\/g, '/').replace(/^\/+/, '/');
|
||||||
|
var normalised = uri !== request.uri;
|
||||||
|
var lastSlash = uri.lastIndexOf('/');
|
||||||
|
var lastSegment = uri.substring(lastSlash + 1);
|
||||||
|
|
||||||
|
// A file, not a route.
|
||||||
|
if (lastSegment.indexOf('.') !== -1) {
|
||||||
|
if (normalised) return moved(uri, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A directory-style route: hand the origin the key it actually holds.
|
||||||
|
if (lastSegment === '') {
|
||||||
|
if (normalised) return moved(uri, request);
|
||||||
|
request.uri = uri + 'index.html';
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extensionless and no trailing slash. Redirect rather than rewrite, so the
|
||||||
|
page has ONE address: serving it at both would put two indexable URLs on the
|
||||||
|
same content, which `docs/04` treats as its primary concern. */
|
||||||
|
return moved(uri + '/', request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 301 to a path on this origin, carrying the query string. `location` is always
|
||||||
|
* built from an already-normalised path, which is what keeps it same-origin.
|
||||||
|
*/
|
||||||
|
function moved(path, request) {
|
||||||
|
var qs = '';
|
||||||
|
var names = Object.keys(request.querystring);
|
||||||
|
for (var i = 0; i < names.length; i++) {
|
||||||
|
var name = names[i];
|
||||||
|
var value = request.querystring[name];
|
||||||
|
if (value.multiValue) {
|
||||||
|
for (var j = 0; j < value.multiValue.length; j++) {
|
||||||
|
qs +=
|
||||||
|
(qs === '' ? '' : '&') +
|
||||||
|
safe(name) +
|
||||||
|
'=' +
|
||||||
|
safe(value.multiValue[j].value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Always `name=value`, so `?ref` and `?ref=` normalise to one form rather
|
||||||
|
than the function guessing which the viewer meant. */
|
||||||
|
qs += (qs === '' ? '' : '&') + safe(name) + '=' + safe(value.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
statusCode: 301,
|
||||||
|
statusDescription: 'Moved Permanently',
|
||||||
|
headers: {
|
||||||
|
location: { value: path + (qs === '' ? '' : '?' + qs) },
|
||||||
|
'cache-control': { value: 'public, max-age=0, must-revalidate' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* Unit test for the viewer-request router. `node infra/cloudfront/router.test.mjs`.
|
||||||
|
*
|
||||||
|
* The function file cannot use module syntax — CloudFront's runtime has no
|
||||||
|
* `export` — so it is read and evaluated rather than imported. `aws cloudfront
|
||||||
|
* test-function` is the authoritative check because it runs the real runtime;
|
||||||
|
* this one runs in a second, catches the branch mistakes, and costs nothing.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const src = readFileSync(join(here, 'router.js'), 'utf8');
|
||||||
|
const handler = new Function(`${src}; return handler;`)();
|
||||||
|
|
||||||
|
const req = (uri, querystring = {}) => ({ request: { uri, querystring } });
|
||||||
|
|
||||||
|
const CASES = [
|
||||||
|
// [uri, querystring, expected] — expected is {uri} for a rewrite/passthrough
|
||||||
|
// or {status, location} for a redirect.
|
||||||
|
['/', {}, { uri: '/index.html' }],
|
||||||
|
['/about/', {}, { uri: '/about/index.html' }],
|
||||||
|
['/practice/construction/', {}, { uri: '/practice/construction/index.html' }],
|
||||||
|
['/contact/received/', {}, { uri: '/contact/received/index.html' }],
|
||||||
|
['/about', {}, { status: 301, location: '/about/' }],
|
||||||
|
['/practice/energy', {}, { status: 301, location: '/practice/energy/' }],
|
||||||
|
// Files are untouched — every one of these is a real object in dist/.
|
||||||
|
['/robots.txt', {}, { uri: '/robots.txt' }],
|
||||||
|
['/sitemap-index.xml', {}, { uri: '/sitemap-index.xml' }],
|
||||||
|
['/404.html', {}, { uri: '/404.html' }],
|
||||||
|
['/favicon.ico', {}, { uri: '/favicon.ico' }],
|
||||||
|
['/pouya-lajevardi-bio.pdf', {}, { uri: '/pouya-lajevardi-bio.pdf' }],
|
||||||
|
['/_astro/schema.Cm5su60K.css', {}, { uri: '/_astro/schema.Cm5su60K.css' }],
|
||||||
|
['/og/mediation.jpg', {}, { uri: '/og/mediation.jpg' }],
|
||||||
|
// The query string survives the redirect, normalised to `name=value`.
|
||||||
|
[
|
||||||
|
'/fees',
|
||||||
|
{ utm_source: { value: 'linkedin' }, ref: { value: '' } },
|
||||||
|
{ status: 301, location: '/fees/?utm_source=linkedin&ref=' },
|
||||||
|
],
|
||||||
|
/* ⚠️ THE OPEN-REDIRECT CASES. CloudFront forwards duplicate leading slashes
|
||||||
|
verbatim (it collapses dot-segments but not `//`), so without normalisation
|
||||||
|
`//evil.example.com/x` produced `Location: //evil.example.com/x/` — a
|
||||||
|
network-path reference that sends the viewer to another host from this
|
||||||
|
domain's own URL. The backslash form defeats a `startsWith('//')` guard,
|
||||||
|
because the URL Standard maps `\` to `/` in special schemes. Both must stay
|
||||||
|
same-origin, and both must keep a SINGLE leading slash. */
|
||||||
|
[
|
||||||
|
'//evil.example.com/x',
|
||||||
|
{},
|
||||||
|
{ status: 301, location: '/evil.example.com/x/' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'///evil.example.com/x',
|
||||||
|
{},
|
||||||
|
{ status: 301, location: '/evil.example.com/x/' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'/\\evil.example.com/x',
|
||||||
|
{},
|
||||||
|
{ status: 301, location: '/evil.example.com/x/' },
|
||||||
|
],
|
||||||
|
/* ⚠️ A NORMALISED PATH IS REDIRECTED, NOT REWRITTEN — this asserted a 200 for
|
||||||
|
one revision, which closed the redirect and opened an unbounded family of
|
||||||
|
duplicate URLs for every page on the site. */
|
||||||
|
[
|
||||||
|
'//evil.example.com/x/',
|
||||||
|
{},
|
||||||
|
{ status: 301, location: '/evil.example.com/x/' },
|
||||||
|
],
|
||||||
|
['//about/', {}, { status: 301, location: '/about/' }],
|
||||||
|
['///about/', {}, { status: 301, location: '/about/' }],
|
||||||
|
['/\\about/', {}, { status: 301, location: '/about/' }],
|
||||||
|
/* A file is normalised too. This branch returned `request` untouched for one
|
||||||
|
revision, so `//robots.txt` reached S3 with the doubled slash and 404'd. */
|
||||||
|
['//robots.txt', {}, { status: 301, location: '/robots.txt' }],
|
||||||
|
['/\\robots.txt', {}, { status: 301, location: '/robots.txt' }],
|
||||||
|
/* An interior `//` is left alone on purpose: it is a key that does not exist,
|
||||||
|
so it resolves to the 404 page. Only the leading run is a security question. */
|
||||||
|
['/a//b/', {}, { uri: '/a//b/index.html' }],
|
||||||
|
/* Header-injection surface: CR, LF, space and the delimiters browsers disagree
|
||||||
|
about are stripped rather than re-encoded — an already-encoded value must not
|
||||||
|
be encoded twice. `%20` therefore passes through untouched. */
|
||||||
|
[
|
||||||
|
'/fees',
|
||||||
|
{ q: { value: 'a b"><x' }, utm: { value: 'a%20b' } },
|
||||||
|
{ status: 301, location: '/fees/?q=abx&utm=a%20b' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'/fees',
|
||||||
|
{ evil: { value: 'x\r\nSet-Cookie: a=b' } },
|
||||||
|
{ status: 301, location: '/fees/?evil=xSet-Cookie:a=b' },
|
||||||
|
],
|
||||||
|
/* `#` changes the STRUCTURE of the Location — without stripping it, `&b=y`
|
||||||
|
lands in a fragment and the parameter is silently lost. */
|
||||||
|
[
|
||||||
|
'/fees',
|
||||||
|
{ a: { value: 'x#&b=y' } },
|
||||||
|
{ status: 301, location: '/fees/?a=x&b=y' },
|
||||||
|
],
|
||||||
|
/* ⚠️ AND THESE MUST SURVIVE. `| ^ ` { }` are not in WHATWG's query
|
||||||
|
percent-encode set, so a browser sends them raw — and `|` is routine in
|
||||||
|
ad-platform tracking values. One revision of `safe()` stripped all of them,
|
||||||
|
silently corrupting exactly the campaign links the 301 exists to preserve. */
|
||||||
|
[
|
||||||
|
'/fees',
|
||||||
|
{ utm_content: { value: 'banner|top' }, k: { value: 'a{b}c^d`e' } },
|
||||||
|
{ status: 301, location: '/fees/?utm_content=banner|top&k=a{b}c^d`e' },
|
||||||
|
],
|
||||||
|
// multiValue, which no case exercised before.
|
||||||
|
[
|
||||||
|
'/fees',
|
||||||
|
{ tag: { value: 'a', multiValue: [{ value: 'a' }, { value: 'b' }] } },
|
||||||
|
{ status: 301, location: '/fees/?tag=a&tag=b' },
|
||||||
|
],
|
||||||
|
/* /api/intake must NEVER be redirected — a 301 turns a POST into a GET and
|
||||||
|
the submission body is gone. This function is not associated with the
|
||||||
|
/api/* behaviour, so this case documents WHY the association matters: if it
|
||||||
|
ever were associated, this is the damage. */
|
||||||
|
['/api/intake', {}, { status: 301, location: '/api/intake/' }],
|
||||||
|
];
|
||||||
|
|
||||||
|
let pass = 0;
|
||||||
|
const failures = [];
|
||||||
|
for (const [uri, qs, expected] of CASES) {
|
||||||
|
const out = handler(req(uri, qs));
|
||||||
|
let actual;
|
||||||
|
if (out.statusCode) {
|
||||||
|
actual = { status: out.statusCode, location: out.headers.location.value };
|
||||||
|
} else {
|
||||||
|
actual = { uri: out.uri };
|
||||||
|
}
|
||||||
|
if (JSON.stringify(actual) === JSON.stringify(expected)) pass += 1;
|
||||||
|
else
|
||||||
|
failures.push(
|
||||||
|
`${uri} -> ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pass + failures.length !== CASES.length) {
|
||||||
|
throw new Error(`case count ${pass + failures.length} != ${CASES.length}`);
|
||||||
|
}
|
||||||
|
console.log(`router: ${pass} of ${CASES.length} cases pass`);
|
||||||
|
for (const f of failures) console.error(' FAIL ' + f);
|
||||||
|
if (failures.length > 0) process.exit(1);
|
||||||
Generated
+1352
-15
File diff suppressed because it is too large
Load Diff
+13
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "adr-smlcompany-ca",
|
"name": "adr-smlcompany-ca",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "The dispute resolution practice of Pouya Lajevardi — Toronto",
|
"description": "The dispute resolution practice of Pouya Lajevardi \u2014 Toronto",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.13.0 || >=24",
|
"node": "^22.13.0 || >=24",
|
||||||
@@ -13,9 +13,15 @@
|
|||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"check": "astro check",
|
"check": "astro check",
|
||||||
|
"check:claims": "node scripts/check-claims.mjs",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"deploy": "bash scripts/deploy-local.sh"
|
"deploy": "bash scripts/deploy-local.sh",
|
||||||
|
"lighthouse": "node scripts/lighthouse.mjs",
|
||||||
|
"og:proof": "node scripts/og-proof.mjs",
|
||||||
|
"check:intake": "node scripts/check-intake.mjs",
|
||||||
|
"bio:pdf": "node scripts/bio-pdf.mjs",
|
||||||
|
"icons": "node scripts/icons.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^7.0.8",
|
"@astrojs/mdx": "^7.0.8",
|
||||||
@@ -26,12 +32,17 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "^0.9.10",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@fontsource/geist": "^5.3.0",
|
||||||
|
"@fontsource/instrument-serif": "^5.3.0",
|
||||||
|
"chrome-launcher": "^1.2.1",
|
||||||
"eslint": "^10.9.1",
|
"eslint": "^10.9.1",
|
||||||
"eslint-plugin-astro": "^3.1.0",
|
"eslint-plugin-astro": "^3.1.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"globals": "^17.11.0",
|
"globals": "^17.11.0",
|
||||||
|
"lighthouse": "^13.4.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"satori": "^0.33.4",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.68.0"
|
"typescript-eslint": "^8.68.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
+22
-10
@@ -2,18 +2,30 @@
|
|||||||
# AI crawlers are deliberately allowed. Being read by an assistant that counsel
|
# AI crawlers are deliberately allowed. Being read by an assistant that counsel
|
||||||
# is using to shortlist a neutral is the point. See docs/04-seo-spec.md.
|
# is using to shortlist a neutral is the point. See docs/04-seo-spec.md.
|
||||||
|
|
||||||
# NOTHING IS DISALLOWED, DELIBERATELY.
|
# EXACTLY ONE THING IS DISALLOWED, AND IT IS A SUBSTITUTE FOR A MECHANISM THIS
|
||||||
# /legal/* is kept out of the index by
|
# DISTRIBUTION IS NOT ALLOWED TO HAVE.
|
||||||
# `<meta name="robots" content="noindex,follow">`, which is the directive that
|
# The bio PDF duplicates /bio/. The intended fix was `X-Robots-Tag: noindex` on
|
||||||
# actually de-indexes. Disallowing them as well would defeat it: a crawler that
|
# *.pdf via a CloudFront response-headers policy, which the distribution's
|
||||||
# is forbidden to FETCH a URL never reads the noindex on it. The legal pages are
|
# pricing plan forbids — AGENTS.md §7, and docs/09 Part 3 for the three failed
|
||||||
# linked from the footer of every page, so Google discovers them regardless and
|
# attempts. Disallow is the remaining lever and it is NOT the same instrument:
|
||||||
# would have listed the bare URLs as "no information available" — the opposite
|
# - it stops the PDF being FETCHED, so its contents are never indexed and the
|
||||||
# of the intent — with the noindex sitting unread behind the wall.
|
# duplicate-content problem it was raised for is solved;
|
||||||
# Add a Disallow only for something that must not be FETCHED. Use noindex for
|
# - it does NOT de-index the URL. The PDF is linked from /bio/ and /about/, so
|
||||||
# something that must not be LISTED. They are different problems.
|
# a search engine can still list the bare URL with no snippet.
|
||||||
|
# That residual is accepted deliberately: a bare link to a bio PDF is not the
|
||||||
|
# harm the noindex was for. Revisit if the pricing plan ever changes.
|
||||||
|
#
|
||||||
|
# /legal/* is still NOT disallowed, and that reasoning is unchanged — it is the
|
||||||
|
# general rule this one path is the exception to. A crawler forbidden to FETCH a
|
||||||
|
# URL never reads the `noindex` on it, so the two cancel; the legal pages are
|
||||||
|
# linked from every footer, so they are discovered regardless, and the pair would
|
||||||
|
# have produced bare "no information available" listings with the directive that
|
||||||
|
# would have suppressed them sitting unread behind the wall.
|
||||||
|
# `noindex` is what de-indexes; `Disallow` is what prevents fetching. Use the one
|
||||||
|
# that matches the problem, and never both on the same path.
|
||||||
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
|
Disallow: /pouya-lajevardi-bio.pdf
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
Sitemap: https://adr.smlcompany.ca/sitemap-index.xml
|
Sitemap: https://adr.smlcompany.ca/sitemap-index.xml
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Renders `/bio/` to `public/pouya-lajevardi-bio.pdf`. `npm run bio:pdf`,
|
||||||
|
* after `npm run build`. Discharges `AGENTS.md` R16 / Q45.
|
||||||
|
*
|
||||||
|
* WHY A LOCAL SCRIPT AND NOT A BUILD STEP. It drives Chrome, and the Gitea
|
||||||
|
* runner has none (`AGENTS.md` §7, Q23) — the same reason `npm run lighthouse`
|
||||||
|
* is a local gate. A build step that cannot run in CI is a control that exists
|
||||||
|
* on paper, which is the shape Q22 turned out to be. So the PDF is **committed**:
|
||||||
|
* the artefact is in the repository, which is also what R14 asks for.
|
||||||
|
*
|
||||||
|
* ⚠️ IT IS NOT BYTE-REPRODUCIBLE, AND AN EARLIER VERSION OF THIS COMMENT SAID
|
||||||
|
* "deterministically". Two consecutive runs produced 89,496 bytes both times and
|
||||||
|
* DIFFERENT SHA-256 digests — Chrome stamps a `/CreationDate` into the document.
|
||||||
|
* Measured by `adversarial-reviewer`, 2026-08-31.
|
||||||
|
*
|
||||||
|
* The consequence is not cosmetic: the "regenerate and re-commit the PDF" item on
|
||||||
|
* `docs/06`'s cutover checklist therefore always produces a binary diff, so a
|
||||||
|
* reviewer cannot tell a real content change from a no-op re-render. Do not
|
||||||
|
* re-commit it out of habit — re-commit it when `/bio/`, §4, the rate card or the
|
||||||
|
* print styles actually changed, and say which in the commit message.
|
||||||
|
*
|
||||||
|
* WHY THE PDF IS A RENDERING OF A PAGE RATHER THAN A DESIGNED DOCUMENT. R16's
|
||||||
|
* worry was never tooling: *"a PDF circulated with an appointment proposal is
|
||||||
|
* read once, by the reader who matters most, and never seen by a reviewer
|
||||||
|
* again."* Rendering it from `/bio/` puts it back inside this project's review
|
||||||
|
* apparatus — `astro check`, `check:claims` on the built HTML, the adversarial
|
||||||
|
* review and the cutover claims pass all see every word of it, because every
|
||||||
|
* word of it is on a page. (That is what caught `/bio/` opening with a clause
|
||||||
|
* that scoped mediation commercial, which Q56 forbids.)
|
||||||
|
*
|
||||||
|
* ⚠️ IT ASSERTS ONE PAGE. A one-page bio that silently becomes two is the defect
|
||||||
|
* this script exists to catch, and it is invisible from the source: it depends on
|
||||||
|
* the print stylesheet, the paper size, and how much §4 has grown since anyone
|
||||||
|
* looked. `printBackground: false` matches Chrome's own default print dialog,
|
||||||
|
* where "Background graphics" is unchecked — `global.css` records what that did
|
||||||
|
* to `/about/`'s inverse band when nobody checked.
|
||||||
|
*/
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { createReadStream } from 'node:fs';
|
||||||
|
import { writeFile, stat } from 'node:fs/promises';
|
||||||
|
import { join, extname } from 'node:path';
|
||||||
|
import * as chromeLauncher from 'chrome-launcher';
|
||||||
|
|
||||||
|
const ROOT = process.cwd();
|
||||||
|
const DIST = join(ROOT, 'dist');
|
||||||
|
const OUT = join(ROOT, 'public', 'pouya-lajevardi-bio.pdf');
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.avif': 'image/avif',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stat(join(DIST, 'bio', 'index.html'));
|
||||||
|
} catch {
|
||||||
|
console.error(
|
||||||
|
'dist/bio/index.html is missing. Run `npm run build` first — this renders ' +
|
||||||
|
'the BUILT page, not the dev server, so what ships is what is measured.',
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
const pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
||||||
|
const file = pathname.endsWith('/')
|
||||||
|
? join(DIST, pathname, 'index.html')
|
||||||
|
: join(DIST, pathname);
|
||||||
|
const stream = createReadStream(file);
|
||||||
|
stream.on('error', () => {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('404');
|
||||||
|
});
|
||||||
|
stream.once('open', () => {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'content-type': MIME[extname(file)] ?? 'application/octet-stream',
|
||||||
|
});
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
const port = server.address().port;
|
||||||
|
|
||||||
|
const chrome = await chromeLauncher.launch({
|
||||||
|
chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu'],
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Minimal CDP client over the DevTools WebSocket. `chrome-launcher` starts the
|
||||||
|
* browser and does not speak the protocol; adding a client library for four
|
||||||
|
* calls would be a dependency for nothing. */
|
||||||
|
async function cdp(port, fn) {
|
||||||
|
const list = await fetch(`http://127.0.0.1:${port}/json/list`).then((r) =>
|
||||||
|
r.json(),
|
||||||
|
);
|
||||||
|
const target = list.find((t) => t.type === 'page');
|
||||||
|
if (!target) throw new Error('no page target in Chrome');
|
||||||
|
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ws.addEventListener('open', resolve, { once: true });
|
||||||
|
ws.addEventListener('error', reject, { once: true });
|
||||||
|
});
|
||||||
|
let id = 0;
|
||||||
|
const pending = new Map();
|
||||||
|
const events = new Map();
|
||||||
|
ws.addEventListener('message', (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
if (message.id && pending.has(message.id)) {
|
||||||
|
const { resolve, reject } = pending.get(message.id);
|
||||||
|
pending.delete(message.id);
|
||||||
|
if (message.error) reject(new Error(JSON.stringify(message.error)));
|
||||||
|
else resolve(message.result);
|
||||||
|
} else if (message.method && events.has(message.method)) {
|
||||||
|
events.get(message.method)();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const send = (method, params = {}) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
id += 1;
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
ws.send(JSON.stringify({ id, method, params }));
|
||||||
|
});
|
||||||
|
const once = (method) =>
|
||||||
|
new Promise((resolve) => events.set(method, resolve));
|
||||||
|
try {
|
||||||
|
return await fn({ send, once });
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pdfBase64;
|
||||||
|
try {
|
||||||
|
pdfBase64 = await cdp(chrome.port, async ({ send, once }) => {
|
||||||
|
await send('Page.enable');
|
||||||
|
const loaded = once('Page.loadEventFired');
|
||||||
|
await send('Page.navigate', { url: `http://127.0.0.1:${port}/bio/` });
|
||||||
|
await loaded;
|
||||||
|
// The page self-hosts its fonts and `document.fonts.ready` is the only
|
||||||
|
// reliable signal that they are laid out — a PDF printed before the serif
|
||||||
|
// arrives is set in the fallback and looks nothing like the site.
|
||||||
|
await send('Runtime.evaluate', {
|
||||||
|
expression: 'document.fonts.ready',
|
||||||
|
awaitPromise: true,
|
||||||
|
});
|
||||||
|
const result = await send('Page.printToPDF', {
|
||||||
|
// Letter, because this circulates in Canada with Canadian counsel.
|
||||||
|
paperWidth: 8.5,
|
||||||
|
paperHeight: 11,
|
||||||
|
marginTop: 0.55,
|
||||||
|
marginBottom: 0.55,
|
||||||
|
marginLeft: 0.6,
|
||||||
|
marginRight: 0.6,
|
||||||
|
printBackground: false,
|
||||||
|
preferCSSPageSize: false,
|
||||||
|
});
|
||||||
|
return result.data;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
chrome.kill();
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdf = Buffer.from(pdfBase64, 'base64');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PAGE COUNT, ASSERTED. Counted from the PDF's own page objects rather than
|
||||||
|
* trusting the layout — this is the whole reason the script exists rather than a
|
||||||
|
* note telling someone to check. A one-page bio that quietly becomes two pages
|
||||||
|
* is exactly the class of defect nobody looks for again.
|
||||||
|
*/
|
||||||
|
const text = pdf.toString('latin1');
|
||||||
|
const pageCount =
|
||||||
|
(text.match(/\/Type\s*\/Page[^s]/g) ?? []).length ||
|
||||||
|
Number((/\/Count\s+(\d+)/.exec(text) ?? [])[1] ?? 0);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`bio:pdf — ${pdf.length.toLocaleString()} bytes, ${pageCount} page(s), Letter.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pageCount !== 1) {
|
||||||
|
console.error(
|
||||||
|
`\nTHE BIO IS ${pageCount} PAGES AND MUST BE ONE.\n` +
|
||||||
|
' It is specified as a one-page bio (docs/01 §/about/ item 7, R16), and a\n' +
|
||||||
|
' second sheet carrying three lines is worse than a denser first one.\n' +
|
||||||
|
' Tighten the @media print block in src/pages/bio.astro — do not widen\n' +
|
||||||
|
' the margins here, which changes the document rather than the layout.\n' +
|
||||||
|
' Nothing was written.',
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(OUT, pdf);
|
||||||
|
console.log(`wrote public/pouya-lajevardi-bio.pdf`);
|
||||||
|
console.log(
|
||||||
|
'It is COMMITTED. Regenerate and re-commit it whenever /bio/, §4, the rate ' +
|
||||||
|
'card or the print styles change — nothing in the build does this for you.',
|
||||||
|
);
|
||||||
@@ -0,0 +1,818 @@
|
|||||||
|
#!/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 — the sole-administrative-access claim on /legal/privacy/ was FALSE ' +
|
||||||
|
'and is permanently barred from returning. The page does NOT make it now; ' +
|
||||||
|
'if this fired, something reintroduced the shape. ' +
|
||||||
|
"⚠️ IF THE SECOND ADMINISTRATOR'S ACCESS IS EVER ACTUALLY REMOVED, this " +
|
||||||
|
'pattern starts failing the build on TRUE copy, and the answer is neither ' +
|
||||||
|
'to delete it nor to work around it: re-run the verification in ' +
|
||||||
|
'`docs/reference/intake-table-access-verification.md`, rewrite the page to ' +
|
||||||
|
'the new measured truth, and narrow THIS pattern deliberately with a ' +
|
||||||
|
'Change Log entry. Q23 records the Gitea instance as jointly administered, ' +
|
||||||
|
'so that removal is live rather than hypothetical.',
|
||||||
|
incident:
|
||||||
|
'The page shipped "nobody else has access to the table. There is no team, ' +
|
||||||
|
'no assistant and no external administrator." while TWO principals could ' +
|
||||||
|
'read it. It was in dist/, `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. The gate was human ' +
|
||||||
|
'memory. Pouya ruled the pattern PERMANENT on 2026-09-01: it no longer ' +
|
||||||
|
'describes live copy, it bars the shape. Do not delete it, and do not ' +
|
||||||
|
'loosen it into a rule about a class. Full history: AGENTS.md entry (an).',
|
||||||
|
/* THREE CONSTRAINTS, AND THEY ARE WHY THIS IS SHAPED AS IT IS.
|
||||||
|
|
||||||
|
1. `\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/`.
|
||||||
|
|
||||||
|
2. EACH ALTERNATIVE IS ONE STRING THAT REACHED `dist/`, NEVER A FAMILY.
|
||||||
|
The first draft of alternative 3 was `(?:or|and)\s+(?:outside|external)`
|
||||||
|
— four phrasings where one was published, so three had no probe and no
|
||||||
|
negative fixture, which is the freeze's stated condition.
|
||||||
|
|
||||||
|
3. ALTERNATIVE 3 IS A DELIBERATE OVER-BAR AND THIS IS ITS COST. The
|
||||||
|
truthful receipt-scoped sentence CONTAINS the published string, so any
|
||||||
|
pattern catching one catches the other — adding that truthful form as a
|
||||||
|
negative fixture failed the self-test, which is the instrument check
|
||||||
|
working. So: the clause "no assistant or outside administrator" cannot
|
||||||
|
be published here in ANY scoping, true or false, without failing this
|
||||||
|
build. That is the right trade for a phrase that has already put a
|
||||||
|
false statement on a privacy policy, and `rule:` says what to do. A
|
||||||
|
lookahead tuned to one guessed phrasing would be the speculative
|
||||||
|
pattern the freeze bars.
|
||||||
|
|
||||||
|
The window in alternative 5 is `[^\w<]{0,8}`, not `\W{0,8}`: `\W` matches
|
||||||
|
`<` and the block sentinel, so it would have been bounded by neither. */
|
||||||
|
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|no\s+assistant\s+or\s+outside\s+administrator|one\s+administrative\s+account,\s+which\s+is\s+mine|honest\s+answer\s+to\s+[^\w<]{0,8}who\s+can\s+see\s+this[^\w<]{0,8}\s*is:\s+me/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 FIVE published clauses from dist/legal/privacy/ as it stood at
|
||||||
|
`bd282aa`, before the Q62 correction. The first form of the pattern
|
||||||
|
caught only the first two: clauses 3 and 4 are the same falsehood in
|
||||||
|
different words in a different section, and clause 5 is the summary
|
||||||
|
that would have re-asserted the struck number. */
|
||||||
|
'nobody else has access to the table',
|
||||||
|
'There is no team, no assistant and no external administrator.',
|
||||||
|
'no analytics on the submission, and no assistant or outside administrator.',
|
||||||
|
'The table is reachable by the function that writes to it and by one administrative account, which is mine',
|
||||||
|
/* The third surface — the summary that would have re-asserted the struck
|
||||||
|
number four lines below the corrected paragraph. */
|
||||||
|
'So the honest answer to "who can see this" is: me, and Google as the company that runs my mail.',
|
||||||
|
],
|
||||||
|
'struck-universal-q39': [
|
||||||
|
'Anyone may be appointed an arbitrator in Ontario',
|
||||||
|
'nothing in law gates the role',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
/* Mostly real published or spec-approved copy on this site. A few are
|
||||||
|
deliberate NEAR MISSES — truthful sentences about the same subject that
|
||||||
|
were never published — because a pattern also has to be proven silent on
|
||||||
|
the wording a correction is likely to reach for. Where a fixture is one of
|
||||||
|
those, the comment beside it says so. */
|
||||||
|
mustNotMatch: [
|
||||||
|
/* NEGATIVE FIXTURES FOR `sole-administrator-q62`. The first FIVE are LIVE
|
||||||
|
PAGE COPY, verbatim from the corrected `/legal/privacy/` — which is the
|
||||||
|
fixture that matters, because Q62's ruling required this pattern to be
|
||||||
|
proven silent on the true sentence as well as loud on the false one.
|
||||||
|
RE-SYNC THEM WHENEVER THAT COPY CHANGES. ⚠️ **A SENTENCE THAT LEAVES THE
|
||||||
|
PAGE LEAVES THIS LIST — it is not kept as a near miss.** Struck copy in a
|
||||||
|
list captioned "what this site legitimately publishes" is an invitation to
|
||||||
|
restore it. The rest below ARE near misses on the same subject: the
|
||||||
|
pattern is anchored on five strings that reached `dist/`, not on the ideas
|
||||||
|
in them, so a truthful sentence about administrative access must pass.
|
||||||
|
Rendered as text, without the `<strong>` wrappers — what is proven is that
|
||||||
|
the PATTERN is silent on the words. */
|
||||||
|
'The record in the table: me, and the small number of people who administer the account it sits in with me.',
|
||||||
|
'The system that receives what you send can only add a record — it cannot read back what is stored.',
|
||||||
|
"The notification goes to the practice's mailbox, which is read by me and by administrative staff and is hosted on Google Workspace — so Google holds a copy of whatever you send me.",
|
||||||
|
'The confirmation that went to you sits with whoever runs your email. That copy is in your hands rather than mine.',
|
||||||
|
'No one else is sent it. There is no CRM, no mailing list and no analytics on the submission.',
|
||||||
|
'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.',
|
||||||
|
/* ⚠️ TWO FIXTURES WERE REMOVED FROM HERE ON 2026-09-02 AND THE REASON
|
||||||
|
MATTERS MORE THAN THE STRINGS: `'Nobody else has access to my mailbox.'`
|
||||||
|
and `'There is no team. Every inquiry is read by me.'` They were added as
|
||||||
|
harmless near-misses, and §7's `info@smlcompany.ca` row then established
|
||||||
|
that the mailbox is DELEGATED — Pouya and administrative staff — which
|
||||||
|
makes both FALSE. The second is also a paraphrase of a `mustMatch` breach
|
||||||
|
string. This list is documented as the copy the site legitimately
|
||||||
|
publishes, so a maintainer reaching for a tidier answer would have found
|
||||||
|
one here, which is how this page acquired its false sentence the first
|
||||||
|
time. Removing a false fixture keeps the list true; it is not a coverage
|
||||||
|
change and the freeze does not reach it. `adversarial-reviewer`, round 1. */
|
||||||
|
'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.',
|
||||||
|
);
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Cross-checks the intake form's two field tables. `npm run check:intake`.
|
||||||
|
*
|
||||||
|
* WHY THERE ARE TWO TABLES AT ALL, because the obvious reaction to this script
|
||||||
|
* is to delete one of them and share an import. `docs/05-backend-spec.md`:
|
||||||
|
* *"Client-side validation is a convenience. **The Lambda re-validates
|
||||||
|
* everything.**"* A server that validates against a list the client shipped it
|
||||||
|
* is not validating — it is asking the caller what the rules are. And the Lambda
|
||||||
|
* is a separately deployed zip that cannot import from `src/` anyway.
|
||||||
|
*
|
||||||
|
* So the duplication is architectural, and what makes it safe is this check
|
||||||
|
* rather than a shared module: the two tables must agree on every field NAME, on
|
||||||
|
* which fields are REQUIRED, on every length CAP, and on every closed OPTION
|
||||||
|
* SET. If they disagree, the form offers something the handler rejects, or the
|
||||||
|
* handler accepts something the form never shows — and the first is a lost
|
||||||
|
* inquiry that looks like a bug in the browser.
|
||||||
|
*
|
||||||
|
* This is the one place in the repo where a duplicated fact is deliberate, and
|
||||||
|
* `AGENTS.md`'s standing rule about duplicated facts is why it needs a mechanism
|
||||||
|
* on top of a comment.
|
||||||
|
*
|
||||||
|
* Both files are read directly — Node strips the types out of the `.ts` — so
|
||||||
|
* this script holds no third copy of the list.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
INTAKE_FIELDS,
|
||||||
|
HONEYPOT_FIELD,
|
||||||
|
DECOY_CHECKBOX_FIELD,
|
||||||
|
} from '../src/data/intake.ts';
|
||||||
|
import {
|
||||||
|
FIELDS as SERVER_FIELDS,
|
||||||
|
HONEYPOT,
|
||||||
|
DECOY_CHECKBOX,
|
||||||
|
} from '../backend/intake/fields.mjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BOTH TABLES ARE IMPORTED, NOT PARSED. The first version of this script read
|
||||||
|
* `handler.mjs` as text, sliced out the `const FIELDS = [ … ]` literal, munged
|
||||||
|
* quotes and commas into JSON, and guarded the result with a regex meant to
|
||||||
|
* refuse anything executable.
|
||||||
|
*
|
||||||
|
* **That guard rejected the table on the word `process`, which is a FIELD NAME.**
|
||||||
|
* A guard that fires on the data it exists to protect is worse than no guard,
|
||||||
|
* and the munging underneath it would have broken on the first apostrophe or
|
||||||
|
* URL in a label. The fix was not a better regex: the server's table moved into
|
||||||
|
* `backend/intake/fields.mjs`, which has no module-scope side effects and can
|
||||||
|
* simply be imported. The independence that matters is that the SERVER's table
|
||||||
|
* lives with the server and the handler trusts nothing from `src/` — not that a
|
||||||
|
* check script refuses to load it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const problems = [];
|
||||||
|
const server = SERVER_FIELDS;
|
||||||
|
|
||||||
|
const clientNames = INTAKE_FIELDS.map((f) => f.name);
|
||||||
|
const serverNames = server.map((f) => f.name);
|
||||||
|
|
||||||
|
for (const name of clientNames) {
|
||||||
|
if (!serverNames.includes(name)) {
|
||||||
|
problems.push(
|
||||||
|
`"${name}" is on the form but the handler does not accept it — the ` +
|
||||||
|
'inquirer would fill it and it would be silently dropped.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const name of serverNames) {
|
||||||
|
if (!clientNames.includes(name)) {
|
||||||
|
problems.push(
|
||||||
|
`"${name}" is validated by the handler but is not on the form.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (HONEYPOT !== HONEYPOT_FIELD) {
|
||||||
|
problems.push(
|
||||||
|
`honeypot name differs: form "${HONEYPOT_FIELD}", handler "${HONEYPOT}". ` +
|
||||||
|
'A bot fills the field the form renders; the handler checks the one it ' +
|
||||||
|
'knows about, so a mismatch disables the honeypot silently.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (serverNames.includes(HONEYPOT_FIELD)) {
|
||||||
|
problems.push(
|
||||||
|
`the honeypot "${HONEYPOT_FIELD}" is in the handler's FIELDS table; it must ` +
|
||||||
|
'be checked separately, or a bot filling it would just fail validation ' +
|
||||||
|
'instead of being sent to the success page.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THE SECOND HONEYPOT GETS THE SAME THREE CHECKS, and it needs a fourth.
|
||||||
|
Added 2026-09-04 with the decoy checkbox. Every failure mode below is silent
|
||||||
|
in production: a mismatched name disables the trap, a name inside `FIELDS`
|
||||||
|
turns it into ordinary validation, and two traps sharing one name is one
|
||||||
|
trap with a comment claiming there are two. */
|
||||||
|
if (DECOY_CHECKBOX !== DECOY_CHECKBOX_FIELD) {
|
||||||
|
problems.push(
|
||||||
|
`decoy checkbox name differs: form "${DECOY_CHECKBOX_FIELD}", handler ` +
|
||||||
|
`"${DECOY_CHECKBOX}". The form renders one name and the handler checks ` +
|
||||||
|
'another, so the trap is disabled and nothing fails.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (serverNames.includes(DECOY_CHECKBOX_FIELD)) {
|
||||||
|
problems.push(
|
||||||
|
`the decoy checkbox "${DECOY_CHECKBOX_FIELD}" is in the handler's FIELDS ` +
|
||||||
|
'table; it must be checked separately, or ticking it would fail ' +
|
||||||
|
'validation instead of sending the bot to the success page.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (clientNames.includes(DECOY_CHECKBOX_FIELD)) {
|
||||||
|
problems.push(
|
||||||
|
`the decoy checkbox "${DECOY_CHECKBOX_FIELD}" is in the form's ` +
|
||||||
|
'INTAKE_FIELDS table; it would render as a real, visible field.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* `consent` is submitted by the form and read by the handler, and it is in
|
||||||
|
NEITHER field table — so the two checks above cannot see a collision with it.
|
||||||
|
A honeypot named `consent` would discard every valid submission behind the
|
||||||
|
success page, which is the worst failure this file can fail to catch. */
|
||||||
|
for (const [what, name] of [
|
||||||
|
['honeypot', HONEYPOT_FIELD],
|
||||||
|
['decoy checkbox', DECOY_CHECKBOX_FIELD],
|
||||||
|
]) {
|
||||||
|
if (name === 'consent') {
|
||||||
|
problems.push(
|
||||||
|
`the ${what} is named "consent", which the form submits and the handler ` +
|
||||||
|
'requires — every valid submission would be discarded behind the ' +
|
||||||
|
'success page.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (DECOY_CHECKBOX_FIELD === HONEYPOT_FIELD) {
|
||||||
|
problems.push(
|
||||||
|
'the two honeypots share the name ' +
|
||||||
|
`"${HONEYPOT_FIELD}" — that is one trap, not two, and the second ` +
|
||||||
|
'mechanism (a checkbox that must arrive absent) would not exist.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* And the first honeypot must not appear on the form's own table either — the
|
||||||
|
mirror of the check above it, which existed only for the handler's side. */
|
||||||
|
if (clientNames.includes(HONEYPOT_FIELD)) {
|
||||||
|
problems.push(
|
||||||
|
`the honeypot "${HONEYPOT_FIELD}" is in the form's INTAKE_FIELDS table; ` +
|
||||||
|
'it would render as a real, visible field.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const clientField of INTAKE_FIELDS) {
|
||||||
|
const serverField = server.find((f) => f.name === clientField.name);
|
||||||
|
if (!serverField) continue;
|
||||||
|
|
||||||
|
if (Boolean(clientField.required) !== Boolean(serverField.required)) {
|
||||||
|
problems.push(
|
||||||
|
`"${clientField.name}": form required=${Boolean(clientField.required)}, ` +
|
||||||
|
`handler required=${Boolean(serverField.required)}. A field the form ` +
|
||||||
|
'marks optional and the handler requires is a rejection the inquirer ' +
|
||||||
|
'cannot see the reason for.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LABELS TOO, since 2026-08-31. The handler now renders `f.label` into the
|
||||||
|
confirmation email the inquirer keeps, so a label that drifts from the
|
||||||
|
form's own wording means the receipt describes fields by names the form
|
||||||
|
never showed. One more comparison; the duplication stays mechanical. */
|
||||||
|
if (clientField.label !== serverField.label) {
|
||||||
|
problems.push(
|
||||||
|
`"${clientField.name}": labels differ.\n` +
|
||||||
|
` form: ${JSON.stringify(clientField.label)}\n` +
|
||||||
|
` handler: ${JSON.stringify(serverField.label ?? null)}\n` +
|
||||||
|
' The handler renders its label into the confirmation email.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((clientField.max ?? null) !== (serverField.max ?? null)) {
|
||||||
|
problems.push(
|
||||||
|
`"${clientField.name}": form max=${clientField.max ?? 'none'}, ` +
|
||||||
|
`handler max=${serverField.max ?? 'none'}. The form's maxlength stops ` +
|
||||||
|
'typing; a lower cap in the handler rejects a submission that looked fine.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientOptions = clientField.options ? [...clientField.options] : null;
|
||||||
|
const serverOptions = serverField.options ? [...serverField.options] : null;
|
||||||
|
if (JSON.stringify(clientOptions) !== JSON.stringify(serverOptions)) {
|
||||||
|
problems.push(
|
||||||
|
`"${clientField.name}": option sets differ.\n` +
|
||||||
|
` form: ${JSON.stringify(clientOptions)}\n` +
|
||||||
|
` handler: ${JSON.stringify(serverOptions)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`check:intake — ${clientNames.length} form fields, ${serverNames.length} ` +
|
||||||
|
'handler fields, compared on name, label, requiredness, cap and option ' +
|
||||||
|
`set; 2 honeypots ("${HONEYPOT_FIELD}", "${DECOY_CHECKBOX_FIELD}") ` +
|
||||||
|
'compared on name and checked out of both tables.',
|
||||||
|
);
|
||||||
|
if (problems.length > 0) {
|
||||||
|
console.error(`\nINTAKE TABLE MISMATCH — ${problems.length}:`);
|
||||||
|
for (const p of problems) console.error(` - ${p}`);
|
||||||
|
console.error(
|
||||||
|
'\ndocs/05: the handler re-validates everything. The two tables are ' +
|
||||||
|
'independent on purpose; they still have to agree.',
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('OK — the form and the handler agree.');
|
||||||
+119
-13
@@ -8,9 +8,10 @@
|
|||||||
# ships.
|
# ships.
|
||||||
#
|
#
|
||||||
# It matches .gitea/workflows/deploy.yml on everything that determines what gets
|
# It matches .gitea/workflows/deploy.yml on everything that determines what gets
|
||||||
# published: the same guard coverage, `npm run check` before the build, the same
|
# published: the same guard coverage, `npm run check` before the build,
|
||||||
# three sync passes in the same order with the same cache headers, and the same
|
# `npm run check:claims` after it, the same three sync passes in the same order
|
||||||
# invalidation. Any change to one must be made to the other.
|
# with the same cache headers, and the same invalidation. Any change to one must
|
||||||
|
# be made to the other.
|
||||||
#
|
#
|
||||||
# Two deliberate differences: it does not run `npm ci` (your node_modules is
|
# Two deliberate differences: it does not run `npm ci` (your node_modules is
|
||||||
# already installed, and CI starts empty), and it refuses to run as user/pouya,
|
# already installed, and CI starts empty), and it refuses to run as user/pouya,
|
||||||
@@ -19,26 +20,42 @@
|
|||||||
# Required environment (values are in AGENTS.md §7 — deliberately not restated
|
# Required environment (values are in AGENTS.md §7 — deliberately not restated
|
||||||
# here; §7 is the single source of truth for operational facts):
|
# here; §7 is the single source of truth for operational facts):
|
||||||
#
|
#
|
||||||
# AWS_REGION S3_BUCKET CLOUDFRONT_DISTRIBUTION_ID INTAKE_ENDPOINT
|
# AWS_REGION S3_BUCKET CLOUDFRONT_DISTRIBUTION_ID
|
||||||
#
|
#
|
||||||
# Credentials: use the scoped deploy user. AGENTS.md Q22 records that it does
|
# ⚠️ INTAKE_ENDPOINT IS NO LONGER ONE OF THEM, AND THE GUARD THAT DEMANDED IT
|
||||||
# NOT yet exist. NEVER run this as user/pouya — see AGENTS.md §10.
|
# WAS BLOCKING A DEPLOY ON A VALUE NOTHING READ. Build step 8 moved the intake
|
||||||
|
# form to the same-origin path /api/intake (see src/data/intake.ts for the four
|
||||||
|
# reasons). After that, `git grep PUBLIC_INTAKE_ENDPOINT -- src/` returned
|
||||||
|
# nothing — the value exported into the build below was consumed by no page —
|
||||||
|
# and the guard's own message was false in both directions: the form posts to
|
||||||
|
# /api/intake whatever that variable holds, and the thing that actually decides
|
||||||
|
# whether it works, the CloudFront /api/* behaviour, was guarded nowhere.
|
||||||
|
#
|
||||||
|
# So the guard now checks the thing that matters, after the deploy, at the
|
||||||
|
# bottom of this script. Found by `adversarial-reviewer`, 2026-08-31.
|
||||||
|
# PUBLIC_BOOKING_URL went with it: `CONTACT.bookingUrl` is `null` in source while
|
||||||
|
# R6 keeps booking parked, and nothing read that variable either.
|
||||||
|
#
|
||||||
|
# Credentials: use the scoped deploy user, `adr-sml-deploy`. AGENTS.md §7 records
|
||||||
|
# it as PROVISIONED, with one inline policy verified by nine
|
||||||
|
# simulate-principal-policy checks; Q22 closed on execution 2026-08-28.
|
||||||
|
# (This comment said it "does NOT yet exist" for three days after it did —
|
||||||
|
# found by `adversarial-reviewer` round 2.)
|
||||||
|
# NEVER run this as user/pouya — see AGENTS.md §10.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Same six values the workflow guards. Emptiness only — no value is echoed.
|
# Same five values the workflow guards. Emptiness only — no value is echoed.
|
||||||
missing=''
|
missing=''
|
||||||
[ -n "${AWS_REGION:-}" ] || missing="$missing AWS_REGION"
|
[ -n "${AWS_REGION:-}" ] || missing="$missing AWS_REGION"
|
||||||
[ -n "${S3_BUCKET:-}" ] || missing="$missing S3_BUCKET"
|
[ -n "${S3_BUCKET:-}" ] || missing="$missing S3_BUCKET"
|
||||||
[ -n "${CLOUDFRONT_DISTRIBUTION_ID:-}" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID"
|
[ -n "${CLOUDFRONT_DISTRIBUTION_ID:-}" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID"
|
||||||
[ -n "${INTAKE_ENDPOINT:-}" ] || missing="$missing INTAKE_ENDPOINT"
|
|
||||||
[ -n "${AWS_ACCESS_KEY_ID:-}" ] || missing="$missing AWS_ACCESS_KEY_ID"
|
[ -n "${AWS_ACCESS_KEY_ID:-}" ] || missing="$missing AWS_ACCESS_KEY_ID"
|
||||||
[ -n "${AWS_SECRET_ACCESS_KEY:-}" ] || missing="$missing AWS_SECRET_ACCESS_KEY"
|
[ -n "${AWS_SECRET_ACCESS_KEY:-}" ] || missing="$missing AWS_SECRET_ACCESS_KEY"
|
||||||
if [ -n "$missing" ]; then
|
if [ -n "$missing" ]; then
|
||||||
echo "Not set:$missing" >&2
|
echo "Not set:$missing" >&2
|
||||||
echo >&2
|
echo >&2
|
||||||
echo "Values are in AGENTS.md §7. An empty INTAKE_ENDPOINT does not fail the" >&2
|
echo "Values are in AGENTS.md §7." >&2
|
||||||
echo "build — it ships a live contact form posting to nothing." >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -52,7 +69,7 @@ case "$caller" in
|
|||||||
echo >&2
|
echo >&2
|
||||||
echo "REFUSING: that is the broadly-permissioned personal user." >&2
|
echo "REFUSING: that is the broadly-permissioned personal user." >&2
|
||||||
echo "AGENTS.md §10 — never use user/pouya to deploy. Use the scoped" >&2
|
echo "AGENTS.md §10 — never use user/pouya to deploy. Use the scoped" >&2
|
||||||
echo "deploy user (Q22: not yet created)." >&2
|
echo "deploy user, adr-sml-deploy — PROVISIONED, AGENTS.md §7." >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -61,11 +78,20 @@ echo "==> Type and template check"
|
|||||||
npm run check
|
npm run check
|
||||||
|
|
||||||
echo "==> Build"
|
echo "==> Build"
|
||||||
|
# Only PUBLIC_SITE_URL, because it is the only one astro.config.mjs reads.
|
||||||
|
# PUBLIC_INTAKE_ENDPOINT and PUBLIC_BOOKING_URL were exported here and consumed
|
||||||
|
# by nothing — see the header.
|
||||||
PUBLIC_SITE_URL="https://adr.smlcompany.ca" \
|
PUBLIC_SITE_URL="https://adr.smlcompany.ca" \
|
||||||
PUBLIC_INTAKE_ENDPOINT="$INTAKE_ENDPOINT" \
|
|
||||||
PUBLIC_BOOKING_URL="${BOOKING_URL:-}" \
|
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
|
# AFTER the build and BEFORE anything is uploaded. AGENTS.md §4 Forbidden,
|
||||||
|
# enforced mechanically on the output rather than by a reviewer reading it.
|
||||||
|
# Pouya's ruling 2026-08-29: "prose in a comment does not govern the writing
|
||||||
|
# that follows it." It also refuses to run against a stale or empty dist, so a
|
||||||
|
# pass here is a pass on the bytes about to be published.
|
||||||
|
echo "==> Claim check"
|
||||||
|
npm run check:claims
|
||||||
|
|
||||||
echo "==> Pass 1/3 — hashed assets and fonts (immutable)"
|
echo "==> Pass 1/3 — hashed assets and fonts (immutable)"
|
||||||
aws s3 sync ./dist "s3://${S3_BUCKET}" \
|
aws s3 sync ./dist "s3://${S3_BUCKET}" \
|
||||||
--exclude "*" \
|
--exclude "*" \
|
||||||
@@ -92,4 +118,84 @@ aws cloudfront create-invalidation \
|
|||||||
--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
|
--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
|
||||||
--paths "/*" >/dev/null
|
--paths "/*" >/dev/null
|
||||||
|
|
||||||
|
# THE CHECK THAT REPLACES THE INTAKE_ENDPOINT GUARD, and it runs AFTER the
|
||||||
|
# deploy because it tests the deployed thing rather than a variable.
|
||||||
|
#
|
||||||
|
# The intake form posts to the same-origin path /api/intake, which only works if
|
||||||
|
# a CloudFront behaviour routes /api/* to the HTTP API origin AGENTS.md §7
|
||||||
|
# records. Nothing in the build can know whether that behaviour exists, and a
|
||||||
|
# deploy that succeeds while the form posts into a 404 is the failure the old
|
||||||
|
# guard was reaching for and could not see.
|
||||||
|
#
|
||||||
|
# ⚠️ IT ASSERTS A POSITIVE, AND THE FIRST VERSION ASSERTED THE ABSENCE OF ONE
|
||||||
|
# CODE. That version was `code=$(curl ... || echo 000)` and passed on anything
|
||||||
|
# that was not literally 404. Two defects, both measured by
|
||||||
|
# `adversarial-reviewer` round 2:
|
||||||
|
#
|
||||||
|
# - `curl -w '%{http_code}'` ALREADY prints 000 on a failed transfer, so
|
||||||
|
# `|| echo 000` double-appended and $code became `000000` — the 000 arm was
|
||||||
|
# unreachable and a connection failure reported success.
|
||||||
|
# - If the /api/* behaviour is MISSING, the POST falls through to the S3
|
||||||
|
# default behaviour and CloudFront answers 403 for a disallowed method —
|
||||||
|
# indistinguishable from the handler's Origin refusal, which is the one
|
||||||
|
# distinction the check exists to draw. It also passed on a real 501.
|
||||||
|
#
|
||||||
|
# So it now sends the correct Origin and asserts the answer it should get:
|
||||||
|
# the handler validates, finds an empty submission, and redirects 303 to
|
||||||
|
# /contact/could-not-send/. That happens BEFORE any DynamoDB write and before
|
||||||
|
# any email, which is what makes the probe safe against production.
|
||||||
|
echo "==> Intake route check"
|
||||||
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
--max-time 15 \
|
||||||
|
-H "Origin: https://adr.smlcompany.ca" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data 'deploy-route-probe=1' \
|
||||||
|
"https://adr.smlcompany.ca/api/intake")
|
||||||
|
rc=$?
|
||||||
|
location=$(curl -sS -o /dev/null -w '%{redirect_url}' -X POST \
|
||||||
|
--max-time 15 \
|
||||||
|
-H "Origin: https://adr.smlcompany.ca" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data 'deploy-route-probe=1' \
|
||||||
|
"https://adr.smlcompany.ca/api/intake" || true)
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
echo >&2
|
||||||
|
echo "WARNING: the POST to /api/intake did not complete (curl exit $rc)." >&2
|
||||||
|
echo "The contact form posts there. The site is deployed and the form is" >&2
|
||||||
|
echo "unverified — see docs/06-deployment.md's cutover checklist." >&2
|
||||||
|
elif [ "$code" = "303" ] && case "$location" in *"/contact/could-not-send/") true;; *) false;; esac; then
|
||||||
|
echo " POST /api/intake -> 303 -> $location (routed, validating, rejecting an empty probe)"
|
||||||
|
else
|
||||||
|
echo >&2
|
||||||
|
echo "WARNING: POST /api/intake returned $code (expected 303 to" >&2
|
||||||
|
echo "/contact/could-not-send/); redirect was '${location:-none}'." >&2
|
||||||
|
# 404 IS AMBIGUOUS BETWEEN THREE CAUSES and the distribution's custom error
|
||||||
|
# response hides the one string that would separate them: API Gateway's
|
||||||
|
# {"message":"Not Found"} is replaced by /404.html, because custom error
|
||||||
|
# responses are distribution-wide. So name the causes and the one command that
|
||||||
|
# tells them apart. Corrected 2026-09-01 by `adversarial-reviewer`; the earlier
|
||||||
|
# text named only the CloudFront behaviour.
|
||||||
|
echo "404 means one of three things, and \`aws apigatewayv2 get-routes" >&2
|
||||||
|
echo "--api-id <id> --query 'Items[].RouteKey'\` separates them in one call:" >&2
|
||||||
|
echo " - the CloudFront /api/* behaviour is missing (docs/09 Part 3);" >&2
|
||||||
|
echo " - the POST /api/intake route is missing or misspelled (Part 6.2);" >&2
|
||||||
|
echo " - the route exists and the distribution's 404 mapping is showing you" >&2
|
||||||
|
echo " /404.html instead of the API's own body." >&2
|
||||||
|
# THE ORIGIN REQUEST POLICY ON /api/* IS NO LONGER A CONSTANT. Since
|
||||||
|
# 2026-09-04 the behaviour may carry the custom `adr-sml-api-viewer-address`
|
||||||
|
# whitelist (docs/09 Part 3, change 8) instead of the managed policy, so this
|
||||||
|
# text no longer names one and tells the operator to read it. Naming the old
|
||||||
|
# one would send them to "restore" what was deliberately replaced.
|
||||||
|
echo "403 means CloudFront rejected the method, or the handler refused the" >&2
|
||||||
|
echo "Origin. Read which origin request policy /api/* carries — since" >&2
|
||||||
|
echo "2026-09-04 it may be the custom whitelist adr-sml-api-viewer-address" >&2
|
||||||
|
echo "rather than Managed-AllViewerExceptHostHeader — because a policy that" >&2
|
||||||
|
echo "drops or fails to forward Origin turns every real submission into a" >&2
|
||||||
|
echo "403. Rollback id: b689b0a8-53d0-40ab-baf2-68738e2966ac." >&2
|
||||||
|
echo "500 means the Lambda invoke permission for this route is missing" >&2
|
||||||
|
echo "(Part 6.1) — the function is never entered, so CloudWatch is silent." >&2
|
||||||
|
echo "Either way the form is not verified working. See docs/09-cutover-" >&2
|
||||||
|
echo "runbook.md Part 7.1 and docs/06's cutover checklist." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> Deployed to https://adr.smlcompany.ca ($(git rev-parse --short HEAD))"
|
echo "==> Deployed to https://adr.smlcompany.ca ($(git rev-parse --short HEAD))"
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
/**
|
||||||
|
* Regenerates `public/favicon.ico` from the committed brand master.
|
||||||
|
*
|
||||||
|
* LOCAL ONLY, like `bio:pdf`. Not wired into `npm run build` or either deploy
|
||||||
|
* path — the icons are committed artefacts and this is what re-derives them.
|
||||||
|
*
|
||||||
|
* Writes the favicon ONLY. `apple-touch-icon.png` is deliberately not touched
|
||||||
|
* and must stay opaque — `docs/reference/brand-assets.md` §The icon set.
|
||||||
|
*/
|
||||||
|
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const root = fileURLToPath(new URL('..', import.meta.url));
|
||||||
|
const MARK = `${root}src/assets/brand/sml-infinity-mark.png`;
|
||||||
|
const MASTER = `${root}src/assets/brand/sml-infinity-mark-master.png`;
|
||||||
|
const OUT = `${root}public/favicon.ico`;
|
||||||
|
|
||||||
|
/** Sizes carried in the container, ascending — the order BaseLayout declares. */
|
||||||
|
const SIZES = [16, 32, 48];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mark spans 7/8 of the canvas and is centred on both axes. Not a taste
|
||||||
|
* decision at this point: it is the composition already shipping, measured off
|
||||||
|
* the previous icon at all three sizes (14/16, 28/32, 42/48) and off the touch
|
||||||
|
* icon (158/180). Regenerating for alpha must not also restyle the mark.
|
||||||
|
*/
|
||||||
|
const INK_FRACTION = 7 / 8;
|
||||||
|
|
||||||
|
/** `--cream` — the ground the previous icon was matted against. */
|
||||||
|
const CREAM = [250, 247, 242];
|
||||||
|
|
||||||
|
const die = (msg) => {
|
||||||
|
rmSync(`${OUT}.candidate`, { force: true });
|
||||||
|
console.error(`icons: ${msg}`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R14 — the icon must be traceable to the artwork in this repository, not to a
|
||||||
|
* file on someone's disk. The render source is a tight crop of the master, so
|
||||||
|
* assert it still IS that crop before deriving anything from it.
|
||||||
|
*/
|
||||||
|
async function assertProvenance() {
|
||||||
|
const mark = await sharp(MARK).metadata();
|
||||||
|
const crop = { left: 159, top: 646, width: 2668, height: 1704 };
|
||||||
|
if (mark.width !== crop.width || mark.height !== crop.height) {
|
||||||
|
die(
|
||||||
|
`render source is ${mark.width}x${mark.height}, expected ${crop.width}x${crop.height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const [a, b] = await Promise.all([
|
||||||
|
sharp(MASTER).extract(crop).raw().toBuffer(),
|
||||||
|
sharp(MARK).raw().toBuffer(),
|
||||||
|
]);
|
||||||
|
if (!a.equals(b))
|
||||||
|
die('render source is no longer the documented crop of the master');
|
||||||
|
console.log(
|
||||||
|
`provenance: ${crop.width}x${crop.height} at (${crop.left},${crop.top}) of the master — identical`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The whole point of the regeneration. A source without alpha would mean
|
||||||
|
* deriving a mask from the cream ground, which is a different and lossier job —
|
||||||
|
* so fail rather than silently ship a matted icon again.
|
||||||
|
*/
|
||||||
|
async function loadMark() {
|
||||||
|
const meta = await sharp(MARK).metadata();
|
||||||
|
if (!meta.hasAlpha)
|
||||||
|
die(
|
||||||
|
`${MARK} has no alpha channel — cannot export a transparent icon from it`,
|
||||||
|
);
|
||||||
|
const { data, info } = await sharp(MARK)
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
let transparent = 0;
|
||||||
|
for (let p = 3; p < data.length; p += 4) if (data[p] === 0) transparent++;
|
||||||
|
if (transparent === 0)
|
||||||
|
die(`${MARK} declares alpha but every pixel is opaque`);
|
||||||
|
console.log(
|
||||||
|
`source: ${info.width}x${info.height} alpha, ${transparent} fully transparent px`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize onto a TRANSPARENT canvas. sharp premultiplies around the resample, so
|
||||||
|
* the ribbon's anti-aliased edge blends toward its own colour rather than
|
||||||
|
* toward the RGB sitting under alpha 0 — that is the cream halo this change
|
||||||
|
* exists to remove, and it would come straight back with a matted background.
|
||||||
|
*/
|
||||||
|
async function frame(size) {
|
||||||
|
const w = Math.round(size * INK_FRACTION);
|
||||||
|
const png = await sharp(MARK)
|
||||||
|
.resize({
|
||||||
|
width: w,
|
||||||
|
kernel: 'lanczos3',
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: false,
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
const { height: h } = await sharp(png).metadata();
|
||||||
|
if (h > size) die(`size ${size}: mark is ${w}x${h}, taller than the canvas`);
|
||||||
|
const left = Math.round((size - w) / 2);
|
||||||
|
const top = Math.round((size - h) / 2);
|
||||||
|
const out = await sharp({
|
||||||
|
create: {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite([{ input: png, left, top }])
|
||||||
|
.png({ compressionLevel: 9, effort: 10, palette: false })
|
||||||
|
.toBuffer();
|
||||||
|
console.log(
|
||||||
|
` ${size}x${size}: mark ${w}x${h} at (${left},${top}), ${out.length} B`,
|
||||||
|
);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ICO container: 6-byte header, one 16-byte directory entry per frame, then the PNG payloads. */
|
||||||
|
function buildIco(frames) {
|
||||||
|
const header = Buffer.alloc(6);
|
||||||
|
header.writeUInt16LE(0, 0); // reserved
|
||||||
|
header.writeUInt16LE(1, 2); // type 1 = icon
|
||||||
|
header.writeUInt16LE(frames.length, 4);
|
||||||
|
|
||||||
|
const dir = Buffer.alloc(16 * frames.length);
|
||||||
|
let offset = header.length + dir.length;
|
||||||
|
frames.forEach(({ size, png }, i) => {
|
||||||
|
const e = i * 16;
|
||||||
|
dir[e] = size; // width — a byte; 0 would mean 256, which SIZES never is
|
||||||
|
dir[e + 1] = size; // height
|
||||||
|
dir[e + 2] = 0; // palette size — 0 for truecolour
|
||||||
|
dir[e + 3] = 0; // reserved
|
||||||
|
dir.writeUInt16LE(1, e + 4); // colour planes
|
||||||
|
dir.writeUInt16LE(32, e + 6); // bits per pixel
|
||||||
|
dir.writeUInt32LE(png.length, e + 8);
|
||||||
|
dir.writeUInt32LE(offset, e + 12);
|
||||||
|
offset += png.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Buffer.concat([header, dir, ...frames.map((f) => f.png)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-read the container FROM DISK and decode each frame, rather than inspecting
|
||||||
|
* the buffers we just built — a check that reads its own inputs proves nothing.
|
||||||
|
* (It is still `sharp` decoding `sharp`'s output, so it is not a second
|
||||||
|
* instrument. The independent reads are in `docs/reference/brand-assets.md`.)
|
||||||
|
*/
|
||||||
|
async function verify(path) {
|
||||||
|
const buf = readFileSync(path);
|
||||||
|
const count = buf.readUInt16LE(4);
|
||||||
|
if (count !== SIZES.length)
|
||||||
|
die(`container declares ${count} frames, expected ${SIZES.length}`);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const e = 6 + i * 16;
|
||||||
|
const size = buf[e];
|
||||||
|
const len = buf.readUInt32LE(e + 8);
|
||||||
|
const off = buf.readUInt32LE(e + 12);
|
||||||
|
if (off + len > buf.length)
|
||||||
|
die(`frame ${i}: range ${off}+${len} exceeds ${buf.length} B`);
|
||||||
|
const { data, info } = await sharp(buf.subarray(off, off + len))
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
if (info.width !== size || info.height !== size)
|
||||||
|
die(`frame ${i}: decoded ${info.width}x${info.height}, dir says ${size}`);
|
||||||
|
const corners = [
|
||||||
|
[0, 0],
|
||||||
|
[size - 1, 0],
|
||||||
|
[0, size - 1],
|
||||||
|
[size - 1, size - 1],
|
||||||
|
];
|
||||||
|
for (const [x, y] of corners) {
|
||||||
|
const a = data[(y * size + x) * 4 + 3];
|
||||||
|
if (a !== 0)
|
||||||
|
die(`frame ${size}: corner (${x},${y}) has alpha ${a}, expected 0`);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* THE CORNER AND TRANSPARENCY CHECKS CANNOT SEE A CREAM HALO. A frame whose
|
||||||
|
* edge was matted against cream and then had its background knocked out has
|
||||||
|
* clear corners, transparent pixels and opaque pixels, and passes every one
|
||||||
|
* of them. What distinguishes it is the colour the edge blends TOWARD.
|
||||||
|
*
|
||||||
|
* ⚠️ AND IT IS THE BOUNDARY, NOT THE PARTIAL-ALPHA PIXELS. A first version
|
||||||
|
* of this guard inspected only pixels at 0 < alpha < 255 and MISSED a
|
||||||
|
* purpose-built haloed fixture entirely, because a knockout sets alpha per
|
||||||
|
* pixel and leaves NO partial alpha at all — 0 such pixels in the fixture.
|
||||||
|
* A guard that cannot see the defect it is named for is worse than none.
|
||||||
|
*
|
||||||
|
* So: take every painted pixel that touches a fully transparent one, and
|
||||||
|
* measure how many sit near cream. Measured on this artwork — correct
|
||||||
|
* frames 1 / 2 / 5 of 61 / 146 / 258 boundary pixels (1.4-1.9%); the haloed
|
||||||
|
* fixture 33 of 115 (28.7%). The gate is 10%, roughly 5x clear of both.
|
||||||
|
*/
|
||||||
|
const NEAR_CREAM = 20;
|
||||||
|
const HALO_SHARE = 0.1;
|
||||||
|
const alphaAt = (x, y) =>
|
||||||
|
x < 0 || y < 0 || x >= size || y >= size
|
||||||
|
? 0
|
||||||
|
: data[(y * size + x) * 4 + 3];
|
||||||
|
let clear = 0;
|
||||||
|
let ink = 0;
|
||||||
|
let boundary = 0;
|
||||||
|
let boundaryNearCream = 0;
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
|
for (let x = 0; x < size; x++) {
|
||||||
|
const i = (y * size + x) * 4;
|
||||||
|
const a = data[i + 3];
|
||||||
|
if (a === 0) {
|
||||||
|
clear++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a === 255) ink++;
|
||||||
|
const onEdge =
|
||||||
|
alphaAt(x - 1, y) === 0 ||
|
||||||
|
alphaAt(x + 1, y) === 0 ||
|
||||||
|
alphaAt(x, y - 1) === 0 ||
|
||||||
|
alphaAt(x, y + 1) === 0;
|
||||||
|
if (!onEdge) continue;
|
||||||
|
boundary++;
|
||||||
|
const d = Math.max(
|
||||||
|
Math.abs(data[i] - CREAM[0]),
|
||||||
|
Math.abs(data[i + 1] - CREAM[1]),
|
||||||
|
Math.abs(data[i + 2] - CREAM[2]),
|
||||||
|
);
|
||||||
|
if (d <= NEAR_CREAM) boundaryNearCream++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const share = boundary === 0 ? 0 : boundaryNearCream / boundary;
|
||||||
|
if (clear === 0)
|
||||||
|
die(`frame ${size}: no transparent pixels — the ground is fully matted`);
|
||||||
|
if (ink === 0)
|
||||||
|
die(`frame ${size}: no opaque pixels — the mark did not render`);
|
||||||
|
if (boundary === 0)
|
||||||
|
die(`frame ${size}: no boundary pixels — cannot test the edge colour`);
|
||||||
|
if (share > HALO_SHARE)
|
||||||
|
die(
|
||||||
|
`frame ${size}: ${boundaryNearCream} of ${boundary} boundary pixels ` +
|
||||||
|
`(${(share * 100).toFixed(1)}%) sit within ${NEAR_CREAM} of cream — ` +
|
||||||
|
`the edge was matted against the ground before the ground was removed`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${size}x${size}: ${clear} transparent, ${ink} opaque, corners clear, ` +
|
||||||
|
`edge ${(share * 100).toFixed(1)}% near cream`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`verified ${path} (${buf.length} B)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertProvenance();
|
||||||
|
await loadMark();
|
||||||
|
console.log('frames:');
|
||||||
|
const frames = [];
|
||||||
|
for (const size of SIZES) frames.push({ size, png: await frame(size) });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verify a CANDIDATE file, then move it into place. Writing OUT first and
|
||||||
|
* verifying after would mean a failing check exits 1 having already replaced a
|
||||||
|
* good favicon with the one it just rejected — and nothing downstream re-checks,
|
||||||
|
* because this script is deliberately outside the build and both deploy paths.
|
||||||
|
*/
|
||||||
|
const candidate = `${OUT}.candidate`;
|
||||||
|
writeFileSync(candidate, buildIco(frames));
|
||||||
|
console.log('verify:');
|
||||||
|
try {
|
||||||
|
await verify(candidate);
|
||||||
|
} catch (err) {
|
||||||
|
rmSync(candidate, { force: true });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
renameSync(candidate, OUT);
|
||||||
|
console.log(`wrote ${OUT}`);
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Prints the intake Lambda's six environment variables as the JSON that
|
||||||
|
* `aws lambda update-function-configuration --environment` takes.
|
||||||
|
*
|
||||||
|
* ⚠️ THIS EXISTS SO THAT TWO PUBLISHED COMMITMENTS ARE NEVER RETYPED INTO A
|
||||||
|
* SHELL COMMAND. `RESPONSE_TIME` and `NO_RETAINER_NOTICE` are read from
|
||||||
|
* `src/data/site.ts` — the same constants `/contact/` renders — because a
|
||||||
|
* hand-typed copy of the notice inside the handler had already dropped a clause
|
||||||
|
* once (`docs/05`, and the handler's own comment on the constant). A deploy
|
||||||
|
* procedure that asks an operator to paste a sentence is the same defect one
|
||||||
|
* step further out, and the notice contains an EN DASH in "mediator–party",
|
||||||
|
* which is exactly the character a retype loses.
|
||||||
|
*
|
||||||
|
* Resource names come from `AGENTS.md` §7 and are passed in, not defaulted from
|
||||||
|
* a second copy here — except the two that are pure site facts.
|
||||||
|
*
|
||||||
|
* usage: node scripts/intake-env.mjs --table <name> --notify <addr> --from <addr>
|
||||||
|
* node scripts/intake-env.mjs ... --shell # export lines instead
|
||||||
|
*/
|
||||||
|
import { CONTACT, NO_RETAINER_NOTICE, SITE } from '../src/data/site.ts';
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const flag = (name) => {
|
||||||
|
const i = args.indexOf(`--${name}`);
|
||||||
|
return i === -1 ? undefined : args[i + 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = flag('table');
|
||||||
|
const notify = flag('notify');
|
||||||
|
const from = flag('from');
|
||||||
|
const missing = [
|
||||||
|
['--table', table],
|
||||||
|
['--notify', notify],
|
||||||
|
['--from', from],
|
||||||
|
]
|
||||||
|
.filter(([, v]) => !v)
|
||||||
|
.map(([k]) => k);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error(`missing: ${missing.join(' ')}`);
|
||||||
|
console.error(
|
||||||
|
'usage: node scripts/intake-env.mjs --table <dynamodb-table> ' +
|
||||||
|
'--notify <address> --from <ses-verified-address> [--shell]',
|
||||||
|
);
|
||||||
|
console.error('Resource names are in AGENTS.md §7.');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The site origin is not a deploy-time choice: the handler compares the request
|
||||||
|
Origin against it and redirects to pages ON it, so it must be the canonical
|
||||||
|
origin `astro.config.mjs` builds against. */
|
||||||
|
const origin = SITE.url.replace(/\/$/, '');
|
||||||
|
|
||||||
|
const vars = {
|
||||||
|
INTAKE_TABLE: table,
|
||||||
|
SITE_ORIGIN: origin,
|
||||||
|
NOTIFY_TO: notify,
|
||||||
|
MAIL_FROM: from,
|
||||||
|
RESPONSE_TIME: CONTACT.responseTime,
|
||||||
|
NO_RETAINER_NOTICE,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Guards, not decoration. Each one is a failure this project has already had or
|
||||||
|
has written down as the next one. */
|
||||||
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
|
if (typeof v !== 'string' || v.trim() === '') {
|
||||||
|
throw new Error(
|
||||||
|
`${k} resolved empty — the handler throws at cold start on that`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!/^https:\/\//.test(origin)) {
|
||||||
|
throw new Error(`SITE_ORIGIN must be an https origin, got ${origin}`);
|
||||||
|
}
|
||||||
|
/* The clause a hand-copy dropped. `docs/01` §/contact/ requires it, so its
|
||||||
|
absence is a published-disclosure defect rather than a typo. */
|
||||||
|
if (!NO_RETAINER_NOTICE.includes('create a conflict check')) {
|
||||||
|
throw new Error(
|
||||||
|
'NO_RETAINER_NOTICE is missing its fourth clause about not itself creating ' +
|
||||||
|
'a conflict check — docs/01 §/contact/ requires it. Do not deploy this.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!/–/.test(NO_RETAINER_NOTICE)) {
|
||||||
|
throw new Error(
|
||||||
|
'NO_RETAINER_NOTICE no longer contains the en dash in "mediator–party". ' +
|
||||||
|
'Either the constant changed deliberately, or something re-typed it.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!/\btwo business days\b/.test(CONTACT.responseTime)) {
|
||||||
|
throw new Error(
|
||||||
|
`RESPONSE_TIME is "${CONTACT.responseTime}" — AGENTS.md §4/Q27 is a ` +
|
||||||
|
'two-business-day commitment. If the commitment changed, /contact/, the ' +
|
||||||
|
'bio and this all move together.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.includes('--shell')) {
|
||||||
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
|
console.log(`export ${k}=${JSON.stringify(v)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify({ Variables: vars }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* The performance gate. Budget: docs/04-seo-spec.md §Performance —
|
||||||
|
* Lighthouse >= 95 on all four categories, on mobile, for every page.
|
||||||
|
*
|
||||||
|
* WHY THIS IS `lighthouse` AND NOT `@lhci/cli`, WHICH IS WHAT R11 SAID TO PUT
|
||||||
|
* BACK. Measured 2026-08-31 from two probe lockfiles, not recalled:
|
||||||
|
*
|
||||||
|
* @lhci/cli@0.15.1 10 vulnerabilities (7 high) pins lighthouse 12.6.1
|
||||||
|
* high: tmp@0.1.0 <- a DIRECT dependency of @lhci/cli itself
|
||||||
|
* high: extract-zip@2.0.1 <- via @puppeteer/browsers
|
||||||
|
* lighthouse@13.4.1 0 vulnerabilities 109 packages
|
||||||
|
* tmp ABSENT, extract-zip ABSENT
|
||||||
|
*
|
||||||
|
* So the carrier was never Lighthouse. AGENTS.md §7 recorded the advisories as
|
||||||
|
* arriving "via lighthouse -> puppeteer-core -> extract-zip", and on that
|
||||||
|
* attribution the tool looked unusable for as long as the advisories stood.
|
||||||
|
* Standalone `lighthouse` measures the same budget with nothing outstanding.
|
||||||
|
* What is given up is real and is recorded in §7: `lhci autorun`'s assertion
|
||||||
|
* config, its server, and its CI upload.
|
||||||
|
*
|
||||||
|
* THIS IS A LOCAL GATE, NOT A CI CHECK, and the reason is Chrome. Standalone
|
||||||
|
* Lighthouse drives an installed browser; the Gitea runner has none (§7 — the
|
||||||
|
* runner is not registered at all yet, Q23). So this runs from a keyboard and
|
||||||
|
* as a blocking item on docs/06's cutover checklist. It is not wired into
|
||||||
|
* `npm run build` or either deploy path, and saying so is the point: a check
|
||||||
|
* described as running where it cannot is the defect Q22 turned out to be.
|
||||||
|
*
|
||||||
|
* PAGES ARE ENUMERATED FROM `dist/`, NEVER LISTED HERE. A hand-written list
|
||||||
|
* silently stops covering the site the first time a page is added — which is
|
||||||
|
* this project's most expensive recurring shape. Every `index.html` under
|
||||||
|
* `dist/` is a page, so the set cannot go stale.
|
||||||
|
*
|
||||||
|
* Usage: npm run build && npm run lighthouse
|
||||||
|
* npm run lighthouse -- /fees/ /insights/ # a subset, by pathname
|
||||||
|
*/
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { createReadStream } from 'node:fs';
|
||||||
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
||||||
|
import { join, extname, relative, sep } from 'node:path';
|
||||||
|
import lighthouse from 'lighthouse';
|
||||||
|
import * as chromeLauncher from 'chrome-launcher';
|
||||||
|
|
||||||
|
const DIST = new URL('../dist/', import.meta.url).pathname;
|
||||||
|
const THRESHOLD = 95;
|
||||||
|
const CATEGORIES = ['performance', 'accessibility', 'best-practices', 'seo'];
|
||||||
|
|
||||||
|
/** docs/04's own budgets, reported alongside the scores rather than asserted
|
||||||
|
* separately — LCP is the one the spec states in seconds. */
|
||||||
|
const LCP_BUDGET_MS = 2000;
|
||||||
|
const CLS_BUDGET = 0.05;
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.json': 'application/json; charset=utf-8',
|
||||||
|
'.xml': 'application/xml; charset=utf-8',
|
||||||
|
'.txt': 'text/plain; charset=utf-8',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.avif': 'image/avif',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.pdf': 'application/pdf',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `trailingSlash: 'always'` + `build.format: 'directory'` (astro.config.mjs),
|
||||||
|
* so `/mediation/` is `dist/mediation/index.html` and an extensionless path
|
||||||
|
* without the slash is a 404 here exactly as it is on CloudFront. Serving it
|
||||||
|
* anyway would measure a URL the site does not have.
|
||||||
|
*/
|
||||||
|
function resolveFile(pathname) {
|
||||||
|
if (pathname.endsWith('/')) return join(DIST, pathname, 'index.html');
|
||||||
|
if (extname(pathname)) return join(DIST, pathname);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectPages(dir = DIST) {
|
||||||
|
const out = [];
|
||||||
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||||
|
const full = join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) out.push(...(await collectPages(full)));
|
||||||
|
else if (entry.name === 'index.html') {
|
||||||
|
const rel = relative(DIST, dir).split(sep).filter(Boolean).join('/');
|
||||||
|
out.push(rel ? `/${rel}/` : '/');
|
||||||
|
}
|
||||||
|
/* ⚠️ `index.html` ALONE MISSED THE 404 PAGE, so the budget was measured on
|
||||||
|
22 pages of 23 while the header above claims it enumerates the site.
|
||||||
|
`404.astro` is emitted as `dist/404.html`, outside `build.format:
|
||||||
|
'directory'`. The path pushed here is a URL this script SERVES, so it is
|
||||||
|
`/404.html` — the form CloudFront's custom error response fetches — and
|
||||||
|
`resolveFile()` resolves it on the `extname` branch. `og-proof.mjs` needs
|
||||||
|
the `OG_CARDS` key `/404/` for the same file; the two differ on purpose. */
|
||||||
|
else if (dir === DIST && entry.name.endsWith('.html')) {
|
||||||
|
out.push(`/${entry.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveDist() {
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
const pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
||||||
|
const file = resolveFile(pathname);
|
||||||
|
if (!file) {
|
||||||
|
res.writeHead(404, { 'content-type': 'text/plain' });
|
||||||
|
res.end('404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stream = createReadStream(file);
|
||||||
|
stream.on('error', () => {
|
||||||
|
res.writeHead(404, { 'content-type': 'text/plain' });
|
||||||
|
res.end('404');
|
||||||
|
});
|
||||||
|
stream.once('open', () => {
|
||||||
|
// NO `cache-control` HEADER, AND THAT IS DELIBERATE — measured
|
||||||
|
// 2026-08-31. `cache-control: no-store` was set here to force a cold
|
||||||
|
// cache, which it did not need to do (Lighthouse resets storage between
|
||||||
|
// runs by default) and which cost the `bf-cache` audit outright:
|
||||||
|
// "Pages whose main resource has cache-control:no-store cannot enter
|
||||||
|
// back/forward cache." The audit failed on every page, in a report whose
|
||||||
|
// whole job is to find defects on the site. Verified by toggling the one
|
||||||
|
// header: bf-cache 0 with it, 1 without, twice each.
|
||||||
|
res.writeHead(200, {
|
||||||
|
'content-type': MIME[extname(file)] ?? 'application/octet-stream',
|
||||||
|
});
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', () =>
|
||||||
|
resolve({ server, port: server.address().port }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pad = (s, n) => String(s).padEnd(n);
|
||||||
|
const scoreOf = (lhr, id) => Math.round((lhr.categories[id]?.score ?? 0) * 100);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ AN INTENTIONALLY `noindex` PAGE CANNOT SCORE 95 ON LIGHTHOUSE'S SEO
|
||||||
|
* CATEGORY, AND THE BUDGET AS WRITTEN DID NOT KNOW THAT.
|
||||||
|
*
|
||||||
|
* Measured 2026-08-31, first full run over 22 pages: five pages scored SEO
|
||||||
|
* **69**, and on every one the ONLY failing audit was `is-crawlable` — *"Page is
|
||||||
|
* blocked from indexing"* — firing on `<meta name="robots" content="noindex,
|
||||||
|
* follow">`. That meta tag is what `docs/04` REQUIRES on `/legal/*`, and it is
|
||||||
|
* deliberate on `/bio/`, `/contact/received/` and `/contact/could-not-send/`.
|
||||||
|
* So the category is measuring the page doing exactly what it was built to do.
|
||||||
|
*
|
||||||
|
* The wrong fix is to drop the SEO threshold, or to except these pages, or to
|
||||||
|
* stop measuring them: each of those hides every OTHER SEO defect on the pages
|
||||||
|
* where a defect is hardest to notice. What is asserted instead is stricter than
|
||||||
|
* a number:
|
||||||
|
*
|
||||||
|
* indexable page -> SEO category >= 95, as before
|
||||||
|
* noindex page -> EVERY SEO audit must pass EXCEPT `is-crawlable`
|
||||||
|
*
|
||||||
|
* A missing canonical, a missing title, an unreadable font size or a bad link on
|
||||||
|
* a noindex page still fails the gate. Only the one audit that is measuring the
|
||||||
|
* intent is set aside, and the page is marked in the table so the number is
|
||||||
|
* never read as unqualified.
|
||||||
|
*
|
||||||
|
* `noindex` is read from the BUILT HTML rather than from a list of paths here —
|
||||||
|
* a list would stop covering the site the first time a page is added.
|
||||||
|
*/
|
||||||
|
const EXPECTED_NOINDEX_FAILURE = 'is-crawlable';
|
||||||
|
|
||||||
|
async function isNoindex(page) {
|
||||||
|
const file = resolveFile(page);
|
||||||
|
const html = await readFile(file, 'utf8');
|
||||||
|
return /<meta[^>]+name="robots"[^>]+content="[^"]*noindex/i.test(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function failingAudits(lhr, category) {
|
||||||
|
return (lhr.categories[category]?.auditRefs ?? [])
|
||||||
|
.map((ref) => lhr.audits[ref.id])
|
||||||
|
.filter((audit) => audit && audit.score !== null && audit.score < 1)
|
||||||
|
.map((audit) => audit.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
await stat(join(DIST, 'index.html'));
|
||||||
|
} catch {
|
||||||
|
console.error(
|
||||||
|
'dist/index.html is missing. Run `npm run build` first — this gate ' +
|
||||||
|
'measures the bytes that would ship, not the dev server.',
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requested = process.argv.slice(2).filter((a) => a.startsWith('/'));
|
||||||
|
const all = await collectPages();
|
||||||
|
const pages = requested.length ? requested : all;
|
||||||
|
const unknown = requested.filter((p) => !all.includes(p));
|
||||||
|
if (unknown.length) {
|
||||||
|
console.error(`Not built: ${unknown.join(', ')}`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { server, port } = await serveDist();
|
||||||
|
const baseFlags = ['--headless', '--no-sandbox', '--disable-gpu'];
|
||||||
|
const chrome = await chromeLauncher.launch({ chromeFlags: baseFlags });
|
||||||
|
/**
|
||||||
|
* ⚠️ A SECOND BROWSER, AND THE ACCESSIBILITY CATEGORY IS MEASURED IN IT.
|
||||||
|
*
|
||||||
|
* `--force-prefers-reduced-motion`. This is a deliberate deviation from a
|
||||||
|
* single default run and it must be stated wherever the number is, which is
|
||||||
|
* why the table below labels the column. Measured 2026-08-31, twice per
|
||||||
|
* condition, on `/process/`:
|
||||||
|
*
|
||||||
|
* motion on a11y = 96 color-contrast FAILED, 24 nodes
|
||||||
|
* motion off a11y = 100 color-contrast passed, 0 nodes
|
||||||
|
*
|
||||||
|
* The 24 nodes were the scroll-driven reveal (`animation-timeline: view()`,
|
||||||
|
* global.css) caught mid-flight: axe reported foregrounds like `#d0cbc4` on
|
||||||
|
* `#f8f4ed`, and NEITHER is in this site's palette — they are the real colours
|
||||||
|
* blended toward the background by an in-progress `opacity` keyframe. So the
|
||||||
|
* audit was measuring animation state, not contrast.
|
||||||
|
*
|
||||||
|
* WHY THIS IS THE HONEST RUN RATHER THAN THE CONVENIENT ONE. A category that
|
||||||
|
* reports 24 known-false nodes on ten of fourteen pages cannot surface the
|
||||||
|
* twenty-fifth, real one — it is a control that has stopped controlling, which
|
||||||
|
* is the shape `AGENTS.md` Q22 and the Lighthouse removal both took. The
|
||||||
|
* reduced-motion rendering is not a synthetic one: it is the branch
|
||||||
|
* `global.css` ships for `prefers-reduced-motion: reduce`, a real user setting,
|
||||||
|
* and it is the branch in which every element sits at its FINAL colour, which
|
||||||
|
* is what a contrast audit is asking about. Contrast ratios for the palette
|
||||||
|
* itself are computed and recorded in `docs/02-design-system.md`.
|
||||||
|
*
|
||||||
|
* Performance is NOT measured here — reduced motion would suppress work the
|
||||||
|
* site really does on a default profile.
|
||||||
|
*/
|
||||||
|
const chromeA11y = await chromeLauncher.launch({
|
||||||
|
chromeFlags: [...baseFlags, '--force-prefers-reduced-motion'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const PERF_CATEGORIES = CATEGORIES.filter((id) => id !== 'accessibility');
|
||||||
|
const rows = [];
|
||||||
|
const breaches = [];
|
||||||
|
try {
|
||||||
|
for (const page of pages) {
|
||||||
|
const url = `http://127.0.0.1:${port}${page}`;
|
||||||
|
// Default config otherwise: Lighthouse's mobile preset — mobile form
|
||||||
|
// factor, mobile screen emulation, simulated Slow 4G. That is the
|
||||||
|
// budget's own wording in docs/04, so none of it is overridden.
|
||||||
|
const run = async (chromeInstance, onlyCategories) => {
|
||||||
|
const result = await lighthouse(url, {
|
||||||
|
logLevel: 'error',
|
||||||
|
output: 'json',
|
||||||
|
port: chromeInstance.port,
|
||||||
|
onlyCategories,
|
||||||
|
});
|
||||||
|
if (!result?.lhr) {
|
||||||
|
throw new Error(`Lighthouse returned nothing for ${page}`);
|
||||||
|
}
|
||||||
|
if (result.lhr.runtimeError?.code) {
|
||||||
|
throw new Error(`${page}: ${result.lhr.runtimeError.message}`);
|
||||||
|
}
|
||||||
|
return result.lhr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lhr = await run(chrome, PERF_CATEGORIES);
|
||||||
|
const lhrA11y = await run(chromeA11y, ['accessibility']);
|
||||||
|
|
||||||
|
const scores = Object.fromEntries([
|
||||||
|
...PERF_CATEGORIES.map((id) => [id, scoreOf(lhr, id)]),
|
||||||
|
['accessibility', scoreOf(lhrA11y, 'accessibility')],
|
||||||
|
]);
|
||||||
|
const lcp = lhr.audits['largest-contentful-paint']?.numericValue ?? NaN;
|
||||||
|
const cls = lhr.audits['cumulative-layout-shift']?.numericValue ?? NaN;
|
||||||
|
const noindex = await isNoindex(page);
|
||||||
|
rows.push({ page, scores, lcp, cls, noindex });
|
||||||
|
|
||||||
|
for (const id of CATEGORIES) {
|
||||||
|
// The SEO category on a noindex page is asserted audit by audit
|
||||||
|
// instead — see the comment on EXPECTED_NOINDEX_FAILURE.
|
||||||
|
if (id === 'seo' && noindex) continue;
|
||||||
|
if (scores[id] < THRESHOLD) {
|
||||||
|
breaches.push(`${page} ${id} = ${scores[id]} (< ${THRESHOLD})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (noindex) {
|
||||||
|
const unexpected = failingAudits(lhr, 'seo').filter(
|
||||||
|
(id) => id !== EXPECTED_NOINDEX_FAILURE,
|
||||||
|
);
|
||||||
|
if (unexpected.length) {
|
||||||
|
breaches.push(
|
||||||
|
`${page} seo — noindex page, so only \`${EXPECTED_NOINDEX_FAILURE}\` ` +
|
||||||
|
`may fail; these also failed: ${unexpected.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// `kill()` is synchronous in chrome-launcher 1.x — `await` on it draws
|
||||||
|
// ts(80007) from `astro check`, which this repo keeps at zero.
|
||||||
|
chrome.kill();
|
||||||
|
chromeA11y.kill();
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = Math.max(28, ...rows.map((r) => r.page.length + 2));
|
||||||
|
console.log(`\n${pad('page', w)} perf a11y* bestp seo LCP CLS`);
|
||||||
|
console.log('-'.repeat(w + 44));
|
||||||
|
for (const r of rows) {
|
||||||
|
const cells = CATEGORIES.map((id) =>
|
||||||
|
pad(id === 'seo' && r.noindex ? `${r.scores[id]}n` : r.scores[id], 6),
|
||||||
|
).join(' ');
|
||||||
|
const lcpCell = pad(`${(r.lcp / 1000).toFixed(2)}s`, 8);
|
||||||
|
console.log(`${pad(r.page, w)} ${cells} ${lcpCell} ${r.cls.toFixed(3)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The worst-of row excludes noindex pages from the SEO column, because
|
||||||
|
// including them would report 69 as the site's worst SEO score forever and
|
||||||
|
// train a reader to ignore the column — which is how a real regression there
|
||||||
|
// would go unnoticed.
|
||||||
|
const worst = (id) => {
|
||||||
|
const relevant = id === 'seo' ? rows.filter((r) => !r.noindex) : rows;
|
||||||
|
return relevant.length
|
||||||
|
? Math.min(...relevant.map((r) => r.scores[id]))
|
||||||
|
: 100;
|
||||||
|
};
|
||||||
|
console.log('-'.repeat(w + 44));
|
||||||
|
console.log(
|
||||||
|
`${pad(`worst of ${rows.length}`, w)} ` +
|
||||||
|
CATEGORIES.map((id) => pad(worst(id), 6)).join(' ') +
|
||||||
|
` ${pad(`${(Math.max(...rows.map((r) => r.lcp)) / 1000).toFixed(2)}s`, 8)} ` +
|
||||||
|
Math.max(...rows.map((r) => r.cls)).toFixed(3),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`\nbudgets: all four categories >= ${THRESHOLD} (mobile) · ` +
|
||||||
|
`LCP < ${LCP_BUDGET_MS / 1000}s · CLS < ${CLS_BUDGET} — docs/04-seo-spec.md`,
|
||||||
|
);
|
||||||
|
const noindexCount = rows.filter((r) => r.noindex).length;
|
||||||
|
if (noindexCount) {
|
||||||
|
console.log(
|
||||||
|
`n = deliberately noindex (${noindexCount} page(s)). Lighthouse's SEO\n` +
|
||||||
|
' category cannot exceed ~69 on such a page: `is-crawlable` fails on the\n' +
|
||||||
|
' `noindex` the page is supposed to carry. Those pages are asserted audit\n' +
|
||||||
|
' by audit instead — every SEO audit must pass except that one — and are\n' +
|
||||||
|
' excluded from the SEO worst-of above.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
'* a11y is measured with prefers-reduced-motion forced. The scroll-driven\n' +
|
||||||
|
" reveal otherwise puts axe's colour-contrast audit on mid-animation\n" +
|
||||||
|
' opacity rather than on the palette — 24 false nodes, measured. See the\n' +
|
||||||
|
' comment on chromeA11y in this script.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reported, not asserted. docs/04 states LCP and CLS as budgets; Lighthouse's
|
||||||
|
// simulated throttling on a loopback server is not the Slow 4G field
|
||||||
|
// measurement they describe, so a hard failure here would be a claim about
|
||||||
|
// the instrument. The category scores ARE the gate.
|
||||||
|
const lcpOver = rows.filter((r) => r.lcp >= LCP_BUDGET_MS);
|
||||||
|
const clsOver = rows.filter((r) => r.cls >= CLS_BUDGET);
|
||||||
|
if (lcpOver.length) {
|
||||||
|
console.log(
|
||||||
|
`note: LCP at or over budget on ${lcpOver.length} page(s): ` +
|
||||||
|
lcpOver.map((r) => r.page).join(', '),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (clsOver.length) {
|
||||||
|
console.log(
|
||||||
|
`note: CLS at or over budget on ${clsOver.length} page(s): ` +
|
||||||
|
clsOver.map((r) => r.page).join(', '),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breaches.length) {
|
||||||
|
console.error(`\nBUDGET BREACH — ${breaches.length}:`);
|
||||||
|
for (const b of breaches) console.error(` - ${b}`);
|
||||||
|
console.error('\nCLAUDE.md: treat a budget breach as a failing build.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`\nOK — ${rows.length} page(s), no category below ${THRESHOLD}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Proves the Open Graph cards, two ways. `npm run og:proof`, after a build.
|
||||||
|
*
|
||||||
|
* WHY THIS EXISTS AT ALL. `AGENTS.md` R15: *"Nobody on this project will ever
|
||||||
|
* see the defect. A link preview is rendered by LinkedIn, Slack and Teams for a
|
||||||
|
* reader who is not us."* Generating the cards does not fix that — it moves the
|
||||||
|
* invisible thing from "wrong image" to "wrong image, generated". So the two
|
||||||
|
* failures that would stay invisible are checked mechanically:
|
||||||
|
*
|
||||||
|
* 1. **Every page's `og:image` resolves to a file that exists in `dist/`.** A
|
||||||
|
* 404 preview image renders as a blank card, and nothing else in this repo
|
||||||
|
* would notice. Checked by reading the built HTML, not the source.
|
||||||
|
*
|
||||||
|
* 2. **Every card's headline and eyebrow are its page's own `<h1>` and first
|
||||||
|
* `.eyebrow`, character for character.** This is the compliance half. Text
|
||||||
|
* baked into a JPEG is text `npm run check:claims` cannot grep, and under D20
|
||||||
|
* that script is the only per-step claims control there is — so a card must
|
||||||
|
* never carry a claim its page does not already make in auditable HTML. The
|
||||||
|
* check enforces that structurally rather than trusting an author to
|
||||||
|
* remember it, and it fails in both directions: editing the page without the
|
||||||
|
* registry, or the registry without the page.
|
||||||
|
*
|
||||||
|
* It also catches the quiet one: a straight apostrophe in the registry
|
||||||
|
* against the typographic apostrophe the page renders. Found exactly that on
|
||||||
|
* the first run, on `/practice/insurance/`.
|
||||||
|
*
|
||||||
|
* It reads `src/data/og-cards.ts` DIRECTLY — Node strips the types — so there is
|
||||||
|
* no second list of cards to keep in step with the first.
|
||||||
|
*
|
||||||
|
* Optional: `npm run og:proof -- --sheet` writes a contact sheet of every card
|
||||||
|
* to `dist/og-proof.jpg` so the set can be looked at in one go. Not part of the
|
||||||
|
* check; a human still has to look.
|
||||||
|
*/
|
||||||
|
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
||||||
|
import { join, relative, sep } from 'node:path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import {
|
||||||
|
OG_CARDS,
|
||||||
|
PORTRAIT_PAGES,
|
||||||
|
articleCard,
|
||||||
|
ogCardPath,
|
||||||
|
} from '../src/data/og-cards.ts';
|
||||||
|
|
||||||
|
const ROOT = process.cwd();
|
||||||
|
const DIST = join(ROOT, 'dist');
|
||||||
|
const SITE = 'https://adr.smlcompany.ca';
|
||||||
|
|
||||||
|
const strip = (html) =>
|
||||||
|
html
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
async function pages(dir = DIST) {
|
||||||
|
const out = [];
|
||||||
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||||
|
const full = join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) out.push(...(await pages(full)));
|
||||||
|
else if (entry.name === 'index.html') {
|
||||||
|
const rel = relative(DIST, dir).split(sep).filter(Boolean).join('/');
|
||||||
|
out.push({ path: rel ? `/${rel}/` : '/', file: full });
|
||||||
|
}
|
||||||
|
/* ⚠️ `index.html` ALONE MISSED A WHOLE PAGE. `build.format: 'directory'`
|
||||||
|
puts every route at `<dir>/index.html` — except the ones Astro emits
|
||||||
|
outside the convention, and `404.astro` becomes `dist/404.html`. So this
|
||||||
|
script enumerated 22 pages of 23, and the symptom was backwards: it
|
||||||
|
reported the 404 page's card as ORPHANED ("generated, but no built page
|
||||||
|
references it") rather than reporting the page as unchecked. `path` here
|
||||||
|
is an `OG_CARDS` key, which is `Astro.url.pathname` — `/404/`, not
|
||||||
|
`/404.html`. `scripts/lighthouse.mjs` had the same blind spot and needs
|
||||||
|
the URL form instead; see the note there. */
|
||||||
|
else if (dir === DIST && entry.name.endsWith('.html')) {
|
||||||
|
out.push({ path: `/${entry.name.replace(/\.html$/, '')}/`, file: full });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.sort((a, b) => a.path.localeCompare(b.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
const problems = [];
|
||||||
|
const fail = (msg) => problems.push(msg);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stat(join(DIST, 'index.html'));
|
||||||
|
} catch {
|
||||||
|
console.error('dist/ is missing or empty. Run `npm run build` first.');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const built = await pages();
|
||||||
|
const seenCards = new Set();
|
||||||
|
let checkedHeadlines = 0;
|
||||||
|
let checkedArticles = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An article's expected card, from the SAME `articleCard()` the endpoint calls,
|
||||||
|
* given the title in that article's own frontmatter. Read from the `.mdx` rather
|
||||||
|
* than from the built page, so the comparison has two independent sides: what
|
||||||
|
* the article says its title is, and what the route rendered as the `<h1>`.
|
||||||
|
*/
|
||||||
|
async function expectedArticleCard(path) {
|
||||||
|
const slug = path.replace(/^\/insights\/|\/$/g, '');
|
||||||
|
for (const ext of ['mdx', 'md']) {
|
||||||
|
try {
|
||||||
|
const src = await readFile(
|
||||||
|
join(ROOT, 'src', 'content', 'insights', `${slug}.${ext}`),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const m = /^title:\s*(.*)$/m.exec(src);
|
||||||
|
if (!m) break;
|
||||||
|
let title = m[1].trim();
|
||||||
|
// YAML scalar: strip one layer of quoting and unescape a doubled single
|
||||||
|
// quote, which is how YAML writes a literal apostrophe inside '…'.
|
||||||
|
if (
|
||||||
|
(title.startsWith("'") && title.endsWith("'")) ||
|
||||||
|
(title.startsWith('"') && title.endsWith('"'))
|
||||||
|
) {
|
||||||
|
title = title.slice(1, -1);
|
||||||
|
}
|
||||||
|
title = title.replace(/''/g, "'");
|
||||||
|
return articleCard(title);
|
||||||
|
} catch {
|
||||||
|
/* try the next extension */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { path, file } of built) {
|
||||||
|
const html = await readFile(file, 'utf8');
|
||||||
|
|
||||||
|
// ---- 1. og:image exists -------------------------------------------------
|
||||||
|
const og = /<meta property="og:image" content="([^"]+)"/.exec(html);
|
||||||
|
if (!og) {
|
||||||
|
fail(`${path}: no og:image meta tag at all`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url = og[1];
|
||||||
|
if (!url.startsWith(SITE + '/')) {
|
||||||
|
fail(`${path}: og:image is not an absolute URL on ${SITE} — ${url}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const assetPath = url.slice(SITE.length);
|
||||||
|
try {
|
||||||
|
await stat(join(DIST, assetPath));
|
||||||
|
} catch {
|
||||||
|
fail(`${path}: og:image points at ${assetPath}, which is not in dist/`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenCards.add(assetPath);
|
||||||
|
|
||||||
|
// ---- 2. card copy is the page's own copy --------------------------------
|
||||||
|
const isPortrait = PORTRAIT_PAGES.includes(path);
|
||||||
|
const card = OG_CARDS[path];
|
||||||
|
|
||||||
|
if (isPortrait) {
|
||||||
|
if (card) fail(`${path}: in PORTRAIT_PAGES and in OG_CARDS — pick one`);
|
||||||
|
if (assetPath.startsWith('/og/')) {
|
||||||
|
fail(`${path}: is a portrait page but its og:image is a generated card`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = ogCardPath(path);
|
||||||
|
if (assetPath !== expected) {
|
||||||
|
fail(`${path}: og:image is ${assetPath}, expected ${expected}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ AN ARTICLE IS CHECKED THE SAME WAY AS A REGISTRY PAGE, AND UNTIL
|
||||||
|
* 2026-08-31 IT WAS NOT CHECKED AT ALL.
|
||||||
|
*
|
||||||
|
* The first version of this script matched an article's card FILENAME and then
|
||||||
|
* `continue`d — skipping the headline and eyebrow comparisons entirely. So the
|
||||||
|
* one surface `check:claims` cannot reach was also the one surface this script
|
||||||
|
* did not compare, which is the opposite of what its own header claims and what
|
||||||
|
* `docs/04` says it enforces.
|
||||||
|
*
|
||||||
|
* `adversarial-reviewer` proved it rather than arguing it: with
|
||||||
|
* `headline: 'DELIBERATELY WRONG CARD TEXT — probe'` set in the endpoint and one
|
||||||
|
* article published, the card rendered that sentence in 68px Instrument Serif
|
||||||
|
* and this script printed `OK — every og:image resolves, and no card asserts
|
||||||
|
* anything its page does not`, exit 0. **`checkedHeadlines` stayed pinned at the
|
||||||
|
* registry size** no matter how many articles published — a coverage number
|
||||||
|
* that reads like completeness and falls further behind as the site grows,
|
||||||
|
* which is exactly the uniform-pass shape `CLAUDE.md` warns is the dangerous
|
||||||
|
* half.
|
||||||
|
*
|
||||||
|
* An article has no registry entry by design — its card comes from the
|
||||||
|
* collection — so the expectation comes from the page instead: the endpoint
|
||||||
|
* sets an article card's headline to `entry.data.title`, which is also the
|
||||||
|
* page's `<h1>`. Comparing the card's source of truth against the rendered
|
||||||
|
* `<h1>` is therefore the same check, and the eyebrow is the literal the
|
||||||
|
* endpoint sets.
|
||||||
|
*/
|
||||||
|
const isArticle = /^\/insights\/[^/]+\/$/.test(path);
|
||||||
|
if (!card && !isArticle) {
|
||||||
|
fail(`${path}: built, not a portrait page, and has no OG_CARDS entry`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let expected_card = card;
|
||||||
|
if (!expected_card) {
|
||||||
|
expected_card = await expectedArticleCard(path);
|
||||||
|
if (!expected_card) {
|
||||||
|
fail(
|
||||||
|
`${path}: could not read a \`title:\` from this article's own .mdx, so ` +
|
||||||
|
'its card cannot be compared against anything. That is a failure, not ' +
|
||||||
|
'a skip — an unchecked card is the one surface check:claims cannot see.',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const expectedEyebrow = expected_card.eyebrow;
|
||||||
|
|
||||||
|
const h1 = /<h1[^>]*>([\s\S]*?)<\/h1>/.exec(html);
|
||||||
|
if (!h1) {
|
||||||
|
fail(`${path}: no <h1> to compare the card headline against`);
|
||||||
|
} else {
|
||||||
|
const text = strip(h1[1]);
|
||||||
|
const expectedHeadline = expected_card.headline;
|
||||||
|
if (text !== expectedHeadline) {
|
||||||
|
fail(
|
||||||
|
`${path}: card headline is not the page's <h1>.\n` +
|
||||||
|
` <h1>: ${JSON.stringify(text)}\n` +
|
||||||
|
` card: ${JSON.stringify(expectedHeadline)}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
checkedHeadlines += 1;
|
||||||
|
if (isArticle) checkedArticles += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eyebrow = /<p class="eyebrow"[^>]*>([\s\S]*?)<\/p>/.exec(html);
|
||||||
|
if (!eyebrow) {
|
||||||
|
fail(`${path}: no .eyebrow to compare the card eyebrow against`);
|
||||||
|
} else {
|
||||||
|
const text = strip(eyebrow[1]);
|
||||||
|
if (text !== expectedEyebrow) {
|
||||||
|
fail(
|
||||||
|
`${path}: card eyebrow is not the page's first .eyebrow.\n` +
|
||||||
|
` page: ${JSON.stringify(text)}\n` +
|
||||||
|
` card: ${JSON.stringify(expectedEyebrow)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ AND THE COVERAGE IS ASSERTED, NOT REPORTED. Printing "20 headlines matched"
|
||||||
|
* beside a growing site is how the gap above stayed invisible: the number went up
|
||||||
|
* and never went up ENOUGH, and nothing said so. Every built page except the
|
||||||
|
* portrait pages carries a generated card, so the count must equal that or a page
|
||||||
|
* was silently skipped.
|
||||||
|
*/
|
||||||
|
const shouldCheck = built.filter(
|
||||||
|
({ path }) => !PORTRAIT_PAGES.includes(path),
|
||||||
|
).length;
|
||||||
|
if (checkedHeadlines !== shouldCheck) {
|
||||||
|
fail(
|
||||||
|
`only ${checkedHeadlines} of ${shouldCheck} non-portrait pages had their ` +
|
||||||
|
'card headline compared against their <h1>. A page was skipped, which is ' +
|
||||||
|
'the failure this assertion exists to make loud.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 3. no card generated for a page that does not exist -------------------
|
||||||
|
// A stray card is not a shipped defect, but it is the signature of a page that
|
||||||
|
// was renamed or removed and a registry entry that was not — which the next
|
||||||
|
// person reads as "the card exists, so the page must".
|
||||||
|
let strays = [];
|
||||||
|
try {
|
||||||
|
const files = await readdir(join(DIST, 'og'));
|
||||||
|
strays = files
|
||||||
|
.filter((f) => f.endsWith('.jpg'))
|
||||||
|
.map((f) => `/og/${f}`)
|
||||||
|
.filter((p) => !seenCards.has(p));
|
||||||
|
} catch {
|
||||||
|
fail('dist/og/ does not exist — no cards were generated');
|
||||||
|
}
|
||||||
|
for (const s of strays) {
|
||||||
|
fail(`${s}: generated, but no built page references it`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- optional contact sheet ----------------------------------------------
|
||||||
|
if (process.argv.includes('--sheet')) {
|
||||||
|
const files = (await readdir(join(DIST, 'og')))
|
||||||
|
.filter((f) => f.endsWith('.jpg'))
|
||||||
|
.sort();
|
||||||
|
const COLS = 3;
|
||||||
|
const W = 400;
|
||||||
|
const H = 210;
|
||||||
|
const rows = Math.ceil(files.length / COLS);
|
||||||
|
const tiles = await Promise.all(
|
||||||
|
files.map(async (f, i) => ({
|
||||||
|
input: await sharp(join(DIST, 'og', f))
|
||||||
|
.resize(W, H)
|
||||||
|
.toBuffer(),
|
||||||
|
left: (i % COLS) * W,
|
||||||
|
top: Math.floor(i / COLS) * H,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const sheet = await sharp({
|
||||||
|
create: {
|
||||||
|
width: COLS * W,
|
||||||
|
height: rows * H,
|
||||||
|
channels: 3,
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite(tiles)
|
||||||
|
.jpeg({ quality: 82 })
|
||||||
|
.toBuffer();
|
||||||
|
await writeFile(join(DIST, 'og-proof.jpg'), sheet);
|
||||||
|
console.log(
|
||||||
|
`contact sheet: dist/og-proof.jpg — ${files.length} cards, ${COLS}x${rows}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`og:proof — ${built.length} built pages, ${seenCards.size} distinct og:image ` +
|
||||||
|
`targets, ${checkedHeadlines} card headlines matched their page <h1> ` +
|
||||||
|
`(${checkedArticles} of them articles).`,
|
||||||
|
);
|
||||||
|
if (problems.length) {
|
||||||
|
console.error(`\nOG CARD PROBLEMS — ${problems.length}:`);
|
||||||
|
for (const p of problems) console.error(` - ${p}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
'OK — every og:image resolves, and no card asserts anything its page does not.',
|
||||||
|
);
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* docs/02: "Title, description, date, topic pills, reading time."
|
||||||
|
*
|
||||||
|
* ONE LINK, AND THE WHOLE CARD IS ITS HIT AREA — the `PracticeCard` pattern,
|
||||||
|
* for the same measured reason: the link wraps only the headline, so its
|
||||||
|
* accessible name is the headline rather than the card's four elements, and a
|
||||||
|
* `::after` stretched over the positioned card carries the click. Three of these
|
||||||
|
* on `/` would otherwise be three links each announcing a date, two pills, a
|
||||||
|
* reading time and a 150-character description.
|
||||||
|
*
|
||||||
|
* THE PARENT MUST NOT TRY TO STYLE THIS ROOT. Astro does not pass a parent's
|
||||||
|
* scope attribute to a child's root element, so a grid's `.card { block-size:
|
||||||
|
* 100% }` compiles against the parent's cid and never matches — `CLAUDE.md`
|
||||||
|
* records this costing twice, and names `ArticleCard` as one of the next places
|
||||||
|
* it would happen. The card sizes itself below; a parent supplies only
|
||||||
|
* `display: grid` and `gap` on its own element.
|
||||||
|
*
|
||||||
|
* `readingTime` IS RENDERED WITH ITS UNIT AND IS NOT A CLAIM ABOUT THE PRACTICE.
|
||||||
|
* §4 Forbidden bars counts of matters, hours mediated and years in practice —
|
||||||
|
* a number describing how long an article takes to read is not in that family,
|
||||||
|
* and `check:claims`'s `counts-and-tenure` pattern is scoped to the practice.
|
||||||
|
* Do not reach for a matter count, a settlement rate, or a case figure here.
|
||||||
|
*/
|
||||||
|
import Pill from './Pill.astro';
|
||||||
|
import { TOPIC_LABELS, formatArticleDate, isoDate } from '../data/insights';
|
||||||
|
import type { InsightTopic } from '../data/insights';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
href: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
date: Date;
|
||||||
|
topics: readonly InsightTopic[];
|
||||||
|
/** Minutes. */
|
||||||
|
readingTime: number;
|
||||||
|
/** Explicit: docs/02 forbids skipped heading levels. */
|
||||||
|
level: 2 | 3;
|
||||||
|
}
|
||||||
|
const { href, title, description, date, topics, readingTime, level } =
|
||||||
|
Astro.props;
|
||||||
|
const H = `h${level}` as 'h2' | 'h3';
|
||||||
|
---
|
||||||
|
|
||||||
|
<article class="acard">
|
||||||
|
<div class="acard-meta">
|
||||||
|
<time datetime={isoDate(date)}>{formatArticleDate(date)}</time>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<span>{readingTime} min read</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<H class="acard-title">
|
||||||
|
<a class="acard-link" href={href}>{title}</a>
|
||||||
|
</H>
|
||||||
|
|
||||||
|
<p class="acard-desc">{description}</p>
|
||||||
|
|
||||||
|
{
|
||||||
|
topics.length > 0 && (
|
||||||
|
<ul class="acard-topics" role="list">
|
||||||
|
{topics.map((topic) => (
|
||||||
|
<li>
|
||||||
|
<Pill>{TOPIC_LABELS[topic]}</Pill>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.acard {
|
||||||
|
position: relative;
|
||||||
|
/* Sizes itself to its cell — see the note on why the grid cannot. */
|
||||||
|
block-size: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-6);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-block-start: 2px solid var(--rule);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition:
|
||||||
|
border-color var(--dur-hover) var(--ease),
|
||||||
|
box-shadow var(--dur-hover) var(--ease);
|
||||||
|
}
|
||||||
|
.acard:hover {
|
||||||
|
border-color: var(--rule);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acard-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acard-title {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
}
|
||||||
|
.acard-link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
/* The card-wide hit area. `inset: 0` on the positioned card, so the click
|
||||||
|
target is the card and the accessible name stays the headline. */
|
||||||
|
.acard-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
/* The ring has to be on the CARD, not on the inline text, or focus draws a
|
||||||
|
box around two words in the middle of a clickable panel. */
|
||||||
|
.acard-link:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.acard-link:focus-visible::after {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: var(--focus-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acard-desc {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acard-topics {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
/* `margin-block-start: auto` pins the pills to the bottom of the card so a
|
||||||
|
row of cards with different description lengths still aligns on them. */
|
||||||
|
margin-block-start: auto;
|
||||||
|
padding-block-start: var(--space-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -66,18 +66,37 @@ const classes = ['btn', `btn-${variant}`, className];
|
|||||||
color: var(--text-inverse);
|
color: var(--text-inverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ⚠️ THESE HOOKS ARE WHY `.btn-ghost` IS LEGIBLE ON A DARK BAND. Its own
|
||||||
|
colours are ink text on an ink-at-10%-alpha border, which on
|
||||||
|
`.section-inverse` and `.section-accent` are the background twice over —
|
||||||
|
`/fees/` shipped this at a measured 1.00:1.
|
||||||
|
|
||||||
|
THEY ARE CUSTOM PROPERTIES AND MUST STAY THAT WAY. A parent cannot style a
|
||||||
|
child component's root (CLAUDE.md), and a `global.css` descendant rule would
|
||||||
|
tie at specificity (0,2,0) with `.btn-ghost[data-astro-cid]` here, so the
|
||||||
|
winner would depend on injection order. Custom properties inherit, which is
|
||||||
|
the one mechanism that crosses the boundary. `global.css` sets them; the
|
||||||
|
fallbacks keep the on-cream appearance identical.
|
||||||
|
|
||||||
|
Do not rely on the accessibility category to catch a regression here: axe
|
||||||
|
SKIPS a foreground identical to its background as "unable to determine", and
|
||||||
|
scored that page 100. AGENTS.md entry (ah) has the measurements. */
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: var(--border);
|
border-color: var(--btn-ghost-border, var(--border));
|
||||||
color: var(--text);
|
color: var(--btn-ghost-fg, var(--text));
|
||||||
}
|
}
|
||||||
.btn-ghost:hover {
|
.btn-ghost:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--btn-ghost-border-hover, var(--accent));
|
||||||
color: var(--accent);
|
color: var(--btn-ghost-fg-hover, var(--accent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* `background: var(--bg-inverse)` is ink, so on an inverse ground this pill has
|
||||||
|
no boundary and reads as bare text. It needs an EDGE, not a new ground — the
|
||||||
|
gold-l label already measures 11.09:1 on ink. */
|
||||||
.btn-gold {
|
.btn-gold {
|
||||||
background: var(--bg-inverse);
|
background: var(--bg-inverse);
|
||||||
|
border-color: var(--btn-gold-border, transparent);
|
||||||
color: var(--text-inverse-2);
|
color: var(--text-inverse-2);
|
||||||
}
|
}
|
||||||
.btn-gold:hover {
|
.btn-gold:hover {
|
||||||
|
|||||||
@@ -13,11 +13,10 @@
|
|||||||
* slots come from src/data/site.ts, which mirrors §4; nothing is typed here.
|
* slots come from src/data/site.ts, which mirrors §4; nothing is typed here.
|
||||||
*
|
*
|
||||||
* <dl> RATHER THAN A DIV GRID. Each pair is a term and its description, which
|
* <dl> RATHER THAN A DIV GRID. Each pair is a term and its description, which
|
||||||
* is what a description list is. It also fixes the reading order: a screen
|
* is what a description list is, and it fixes the reading order: a screen
|
||||||
* reader gets "Q.Arb — Commenced August 2026" as one associated pair, which is
|
* reader gets "Q.Arb — ADRIC / ADRIO" as one associated pair rather than four
|
||||||
* §4's paired-disclosure condition surviving into assistive technology rather
|
* values then four labels. Wrapping each dt/dd pair in a <div> inside <dl> is
|
||||||
* than being a visual arrangement only. Wrapping each dt/dd pair in a <div>
|
* valid HTML and is what makes the grid tractable.
|
||||||
* inside <dl> is valid HTML and is what makes the grid tractable.
|
|
||||||
*/
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
slots: ReadonlyArray<{ value: string; label: string }>;
|
slots: ReadonlyArray<{ value: string; label: string }>;
|
||||||
@@ -45,18 +44,25 @@ const { slots } = Astro.props;
|
|||||||
This read `repeat(auto-fit, minmax(11rem, 1fr))` under a comment saying
|
This read `repeat(auto-fit, minmax(11rem, 1fr))` under a comment saying
|
||||||
"two up on a phone, four up where there is room". `adversarial-reviewer`
|
"two up on a phone, four up where there is room". `adversarial-reviewer`
|
||||||
measured it: at 390px the resolved template was a SINGLE 342px track and
|
measured it: at 390px the resolved template was a SINGLE 342px track and
|
||||||
all four items stacked, running the band ~430px tall — with
|
all four items stacked, running the band ~430px tall. The longest label in
|
||||||
`Q.Arb / Commenced August 2026`, which §4's paired-disclosure condition
|
the row is and remains `Legal training and engineering practice` (Q37, 39
|
||||||
puts on this page, at the bottom of it. The arithmetic is not subtle: two
|
chars — see below), which is what drives the track; the fourth slot's
|
||||||
|
label went from `Commenced August 2026` (21) to `ADRIC / ADRIO
|
||||||
|
designation` (25), so it got slightly LONGER and is still well inside
|
||||||
|
that 39. *(A previous version of this comment said "shorter". It was not
|
||||||
|
measured; `claims-auditor` counted the characters.)* The arithmetic is not
|
||||||
|
subtle: two
|
||||||
tracks at an 11rem (176px) floor plus a 24px gap need 376px and the
|
tracks at an 11rem (176px) floor plus a 24px gap need 376px and the
|
||||||
container is 342px, so `auto-fit` correctly dropped to one. A
|
container is 342px, so `auto-fit` correctly dropped to one. A
|
||||||
measured-sounding comment that was false is this project's own named
|
measured-sounding comment that was false is this project's own named
|
||||||
failure mode.
|
failure mode.
|
||||||
|
|
||||||
`minmax(0, 1fr)` cannot overflow at any width or any root font size,
|
`minmax(0, 1fr)` means the TRACK cannot overflow at any width or any root
|
||||||
which also retires the `min(11rem, 100%)` guard this line briefly
|
font size, which retires the `min(11rem, 100%)` guard this line briefly
|
||||||
carried — that guard was fixing the overflow symptom of a floor that
|
carried — that guard was fixing the overflow symptom of a floor that
|
||||||
should not have been there. */
|
should not have been there. ⚠️ **THE TRACK IS NOT THE CONTENT:** a label's
|
||||||
|
own words can overflow the track, and at a 200% default font size they
|
||||||
|
did. That is why `.credential-label` below carries `overflow-wrap`. */
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: var(--space-6) var(--space-5);
|
gap: var(--space-6) var(--space-5);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -98,7 +104,10 @@ const { slots } = Astro.props;
|
|||||||
deliberately long (AGENTS.md Q37) — it wraps to two lines at every width
|
deliberately long (AGENTS.md Q37) — it wraps to two lines at every width
|
||||||
and must not be prevented from doing so. Do not add `white-space: nowrap`
|
and must not be prevented from doing so. Do not add `white-space: nowrap`
|
||||||
here, and do not shorten the label to make the row tidier: the asymmetry
|
here, and do not shorten the label to make the row tidier: the asymmetry
|
||||||
is the honest part. */
|
is the honest part. ⚠️ `overflow-wrap: anywhere` is LOAD-BEARING here, not
|
||||||
|
cosmetic — `anywhere`, not `break-word`: see `docs/02` §Reflow, instrument
|
||||||
|
finding 1. */
|
||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* A `<dl>` of term/description pairs on an auto-fitting grid.
|
||||||
|
*
|
||||||
|
* EXTRACTED AT STEP 4 ON `adversarial-reviewer`'S FINDING, and the finding was
|
||||||
|
* that `/mediation/`'s `.formats` and `/arbitration/`'s `.cols` were the same
|
||||||
|
* component under two names — identical markup, near-identical CSS, and
|
||||||
|
* `.cols` was already serving two different content types on one page.
|
||||||
|
* `/practice/*` at step 5 wants it a fourth time. Same argument that extracted
|
||||||
|
* `ContactBand`: two call sites, one already divergent, pages to come.
|
||||||
|
*
|
||||||
|
* `<dl>` RATHER THAN A DIV GRID, for `CredentialRow`'s reason: each pair is a
|
||||||
|
* term and its description, so a screen reader gets them as an associated pair
|
||||||
|
* rather than as a visual arrangement. Wrapping each `<dt>`/`<dd>` in a `<div>`
|
||||||
|
* inside the `<dl>` is valid HTML and is what makes the grid tractable.
|
||||||
|
*
|
||||||
|
* A `<dt>` IS NOT A HEADING and must not become one. These sit under the
|
||||||
|
* section's `<h2>`; promoting them to `<h3>` would be a heading level that adds
|
||||||
|
* nothing to the outline, and `docs/02` forbids skipped levels either way.
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
items: ReadonlyArray<{ name: string; body: string }>;
|
||||||
|
/** The grid's per-column floor, passed to `.grid-autofit` as `--grid-min`.
|
||||||
|
* The `min(N, 100%)` guard lives there, in one place. */
|
||||||
|
minColumn?: string;
|
||||||
|
}
|
||||||
|
const { items, minColumn = '17rem' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<dl class="grid-autofit defs" style={`--grid-min: ${minColumn}`}>
|
||||||
|
{
|
||||||
|
items.map((item) => (
|
||||||
|
<div class="def">
|
||||||
|
<dt class="def-name">{item.name}</dt>
|
||||||
|
<dd class="def-body">{item.body}</dd>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Columns and the `min()` guard come from `.grid-autofit` (global.css); this
|
||||||
|
sets only the gap. It re-implemented them for one pass — a second copy of
|
||||||
|
the guard, inside the extraction made to remove copies of the guard.
|
||||||
|
`--grid-min` is passed inline by the caller because a parent cannot style
|
||||||
|
this component's root, and a custom property is the one mechanism that
|
||||||
|
crosses that boundary. */
|
||||||
|
.defs {
|
||||||
|
gap: var(--space-7);
|
||||||
|
}
|
||||||
|
/* ⚠️ `--def-name-fg`, NOT `--text-meta` DIRECTLY. `--text-meta` is `--muted`,
|
||||||
|
and `tokens.css` states the constraint on that token in terms: "metadata —
|
||||||
|
ON CREAM ONLY (3.07:1 on ink)". `/practice/` is the first page to put this
|
||||||
|
component on an inverse ground, and it shipped these labels at **3.07:1 at
|
||||||
|
12px** against a 4.5:1 AA floor — measured three ways by
|
||||||
|
`adversarial-reviewer` (token arithmetic, `getComputedStyle` against the
|
||||||
|
served build, and a screenshot), all agreeing.
|
||||||
|
|
||||||
|
A custom property is the fix rather than a `:global()` rule because it is
|
||||||
|
the one mechanism that crosses Astro's component-scope boundary — the same
|
||||||
|
route `Pill` already uses, and `global.css` sets this alongside `--pill-fg`
|
||||||
|
on `.section-inverse, .section-accent`. The fallback keeps cream correct. */
|
||||||
|
.def-name {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--def-name-fg, var(--text-meta));
|
||||||
|
}
|
||||||
|
.def-body {
|
||||||
|
margin-block-start: var(--space-2);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+10
-17
@@ -81,23 +81,16 @@ void _props;
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--pill-fg, var(--text-meta));
|
color: var(--pill-fg, var(--text-meta));
|
||||||
/* `nowrap` UNTIL 2026-08-28, AND IT WAS FINE UNTIL A PILL HAD FOUR WORDS.
|
/* NEVER `nowrap`. `Pill` takes arbitrary children, and a four-word pill at a
|
||||||
`/`'s six pills are one or two words (longest "Cross-cultural").
|
200% default font size (root 32px) overflowed the document by 111px at
|
||||||
`/about/` ships `Commenced August 2026`, and at a 200% DEFAULT FONT SIZE
|
320px — WCAG 1.4.10 Reflow. Measured 2026-08-28; no pill on the site is
|
||||||
(root 32px — a real browser setting, not page zoom) that pill measured
|
that long today, which is exactly why this is easy to delete and must not
|
||||||
382.6px wide with its right edge at 430.6 in a 390px viewport:
|
be. `normal` costs nothing at default size.
|
||||||
**41px of document overflow at 390, 111px at 320.** Injecting
|
⚠️ `normal` ALONE IS NOT ENOUGH — a one-word pill cannot wrap at a space
|
||||||
`white-space: normal` took 390 to **0** and 320 to **63**, 63 being the
|
that is not there. `anywhere`, not `break-word`, and this is the BACKSTOP
|
||||||
header residual docs/02 already accepts. WCAG 1.4.10 Reflow.
|
rather than the cause: `docs/02` §Reflow, instrument finding 1 and the
|
||||||
|
`Pill` row. */
|
||||||
`normal` costs nothing at default size — a pill only wraps when it cannot
|
|
||||||
fit, which is exactly when wrapping is the right answer.
|
|
||||||
|
|
||||||
WHAT IT LOOKS LIKE AT THE EXTREME, recorded so it is not later read as a
|
|
||||||
new bug: at 320px with root at 32px, `Commenced August 2026` renders
|
|
||||||
**224 x 119px** inside `border-radius: 999px` — a three-line stadium. It
|
|
||||||
is ungainly and it is legible, in-viewport, and the alternative was
|
|
||||||
111px of document overflow. */
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -48,7 +48,13 @@ const H = `h${level}` as 'h2' | 'h3';
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
padding: var(--space-6);
|
/* CLAMPED, not a flat --space-6, for the reason `.feature` on `/` carries:
|
||||||
|
the space scale is rem-based, so `2rem` is 64 px a side at a 200% default
|
||||||
|
font size — 128 px of padding inside a ~224 px content box, which left
|
||||||
|
~96 px for the whole card column and was the real cause of a `Pill`
|
||||||
|
overflowing. The 10vw term holds it at 32 px on every viewport from 320 px
|
||||||
|
up and lets it collapse only when the rem is doubled. */
|
||||||
|
padding: clamp(var(--space-4), 10vw, var(--space-6));
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-block-start: 2px solid var(--rule);
|
border-block-start: 2px solid var(--rule);
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* docs/02: "Long-form wrapper. Owns all typographic defaults for MDX."
|
||||||
|
*
|
||||||
|
* WHY IT HAS TO OWN THEM. `global.css`'s reset sets `* { margin: 0 }` and the
|
||||||
|
* base type rules deliberately do not style `<h2>`, `<ul>`, `<blockquote>` or
|
||||||
|
* `<code>` in body flow — every page so far has written its own section markup,
|
||||||
|
* so nothing on the site has ever needed defaults for a document. An MDX article
|
||||||
|
* is the first content this repo does not hand-mark up, and without a wrapper it
|
||||||
|
* would render as one undifferentiated block. `global.css` already records that
|
||||||
|
* exact failure for `.prose` itself: two `<p>` children with a 0.0 px gap,
|
||||||
|
* shipped, because nothing supplied paragraph spacing.
|
||||||
|
*
|
||||||
|
* `:where()` ON EVERY SELECTOR, so specificity stays at zero and a page or a
|
||||||
|
* component can override any of it without `!important` — the same device
|
||||||
|
* `global.css` uses for `:where(.prose) > p + p`, and for the same reason.
|
||||||
|
*
|
||||||
|
* SCOPED STYLES NEED `:global()` HERE, and this is the one component where that
|
||||||
|
* is correct rather than a smell: the elements being styled come from MDX at
|
||||||
|
* build time and carry no `data-astro-cid` of this component's, so a scoped
|
||||||
|
* descendant selector would match nothing. Astro's own `is:global` guidance.
|
||||||
|
* The wrapper element itself is ours, so everything stays inside `.prose-body`.
|
||||||
|
*
|
||||||
|
* NO `max-inline-size` OF ITS OWN — it composes with `global.css`'s `.prose`,
|
||||||
|
* which caps the reading measure at `--width-prose`. A second cap here would be
|
||||||
|
* a second number to keep true.
|
||||||
|
*/
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="prose prose-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* --- Rhythm ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.prose-body :global(:where(p, ul, ol, blockquote, figure, hr, table)) {
|
||||||
|
margin-block-start: var(--space-5);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(p, li)) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Headings -------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* An article's own `<h1>` is the page's, rendered by the route. MDX bodies
|
||||||
|
start at `##`, so these are h2/h3/h4. A skipped level is a docs/02 breach
|
||||||
|
and is caught by review, not by CSS. */
|
||||||
|
.prose-body :global(:where(h2)) {
|
||||||
|
margin-block-start: var(--space-8);
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: var(--text-3xl);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(h3)) {
|
||||||
|
margin-block-start: var(--space-7);
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(h4)) {
|
||||||
|
margin-block-start: var(--space-6);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--weight-semi);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
/* Nothing may collapse against the top of the article. */
|
||||||
|
.prose-body :global(:where(:first-child)) {
|
||||||
|
margin-block-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Lists ------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* `global.css` strips list styling from `ul[role='list']` only, so an MDX
|
||||||
|
list keeps the UA marker and needs indenting rather than resetting. */
|
||||||
|
.prose-body :global(:where(ul, ol)) {
|
||||||
|
padding-inline-start: var(--space-6);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(li + li)) {
|
||||||
|
margin-block-start: var(--space-3);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(li)) {
|
||||||
|
padding-inline-start: var(--space-1);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(li::marker)) {
|
||||||
|
color: var(--text-meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Emphasis, links, quotes ----------------------------------------- */
|
||||||
|
|
||||||
|
.prose-body :global(:where(strong)) {
|
||||||
|
font-weight: var(--weight-semi);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(em)) {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
/* Links keep `global.css`'s colour and underline; only the offset is set, so
|
||||||
|
a descender does not sit on the rule at body size. */
|
||||||
|
.prose-body :global(:where(a)) {
|
||||||
|
text-underline-offset: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose-body :global(:where(blockquote)) {
|
||||||
|
padding-inline-start: var(--space-5);
|
||||||
|
border-inline-start: 2px solid var(--rule);
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(blockquote p)) {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose-body :global(:where(hr)) {
|
||||||
|
margin-block: var(--space-8);
|
||||||
|
border: none;
|
||||||
|
border-block-start: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Code ------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Inline code only. A statute reference or a header name, not a code block:
|
||||||
|
nothing in docs/03's content territories calls for one, and `<pre>` would
|
||||||
|
need horizontal overflow handling this component has no call site for. Add
|
||||||
|
it with the first article that needs it, and give it `overflow-x: auto`. */
|
||||||
|
.prose-body :global(:where(code)) {
|
||||||
|
padding: 0.1em 0.35em;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9em;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Figures and tables ---------------------------------------------- */
|
||||||
|
|
||||||
|
.prose-body :global(:where(img)) {
|
||||||
|
max-inline-size: 100%;
|
||||||
|
block-size: auto;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.prose-body :global(:where(figcaption)) {
|
||||||
|
margin-block-start: var(--space-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-meta);
|
||||||
|
}
|
||||||
|
/* ⚠️ NO TABLE RULES, DELIBERATELY. Do not re-add `display: block;
|
||||||
|
overflow-x: auto` to the `<table>` itself: `display: block` removes the
|
||||||
|
table role in Chromium and WebKit, and an `overflow-x` box with no
|
||||||
|
`tabindex="0"` cannot be scrolled by keyboard (WCAG 2.1.1). A table needs a
|
||||||
|
real wrapper with `tabindex="0"`, `role="region"` and a name — in MDX that
|
||||||
|
means a rehype plugin or a `<Table>` component. No article uses one yet, so
|
||||||
|
it arrives with the first that does, exactly as `<pre>` does above. */
|
||||||
|
</style>
|
||||||
+83
-14
@@ -12,6 +12,7 @@
|
|||||||
import { getImage } from 'astro:assets';
|
import { getImage } from 'astro:assets';
|
||||||
import ogDefault from '../assets/og-portrait.jpg';
|
import ogDefault from '../assets/og-portrait.jpg';
|
||||||
import { SITE, PORTRAIT } from '../data/site';
|
import { SITE, PORTRAIT } from '../data/site';
|
||||||
|
import { OG_CARDS, PORTRAIT_PAGES, ogCardPath } from '../data/og-cards';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
/** The full rendered <title>. Pattern: "<Page> · Pouya Lajevardi". 50–60. */
|
/** The full rendered <title>. Pattern: "<Page> · Pouya Lajevardi". 50–60. */
|
||||||
@@ -21,8 +22,15 @@ export interface Props {
|
|||||||
/** Overrides the canonical path. Defaults to this page's own URL. */
|
/** Overrides the canonical path. Defaults to this page's own URL. */
|
||||||
canonical?: string;
|
canonical?: string;
|
||||||
ogType?: 'website' | 'article' | 'profile';
|
ogType?: 'website' | 'article' | 'profile';
|
||||||
/** 1200×630 source. Defaults to the portrait crop in src/assets.
|
/**
|
||||||
* `ImageMetadata` is an Astro ambient global — there is nothing to import. */
|
* AN EXPLICIT PER-PAGE OVERRIDE, AND ALMOST NOTHING SHOULD PASS IT. Which
|
||||||
|
* pages take the portrait is decided by `PORTRAIT_PAGES` and everything else
|
||||||
|
* takes its generated card — both resolved below from the pathname, so the
|
||||||
|
* decision lives in `src/data/og-cards.ts` rather than in nineteen call sites.
|
||||||
|
* This exists for an article that sets its own `image` in frontmatter. Passing
|
||||||
|
* it to get the portrait onto a third page would reinstate the interim R15
|
||||||
|
* exists to end. `ImageMetadata` is an Astro ambient global — nothing to import.
|
||||||
|
*/
|
||||||
image?: ImageMetadata;
|
image?: ImageMetadata;
|
||||||
imageAlt?: string;
|
imageAlt?: string;
|
||||||
/** /legal/* and any temporary page. Emits noindex,follow per docs/04. */
|
/** /legal/* and any temporary page. Emits noindex,follow per docs/04. */
|
||||||
@@ -78,16 +86,77 @@ if (!Astro.site) {
|
|||||||
}
|
}
|
||||||
const canonicalUrl = new URL(canonical ?? Astro.url.pathname, Astro.site);
|
const canonicalUrl = new URL(canonical ?? Astro.url.pathname, Astro.site);
|
||||||
|
|
||||||
// JPEG on purpose. Page images are AVIF/WebP with a fallback (CLAUDE.md), but
|
/**
|
||||||
// link-preview crawlers are not browsers — LinkedIn and Slack do not negotiate
|
* THE OG IMAGE, AND THIS IS WHERE R15 IS DISCHARGED — build step 7b.
|
||||||
// content types, and several still do not decode WebP at all.
|
*
|
||||||
const ogImage = await getImage({
|
* Two kinds of card, per Q40 and docs/04, both resolved from the pathname: the
|
||||||
src: image ?? ogDefault,
|
* pages in `PORTRAIT_PAGES` get the portrait crop, and every other page gets the
|
||||||
format: 'jpeg',
|
* card generated for it by `src/pages/og/[...slug].jpg.ts`.
|
||||||
width: 1200,
|
*
|
||||||
height: 630,
|
* ⚠️ A MISSING REGISTRY ENTRY THROWS RATHER THAN FALLING BACK TO THE PORTRAIT.
|
||||||
});
|
* That is the whole mechanism. R15's failure mode is not that the wrong image
|
||||||
const ogImageUrl = new URL(ogImage.src, Astro.site);
|
* ships — it is that the wrong image ships *invisibly*, because no one on this
|
||||||
|
* project ever sees a link preview. A silent fallback reproduces exactly that,
|
||||||
|
* and reads as intentional. Both sides derive the path from `ogCardPath()`, so a
|
||||||
|
* page with an entry cannot point at a card the endpoint did not generate.
|
||||||
|
*
|
||||||
|
* Articles are exempt from the registry check: their cards come from the same
|
||||||
|
* `getCollection('insights', not draft)` the article route pages come from, so
|
||||||
|
* a built article always has one and a draft has neither.
|
||||||
|
*/
|
||||||
|
const path = Astro.url.pathname;
|
||||||
|
const isArticle = /^\/insights\/[^/]+\/$/.test(path);
|
||||||
|
const usesPortrait = (PORTRAIT_PAGES as readonly string[]).includes(path);
|
||||||
|
const hasCard = isArticle || path in OG_CARDS;
|
||||||
|
|
||||||
|
if (!image && !usesPortrait && !hasCard) {
|
||||||
|
throw new Error(
|
||||||
|
`No Open Graph card for ${path}.\n` +
|
||||||
|
' Add an entry to OG_CARDS in src/data/og-cards.ts whose `headline` is ' +
|
||||||
|
"this page's own <h1>, verbatim — `npm run og:proof` compares the two.\n" +
|
||||||
|
' Only the pages in PORTRAIT_PAGES use the portrait (AGENTS.md Q40, R15).',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JPEG on purpose, for both kinds. Page images are AVIF/WebP with a fallback
|
||||||
|
// (CLAUDE.md), but link-preview crawlers are not browsers — LinkedIn and Slack
|
||||||
|
// do not negotiate content types, and several still do not decode WebP at all.
|
||||||
|
// The generated card is already a 1200×630 JPEG, so it takes no `getImage` pass;
|
||||||
|
// running one would re-encode a finished image for nothing.
|
||||||
|
const portraitSource = image ?? ogDefault;
|
||||||
|
const ogImageUrl =
|
||||||
|
image || usesPortrait
|
||||||
|
? new URL(
|
||||||
|
(
|
||||||
|
await getImage({
|
||||||
|
src: portraitSource,
|
||||||
|
format: 'jpeg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
})
|
||||||
|
).src,
|
||||||
|
Astro.site,
|
||||||
|
)
|
||||||
|
: new URL(ogCardPath(path), Astro.site);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ THE ALT IS THE CARD'S HEADLINE, AND IT WAS THE PAGE `<title>`.
|
||||||
|
*
|
||||||
|
* The comment here claimed *"a typographic card's alt is its headline"* while
|
||||||
|
* the code fell back to `title`. Measured: `/fees/` emitted
|
||||||
|
* `og:image:alt="Fees · Mediation and Arbitration Rates · Pouya Lajevardi"`
|
||||||
|
* against a card reading *"Published in full, including what overruns cost."* —
|
||||||
|
* an alt that did not describe the image, on 20 pages, and it would have
|
||||||
|
* diverged further for the one article that sets `seoTitle`. Found by
|
||||||
|
* `adversarial-reviewer` round 2.
|
||||||
|
*
|
||||||
|
* `OG_CARDS[path]?.headline` is the card's actual text for a registry page.
|
||||||
|
* `title` remains the fallback for an article, where the card headline IS the
|
||||||
|
* title, and `PORTRAIT.alt` for the two portrait pages.
|
||||||
|
*/
|
||||||
|
const resolvedImageAlt =
|
||||||
|
imageAlt ??
|
||||||
|
(image || usesPortrait ? PORTRAIT.alt : (OG_CARDS[path]?.headline ?? title));
|
||||||
|
|
||||||
// JSON.stringify does not escape `<`, so a "</script>" inside any string value
|
// JSON.stringify does not escape `<`, so a "</script>" inside any string value
|
||||||
// would close this element early and hand the rest of the payload to the HTML
|
// would close this element early and hand the rest of the payload to the HTML
|
||||||
@@ -114,13 +183,13 @@ const jsonLdText =
|
|||||||
<meta property="og:image" content={ogImageUrl.href} />
|
<meta property="og:image" content={ogImageUrl.href} />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
<meta property="og:image:alt" content={imageAlt ?? PORTRAIT.alt} />
|
<meta property="og:image:alt" content={resolvedImageAlt} />
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content={title} />
|
<meta name="twitter:title" content={title} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
<meta name="twitter:image" content={ogImageUrl.href} />
|
<meta name="twitter:image" content={ogImageUrl.href} />
|
||||||
<meta name="twitter:image:alt" content={imageAlt ?? PORTRAIT.alt} />
|
<meta name="twitter:image:alt" content={resolvedImageAlt} />
|
||||||
|
|
||||||
{
|
{
|
||||||
jsonLdText && (
|
jsonLdText && (
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ const year = new Date().getFullYear();
|
|||||||
// sitemap and lists /med-arb/ as deliberately out of the primary nav, "linked
|
// sitemap and lists /med-arb/ as deliberately out of the primary nav, "linked
|
||||||
// contextually" — so dropping it here left the page with no site-wide link at
|
// contextually" — so dropping it here left the page with no site-wide link at
|
||||||
// all, on a project whose entire premise is crawlability. (2) The label names a
|
// all, on a project whose entire premise is crawlability. (2) The label names a
|
||||||
// PAGE, and docs/01 frames that page around the C.Med-Arb arc rather than as a
|
// PAGE, not an offering, so listing it is not the §4 inference it looked like.
|
||||||
// present offering, so listing it is not the §4 inference it looked like.
|
|
||||||
// (3) It settled half of Q35 unilaterally while the other half — Energy and
|
// (3) It settled half of Q35 unilaterally while the other half — Energy and
|
||||||
// Shareholder — stayed in both nav and footer, and the record claimed no
|
// Shareholder — stayed in both nav and footer, and the record claimed no
|
||||||
// unilateral action had been taken. Both halves of Q35 go to Pouya together.
|
// unilateral action had been taken. Both halves of Q35 go to Pouya together.
|
||||||
@@ -60,25 +59,19 @@ const aboutLinks = [
|
|||||||
<span class="footer-brand-name">{SITE.name}</span>
|
<span class="footer-brand-name">{SITE.name}</span>
|
||||||
</a>
|
</a>
|
||||||
{
|
{
|
||||||
/* The designation strip carries the credentialing STAGE, not just the
|
/* HELD DESIGNATIONS ONLY. §4's paired-disclosure condition made this
|
||||||
held designation, and that is a §4 Offerings condition rather than a
|
strip carry a credentialing stage too; that dissolved on 2026-08-29
|
||||||
flourish. The masthead says "Mediation · Arbitration · Toronto" on
|
when Q.Arb became held. */
|
||||||
every page; §4 permits the arbitration half on the condition that the
|
|
||||||
site "makes the first while stating the second plainly", and
|
|
||||||
"neither half may be dropped". Before this line rendered
|
|
||||||
CREDENTIALS.inProgress, that condition was unmet on every page that
|
|
||||||
ships — the constant existed in site.ts and was rendered nowhere.
|
|
||||||
Q.Arb reads as commenced, never as held (§4). */
|
|
||||||
}
|
}
|
||||||
<p class="footer-designation">
|
<p class="footer-designation">
|
||||||
{[...CREDENTIALS.designations, ...CREDENTIALS.inProgress].join(' · ')}
|
{CREDENTIALS.designations.join(' · ')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-grid">
|
<div class="footer-grid">
|
||||||
<nav class="footer-nav" aria-label="Footer">
|
<nav class="footer-nav" aria-label="Footer">
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h2 class="footer-heading">Practice areas</h2>
|
<h2 class="eyebrow footer-heading">Practice areas</h2>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
{
|
{
|
||||||
PRACTICE_AREAS.map((area) => (
|
PRACTICE_AREAS.map((area) => (
|
||||||
@@ -92,7 +85,7 @@ const aboutLinks = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h2 class="footer-heading">Process</h2>
|
<h2 class="eyebrow footer-heading">Process</h2>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
{
|
{
|
||||||
processLinks.map((link) => (
|
processLinks.map((link) => (
|
||||||
@@ -105,7 +98,7 @@ const aboutLinks = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h2 class="footer-heading">About</h2>
|
<h2 class="eyebrow footer-heading">About</h2>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
{
|
{
|
||||||
aboutLinks.map((link) => (
|
aboutLinks.map((link) => (
|
||||||
@@ -119,7 +112,7 @@ const aboutLinks = [
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="footer-col footer-contact">
|
<div class="footer-col footer-contact">
|
||||||
<h2 class="footer-heading">Contact</h2>
|
<h2 class="eyebrow footer-heading">Contact</h2>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
<li><a href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a></li>
|
<li><a href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a></li>
|
||||||
<li><span class="footer-meta">{CONTACT.phoneFallback}</span></li>
|
<li><span class="footer-meta">{CONTACT.phoneFallback}</span></li>
|
||||||
@@ -178,8 +171,14 @@ const aboutLinks = [
|
|||||||
border-block-end: 1px solid var(--rule);
|
border-block-end: 1px solid var(--rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* `flex-wrap: wrap` is the reflow fix, chosen OVER `overflow-wrap: anywhere` on
|
||||||
|
the name: the name is a flex item at `min-width: auto` and cannot shrink
|
||||||
|
below "Lajevardi", and wrapping the flex line breaks no word where
|
||||||
|
`anywhere` would have hyphenated a person's name. No effect at any normal
|
||||||
|
size. `docs/02` §Reflow carries the measurement. */
|
||||||
.footer-brand {
|
.footer-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
min-block-size: 44px;
|
min-block-size: 44px;
|
||||||
@@ -214,12 +213,10 @@ const aboutLinks = [
|
|||||||
gap: var(--space-7) var(--space-6);
|
gap: var(--space-7) var(--space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Type comes from the global `.eyebrow` class on the element; only what differs
|
||||||
|
is here. The colour is load-bearing, not decorative: gold-l on ink is
|
||||||
|
11.09:1 and `.eyebrow`'s own `--text-meta` on ink is 3.07:1, which fails. */
|
||||||
.footer-heading {
|
.footer-heading {
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
font-weight: var(--weight-medium);
|
|
||||||
letter-spacing: var(--tracking-eyebrow);
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--text-inverse-2);
|
color: var(--text-inverse-2);
|
||||||
margin-block-end: var(--space-4);
|
margin-block-end: var(--space-4);
|
||||||
}
|
}
|
||||||
@@ -247,6 +244,12 @@ const aboutLinks = [
|
|||||||
color: var(--text-inverse-2);
|
color: var(--text-inverse-2);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* `anywhere`, NOT `break-word` — only `anywhere` reduces min-content size, which
|
||||||
|
is the defect: the address has no break opportunity. `docs/02` §Reflow. */
|
||||||
|
.footer-contact a[href^='mailto:'] {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
/* Not a link, so no target floor — but it shares a column with links and
|
/* Not a link, so no target floor — but it shares a column with links and
|
||||||
should sit on the same rhythm. */
|
should sit on the same rhythm. */
|
||||||
.footer-meta {
|
.footer-meta {
|
||||||
|
|||||||
+105
-29
@@ -23,7 +23,24 @@ import InfinityMark from './InfinityMark.astro';
|
|||||||
import Button from './Button.astro';
|
import Button from './Button.astro';
|
||||||
|
|
||||||
const published = await getCollection('insights', ({ data }) => !data.draft);
|
const published = await getCollection('insights', ({ data }) => !data.draft);
|
||||||
|
/* ⚠️ GATED BY A BUILD FAILURE, NOT BY THIS COMMENT — `AGENTS.md` R20 and Q61.
|
||||||
|
The seventh nav item arms two measured header defects under fallback font
|
||||||
|
metrics. Prose cross-references did not gate it: every check passed while it
|
||||||
|
fired. Delete the throw as part of the fix, not before it. */
|
||||||
const showInsights = published.length >= 2;
|
const showInsights = published.length >= 2;
|
||||||
|
if (showInsights) {
|
||||||
|
throw new Error(
|
||||||
|
'AGENTS.md R20 — the seventh nav item is gated and this build would ship it.\n' +
|
||||||
|
`A second Insights article is published (${published.length} live), which puts ` +
|
||||||
|
'Insights in the primary nav. With seven items under FALLBACK font metrics — ' +
|
||||||
|
'the font-display: swap window, at the default text size, no reader setting ' +
|
||||||
|
'involved — the masthead measures 141px across 1056-1091px instead of 81px: a ' +
|
||||||
|
'60px shift on every page against the CLS < 0.05 budget, and 44px of #main ' +
|
||||||
|
'behind the sticky header after "Skip to content".\n' +
|
||||||
|
'Fix that first (docs/02 §Reflow lists the two candidate fixes), then delete ' +
|
||||||
|
'this guard. Do not work around it by unpublishing the article.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const items = PRIMARY_NAV.filter(
|
const items = PRIMARY_NAV.filter(
|
||||||
(item) => item.href !== '/insights/' || showInsights,
|
(item) => item.href !== '/insights/' || showInsights,
|
||||||
@@ -47,9 +64,8 @@ const inSection = (href: string) => path === href || path.startsWith(href);
|
|||||||
* is the placement Q33-orig objected to in the first place: a line under his
|
* is the placement Q33-orig objected to in the first place: a line under his
|
||||||
* name with nothing to qualify it, reading as a designation strip.
|
* name with nothing to qualify it, reading as a designation strip.
|
||||||
*
|
*
|
||||||
* This only ever REMOVES a claim from one page, so no §4 disclosure condition
|
* This only ever REMOVES a claim from one page. The footer's designation strip
|
||||||
* is touched — the footer's designation strip carries `Q.Arb — commenced
|
* carries the held designations on every page including this one.
|
||||||
* August 2026` on every page including this one.
|
|
||||||
*/
|
*/
|
||||||
const isHome = path === '/';
|
const isHome = path === '/';
|
||||||
---
|
---
|
||||||
@@ -156,12 +172,18 @@ const isHome = path === '/';
|
|||||||
|
|
||||||
/* The tagline is BACK, and the reasoning is worth keeping rather than just
|
/* The tagline is BACK, and the reasoning is worth keeping rather than just
|
||||||
the outcome. It was removed on 2026-08-26 because `Arbitration` under
|
the outcome. It was removed on 2026-08-26 because `Arbitration` under
|
||||||
Pouya's name read as a held capability, and §4 records Q.Arb as merely
|
Pouya's name read as a held capability, and §4 then recorded Q.Arb as
|
||||||
commenced. Q33 answered that the same day, and the premise was wrong:
|
merely commenced. Q33 answered that the same day, and the premise was
|
||||||
ADR designations are voluntary credentials, not licences, and COMMERCIAL
|
wrong twice over — §4 now records Q.Arb as HELD, and even before it did:
|
||||||
arbitral appointment in Ontario is not gated behind a designation — so the
|
ADR designations are voluntary credentials rather than licences, and on
|
||||||
constraint was always positional, never legal, and Pouya accepts sole,
|
**Pouya's stated position, which §4 Offerings records attributed to him and
|
||||||
party-appointed and co-arbitration work today. See §4 Offerings.
|
deliberately unstamped**, commercial arbitral appointment in Ontario is not
|
||||||
|
gated behind a designation — so the constraint was always positional rather
|
||||||
|
than legal, and he accepts sole, party-appointed and co-arbitration work
|
||||||
|
today. ⚠️ **STATED AS HIS POSITION, NOT AS FACT, AND THAT IS REQUIRED:** §4
|
||||||
|
Forbidden bars the class claim about arbitral gating **in both directions**,
|
||||||
|
and this repository does not conclude a proposition of law. See §4 Offerings
|
||||||
|
and the `struck-universal-q39` row.
|
||||||
|
|
||||||
SCOPED 2026-08-27 (Q39). This comment said "Anyone may be appointed an
|
SCOPED 2026-08-27 (Q39). This comment said "Anyone may be appointed an
|
||||||
arbitrator in Ontario", which Pouya checked and found FALSE as a universal:
|
arbitrator in Ontario", which Pouya checked and found FALSE as a universal:
|
||||||
@@ -176,9 +198,11 @@ const isHome = path === '/';
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
/* 48px, not 44. Still clears the touch floor, and it reserves the height the
|
/* 48px, not 44. Still clears the touch floor, and it reserves the height the
|
||||||
two-line brand takes at >=76rem so the sticky header is one constant 81px
|
two-line brand takes at >=76rem so the sticky header is 81px at every width
|
||||||
across every width where it is sticky — which is what --header-h and
|
where it is sticky — AT THE DEFAULT TEXT SIZE, which is what `--header-h`
|
||||||
scroll-padding-top are keyed to. One number instead of two bands. */
|
and `scroll-padding-top` are keyed to. One number instead of two bands.
|
||||||
|
Above the default the masthead wraps and is taller on purpose, and the
|
||||||
|
sticky gate at the 66rem block is what keeps that safe. */
|
||||||
min-block-size: 48px;
|
min-block-size: 48px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -190,22 +214,35 @@ const isHome = path === '/';
|
|||||||
gap: var(--space-05);
|
gap: var(--space-05);
|
||||||
}
|
}
|
||||||
/* The tagline appears only where there is room for it — see the 76rem block.
|
/* The tagline appears only where there is room for it — see the 76rem block.
|
||||||
Measured: at 11px with 0.18em tracking the string is ~285px wide, and
|
Measured: at 11px with 0.18em tracking the string is 283.1px wide. (Two
|
||||||
restoring it under the name pushed the one-row header past its content box
|
figures for 1024px were struck 2026-08-31: below 66rem `.nav` takes
|
||||||
by 18px at 1024 with six items and 84px with seven. The brand name carries
|
`flex-basis: 100%` and `.header-cta` is `display: none`, so there is no
|
||||||
|
one-row header there and no CTA box to be past — neither number could be
|
||||||
|
re-derived. See the rule at the 66rem block below.) The brand name carries
|
||||||
the identity on its own; the tagline is a flourish, and `/` opens with the
|
the identity on its own; the tagline is a flourish, and `/` opens with the
|
||||||
same words as the hero eyebrow (docs/01). */
|
same words as the hero eyebrow (docs/01). */
|
||||||
.brand-tagline {
|
.brand-tagline {
|
||||||
display: none;
|
display: none;
|
||||||
font-size: var(--text-2xs); /* 11px — the eyebrow floor in docs/02 */
|
/* HELD AT 11px, BELOW THE `.eyebrow` THIS ELEMENT CARRIES. At 14px the header
|
||||||
|
WRAPS and stands at 144.98px instead of 81.00px — at 1216 with six nav items,
|
||||||
|
and at EVERY width from 1216 up with a seventh. Nothing overflows and the CTA
|
||||||
|
stays on the content edge, so the cost is now 64px of header height on every
|
||||||
|
page: larger than the pre-wrap cost it replaces, and visible rather than
|
||||||
|
invisible. `docs/02` §Reflow carries the superseded figures. Insights is that
|
||||||
|
seventh item; `showInsights` turns it on at two published articles. */
|
||||||
|
font-size: var(--text-2xs);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
/* ⚠️ NEVER `white-space: nowrap` HERE. It was, until 2026-08-31, and because the
|
||||||
|
masthead is on all 22 pages that one declaration was the site's binding
|
||||||
|
reflow defect at a 200% default font size. The name is two words and takes
|
||||||
|
two lines when it has to; at every normal size it never wraps. WCAG 1.4.4 /
|
||||||
|
1.4.10; `docs/02` §Reflow carries the measurement. */
|
||||||
.brand-name {
|
.brand-name {
|
||||||
font-family: var(--font-serif);
|
font-family: var(--font-serif);
|
||||||
font-size: var(--text-xl);
|
font-size: var(--text-xl);
|
||||||
line-height: var(--leading-tight);
|
line-height: var(--leading-tight);
|
||||||
letter-spacing: var(--tracking-tight);
|
letter-spacing: var(--tracking-tight);
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.brand:hover .brand-name {
|
.brand:hover .brand-name {
|
||||||
@@ -364,14 +401,30 @@ const isHome = path === '/';
|
|||||||
|
|
||||||
Sticky only from here up, too. Below this the nav takes a second row and
|
Sticky only from here up, too. Below this the nav takes a second row and
|
||||||
the header stands at 137px, which is more of a small viewport than a
|
the header stands at 137px, which is more of a small viewport than a
|
||||||
sticky header is worth. Deviation from docs/02 "Sticky"; recorded there. */
|
sticky header is worth. Deviation from docs/02 "Sticky"; recorded there.
|
||||||
|
That same reasoning is what the sticky gate below extends to text size: a
|
||||||
|
tall header is not worth sticking whether the height comes from a narrow
|
||||||
|
viewport or from large type. */
|
||||||
@media (min-width: 66rem) {
|
@media (min-width: 66rem) {
|
||||||
.site-header {
|
.site-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
inset-block-start: 0;
|
/* ⚠️ STICKY ONLY WHILE THE MASTHEAD IS ONE ROW. A media query cannot say so:
|
||||||
}
|
its `rem` resolves against the browser's DEFAULT font size, a property's
|
||||||
.header-inner {
|
against the root element. TWO terms, both load-bearing —
|
||||||
flex-wrap: nowrap;
|
`100vw - 66rem` catches a root ABOVE the default (the row wraps and the
|
||||||
|
header stands 244–351px); `1rem - 16px` catches a root BELOW it, where the
|
||||||
|
80rem content cap shrinks faster than the header's px minimums and the row
|
||||||
|
wraps at EVERY viewport width — 65px of `#main` sat behind the header at
|
||||||
|
Chrome's "Very small" (9px) with only the first term. `* 100000` saturates
|
||||||
|
because wrapping is a step and a ramp left 10–69px covered across roots
|
||||||
|
18–30; `-100vh` bounds the result. Both terms are >= 0 at the default size,
|
||||||
|
so this is exactly `0px`. `docs/02` §Reflow carries the measurements and
|
||||||
|
the cases where the offset can still be short. */
|
||||||
|
inset-block-start: clamp(
|
||||||
|
-100vh,
|
||||||
|
min(calc((100vw - 66rem) * 100000), calc((1rem - 16px) * 100000)),
|
||||||
|
0px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
.brand {
|
.brand {
|
||||||
margin-inline-end: var(--space-5);
|
margin-inline-end: var(--space-5);
|
||||||
@@ -379,14 +432,23 @@ const isHome = path === '/';
|
|||||||
.nav {
|
.nav {
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
}
|
}
|
||||||
/* nowrap, and flex:none so the nav is never squeezed below its content
|
/* ⚠️ NOTHING HERE MAY SAY `flex-wrap: nowrap`, ON `.header-inner` OR ON
|
||||||
width. Measured before this: at 960-1250px the seven-item nav broke to
|
`.nav-list`. `.header-inner`'s was the binding one, measured necessary AND
|
||||||
two rows and the header stood at 141px instead of 81px. */
|
sufficient: a `nowrap` line cannot break, so at a 200% text size the row ran
|
||||||
|
944px past a 1280px viewport with Practice, Fees, Contact and the CTA
|
||||||
|
off-screen. `.nav-list`'s is INERT — identical at every width and text
|
||||||
|
setting, six items and seven — and stays removed only so this prohibition is
|
||||||
|
not contradicted by a `nowrap` in the same file. Wrapping is the only
|
||||||
|
mechanism that reflows under all THREE ways a reader enlarges text, because
|
||||||
|
it is driven by used sizes rather than by a query.
|
||||||
|
⚠️ "It never wraps above this breakpoint" holds only with the WEBFONTS
|
||||||
|
LOADED: under fallback metrics with a seventh nav item the header is 141px
|
||||||
|
across 1056–1091, which is both a 60px swap-in shift and 44px of `#main`
|
||||||
|
behind the sticky header. Latent — six items never wrap. `docs/02` §Reflow. */
|
||||||
.nav-list {
|
.nav-list {
|
||||||
flex-wrap: nowrap;
|
/* 16px from 66rem, widening to 24px at 80rem — see that block; the 76rem
|
||||||
flex: none;
|
block deliberately does NOT widen it. Measured with seven items at every
|
||||||
/* 16px from 66rem, widening to 24px at 76rem where there is room for it.
|
width from 1024px up. */
|
||||||
Measured with seven items at every width from 1024px up. */
|
|
||||||
column-gap: var(--space-4);
|
column-gap: var(--space-4);
|
||||||
}
|
}
|
||||||
.header-cta {
|
.header-cta {
|
||||||
@@ -440,6 +502,20 @@ const isHome = path === '/';
|
|||||||
.brand-tagline {
|
.brand-tagline {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
/* THE STICKY GATE MOVES WITH THE TAGLINE, and this is the same threshold the
|
||||||
|
binary search above produced: with the tagline the one-row masthead fits
|
||||||
|
from 1207px = 75.4rem, so 66rem is no longer the width it needs. Gating the
|
||||||
|
wider band on 66rem left the header sticky and wrapped from root 18 up —
|
||||||
|
46px of `#main` behind it at 1216/1280, 51px at 1440, 69px at 1920 — while
|
||||||
|
`/` measured clean throughout, because `/` is the one page that suppresses
|
||||||
|
the tagline. Each band gates on the width ITS layout requires. */
|
||||||
|
.site-header {
|
||||||
|
inset-block-start: clamp(
|
||||||
|
-100vh,
|
||||||
|
min(calc((100vw - 76rem) * 100000), calc((1rem - 16px) * 100000)),
|
||||||
|
0px
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 80rem) {
|
@media (min-width: 80rem) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* A conduct undertaking — §4's third class of claim (Q54, 2026-08-29).
|
||||||
|
*
|
||||||
|
* ONE TREATMENT ON ALL THREE PAGES, so a reader can tell a promise from a
|
||||||
|
* description. Set as body prose, "the agreement should settle the switch" and
|
||||||
|
* "I will not take the appointment unless it does" look the same, and the
|
||||||
|
* second is the half a party weighs.
|
||||||
|
*
|
||||||
|
* THE TEXT ALWAYS COMES FROM `CONDUCT_UNDERTAKINGS` in `src/data/site.ts`.
|
||||||
|
* Never type a sentence into this slot: a softened undertaking is a change to a
|
||||||
|
* published commitment, and a page-local copy is where that happens silently.
|
||||||
|
*
|
||||||
|
* NO PROPS BUT `children`, AND THE INTERFACE IS LOAD-BEARING. With
|
||||||
|
* comment-only frontmatter an Astro component's props widen to `any` and
|
||||||
|
* `<Undertaking class="x">` compiles clean while matching nothing — the
|
||||||
|
* parent-scope defect `CLAUDE.md` records, and the one `Pill` shipped carrying.
|
||||||
|
* Deleting it re-disables checking at every call site.
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
children?: unknown;
|
||||||
|
}
|
||||||
|
const _props: Props = Astro.props;
|
||||||
|
void _props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<p class="undertaking"><slot /></p>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* The gold rule is DECORATIVE, never text. docs/02: gold on cream measures
|
||||||
|
2.10:1 and fails AA for body and large text alike, which is why the site
|
||||||
|
uses it for rules, dividers and icon strokes and nowhere else on cream.
|
||||||
|
`.rule-gold` in global.css is the same decision at full width. */
|
||||||
|
.undertaking {
|
||||||
|
padding-inline-start: var(--space-5);
|
||||||
|
border-inline-start: 2px solid var(--rule);
|
||||||
|
max-inline-size: 54ch;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* global.css sets `:where(.prose) > p + p` at zero specificity, and it DOES
|
||||||
|
reach this root — global.css is a plain stylesheet, not a scoped one, so
|
||||||
|
the usual parent-scope boundary does not apply here. This overrides it
|
||||||
|
deliberately with a larger step: an undertaking that sits on the same
|
||||||
|
rhythm as the paragraphs around it reads as one of them. */
|
||||||
|
.undertaking:not(:first-child) {
|
||||||
|
margin-block-start: var(--space-6);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+16
-16
@@ -2,6 +2,7 @@ import { defineCollection } from 'astro:content';
|
|||||||
import { glob } from 'astro/loaders';
|
import { glob } from 'astro/loaders';
|
||||||
import { z } from 'astro/zod';
|
import { z } from 'astro/zod';
|
||||||
import { PRACTICE_SLUGS } from './data/site';
|
import { PRACTICE_SLUGS } from './data/site';
|
||||||
|
import { INSIGHT_TOPICS } from './data/insights';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `<title>` length, from docs/04-seo-spec.md.
|
* `<title>` length, from docs/04-seo-spec.md.
|
||||||
@@ -97,16 +98,12 @@ const insights = defineCollection({
|
|||||||
* regulatory and industry commentary.
|
* regulatory and industry commentary.
|
||||||
*/
|
*/
|
||||||
topics: z
|
topics: z
|
||||||
.array(
|
/* The tuple lives in `src/data/insights.ts`, imported rather than
|
||||||
z.enum([
|
written out here — build step 7b. It was inline until then, which
|
||||||
'process-explainer',
|
made the display labels a second copy of the same list, and the
|
||||||
'regulatory-commentary',
|
copy that drifts is the one nobody re-reads. `TOPIC_LABELS` is
|
||||||
'industry-commentary',
|
keyed off it, so an unlabelled topic is a type error. */
|
||||||
'reflection',
|
.array(z.enum(INSIGHT_TOPICS))
|
||||||
'technical-explainer',
|
|
||||||
'credentialing',
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.min(1)
|
.min(1)
|
||||||
.refine((t) => new Set(t).size === t.length, 'No duplicate topics.'),
|
.refine((t) => new Set(t).size === t.length, 'No duplicate topics.'),
|
||||||
practiceAreas: z
|
practiceAreas: z
|
||||||
@@ -121,12 +118,15 @@ const insights = defineCollection({
|
|||||||
image: image().optional(),
|
image: image().optional(),
|
||||||
imageAlt: z.string().trim().min(1).optional(),
|
imageAlt: z.string().trim().min(1).optional(),
|
||||||
/**
|
/**
|
||||||
* INTENT, not yet enforced — there is no /insights/ route to enforce it
|
* ENFORCED SINCE BUILD STEP 7b, and by ONE predicate rather than four.
|
||||||
* in. The mechanism, when step 7 builds that route: filter drafts out of
|
* `!data.draft` is the filter passed to every `getCollection('insights')`
|
||||||
* `getCollection('insights', ...)` so no page is generated, which keeps
|
* call on the site — the article route, the index, the home page's latest
|
||||||
* them out of the build, the index, and the sitemap in one move. The
|
* strip, the `SiteHeader` nav gate, and the OG card endpoint. A draft
|
||||||
* sitemap filter in astro.config.mjs cannot see collection data and is
|
* therefore produces no page, so it is absent from the build, the index,
|
||||||
* not the right place for it. See docs/04-seo-spec.md.
|
* the sitemap and the card set as a consequence of not existing, not
|
||||||
|
* because four places each remembered to exclude it. The sitemap filter
|
||||||
|
* in astro.config.mjs cannot see collection data and is deliberately not
|
||||||
|
* where this lives. See docs/04-seo-spec.md.
|
||||||
*/
|
*/
|
||||||
draft: z.boolean().default(true),
|
draft: z.boolean().default(true),
|
||||||
/** Every article is reviewed by Pouya before publication — D9. */
|
/** Every article is reviewed by Pouya before publication — D9. */
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: 'Bill 40 and grid connection: a dispute-resolution read'
|
||||||
|
description: 'Ontario Bill 40 of the 44th Parliament, 1st Session widened what the OEB may weigh on leave to construct and gated grid connection for large loads.'
|
||||||
|
# publishDate is the drafting date. Set it on approval (D9).
|
||||||
|
publishDate: 2026-08-31
|
||||||
|
topics: ['regulatory-commentary']
|
||||||
|
practiceAreas: ['energy']
|
||||||
|
readingTime: 8
|
||||||
|
draft: true
|
||||||
|
reviewedByPouya: false
|
||||||
|
---
|
||||||
|
|
||||||
|
import { NEUTRAL_ROLE_LINE } from '../../data/site';
|
||||||
|
|
||||||
|
## Which Bill 40
|
||||||
|
|
||||||
|
Bill numbers are reused every parliament. Bill 40 of the 43rd Parliament, 1st Session is the Moving Ontarians Safely Act, 2023, amending the Highway Traffic Act. Bill 40 of the 42nd Parliament, 2nd Session is the Support for Adults in Need of Assistance Act, 2021. Neither touches electricity.
|
||||||
|
|
||||||
|
The energy one is Bill 40 of the 44th Parliament, 1st Session: the Protect Ontario by Securing Affordable Energy for Generations Act, 2025, sponsored by the Minister of Energy and Mines. The Legislative Assembly's status page for the Bill records First Reading on 3 June 2025 and Royal Assent on 11 December 2025. It is now chapter 22 of the Statutes of Ontario, 2025. Both dates do work below.
|
||||||
|
|
||||||
|
Cite it with the parliament and the session, because a reference carrying neither points at three unrelated statutes across three different parliaments.
|
||||||
|
|
||||||
|
Its long title is accurate about the method: "An Act to amend various statutes with respect to energy, the electrical sector and public utilities". Its three schedules amend the Electricity Act, 1998, the Municipal Franchises Act, and the Ontario Energy Board Act, 1998. What interests me is not what it created. It is which existing negotiations it moved.
|
||||||
|
|
||||||
|
## Section 96 grew a second branch
|
||||||
|
|
||||||
|
Leave to construct is section 92 of the Ontario Energy Board Act, 1998. No person may construct, expand or reinforce an electricity transmission or distribution line, or make an interconnection, without an order of the Board granting leave. Section 89 draws the line at voltage. Above 50 kilovolts is transmission; 50 kilovolts or less is distribution.
|
||||||
|
|
||||||
|
The thresholds people actually argue about are not in the section. They are exemptions in O. Reg. 161/99, which carves distribution lines out of section 92 outright and exempts a transmission line of two kilometres or less.
|
||||||
|
|
||||||
|
Section 96(1) supplies the test. If the Board is of the opinion that the work is in the public interest, it shall make an order granting leave. On a section 92 application, section 96(2) confines what the public interest may mean: the Board "shall only consider" the matters enumerated there. Bill 40's Schedule 3 lengthened that list. It now runs to the interests of consumers with respect to prices and the reliability and quality of electricity service, and to supporting economic growth consistent with Government of Ontario policy. A new section 96(3) requires the Board, on such an application, to consider such reports, documents or other information as may be prescribed by regulation. Both came into force on 11 December 2025.
|
||||||
|
|
||||||
|
That changes the shape of the record rather than the outcome of any application. A proponent's economic case now sits inside the statutory test instead of behind it, and part of the evidentiary burden can be set by regulation after the project's commercial arrangements are signed. Two allocations follow, and section 96(2) excludes both from the Board's public-interest inquiry: who pays to produce that material, and who carries the delay if it comes back thin. Both belong in the parties' commercial agreements, not the Board's record.
|
||||||
|
|
||||||
|
## A priority project settles need, not route
|
||||||
|
|
||||||
|
Section 96.1(1) lets the Lieutenant Governor in Council declare the construction, expansion or reinforcement of a specified transmission line to be needed as a priority project. The OEB's own page on leave-to-construct applications for priority transmission projects is direct about the consequence: approval under section 92 is still required, but "in these cases the OEB must accept that the project is needed when forming its opinion under section 96 of the Act."
|
||||||
|
|
||||||
|
A declaration removes one argument and leaves the others standing. Need is settled. Route, land, conditions and cost responsibility are not. Section 94 shows where the friction lives. The applicant files a map showing the municipalities, highways, railways, utility lines and navigable waters the proposed work passes through, under, over, upon or across. Each of those is a counterparty, an approval, or both.
|
||||||
|
|
||||||
|
As at the end of August 2026 that same OEB page recorded that no leave-to-construct application for a declared priority project was before the Board. That will change, and the first one will run alongside private disputes about access and cost, neither of which is among the two matters section 96(2) lets the Board weigh.
|
||||||
|
|
||||||
|
## The gate for large loads, and the date that sorts the pipeline
|
||||||
|
|
||||||
|
Schedule 1 added section 28.1 to the Electricity Act, 1998, in force 11 December 2025. It is a hard gate. Unless a transmitter or distributor is satisfied that the specified connection requirements have been complied with, it shall not connect a specified load facility to its system, or reconnect one that was disconnected for breach of those requirements.
|
||||||
|
|
||||||
|
"Specified load facility" is defined two ways. A facility that is a data centre and meets any criteria set out in the regulations. Or a facility that withdraws electricity from the IESO-controlled grid or from a distributor's system, whose connection demand exceeds a prescribed amount, and which meets any other prescribed criteria. Both limbs point outward. Bill 40 amended the regulation-making authority in section 114 to match, adding the power to define "data centre" for the purposes of section 28.1 and to prescribe criteria by geographic area, volume withdrawn, or connection demand.
|
||||||
|
|
||||||
|
The enabling section has been in force since 11 December 2025. The Ministry of Energy and Mines' Environmental Registry of Ontario posting of 13 August 2026, ERO 026-0853, described the connection-approval regulation as something "the province is considering drafting". The same posting proposes a Data Centre Playbook assessed against economic development, data security and digital sovereignty, and community trust, and separately proposes a new rate class under O. Reg. 429/04 for data centres above a demand threshold that would not be eligible for the Industrial Conservation Initiative. That comment period closes on 12 September 2026, so anything in it may move.
|
||||||
|
|
||||||
|
The provision that does not move is the transition rule. Section 28.1(6) says the section does not apply to a specified load facility whose connection request was submitted to a transmitter or distributor, in accordance with the Transmission System Code or the Distribution System Code, before 3 June 2025. That is the day Bill 40 had First Reading.
|
||||||
|
|
||||||
|
So one date sorts a pipeline into two regimes, and the requirements the later one turns on were still described by the Ministry in August 2026 as something the province is considering drafting. I expect arguments about which side a given project falls on: what was submitted, to whom, on what date, and whether it was a connection request in accordance with the applicable code at all. Those are questions about documents, which a neutral can work through with the parties rather than around them. The [energy disputes](/practice/energy/) I am built for start there rather than at the Board.
|
||||||
|
|
||||||
|
## The connection process is where the schedule lives
|
||||||
|
|
||||||
|
The mechanics of getting connected sit outside Bill 40, and they are what a supply agreement or a construction programme is quietly dated against.
|
||||||
|
|
||||||
|
The IESO's own description of the connection process sets out up to six stages, beginning with preparing the application and ending after the equipment is registered and tested. A transmitter's connections are generally subject to all six; a distributor's may be subject only to the first three. The IESO decides whether an application qualifies for a system impact assessment or an expedited one, and the transmitter generally runs its own customer impact assessment after the IESO's draft report, under a separate agreement. The final report goes out with either a notification of conditional approval or a notification of disapproval with reasons.
|
||||||
|
|
||||||
|
There is no queue. The IESO states in terms that it is not using an interconnection queue, and works instead from the concept of committed projects defined in its Market Manual 1.4. An argument built on a project's place in line is an argument about nothing.
|
||||||
|
|
||||||
|
The published timings are long. The IESO puts conditional approval at typically one year, and the process as a whole at anywhere from a few months for a small modification to more than three years for a new facility. A supply agreement, a site lease or a construction programme written against an earlier assumption is the dispute, and a dispute in that shape is a [construction claim](/practice/construction/) as much as an energy one.
|
||||||
|
|
||||||
|
## Schedule 2 takes the electors out of a municipal by-law
|
||||||
|
|
||||||
|
Schedule 2 amended the Municipal Franchises Act. The Bill's own summary of that schedule records that section 3 was re-enacted to remove the requirement for the municipal electors to assent to the by-law, and instead to require that a municipality pass a by-law setting out the terms and conditions.
|
||||||
|
|
||||||
|
That is a small amendment and it moves a step. Where that amendment is in force, the pressure moves to the drafting: assent to a by-law is one yes-or-no step, and terms and conditions in a by-law are not. Terms that have to be settled between a municipality and the party a by-law concerns are the kind of thing a facilitated process can move.
|
||||||
|
|
||||||
|
## What this changes about choosing a process
|
||||||
|
|
||||||
|
Each of these amendments puts a commercial argument next to a regulatory process that keeps its own timetable. That timetable is not something the parties can agree to move. Almost everything around it is: cost responsibility, schedule risk, the consequences of a condition attached to a conditional approval, and the allocation of a delay that nobody caused.
|
||||||
|
|
||||||
|
Two points follow for counsel. The first is timing. A session booked before the final system impact assessment report exists is one at which the conditional approval, and the conditions attached to it, do not exist yet, so the regulatory sequence belongs in the scheduling conversation. [The shape of an engagement](/process/) sets out where I put it. The second is the record. Where the parties want a decision instead of a settlement, a [commercial arbitration](/arbitration/) keeps the study, the conditions and the technical argument in front of one decision-maker rather than split across a hearing and a negotiation.
|
||||||
|
|
||||||
|
The disputes Bill 40 will generate have mostly not been had yet, because the regulation the large-load gate depends on was still under consideration as at August 2026. That is the honest description of this area, and it is why I describe energy as a position I am building into rather than a volume of work I have already done. Everything above is as at the end of August 2026 and should be checked before it is relied on. Nothing here is applied to a particular matter. {NEUTRAL_ROLE_LINE}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: 'Choosing a neutral: what counsel should actually ask'
|
||||||
|
description: 'What counsel should ask before appointing a mediator or a commercial arbitrator: what a designation records, whose rules apply, and who reads the record.'
|
||||||
|
# publishDate is the drafting date. Set it on approval (D9).
|
||||||
|
publishDate: 2026-08-31
|
||||||
|
topics: ['process-explainer', 'credentialing']
|
||||||
|
practiceAreas: ['construction', 'technology']
|
||||||
|
readingTime: 7
|
||||||
|
draft: true
|
||||||
|
reviewedByPouya: false
|
||||||
|
---
|
||||||
|
|
||||||
|
import { CONDUCT_UNDERTAKINGS } from '../../data/site';
|
||||||
|
|
||||||
|
## What a designation records, and what it does not
|
||||||
|
|
||||||
|
Counsel choosing a neutral usually has a short list of names, a rate for each, and a signature block full of abbreviations. The abbreviations are the part most often skipped. They are also the part that can be checked in a few minutes.
|
||||||
|
|
||||||
|
The ADR Institute of Ontario publishes its own expansions on its professional designations page. Q.Med is Qualified Mediator. Q.Arb is Qualified Arbitrator. C.Med is Chartered Mediator. C.Arb is Chartered Arbitrator. The long forms are worth taking from the conferring body's own page rather than from recall, because the abbreviations sit close together and a wrong expansion is easy to write.
|
||||||
|
|
||||||
|
What those designations record is training. ADRIO's page for the Qualified designations describes them as recognising practitioners who have completed sufficient mediation or arbitration training, and related dispute resolution training. The same page notes that Q.Med criteria vary across affiliates, and points an applicant to the checklist on the application form for the criteria specific to Ontario.
|
||||||
|
|
||||||
|
What the page does not describe is what any activity requires. ADRIO sets out what its own designations recognise, and it says nothing about permission. A designation should not be read as though it did. So the question a designation answers is narrow: which body conferred it, against which criteria, and is it current.
|
||||||
|
|
||||||
|
Currency is the half that gets assumed. ADRIO's pages for C.Med and C.Arb each state that there is an annual fee to maintain the designation, payable to the ADR Institute of Canada, Inc., and that the holder must remain a member in good standing with the ADR Institute of Ontario to retain it. The Qualified page addresses neither fees nor retention. That is a fact about the page rather than an answer, particularly since ADRIO records on the same page that the criteria vary across affiliates. A signature block cannot settle currency. That is a question for the neutral, or for the conferring body.
|
||||||
|
|
||||||
|
## Whose rules the process will run under
|
||||||
|
|
||||||
|
The second question is whose rules the process runs under, and it is cheaper to ask before an appointment than to discover at the first call.
|
||||||
|
|
||||||
|
For mediation, the ADR Institute of Canada publishes the ADRIC National Mediation Rules. ADRIC's own description is that the rules provide for initiating mediations, including the appointment of a mediator should the parties be unable to come to an agreement. The document carries more than the rules themselves: a code of conduct, a standard form agreement to mediate at Schedule B, ADRIC's administration fees at Schedule A, and a model dispute resolution clause for contracts.
|
||||||
|
|
||||||
|
One currency note on the same rules. ADRIC's page states, as of 2025, that its Mediation Committee is reviewing the Mediation Rules, and that the existing rules remain in effect and should continue to be used until any updates are formally adopted. The sensible course is to check it at the point of appointment rather than to date the rules in a submission.
|
||||||
|
|
||||||
|
For arbitration, ADRIC adopted new Arbitration Rules and a new Arbitrator Appointment Protocol effective 1 March 2025, published as the ADRIC Arbitration Rules – Effective 2025. It publishes named forms alongside them: Notice to Arbitrate, Request to Administer the Arbitration, Request for the appointment of an arbitrator, Application for Urgent Interim Measures, Application to Challenge an Arbitrator, and Notice of Appeal.
|
||||||
|
|
||||||
|
None of that is a statement of what the rules require. The rules are published documents, and where an appointment will run under them the document is the thing to read rather than a summary of it, this one included. What can be settled in advance is which rule set applies, what governs where the contract is silent, and what the tribunal is left to decide. Where it is silent, the protocol is settled in writing before the session. I set out the rule sets I work under on [mediation](/mediation/) and [commercial arbitration](/arbitration/).
|
||||||
|
|
||||||
|
## What the neutral does with what is said in caucus
|
||||||
|
|
||||||
|
Third, and this is the question that discriminates most: what happens to caucus material.
|
||||||
|
|
||||||
|
In mediation the answer should be stated rather than assumed. Mine is published, and it is this. "{CONDUCT_UNDERTAKINGS.mediationCaucus}"
|
||||||
|
|
||||||
|
In med-arb the question is harder, because the neutral who hears the caucus may later decide the matter. ADRIC publishes ADRIC Med-Arb Rules, presented to its membership as a discussion draft at its 2019 annual conference and designed, in ADRIC's words, to "work in tandem with ADRIC's existing Mediation Rules and Arbitration Rules." Nothing here is a claim about what that draft provides, or about its status. Where a med-arb appointment names a rule set, the document is the thing to read.
|
||||||
|
|
||||||
|
Two things are worth asking of any med-arb appointment, and both are answerable in writing before it starts. The first is when and how the switch from mediation to arbitration happens, and what has to be agreed for it to happen at all. The second is what becomes of something said in confidence that turns out to matter to the decision. I accept med-arb appointments in commercial matters, and both answers are set out on [med-arb](/med-arb/). The second is the harder one. "{CONDUCT_UNDERTAKINGS.medArbStepOut}"
|
||||||
|
|
||||||
|
## Dates, and whether they are real
|
||||||
|
|
||||||
|
Fourth: availability. Three questions get at it. Which dates are actually held. How long a date is held without a signed agreement to mediate. Whether a second day is booked at the outset or looked for after the first one runs out.
|
||||||
|
|
||||||
|
Where the parties cannot agree on a name, ADRIC's National Mediation Rules cover the appointment of a mediator. That is a route rather than a date.
|
||||||
|
|
||||||
|
In a commercial arbitration the date that matters most is the award. Mine is published, and it is this. "{CONDUCT_UNDERTAKINGS.arbitrationAwardDate}"
|
||||||
|
|
||||||
|
No turnaround figure is published here. A time to award quoted before anyone has seen the record is a guess, whoever quotes it. A date in the first procedural order is a different thing: it is fixed once the shape of the record is known, and both parties can see it.
|
||||||
|
|
||||||
|
## Fees, and what happens when the day runs long
|
||||||
|
|
||||||
|
Fifth. The rate is the easy part of the fee question. What a day means and when it ends, whether preparation is charged separately and how it is estimated, the cancellation schedule and the notice period it turns on, who is billed and in what shares — those are the terms that decide what a process actually costs.
|
||||||
|
|
||||||
|
The overrun question is specific enough to be worth its own sentence. A session is booked to five o'clock, and at seven the parties are close. The possible answers are all defensible: the day converts to hourly, a day is a day whatever it runs to, the neutral stops. What is not defensible is finding out which one applies at half past six.
|
||||||
|
|
||||||
|
ADRIC's mediation rules publish the institute's administration fees at Schedule A of the same document. Whether they apply to a given appointment is a question for the institute, and it is not the neutral's own fee.
|
||||||
|
|
||||||
|
## Whether the neutral can read the record the dispute turns on
|
||||||
|
|
||||||
|
Sixth, and last. Some disputes turn on a document rather than on a submission — a critical-path analysis is one, a model card is another.
|
||||||
|
|
||||||
|
I work as a machine-learning and infrastructure engineer. On a [construction](/practice/construction/) file that means the baseline programme, the as-built, the change-order log and the delay analysis are documents I read, rather than take on trust from whichever expert explains them most confidently. On a [technology](/practice/technology/) file it means an API trace, a set of monitoring dashboards, a model card, an evaluation harness and a data-processing addendum.
|
||||||
|
|
||||||
|
The question that gets at this with any neutral is which primary documents will be read before the session, and which will be taken on an expert's account of them. An answer that names documents can be checked against the productions. An answer that names an industry cannot.
|
||||||
|
|
||||||
|
## Asked before the appointment, and answered in writing
|
||||||
|
|
||||||
|
None of this requires a long call. All of it is easier to raise before an appointment than after, because before the appointment an answer is a term and after it is a complaint.
|
||||||
|
|
||||||
|
[The shape of an engagement](/process/) sets out when conflicts are run: on the intake call, before anything is agreed. Where a party has no counsel, [what happens at a mediation](/for-parties/) is the more useful page. Everything else above is a question, and the answers are what counsel is actually choosing between.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: 'What the Ontario data-centre build-out means for dispute resolution'
|
||||||
|
seoTitle: 'Ontario''s data-centre build-out and dispute resolution'
|
||||||
|
description: 'A large-load grid connection, a construction contract and a technology contract meet on one date. Why litigating first and mediating late fits that badly.'
|
||||||
|
# publishDate is the drafting date. Set it on approval (D9).
|
||||||
|
publishDate: 2026-08-31
|
||||||
|
topics: ['industry-commentary', 'technical-explainer']
|
||||||
|
practiceAreas: ['technology', 'construction', 'energy']
|
||||||
|
readingTime: 8
|
||||||
|
draft: true
|
||||||
|
reviewedByPouya: false
|
||||||
|
---
|
||||||
|
|
||||||
|
## Three sets of rules over one connection date
|
||||||
|
|
||||||
|
A large data centre in Ontario is three projects wearing one name.
|
||||||
|
|
||||||
|
There is a connection: an assessment run by the Independent Electricity System Operator and by the transmitter, ending in an approval that arrives on the date the financial model assumed, or does not. There is a build: a construction contract, subcontracts under it, and the Construction Act's prompt payment and interim adjudication machinery standing behind every invoice. And there is a load: the computing the building exists to house, under a technology contract with its own service levels, capacity terms and data terms.
|
||||||
|
|
||||||
|
Each has a different decision-maker, a different vocabulary, and a different idea of what a deadline is. They converge on the date the facility can energise. That convergence is the shape of the dispute, and a dispute clause drafted for one of the three contracts alone will not hold it.
|
||||||
|
|
||||||
|
## How a connection is assessed and approved
|
||||||
|
|
||||||
|
The terminology is precise and the wrong word travels badly, so it is worth taking from the IESO's own description of the connection process. Obtaining conditional approval runs through the IESO's and transmitter's connection assessment and approval (CAA) process. Within it the IESO performs a System Impact Assessment (SIA), or an expedited SIA where the application qualifies, and assigns a unique CAA ID. The transmitter performs a Customer Impact Assessment (CIA), which the IESO says the transmitter generally initiates after the draft SIA report. The SIA agreement is prepared in accordance with section 6.1.15.3 of chapter 0.4 of the Market Rules. The IESO issues a draft SIA report to the applicant and the transmitter for comment, then a final report, and with it either a Notification of Conditional Approval or a Notification of Disapproval with Reasons.
|
||||||
|
|
||||||
|
The IESO's published connection process runs to as many as six stages. Connections to a transmitter's system are generally subject to all six; connections to a distributor's system may be subject only to the first three. On the IESO's own figures, obtaining conditional approval "typically takes one year", registering equipment "takes at least three months", and the whole process can run "anywhere from a few months for small modifications to existing facilities, to more than three years for major modifications or to connect new facilities".
|
||||||
|
|
||||||
|
Two features matter to anyone drafting a dispute clause. The SIA assesses the proposed connection's impact on the reliability of the integrated power system; what comes out of it is a report and a notification, not a ruling between parties. And there is no ordered line to be moved up. The IESO says so in terms in its connection-process FAQ: it works from "committed projects", a concept defined in section 3.3 of Market Manual 1.4, Connection Assessment and Approval, each assessment following section 5.8 of the same manual. The four IESO connection-process pages read for this piece describe only the six-stage process; no large-load or data-centre variant appears. This is the process I write about under [energy, grid and regulatory disputes](/practice/energy/).
|
||||||
|
|
||||||
|
## The statutory gate is in force; the regulation behind it is still a consultation
|
||||||
|
|
||||||
|
There is now a second gate, aimed squarely at this sector. Section 28.1 of the Electricity Act, 1998 came into force on 11 December 2025, through Bill 40 of the 44th Parliament, first session — the Protect Ontario by Securing Affordable Energy for Generations Act, 2025, chapter 22 of the Statutes of Ontario, 2025.
|
||||||
|
|
||||||
|
Section 28.1 provides that unless a transmitter or distributor is satisfied that the "specified connection requirements" have been complied with, it "shall not" connect a "specified load facility" — or reconnect one that was disconnected for breaching those requirements. A specified load facility is a data centre meeting whatever criteria the regulations may set, or a facility drawing from the grid with demand at the point of connection above a regulation-prescribed threshold, meeting any other prescribed criteria. Subsection (6) carries a transition: the section does not apply to a facility whose connection request under the Transmission System Code or the Distribution System Code was submitted before 3 June 2025.
|
||||||
|
|
||||||
|
The regulation is the part to watch, because as this is written at the end of August 2026 the ministry consulting on it still describes it as prospective. The Ministry of Energy and Mines' consultation notice on an Economic and Strategic Assessment Framework for New Data Centres, ERO 026-0853, open for comment from 13 August to 12 September 2026, says that "the province is considering drafting a proposed regulation" that would require new large data centres to obtain the approval of the government to connect or reconnect. The same notice proposes a Data Centre Playbook, one of whose three pillars is protecting data security and digital sovereignty. It records the ministry's estimate that data-centre connection proposals could cumulatively total more than 10,000 MW.
|
||||||
|
|
||||||
|
The notice puts the Playbook forward to attract investments that, among other things, "ensure Canadians' data remains in Canada". That phrase lives in a technology contract long before it reaches a grid application: it is a question about where workloads run, which subprocessors touch them, and what the operator has promised its own customers. An SIA report will not answer it, which is why the [technology](/practice/technology/) side of a data-centre project cannot be quarantined from the energy side.
|
||||||
|
|
||||||
|
## The construction contract runs on a different clock
|
||||||
|
|
||||||
|
Underneath the connection sits an ordinary Ontario construction project, on the statutory payment timetable. Proper invoices go to the owner monthly unless the contract says otherwise. The owner pays within 28 days, or gives a notice of non-payment within 14 days detailing the reasons. A contractor paid in full passes payment down within seven days; a contractor the owner has not paid must pay its subcontractors within 35 days of giving the invoice unless it serves a notice of non-payment, and one route through that notice requires an undertaking to refer the matter to adjudication within 21 days.
|
||||||
|
|
||||||
|
Interim adjudication under Part II.1 of the Construction Act has been available since October 2019, administered by Ontario Dispute Adjudication for Construction Contracts as the Authorized Nominating Authority, with amendments in force from 1 January 2026. What may be adjudicated without the other side's agreement is a prescribed list, now in section 19(1) of O. Reg. 264/25: the valuation of services or materials; payment under the contract, including a change order, approved or not, or a proposed change order; a notice-of-non-payment dispute; amounts retained by way of set-off; payment of a holdback; and, only where reasonably necessary to resolve another adjudicable matter, the scope of work, a change-in-price request, and an extension-of-time request.
|
||||||
|
|
||||||
|
Then the pace. The adjudicator must determine the matter no later than 30 days after receiving the claimant's documents, which are due within five days of the appointment. That deadline can be extended by up to 14 days at the adjudicator's request with written consent, or for a period the parties agree in writing, subject to the adjudicator's consent. A determination made late is "of no force or effect". A party ordered to pay must pay within 15 days. The determination binds until a court or an Arbitration Act, 1991 arbitration determines the matter, or the parties agree otherwise in writing; judicial review needs leave of the Divisional Court. An adjudication addresses a single dispute unless the parties and the adjudicator agree otherwise.
|
||||||
|
|
||||||
|
Put the two clocks beside each other. Conditional approval to connect typically takes a year. An adjudication is designed to be finished, with written reasons and a payment obligation, about five weeks after the adjudicator is appointed — seven if the deadline is extended. The prescribed list is a payment list. It does not reach the question the project turns on — when the facility will connect. That question reaches adjudication only if both parties agree to send it there, which nobody negotiates once the date has slipped. A slipped connection date arrives as a bundle: valuation, delay, scope, and a change order nobody approved, split between what the list reaches and what it does not. That is the [construction](/practice/construction/) half of the problem.
|
||||||
|
|
||||||
|
## Why litigating first and mediating late fits this badly
|
||||||
|
|
||||||
|
The Construction Act fixes no mediation step, so on a file where the contract is silent, the timing of any mediation is set by the litigation timetable rather than by the connection timetable. That assumes the amount in dispute is fixed and the commercial relationship has finished. On a live connection neither is true. The assessment is still running, the transmitter is still a counterparty rather than a witness, and every month of argument moves the energisation date all three contracts are priced against.
|
||||||
|
|
||||||
|
The statutory design points the other way. An interim determination is expressly provisional: it binds until a court, an arbitrator or the parties' own written agreement replaces it, and both a court and an arbitrator may consider the merits afresh. The lien timetable is short at the front and long at the back: 60 days to preserve, a further 90 to perfect, and a perfected lien expires immediately after the second anniversary of the action that perfected it unless that action has been set down or ordered to trial. A mediation two years into that arc is a mediation of a project whose connection window has closed.
|
||||||
|
|
||||||
|
The Construction Act contains no mediation provision at all: no mediation part, no step, no mediator's role. Whatever mediated step happens on a construction file comes from somewhere other than that statute.
|
||||||
|
|
||||||
|
## What I would fix before the first proper invoice
|
||||||
|
|
||||||
|
Most of the work here is sequencing rather than drafting.
|
||||||
|
|
||||||
|
The process and the neutral are worth naming before the draft SIA report lands, not after it. The point at which the connection date first moves is the point at which positions harden, and a poor moment to negotiate who resolves what. [Mediation](/mediation/) is available at that stage without characterising anything.
|
||||||
|
|
||||||
|
The same words are worth carrying across the three contracts. Milestones defined one way in the construction contract, another way in the technology contract, and a third way against a Notification of Conditional Approval produce disputes about which document governs before anyone reaches the merits.
|
||||||
|
|
||||||
|
Which questions go to adjudication and which go to [commercial arbitration](/arbitration/) is worth deciding in advance. A determination is provisional by design and the merits stay open, so an adjudication treated as final is a dispute deferred rather than resolved.
|
||||||
|
|
||||||
|
Where the parties want one neutral to mediate and then arbitrate, that switch is worth settling at the outset rather than in the room. What I undertake about the switch, and about caucus material afterwards, is set out on [med-arb](/med-arb/); [how I run a file](/process/) sets out the rest.
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
title: 'What a System Impact Assessment actually evaluates'
|
||||||
|
description: 'What an IESO System Impact Assessment evaluates, who performs it, where the transmitter customer impact assessment sits, and what to look for in one.'
|
||||||
|
# publishDate is the drafting date. Set it on approval (D9).
|
||||||
|
publishDate: 2026-08-31
|
||||||
|
topics: ['technical-explainer']
|
||||||
|
practiceAreas: ['energy', 'technology']
|
||||||
|
readingTime: 8
|
||||||
|
draft: true
|
||||||
|
reviewedByPouya: false
|
||||||
|
---
|
||||||
|
|
||||||
|
## An SIA is not an assessment of the project
|
||||||
|
|
||||||
|
A connection date is a common term in Ontario energy contracts: EPC schedules,
|
||||||
|
equipment supply terms, the covenants around a commercial operation date. When
|
||||||
|
it moves, the System Impact Assessment is the document the argument turns to,
|
||||||
|
and it invites one specific misreading. An SIA does not assess the project; it
|
||||||
|
assesses what happens to the grid if the project connects to it.
|
||||||
|
|
||||||
|
The term is the Independent Electricity System Operator's own, and so is its
|
||||||
|
companion. In the IESO's description of the connection process, "New connections
|
||||||
|
or modifications to facilities connected to a transmitter's system are subject
|
||||||
|
to the IESO's system impact assessment (SIA) and the transmitter's customer
|
||||||
|
impact assessment (CIA)." Two documents, two authors. The IESO conducts the SIA.
|
||||||
|
The transmitter conducts the CIA. Treating the pair as one exhibit loses the
|
||||||
|
distinction most of these disputes turn on. Both sit in the IESO's and
|
||||||
|
transmitter's connection assessment and approval process, CAA in the IESO's
|
||||||
|
usage, and each application is given a unique CAA ID.
|
||||||
|
|
||||||
|
## What the assessment is actually of
|
||||||
|
|
||||||
|
The IESO describes its study step as assessing "the impact of [the] proposed new
|
||||||
|
or modified connection on the reliability of the integrated power system". Stage
|
||||||
|
one of the same process puts it more broadly: planned connections and
|
||||||
|
modifications "must be assessed to identify and mitigate any potential adverse
|
||||||
|
effect on the reliability of the electricity grid and its existing customers".
|
||||||
|
|
||||||
|
The subject of the assessment is the system, not the applicant. The IESO
|
||||||
|
describes its own function as coordinator and integrator of Ontario's
|
||||||
|
electricity system, balancing supply against provincial demand in real time and
|
||||||
|
directing the flow across the transmission lines, and it names five pillars of
|
||||||
|
reliability it is responsible for meeting: capacity, energy, transmission,
|
||||||
|
operability and ancillary services. An SIA asks whether a new connection
|
||||||
|
disturbs those.
|
||||||
|
|
||||||
|
That is also how to read a condition: the assessment's subject is the system,
|
||||||
|
so a condition speaks to how the system behaves with the facility on it. The
|
||||||
|
published process does not describe what conditions a report may carry — that
|
||||||
|
question is answered in the report. A pleading that reads a condition as an
|
||||||
|
admission of defective work is reading the document as though the other side
|
||||||
|
had commissioned it.
|
||||||
|
|
||||||
|
The IESO's connection-process FAQ names the tools: "The IESO uses DSA and PSSE
|
||||||
|
tools to conduct SIA studies." Naming the tools is not describing the study, and
|
||||||
|
the published process description does not say what a given study assumed,
|
||||||
|
modelled or tested. Where the argument is about the study itself, the report and
|
||||||
|
the record behind it are what answer it — not this outline of the process that
|
||||||
|
produced it.
|
||||||
|
|
||||||
|
## Where it sits, and how long it takes
|
||||||
|
|
||||||
|
The IESO runs connection in up to six stages: prepare application; obtain
|
||||||
|
conditional approval to connect; design and build; authorize market and program
|
||||||
|
participation; register equipment; commission equipment and validate
|
||||||
|
performance.
|
||||||
|
|
||||||
|
The SIA and the CIA both live in stage two, which "typically takes one year" on
|
||||||
|
the IESO's figure. Stage four typically takes about a month, stage five at least
|
||||||
|
three months, and the whole process "can take anywhere from a few months for
|
||||||
|
small modifications to existing facilities, to more than three years for major
|
||||||
|
modifications or to connect new facilities". All applicable stages have to be
|
||||||
|
completed before final approval to connect and the start of commercial
|
||||||
|
operation.
|
||||||
|
|
||||||
|
Which stages apply depends on what the facility connects to: "New or modified
|
||||||
|
connections to a transmitter's system are generally subject to all six stages,
|
||||||
|
while new or modified connections to a distributor's system may only be subject
|
||||||
|
to the first three."
|
||||||
|
|
||||||
|
That last point is about parties as much as engineering. Distribution
|
||||||
|
connections run through the distributor's own assessment process, and the IESO
|
||||||
|
records that a distributor may itself need to participate in the IESO's and the
|
||||||
|
transmitter's processes on the applicant's behalf. The entity handling the
|
||||||
|
assessment correspondence is not always the entity whose contract is in dispute.
|
||||||
|
|
||||||
|
## Two documents, two authors, two agreements
|
||||||
|
|
||||||
|
The sequence is where the SIA and the CIA come apart.
|
||||||
|
|
||||||
|
On the IESO's account of stage two, a pre-application meeting comes first. The
|
||||||
|
IESO then determines whether the application qualifies for a system impact
|
||||||
|
assessment or an expedited system impact assessment (ESIA). Once the application
|
||||||
|
and its deposit are in, it prepares an SIA agreement, "in accordance with
|
||||||
|
section 6.1.15.3 of chapter 0.4 of the Market Rules", for execution by the
|
||||||
|
applicant's authorized representative. Once all required information has been
|
||||||
|
provided, it carries out the studies and issues a draft SIA report to the
|
||||||
|
applicant and the transmitter for review and comments. After addressing the
|
||||||
|
comments on the draft or on a revised draft, it sends the final report to both,
|
||||||
|
with either a "Notification of conditional approval (NoCA)" or a "Notification
|
||||||
|
of disapproval with reasons (NoDR)".
|
||||||
|
|
||||||
|
The CIA runs on a different clock. The transmitter "generally initiates the
|
||||||
|
customer impact assessment (CIA) after the draft SIA report from the IESO", and
|
||||||
|
the CIA has its own agreement, between the applicant and the transmitter.
|
||||||
|
|
||||||
|
Three consequences follow. The assessments are generally sequenced rather than
|
||||||
|
parallel, so a slipped draft SIA ordinarily pushes the CIA start behind it.
|
||||||
|
There are two contracts before there are two reports, and the obligations
|
||||||
|
parties argue about, which information was owed and by when, live in those two
|
||||||
|
agreements. And the draft-and-comment step is a record: what a party said about
|
||||||
|
a study assumption at draft stage, and what it declined to say, sits in that
|
||||||
|
record alongside the final report.
|
||||||
|
|
||||||
|
## What to ask for, and what the record will not support
|
||||||
|
|
||||||
|
Where a dispute turns on an SIA, the productive order is the order in which the
|
||||||
|
record was made, not the order of the pleadings. The application first, and the
|
||||||
|
IESO's FAQ names the instrument: Form 128 initiates the SIA process. Then the
|
||||||
|
two agreements. Then the information the applicant supplied, with dates, because
|
||||||
|
the study step begins once all required information has been provided:
|
||||||
|
completeness is the hinge on which a year-long stage moves. Then the draft SIA
|
||||||
|
report and each set of comments on it. Then any revised draft. Then the final
|
||||||
|
report with the NoCA or the NoDR. Then the CIA.
|
||||||
|
|
||||||
|
The final report may already be public: the IESO states that it "will be
|
||||||
|
published on the IESO website in the Application Status table at the end of the
|
||||||
|
month in which it was finalized". Upstream of all this sits an optional
|
||||||
|
technical feasibility study, a "confidential service" provided "on a
|
||||||
|
cost-recovery basis to identify and mitigate potential issues with various
|
||||||
|
connection options"; whether one was run often explains why a particular option
|
||||||
|
was chosen.
|
||||||
|
|
||||||
|
Two arguments the published process will not carry. First, the queue. Ontario
|
||||||
|
has no interconnection queue. The IESO is explicit: it "is not using an
|
||||||
|
'interconnection queue'", adopting instead "the concept of 'committed projects'
|
||||||
|
that is defined in Section 3.3 of Market Manual 1.4: Connection Assessment and
|
||||||
|
Approval", and there is "no option to 'skip the interconnection queue'". Each
|
||||||
|
assessment follows the timelines in section 5.8 of that manual. A head of loss
|
||||||
|
framed as a lost place in a queue rests on a mechanism the system operator says
|
||||||
|
it does not operate.
|
||||||
|
|
||||||
|
Second, differential treatment. Renewable generation is not assessed
|
||||||
|
differently: "The treatment of new renewable generation facilities is no
|
||||||
|
different than any other new facility, the normal System Impact Assessment (SIA)
|
||||||
|
process applies to the connection of all generation facilities, renewable or
|
||||||
|
non-renewable, equally." A delay theory resting on technology-specific handling
|
||||||
|
has nothing in the published process to stand on.
|
||||||
|
|
||||||
|
## Why more contracts are about to depend on this
|
||||||
|
|
||||||
|
As this is written in August 2026, the gate in front of large loads is being
|
||||||
|
rebuilt around the assessment, not in place of it.
|
||||||
|
|
||||||
|
Section 28.1 of the Electricity Act, 1998 came into force on 11 December 2025.
|
||||||
|
Unless a transmitter or distributor is satisfied that the "specified connection
|
||||||
|
requirements" have been complied with, it "shall not" connect or reconnect a
|
||||||
|
"specified load facility". That category is defined to include a data centre
|
||||||
|
meeting criteria that may be set out in the regulations, and a facility whose
|
||||||
|
demand at the point of connection exceeds a prescribed amount. The section
|
||||||
|
arrived through Bill 40 of the 44th Parliament, 1st Session — the Protect
|
||||||
|
Ontario by Securing Affordable Energy for Generations Act, 2025 — which
|
||||||
|
received Royal Assent on 11 December 2025 as chapter 22 of the Statutes of
|
||||||
|
Ontario, 2025. Its transition rule turns on a date and a form: the section does
|
||||||
|
not apply where a connection request made in accordance with the Transmission
|
||||||
|
System Code or the Distribution System Code was submitted to the transmitter or
|
||||||
|
distributor before 3 June 2025, the day Bill 40 had First Reading.
|
||||||
|
|
||||||
|
The regulation that would fill in those criteria is the part to watch. The
|
||||||
|
Ministry of Energy and Mines' August 2026 consultation on an economic and
|
||||||
|
strategic assessment framework for new data centres describes the province as
|
||||||
|
"considering drafting" a regulation that would require new large data centres to
|
||||||
|
obtain government approval to connect or reconnect. Its comment period runs to
|
||||||
|
12 September 2026, and the same notice carries the Ministry's estimate that
|
||||||
|
data-centre connection proposals could total more than 10,000 MW cumulatively.
|
||||||
|
|
||||||
|
None of that displaces the SIA; it sits on top of it. A large load will still be
|
||||||
|
assessed for its effect on the reliability of the integrated power system, in
|
||||||
|
stage two, and its transmitter will still run a CIA. What changes is the number
|
||||||
|
of contracts written against a connection date whose gating conditions were
|
||||||
|
still under consideration as at August 2026.
|
||||||
|
|
||||||
|
## Reading the study and the contract on the same page
|
||||||
|
|
||||||
|
Grid connection disputes are argued through technical studies. I work as a
|
||||||
|
machine-learning and DevOps infrastructure engineer. The study assumptions, the
|
||||||
|
modelling inputs and the constraint that produced a condition are documents I
|
||||||
|
read directly and work through with the parties.
|
||||||
|
|
||||||
|
In a [mediation](/mediation/) that means a technical disagreement can be tested
|
||||||
|
in the room rather than deferred to an expert exchange. In a
|
||||||
|
[commercial arbitration](/arbitration/) it means the first procedural order can
|
||||||
|
be built around the documents that decide the matter.
|
||||||
|
[The shape of an engagement](/process/) sets out where each one starts.
|
||||||
|
|
||||||
|
Connection is one of the areas I take appointments in, set out at
|
||||||
|
[energy and grid disputes](/practice/energy/); its large-load half overlaps
|
||||||
|
with [technology and data disputes](/practice/technology/). Every date above is
|
||||||
|
as at August 2026, and the instruments move. Nothing here is applied to a
|
||||||
|
particular matter, and each party to a dispute should have their own legal
|
||||||
|
advice.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: 'When Med-Arb is the right answer, and when it is not'
|
||||||
|
description: 'Med-arb is mediation that converts to binding arbitration if it does not resolve. What it is, the fairness objection, and when it does not fit.'
|
||||||
|
# publishDate is the drafting date. Set it on approval (D9).
|
||||||
|
publishDate: 2026-08-31
|
||||||
|
topics: ['process-explainer']
|
||||||
|
practiceAreas: ['construction', 'shareholder']
|
||||||
|
readingTime: 8
|
||||||
|
draft: true
|
||||||
|
reviewedByPouya: false
|
||||||
|
---
|
||||||
|
|
||||||
|
import { CONDUCT_UNDERTAKINGS } from '../../data/site';
|
||||||
|
|
||||||
|
## What med-arb is, and what one appointment buys
|
||||||
|
|
||||||
|
Med-arb is mediation that converts to binding arbitration if the mediation does not resolve the dispute. One neutral is appointed for both phases. The matter is mediated. Whatever settles is recorded and is finished. Whatever does not settle moves to arbitration in front of the same neutral, on the terms the parties agreed before any of it began, and ends in an award. The two phases are the same two processes I offer on their own: [mediation](/mediation/) and [commercial arbitration](/arbitration/).
|
||||||
|
|
||||||
|
The commercial case for it is the gap it closes. A mediation that does not settle ordinarily means starting over. A new neutral, a second round of briefs, a fresh procedural timetable, and the same argument re-run in front of someone who did not watch the first attempt. Whatever narrowing the mediation achieved is re-argued, because nobody in the second room is bound by a concession made in the first. Med-arb keeps that work inside one appointment and one agreement.
|
||||||
|
|
||||||
|
## The objection is the right one
|
||||||
|
|
||||||
|
A mediator learns things a decision-maker is not supposed to know. What a party would actually take. What it is afraid of. What its own counsel thinks of the weak limb of its case. In med-arb the person holding that knowledge may go on to decide the matter.
|
||||||
|
|
||||||
|
Counsel who refuse med-arb on that ground are not being obstructive. The problem is structural rather than hypothetical, and no amount of drafting makes it disappear. What drafting decides is who carries it, and on what terms.
|
||||||
|
|
||||||
|
Two things carry it. The first is consent that is real: informed, in writing, and settled before the mediation phase starts, with the trigger for the switch and the treatment of caucus material dealt with in terms rather than left to good faith. Vagueness about either is what turns a procedural objection into a live one.
|
||||||
|
|
||||||
|
The second is what the neutral will actually do. That is a different question, and it is the one a party weighs when choosing between candidates. It is answered below rather than left to be inferred from the drafting.
|
||||||
|
|
||||||
|
## What I undertake
|
||||||
|
|
||||||
|
{/* ⚠️ RENDERED FROM `CONDUCT_UNDERTAKINGS`, NEVER TYPED — §4's third class says
|
||||||
|
so in terms: "The strings live in `CONDUCT_UNDERTAKINGS` in
|
||||||
|
`src/data/site.ts` and the pages render them, so the diff that would soften
|
||||||
|
one is visible on one constant rather than distributed through three
|
||||||
|
templates." They were hand-typed here in the first draft, which put a fourth
|
||||||
|
hand-copy of a published commitment outside that mechanism — and the
|
||||||
|
characteristic failure mode of this class is SILENT: nothing in a build fails
|
||||||
|
when a promise gets a little smaller, and the diff reads like tightening.
|
||||||
|
⚠️ AND IF THIS ARTICLE IS APPROVED FOR PUBLICATION, §4's rows (a), (b) and
|
||||||
|
(c) EACH GAIN A SURFACE and their "where it ships" column has to say so. */}
|
||||||
|
|
||||||
|
> {CONDUCT_UNDERTAKINGS.medArbSwitch}
|
||||||
|
|
||||||
|
> {CONDUCT_UNDERTAKINGS.medArbCaucus}
|
||||||
|
|
||||||
|
> {CONDUCT_UNDERTAKINGS.medArbStepOut}
|
||||||
|
|
||||||
|
The third is the expensive one, and its cost falls on the neutral rather than on the parties. It is also less costly in practice than it sounds. The arbitral phase runs on the evidentiary record, not on the caucus. Where the switch sits inside a whole engagement is set out under [the shape of an engagement](/process/).
|
||||||
|
|
||||||
|
## The rule set ADRIC publishes, and what a summary of it is worth
|
||||||
|
|
||||||
|
The ADR Institute of Canada's rules page carries ADRIC Med-Arb Rules. A discussion draft was presented to the membership at ADRIC's annual conference in November 2019. ADRIC's own framing of the process is worth quoting rather than paraphrasing:
|
||||||
|
|
||||||
|
> "Med-Arb is not merely the merging of separate mediation and arbitration processes, but a unique process designed to meet the needs of particular disputants. It involves nuances and complexities that can be fine-tuned to the needs of the parties as a customized dispute resolution process…"
|
||||||
|
|
||||||
|
ADRIC states that the rules are "designed to work in tandem with ADRIC's existing Mediation Rules and Arbitration Rules, integrating seamlessly", and on scope: "Although the Med-Arb Rules were drafted to assist in resolving domestic commercial disputes, parties may want to apply them to international or non-commercial disputes."
|
||||||
|
|
||||||
|
The two rule sets they sit alongside are published in their own right, as The ADRIC National Mediation Rules and as ADRIC Arbitration Rules – Effective 2025. The mediation document also carries a Model Dispute Resolution Clause, whose wording refers a dispute to mediation "pursuant to the National Mediation Rules of the ADR Institute of Canada, Inc."
|
||||||
|
|
||||||
|
Two cautions, and both apply to any account of a rule set, this one included. The first is currency. ADRIC records that "As of 2025, the ADRIC Mediation Committee is currently reviewing the Mediation Rules", and that "the existing rules remain in effect and should continue to be used until any updates are formally adopted". ADRIC's rules page carried that note when this piece was written, in August 2026; the current state of the review is on ADRIC's own page.
|
||||||
|
|
||||||
|
The second is that a description is not the rule set. What the rules require of the parties, of the neutral, and of caucus material sits in the documents themselves, not in anything quoted here. Nothing above states what any of them provides. Where this piece says what happens to caucus material, that is my own undertaking and not a rule.
|
||||||
|
|
||||||
|
## Where med-arb fits
|
||||||
|
|
||||||
|
**A deadlock that has to end by a date.** A closing, a fiscal year end, a lender's deadline, a milestone with liquidated damages behind it. Mediation on its own cannot promise an end. Arbitration on its own reaches one, and spends the interval as a contest. Med-arb reaches the date either way, and the parties know at the outset which way it will be reached if the room does not settle.
|
||||||
|
|
||||||
|
**A relationship that has to survive the dispute.** Shareholders in a closely held company. A general contractor and a trade it will meet again on the next tender. A distributor in the middle of a term. A unanimous shareholder agreement can specify how a dispute under it is resolved: section 108(6)(b) of Ontario's Business Corporations Act contemplates that where shareholders who are parties to such an agreement cannot agree on or resolve a matter pertaining to it, the matter may be referred to arbitration "under such procedures and conditions as are specified in the unanimous shareholder agreement". What that means for a particular company is a question for each party's own counsel. The dispute types are on [shareholder and family business](/practice/shareholder/).
|
||||||
|
|
||||||
|
**A narrow set of remaining issues.** Med-arb earns its keep when the mediation has done most of the work and two or three points are left, each capable of being decided on the documents. The parties take their settlement on everything they agreed and a decision on the residue, from one appointment, without a second procedural runway.
|
||||||
|
|
||||||
|
## Where it does not fit
|
||||||
|
|
||||||
|
**Where a statutory route already gives what med-arb is being asked to give.** Construction payment is the Ontario example. Part II.1 of the Construction Act, headed Construction Dispute Interim Adjudication, has been in force since 1 October 2019, and further amendments to the Act came into force on 1 January 2026. O. Reg. 264/25 prescribes the matters that may be adjudicated, among them the valuation of services or materials provided under the contract and payment in respect of a change order, whether approved or not. Ontario Dispute Adjudication for Construction Contracts, which states on its own site that it is the Authorized Nominating Authority under the Act, describes adjudication as "available as a right" and says a party "can commence an adjudication without the other Party's consent". An adjudicator must determine the referred matter no later than 30 days after receiving the referring party's documents, unless that date is extended in the way the Act allows, and the Act treats that determination as binding on the parties until the matter is determined by a court, by arbitration under the Arbitration Act, 1991, or by written agreement. A party that wants money moving on a change order does not need the other side's agreement to a process, and med-arb needs exactly that. Where the dispute is not a prescribed matter, or where the parties want the whole of it finally decided, the calculation changes. The dispute types are on [construction and infrastructure](/practice/construction/).
|
||||||
|
|
||||||
|
**Where the parties are not equally advised.** The caucus asymmetry compounds. One side with counsel and one without, or one a repeat player in this kind of dispute and the other in it once, is the configuration where a single neutral holding both roles is hardest to justify.
|
||||||
|
|
||||||
|
**Where one side needs a finding more than a settlement.** A party facing the same argument from a row of counterparties may want a reasoned determination on the record more than it wants this dispute closed quietly. Med-arb is built to settle first.
|
||||||
|
|
||||||
|
**Where consent is grudging.** A party that agrees to med-arb reluctantly has not agreed to it in the sense that matters, and the reluctance tends to come back in the arbitral phase as a complaint about the process. That is a reason not to take the appointment rather than a drafting problem.
|
||||||
|
|
||||||
|
## The name in the contract is worth reading twice
|
||||||
|
|
||||||
|
It is not arb-med. The two names are one syllable apart and the processes are not interchangeable. Where a contract names one of them, the thing to check is which one, and to check it against the rule set the contract adopts rather than against a page like this one.
|
||||||
|
|
||||||
|
Ontario's statute book names a version of the process, in a place written for family arbitration. O. Reg. 134/07 under the Arbitration Act, 1991 defines a "mediation-arbitration agreement" as a family arbitration agreement providing that "a mediation between the parties is to be conducted before any arbitration is conducted" and that "if the mediation fails, the mediator shall arbitrate the dispute and make a binding resolution of it". The same regulation requires that every arbitrator who conducts a family arbitration "shall have received the training approved by the Attorney General". I do not accept family law matters. The regulation is worth knowing about anyway: a search for the term surfaces it, and a definition written for family arbitration is easy to mistake for a general one.
|
||||||
|
|
||||||
|
I accept med-arb appointments in commercial matters. [Med-arb](/med-arb/) sets out the process and the objection at greater length. The part that cannot be fixed later is the switch, and it is settled in writing before the mediation starts or it is not settled at all.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Insights vocabulary. Build step 7b. The six territories are the strategy
|
||||||
|
* brief's (§VII), restated in `docs/03-content-spec.md` §Insights.
|
||||||
|
*
|
||||||
|
* `TOPIC_LABELS` is annotated `Record<InsightTopic, string>`, so adding a topic
|
||||||
|
* without labelling it does not compile. `src/content.config.ts` imports the
|
||||||
|
* tuple rather than repeating it.
|
||||||
|
*
|
||||||
|
* ⚠️ These are EDITORIAL categories, not claims. `credentialing` in particular
|
||||||
|
* labels writing *about* credentialing in the field — never a credential of his.
|
||||||
|
*/
|
||||||
|
export const INSIGHT_TOPICS = [
|
||||||
|
'process-explainer',
|
||||||
|
'regulatory-commentary',
|
||||||
|
'industry-commentary',
|
||||||
|
'reflection',
|
||||||
|
'technical-explainer',
|
||||||
|
'credentialing',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type InsightTopic = (typeof INSIGHT_TOPICS)[number];
|
||||||
|
|
||||||
|
/** Pill text. Sentence case, because these sit beside a serif headline rather
|
||||||
|
* than in the mono eyebrow style — `Pill` sets its own type. */
|
||||||
|
export const TOPIC_LABELS: Record<InsightTopic, string> = {
|
||||||
|
'process-explainer': 'Process',
|
||||||
|
'regulatory-commentary': 'Regulatory',
|
||||||
|
'industry-commentary': 'Industry',
|
||||||
|
reflection: 'Reflection',
|
||||||
|
'technical-explainer': 'Technical',
|
||||||
|
credentialing: 'Credentialing',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date display. `en-CA` with an explicit UTC time zone, and the time zone is the
|
||||||
|
* load-bearing part: `src/content.config.ts` parses frontmatter dates as
|
||||||
|
* midnight UTC, so formatting them in a local zone west of Greenwich renders
|
||||||
|
* the day before — a published date one day early, on every article, silently.
|
||||||
|
* Same failure the schema's round-trip check exists to stop, one layer down.
|
||||||
|
*/
|
||||||
|
export function formatArticleDate(date: Date): string {
|
||||||
|
return new Intl.DateTimeFormat('en-CA', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `<time datetime>` wants the date-only ISO form, in the same zone. */
|
||||||
|
export function isoDate(date: Date): string {
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
/**
|
||||||
|
* The intake form's fields. Spec: docs/05-backend-spec.md §Form fields.
|
||||||
|
*
|
||||||
|
* ⚠️ **THE LAMBDA HAS ITS OWN COPY OF THIS TABLE, AND THAT DUPLICATION IS
|
||||||
|
* DELIBERATE — IT IS NOT THE SES-DKIM SHAPE.** `docs/05` is explicit: *"Client
|
||||||
|
* side validation is a convenience. The Lambda re-validates everything."* A
|
||||||
|
* server that validates against a list the client shipped it is not validating;
|
||||||
|
* it is asking the attacker what the rules are. So `backend/intake/handler.mjs`
|
||||||
|
* carries an independent table and trusts nothing from here.
|
||||||
|
*
|
||||||
|
* What stops the two drifting is a check rather than a shared import:
|
||||||
|
* **`npm run check:intake`** asserts that the two tables agree on every field
|
||||||
|
* name, on which are required, on every length cap, and — since 2026-09-04 — on
|
||||||
|
* both honeypot names.
|
||||||
|
*
|
||||||
|
* ⚠️ **IT IS A KEYBOARD GATE, NOT A DEPLOY GATE, AND THIS COMMENT SAID IT "fails
|
||||||
|
* the build script".** It does not: `npm run build` is `astro build`, and
|
||||||
|
* `scripts/deploy-local.sh` runs `check`, `build` and `check:claims` and not this
|
||||||
|
* one. Run it yourself. A control described as running where it does not is
|
||||||
|
* `AGENTS.md` Q22, and this change set makes this check the only thing keeping
|
||||||
|
* the second honeypot's two names in step. Independent validation, mechanically cross-checked. If
|
||||||
|
* you add a field here, add it there, and the check will tell you if you didn't.
|
||||||
|
*
|
||||||
|
* WHAT THIS DATA IS, because it changes how the form is built (`docs/05`): in a
|
||||||
|
* live legal dispute this collects the inquirer's identity, **the names of
|
||||||
|
* opposing parties and their counsel**, and the nature of the dispute. That is
|
||||||
|
* personal information about identifiable third parties who have not consented
|
||||||
|
* and do not know the submission happened. Hence: no dollar amounts, no
|
||||||
|
* uploads, an explicit unchecked consent box, and a matter summary whose hint
|
||||||
|
* tells the writer not to put privileged detail in it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type IntakeField = {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
/** `select` and `radio` carry `options`; everything else does not. */
|
||||||
|
type: 'text' | 'email' | 'tel' | 'select' | 'radio' | 'textarea' | 'checkbox';
|
||||||
|
required: boolean;
|
||||||
|
/** Maximum characters. The Lambda REJECTS over this rather than truncating —
|
||||||
|
* a silently truncated matter summary is a misread file. */
|
||||||
|
max?: number;
|
||||||
|
options?: readonly string[];
|
||||||
|
/** Rendered under the field. */
|
||||||
|
hint?: string;
|
||||||
|
/** `autocomplete` token, where one genuinely applies. Omitted rather than
|
||||||
|
* guessed: a wrong token makes a browser fill the wrong value. */
|
||||||
|
autocomplete?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ **DO NOT ADD A DOLLAR-AMOUNT FIELD.** `docs/05`: *"Do not collect dollar
|
||||||
|
* amounts, document uploads, or anything the inquirer might reasonably treat as
|
||||||
|
* privileged. The intake call is for that."* The old site invented matter values;
|
||||||
|
* this form is the one place a real one could arrive and then need storing.
|
||||||
|
*/
|
||||||
|
export const INTAKE_FIELDS: readonly IntakeField[] = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: 'Your name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
max: 120,
|
||||||
|
autocomplete: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
max: 254, // RFC 5321 maximum path length; not a round number by choice.
|
||||||
|
autocomplete: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'phone',
|
||||||
|
label: 'Phone',
|
||||||
|
type: 'tel',
|
||||||
|
required: false,
|
||||||
|
max: 40,
|
||||||
|
autocomplete: 'tel',
|
||||||
|
hint: 'Optional.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
label: 'Your role',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: ['Counsel', 'In-house', 'Party', 'Institution', 'Other'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'organisation',
|
||||||
|
label: 'Firm or organisation',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
max: 160,
|
||||||
|
autocomplete: 'organization',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'process',
|
||||||
|
label: 'Process sought',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
/* The five from docs/05. "ENE" is expanded here because this is a form label
|
||||||
|
read by a party as well as by counsel, and §11's glossary authority is
|
||||||
|
about site copy rather than about abbreviating in a select. */
|
||||||
|
options: [
|
||||||
|
'Mediation',
|
||||||
|
'Arbitration',
|
||||||
|
'Med-Arb',
|
||||||
|
'Early neutral evaluation',
|
||||||
|
'Not sure',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'practiceArea',
|
||||||
|
label: 'Subject matter',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
/* THE SIX AREAS PLUS OTHER. Deliberately the short display names rather
|
||||||
|
than `PRACTICE_AREAS[].name`: those carry the full "Construction &
|
||||||
|
Infrastructure" form for a card heading, and a select is not a card. The
|
||||||
|
cross-check in `scripts/check-intake.mjs` compares these against the
|
||||||
|
handler's list, and `PRACTICE_SLUGS` remains the site's own source for
|
||||||
|
which areas exist. */
|
||||||
|
options: [
|
||||||
|
'Construction',
|
||||||
|
'Technology',
|
||||||
|
'Energy',
|
||||||
|
'Insurance',
|
||||||
|
'Shareholder',
|
||||||
|
'Cross-border',
|
||||||
|
'Other',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherParties',
|
||||||
|
label: 'Other parties',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
max: 300,
|
||||||
|
hint: 'Needed to run a conflicts check. Names only.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'opposingCounsel',
|
||||||
|
label: 'Opposing counsel',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
max: 300,
|
||||||
|
hint: 'Also for the conflicts check.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'summary',
|
||||||
|
label: 'What the dispute is about',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
max: 2000,
|
||||||
|
hint: 'A few sentences is enough. Please do not include privileged or confidential detail — that is what the intake call is for.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timing',
|
||||||
|
label: 'Timing',
|
||||||
|
type: 'select',
|
||||||
|
required: false,
|
||||||
|
options: ['Urgent', 'Within 30 days', 'Within 90 days', 'Exploring'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preferredContact',
|
||||||
|
label: 'Preferred reply',
|
||||||
|
type: 'radio',
|
||||||
|
required: false,
|
||||||
|
options: ['Email', 'Phone'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE CONSENT TEXT, VERBATIM FROM `docs/05` §Consent text. It is a legal notice
|
||||||
|
* the inquirer agrees to, so it is rendered from here and never retyped or
|
||||||
|
* reworded to fit a layout. Note that it says the same three things
|
||||||
|
* `NO_RETAINER_NOTICE` says — that constant is the site-wide statement and this
|
||||||
|
* is the one the inquirer ticks; both ship on `/contact/`, which is deliberate:
|
||||||
|
* `docs/01` requires the page to carry the notice, and `docs/05` requires the
|
||||||
|
* checkbox to carry it too.
|
||||||
|
*
|
||||||
|
* ⚠️ **IT NAMES SML COMPANY LTD — Pouya's ruling, 2026-09-02 — AND THREE
|
||||||
|
* CONSTRAINTS RIDE ON THAT.** **Name only, no terminal period**, and never
|
||||||
|
* beside the licence-status row (`AGENTS.md` §4). **`docs/05` §Consent text is a
|
||||||
|
* byte-identical second copy with no `check:` script over it**, so it moves with
|
||||||
|
* this string. And **`/legal/privacy/` must keep naming the same party** — it
|
||||||
|
* does, under §Why it is collected; a consent naming a company the linked policy
|
||||||
|
* never mentions is an accountability gap, not a matter of voice.
|
||||||
|
*/
|
||||||
|
export const CONSENT_TEXT =
|
||||||
|
'I consent to SML Company Ltd storing and using the information in this form ' +
|
||||||
|
'to respond to my inquiry and to run a conflicts check. I understand that ' +
|
||||||
|
'submitting this form does not create a retainer, does not appoint a neutral, ' +
|
||||||
|
'and does not itself establish a mediator–party relationship.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The honeypot. `docs/05`: *"hidden from sighted and screen-reader users, must
|
||||||
|
* be empty"*.
|
||||||
|
*
|
||||||
|
* ⚠️ **`display: none` PLUS `tabindex="-1"` PLUS `aria-hidden`, AND THE NAME
|
||||||
|
* MATTERS.** A honeypot named `honeypot` is skipped by any bot worth stopping;
|
||||||
|
* one named like a real field is filled. `company_website` is a plausible field
|
||||||
|
* on a professional intake form and is not one this form has. It must never be
|
||||||
|
* reachable by keyboard or announced by a screen reader — a honeypot that traps
|
||||||
|
* a screen-reader user is an accessibility defect that also loses a real inquiry.
|
||||||
|
*/
|
||||||
|
export const HONEYPOT_FIELD = 'company_website';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE SECOND HONEYPOT, AND IT IS A DIFFERENT TRAP RATHER THAN A SECOND COPY OF
|
||||||
|
* THE FIRST. Pouya's ruling, 2026-09-04, after two automated submissions walked
|
||||||
|
* through `HONEYPOT_FIELD` (`docs/05` §Observed abuse).
|
||||||
|
*
|
||||||
|
* ⚠️ **THE MECHANISM IS INVERTED, WHICH IS THE POINT.** `HONEYPOT_FIELD` is a
|
||||||
|
* text input that must arrive EMPTY — it catches a bot that fills every input it
|
||||||
|
* finds. The pair of 2026-09-04 did not fill it, so a second field of the same
|
||||||
|
* kind would catch them exactly as well as the first did: not at all.
|
||||||
|
*
|
||||||
|
* This is a CHECKBOX, and what it catches is a bot that sets every control it
|
||||||
|
* enumerates rather than one that fills every text field.
|
||||||
|
*
|
||||||
|
* ⚠️ **WHAT IT IS AIMED AT, AND WHAT THE EVIDENCE ACTUALLY SUPPORTS — READ THIS
|
||||||
|
* BEFORE RELYING ON IT.** An earlier version of this comment said the decoy
|
||||||
|
* targets *"a behaviour anything reaching validation must have"*, because the
|
||||||
|
* consent box is required and unchecked by default, so a submission that
|
||||||
|
* validated must have sent `consent=on`. **That argument does not survive its own
|
||||||
|
* premise.** The 2026-09-04 pair did NOT fill the text honeypot, so they are
|
||||||
|
* selective about hidden fields — and a bot selective enough to skip a hidden
|
||||||
|
* text input is selective enough to skip a hidden checkbox. Sending `consent=on`
|
||||||
|
* shows only that it knows one field name, not that it ticks everything it finds.
|
||||||
|
*
|
||||||
|
* **So this trap is very likely INERT against the traffic it was built from**,
|
||||||
|
* and it is defence in depth against a different and common class: the bot that
|
||||||
|
* enumerates controls and sets all of them. That is worth having and it is not
|
||||||
|
* what the observation proved. `docs/05` §Observed abuse states the same limit;
|
||||||
|
* the two must not drift, because the tempting sentence is the confident one.
|
||||||
|
*
|
||||||
|
* ⚠️ **ABSENCE IS THE PASS, AND SO IS AN EMPTY VALUE.** A browser sends nothing
|
||||||
|
* at all for an unchecked box, so every way this field can fail to arrive — a
|
||||||
|
* stripping extension, a proxy, a future template that drops it — reads as a
|
||||||
|
* HUMAN; and a serialiser that emits `updates_optin=` without reading the
|
||||||
|
* checked state reads as one too, because the handler tests for a NON-EMPTY
|
||||||
|
* value rather than for presence. The failure mode of a trap is a lost legal
|
||||||
|
* inquiry that looks like a successful one, and this trap fires only on
|
||||||
|
* something that deliberately ticked a box no person can see.
|
||||||
|
*
|
||||||
|
* The name is a plausible marketing opt-in, which is what a bot expects to find
|
||||||
|
* and a real form here does not have. Hidden the same way as the first — the
|
||||||
|
* hiding is standard, the mechanism is not.
|
||||||
|
*/
|
||||||
|
export const DECOY_CHECKBOX_FIELD = 'updates_optin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WHERE THE FORM POSTS — AND IT IS A SAME-ORIGIN PATH, NOT THE API GATEWAY
|
||||||
|
* HOSTNAME. This is a design decision with four consequences, taken at step 8
|
||||||
|
* and recorded because the obvious implementation is the other one.
|
||||||
|
*
|
||||||
|
* The obvious version posts to the execute-api hostname `AGENTS.md` §7 records.
|
||||||
|
* Posting to `/api/intake` instead, with a CloudFront behaviour routing `/api/*`
|
||||||
|
* to that origin:
|
||||||
|
*
|
||||||
|
* ⚠️ **AND IT IS ALL LIVE SINCE 2026-09-02** — see the closing paragraph of this
|
||||||
|
* block. The same stale sentence was corrected in `handler.mjs`, `docs/01` and
|
||||||
|
* `docs/05` before it was corrected here; this note was added, in the same pass,
|
||||||
|
* ABOVE a paragraph that still said the opposite twenty lines below it. **A note
|
||||||
|
* asserting a correction is not the correction**, and the two sat contradicting
|
||||||
|
* each other until `adversarial-reviewer` round 2.
|
||||||
|
*
|
||||||
|
* 1. **`Content-Security-Policy: form-action 'self'`** — `docs/05` specifies
|
||||||
|
* `form-action 'self' <api-endpoint>`; with a same-origin post the second
|
||||||
|
* term is unnecessary, so the policy is strictly tighter.
|
||||||
|
* 2. **No cross-origin POST at all**, so no CORS question for the form. (CORS
|
||||||
|
* never governed it anyway — a form POST is a top-level navigation, not an
|
||||||
|
* XHR, so it is exempt from preflight. `docs/05`'s CORS line protects the
|
||||||
|
* endpoint against scripted calls from other origins, which is a different
|
||||||
|
* control, and the handler's `Origin` check is what covers the form.)
|
||||||
|
* 3. **The endpoint id stays out of the HTML.** ⚠️ It is NOT true that §7 is
|
||||||
|
* the only place it lives, and this bullet said so: `.env.example` still
|
||||||
|
* sets `PUBLIC_INTAKE_ENDPOINT` to the full execute-api hostname. That
|
||||||
|
* variable is now read by nothing, so the line is dead as well as
|
||||||
|
* duplicative. It is not edited here because this environment denies read
|
||||||
|
* access to `.env.example`, and nothing may edit a file it cannot read —
|
||||||
|
* it is in the batched list for Pouya instead. Found by
|
||||||
|
* `adversarial-reviewer` round 2, against an unscoped sweep.
|
||||||
|
* 4. **Submitting locally does nothing.** `astro dev` has no `/api/` route, so
|
||||||
|
* a POST 404s. Under the alternative, clicking Submit on a laptop would
|
||||||
|
* write a real DynamoDB record and send two real emails.
|
||||||
|
*
|
||||||
|
* ⚠️ **THE COST, WHICH WAS REAL AND IS NOW PAID: THE FORM DID NOT WORK UNTIL
|
||||||
|
* THAT CLOUDFRONT BEHAVIOUR EXISTED AND THE HANDLER WAS DEPLOYED.** Both ran at
|
||||||
|
* cutover on 2026-09-02 — `AGENTS.md` §7 holds the state and this comment does
|
||||||
|
* not restate it. `/contact/` still publishes the email address beside the form,
|
||||||
|
* which is now a courtesy rather than a fallback.
|
||||||
|
*/
|
||||||
|
export const INTAKE_ACTION = '/api/intake';
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* The Open Graph card registry — one entry per page that does NOT use the
|
||||||
|
* portrait. Spec: docs/04-seo-spec.md §Metadata; discharges `AGENTS.md` R15.
|
||||||
|
*
|
||||||
|
* ⚠️ **EVERY HEADLINE HERE IS ITS PAGE'S OWN `<h1>`, VERBATIM, AND THAT IS A
|
||||||
|
* COMPLIANCE MECHANISM RATHER THAN A CONVENIENCE.** Text baked into a JPEG is
|
||||||
|
* text `npm run check:claims` cannot see, and under D20 that script is the only
|
||||||
|
* per-step claims control there is. New prose on a card would therefore be the
|
||||||
|
* one kind of copy on this site with no mechanical check over it at all.
|
||||||
|
*
|
||||||
|
* So a card asserts nothing its page does not already assert in auditable HTML —
|
||||||
|
* and `npm run og:proof` **verifies it**, by pulling the `<h1>` out of each
|
||||||
|
* built page and comparing. A headline edited here without editing the page
|
||||||
|
* fails that check; so does the reverse. `scripts/og-proof.mjs` is where the
|
||||||
|
* comparison lives.
|
||||||
|
*
|
||||||
|
* THE EYEBROWS ARE EACH PAGE'S FIRST `.eyebrow`, VERBATIM, on the same
|
||||||
|
* reasoning. `/` and `/about/` are absent by design — Q40 decided the portrait
|
||||||
|
* for those two and called it "not an interim".
|
||||||
|
*
|
||||||
|
* ONE ENTRY PER PAGE, AND A PAGE WITHOUT ONE IS A BUILD ERROR (`SEO.astro`).
|
||||||
|
* The alternative — falling back to the portrait when no entry exists — is how
|
||||||
|
* "portrait everywhere" became an eighteen-page interim in the first place: it
|
||||||
|
* fails silently and looks intentional.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Articles are not listed here. Their cards come from the collection itself —
|
||||||
|
* see `src/pages/og/[...slug].jpg.ts`, which is the only place that knows
|
||||||
|
* about both sources, so the two cannot disagree about which cards exist. */
|
||||||
|
export const OG_CARDS: Record<string, { eyebrow: string; headline: string }> = {
|
||||||
|
'/mediation/': {
|
||||||
|
eyebrow: 'Mediation',
|
||||||
|
headline: 'A mediator decides nothing.',
|
||||||
|
},
|
||||||
|
'/arbitration/': {
|
||||||
|
eyebrow: 'Arbitration',
|
||||||
|
headline: 'Sole, party-appointed, co-arbitration.',
|
||||||
|
},
|
||||||
|
'/med-arb/': {
|
||||||
|
eyebrow: 'Med-Arb',
|
||||||
|
headline: 'One neutral. Two processes. One agreement, written first.',
|
||||||
|
},
|
||||||
|
'/practice/': {
|
||||||
|
eyebrow: 'Practice',
|
||||||
|
headline: 'Six areas, one reason.',
|
||||||
|
},
|
||||||
|
'/practice/construction/': {
|
||||||
|
eyebrow: 'Construction',
|
||||||
|
headline: 'The dispute is in the change orders.',
|
||||||
|
},
|
||||||
|
'/practice/technology/': {
|
||||||
|
eyebrow: 'Technology',
|
||||||
|
headline: 'I read the contract and the system.',
|
||||||
|
},
|
||||||
|
'/practice/energy/': {
|
||||||
|
eyebrow: 'Energy',
|
||||||
|
headline:
|
||||||
|
'Grid disputes are engineering disputes with a regulator attached.',
|
||||||
|
},
|
||||||
|
'/practice/insurance/': {
|
||||||
|
eyebrow: 'Insurance',
|
||||||
|
headline: "Private mediation, not the Tribunal's case conference.",
|
||||||
|
},
|
||||||
|
'/practice/shareholder/': {
|
||||||
|
eyebrow: 'Shareholder',
|
||||||
|
headline: 'The company still has to trade on Monday.',
|
||||||
|
},
|
||||||
|
'/practice/cross-cultural/': {
|
||||||
|
eyebrow: 'Cross-cultural',
|
||||||
|
headline: 'A session in the language the deal was made in.',
|
||||||
|
},
|
||||||
|
'/process/': {
|
||||||
|
eyebrow: 'Process',
|
||||||
|
headline: 'The shape of an engagement.',
|
||||||
|
},
|
||||||
|
'/for-parties/': {
|
||||||
|
eyebrow: 'For parties',
|
||||||
|
headline: 'What happens at a mediation.',
|
||||||
|
},
|
||||||
|
'/fees/': {
|
||||||
|
eyebrow: 'Fees',
|
||||||
|
headline: 'Published in full, including what overruns cost.',
|
||||||
|
},
|
||||||
|
'/insights/': {
|
||||||
|
eyebrow: 'Insights',
|
||||||
|
headline: 'Notes on process, regulation, and the technical record.',
|
||||||
|
},
|
||||||
|
'/contact/': {
|
||||||
|
eyebrow: 'Contact',
|
||||||
|
headline: 'Start with a confidential call.',
|
||||||
|
},
|
||||||
|
/* The two POST-redirect-GET landing pages. Both are `noindex` and excluded
|
||||||
|
from the sitemap, and neither is a URL anyone would share — but they get
|
||||||
|
cards for the same reason every other page does: `SEO.astro` throws without
|
||||||
|
an entry, and the alternative is a silent portrait fallback, which is the
|
||||||
|
failure R15 exists to prevent. Cheap, and it keeps one rule with no
|
||||||
|
exceptions. */
|
||||||
|
'/contact/received/': {
|
||||||
|
eyebrow: 'Received',
|
||||||
|
headline: 'Your inquiry has been received.',
|
||||||
|
},
|
||||||
|
'/contact/could-not-send/': {
|
||||||
|
eyebrow: 'Not sent',
|
||||||
|
headline: 'That inquiry was not recorded.',
|
||||||
|
},
|
||||||
|
/* `/bio/` is the source of the one-page PDF (R16). `noindex` and out of the
|
||||||
|
sitemap, but it still needs an entry — one rule, no exceptions. */
|
||||||
|
'/bio/': {
|
||||||
|
eyebrow: 'Bio',
|
||||||
|
headline: 'Pouya Lajevardi',
|
||||||
|
},
|
||||||
|
/* `/404/` builds to `dist/404.html` and is `noindex`, but a shared 404 link is
|
||||||
|
exactly the kind of URL that gets pasted into a chat window — so it gets a
|
||||||
|
card on the same one-rule-no-exceptions basis as `/bio/`. */
|
||||||
|
'/404/': {
|
||||||
|
eyebrow: 'Not found',
|
||||||
|
headline: 'That page is not here.',
|
||||||
|
},
|
||||||
|
'/legal/privacy/': {
|
||||||
|
eyebrow: 'Privacy',
|
||||||
|
headline: 'What the intake form collects, and for how long.',
|
||||||
|
},
|
||||||
|
'/legal/terms/': {
|
||||||
|
eyebrow: 'Terms',
|
||||||
|
headline: 'Terms of use for this site.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** `/` and `/about/` — the portrait, decided rather than deferred (Q40). */
|
||||||
|
export const PORTRAIT_PAGES = ['/', '/about/'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AN ARTICLE'S CARD, DERIVED HERE RATHER THAN IN THE ENDPOINT — and the move is
|
||||||
|
* the fix for a defect, not a tidy-up.
|
||||||
|
*
|
||||||
|
* `scripts/og-proof.mjs` compares every card's headline against its page's own
|
||||||
|
* `<h1>`, which is what keeps card copy inside the claim register — text baked
|
||||||
|
* into a JPEG is text `check:claims` cannot grep. Articles have no registry
|
||||||
|
* entry, so the first version of that check **skipped them entirely**, and
|
||||||
|
* `adversarial-reviewer` proved it by putting `DELIBERATELY WRONG CARD TEXT` in
|
||||||
|
* the endpoint and watching the check pass. **The first repair was worse**: it
|
||||||
|
* compared the article's `<h1>` against itself, which is a tautology, and the
|
||||||
|
* same probe passed again.
|
||||||
|
*
|
||||||
|
* The working fix is not a cleverer comparison — it is to leave nothing to
|
||||||
|
* compare. The derivation lives here, both the endpoint and the proof script
|
||||||
|
* call it, and the endpoint no longer holds a headline literal that could
|
||||||
|
* disagree with anything. What the proof script then checks is the one thing
|
||||||
|
* still capable of drifting: whether the article's own `title` is what the route
|
||||||
|
* renders as its `<h1>`.
|
||||||
|
*/
|
||||||
|
export function articleCard(title: string): {
|
||||||
|
eyebrow: string;
|
||||||
|
headline: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
eyebrow: 'Insights',
|
||||||
|
/* The headline is the article's title, which is also its `<title>` (docs/04)
|
||||||
|
and its `<h1>`. Never `description`: a 140–160 character sentence cannot
|
||||||
|
set as a display line, and truncating it would put half a sentence in
|
||||||
|
front of the reader the card exists for. */
|
||||||
|
headline: title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `/practice/construction/` → `practice-construction`, and back again in the
|
||||||
|
* endpoint. Flattening rather than nesting because an Astro rest route serving
|
||||||
|
* `/og/a/b.jpg` has to reassemble the path anyway, and one transform in one
|
||||||
|
* place is cheaper to keep true than two.
|
||||||
|
*
|
||||||
|
* A hyphen cannot collide here: no page slug in `docs/01`'s sitemap contains
|
||||||
|
* one at a position that would reproduce another page's flattened form, and
|
||||||
|
* `PRACTICE_SLUGS` is the only nested namespace besides `/insights/` and
|
||||||
|
* `/legal/`. If a slug is ever added that would collide, `og:proof` catches it —
|
||||||
|
* two pages resolving to one card file means one page's `<h1>` will not match.
|
||||||
|
*/
|
||||||
|
export function ogSlug(pathname: string): string {
|
||||||
|
return pathname.replace(/^\/|\/$/g, '').replace(/\//g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The site-root-relative path of a page's card. */
|
||||||
|
export function ogCardPath(pathname: string): string {
|
||||||
|
return `/og/${ogSlug(pathname)}.jpg`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,728 @@
|
|||||||
|
/**
|
||||||
|
* The copy for the six `/practice/<area>/` pages. Rendered by
|
||||||
|
* `src/pages/practice/[slug].astro`, which owns the shape; this file owns the
|
||||||
|
* words.
|
||||||
|
*
|
||||||
|
* ⚠️ **EVERY FACT ABOUT THE WORLD ON THESE PAGES IS SOURCED IN
|
||||||
|
* `docs/reference/`, AND A FACT THAT IS NOT THERE IS NOT PUBLISHED HERE.**
|
||||||
|
* That is R14 — *anything a spec makes a claim about must be reachable from the
|
||||||
|
* repository* — applied to the six pages that need the most external material.
|
||||||
|
* The extracts were retrieved 2026-08-29 and each carries a "What this does NOT
|
||||||
|
* establish" section. Read that section before adding a sentence.
|
||||||
|
*
|
||||||
|
* - `ontario-construction-act.md` Construction Act, adjudication, ODACC,
|
||||||
|
* prompt payment; Darlington and Bruce C
|
||||||
|
* - `ontario-energy-regulatory.md` OEB s. 92, IESO connection assessment,
|
||||||
|
* Bill 40, Electricity Act s. 28.1
|
||||||
|
* - `ontario-sabs-lat.md` SABS, the MIG, LAT-AABS, caseload
|
||||||
|
* - `lat-case-conference.md` why the LAT's case conference is not this
|
||||||
|
* - `ontario-shareholder-remedies.md` OBCA/CBCA oppression, OBCA s. 108(6)
|
||||||
|
* - `adr-institution-names.md` the exact names of the rule sets
|
||||||
|
*
|
||||||
|
* ⚠️ **NO PAGE MAY CLAIM VOLUME, HISTORY OR A NAMED MATTER.** §4's publication
|
||||||
|
* gate for a practice area has two conditions and the second is this page's
|
||||||
|
* job: *"The page frames it as positioning, not as claimed history."* docs/03:
|
||||||
|
* *"Built to facilitate procurement and subcontract disputes on Ontario's
|
||||||
|
* megaproject pipeline" — not "extensive experience resolving".* A page that
|
||||||
|
* claims volume fails the gate even though the label passes.
|
||||||
|
*
|
||||||
|
* ⚠️ **AND NAMING A PROJECT IS NOT CLAIMING A CONNECTION TO IT.** Darlington
|
||||||
|
* and Bruce C are named as programme context because `docs/01` names them.
|
||||||
|
* `ontario-construction-act.md` records, in terms, that nothing retrieved links
|
||||||
|
* either project to any dispute, adjudication, lien or payment proceeding — and
|
||||||
|
* that it must not be used to imply one. The copy names the programme, never a
|
||||||
|
* matter.
|
||||||
|
*
|
||||||
|
* ⚠️ **STATUTE IS DESCRIBED, NEVER APPLIED.** §4 bars this repository from
|
||||||
|
* concluding a proposition of law, and D13 governs what may be implied about
|
||||||
|
* who is entitled to advise on one. So these pages say what an instrument
|
||||||
|
* provides and where it sits, and each section that recites one carries a note
|
||||||
|
* pointing the reader to their own counsel for what it means on their file.
|
||||||
|
* **Limitation periods are deliberately absent** — lien preservation and
|
||||||
|
* perfection deadlines are the single highest-consequence thing on these pages
|
||||||
|
* to get wrong, and nobody should take one from a marketing page. The extract
|
||||||
|
* has them; the site does not.
|
||||||
|
*/
|
||||||
|
import type { PracticeSlug } from './site';
|
||||||
|
import type { PublishableServiceType } from './schema';
|
||||||
|
|
||||||
|
/** A paragraph. `lead` renders as the bolded opener the rest of the site uses. */
|
||||||
|
export type PracticePara = { lead?: string; text: string };
|
||||||
|
|
||||||
|
export type PracticeSection = {
|
||||||
|
eyebrow: string;
|
||||||
|
heading: string;
|
||||||
|
lede?: string;
|
||||||
|
paragraphs: readonly PracticePara[];
|
||||||
|
/** Set off with a gold rule. One sentence or two — never a section's worth.
|
||||||
|
* docs/01 on the family-law exclusion: "One sentence, not a section: it
|
||||||
|
* saves a wasted intake call, which is the only reason it earns its place."
|
||||||
|
* A disclaimer that grows into a paragraph reads as defensive. */
|
||||||
|
note?: string;
|
||||||
|
/** Declared, not computed from the index, so inserting a section cannot
|
||||||
|
* silently restyle the ones below it. */
|
||||||
|
ground?: 'alt' | 'inverse';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PracticePage = {
|
||||||
|
/**
|
||||||
|
* The processes this area actually carries, for the `Service` node. **Per
|
||||||
|
* area, because the JSON-LD has to say what the page says** — `/practice/
|
||||||
|
* insurance/` offers private mediation and recites the LAT's exclusive
|
||||||
|
* jurisdiction, so it must not assert commercial arbitration to a crawler.
|
||||||
|
* `serviceLabel` leads the node's name and must match what `serviceType`
|
||||||
|
* carries.
|
||||||
|
*/
|
||||||
|
serviceType: PublishableServiceType | readonly PublishableServiceType[];
|
||||||
|
serviceLabel: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
h1: string;
|
||||||
|
lede: string;
|
||||||
|
disputeTypesLede: string;
|
||||||
|
disputeTypes: readonly { name: string; body: string }[];
|
||||||
|
sections: readonly PracticeSection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
construction: {
|
||||||
|
serviceType: ['Mediation', 'Commercial arbitration'],
|
||||||
|
serviceLabel: 'Mediation and arbitration',
|
||||||
|
title: 'Construction Disputes · Pouya Lajevardi · Toronto · Q.Med',
|
||||||
|
description:
|
||||||
|
'Liens, delay and change-order claims, subcontract and deficiency ' +
|
||||||
|
'disputes. A neutral who reads the schedule and the change orders, not ' +
|
||||||
|
'a summary of them.',
|
||||||
|
h1: 'The dispute is in the change orders.',
|
||||||
|
lede:
|
||||||
|
'Construction files turn on documents nobody wants to read: the ' +
|
||||||
|
'baseline programme, the as-built, the fourteenth revision of a scope ' +
|
||||||
|
'letter. I read them. That is most of what a construction mediation ' +
|
||||||
|
'needs, and it is the work that happens before the day.',
|
||||||
|
disputeTypesLede:
|
||||||
|
'Commercial construction and infrastructure. Owner, contractor, ' +
|
||||||
|
'subcontractor and consultant.',
|
||||||
|
disputeTypes: [
|
||||||
|
{
|
||||||
|
name: 'Lien claims',
|
||||||
|
body: 'Priority, holdback, trust and set-off arguments running alongside the substantive dispute rather than instead of it.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delay and disruption',
|
||||||
|
body: 'Concurrency, float ownership, acceleration, and the gap between a critical-path analysis and what actually happened on site.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Change orders and scope',
|
||||||
|
body: 'Directed change, constructive change, and the familiar position that the work was always in the base scope.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Deficiencies',
|
||||||
|
body: 'Whether the work meets the specification, whether the specification was buildable, and what the cost to correct actually is.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Subcontract and payment',
|
||||||
|
body: 'Pay-when-paid, back-charges, and the disputes that surface when a prompt-payment clock starts running.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Consultant and design',
|
||||||
|
body: 'Coordination failures, errors and omissions, and the split between design responsibility and means and methods.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Why me',
|
||||||
|
heading:
|
||||||
|
'Litigation exposure in the same matters, and an engineer who reads the record.',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'Construction is one of the matter types behind my active litigation exposure at a Toronto litigation and ADR boutique. That is not a claim to have decided construction cases. It is a claim to know how these files are actually built, what a set of productions looks like, and which arguments survive contact with a schedule.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'The second half is the one worth being specific about. I work as an infrastructure engineer, so a programme, a delay analysis and a set of site records are documents I can interrogate rather than take on trust from whichever expert explains them most confidently. In a construction mediation that is usually where the day is won or lost.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The machinery',
|
||||||
|
heading: 'These disputes now run inside a statutory timetable.',
|
||||||
|
lede: 'Which changes what a mediation or an arbitration is for.',
|
||||||
|
ground: 'inverse',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
lead: 'Interim adjudication.',
|
||||||
|
text: 'Part II.1 of the Construction Act — "Construction Dispute Interim Adjudication" — has been in force since 1 October 2019. An adjudicator must determine the referred matter no later than 30 days after receiving the referring party\'s documents, unless that date is extended in the way the Act allows. A determined amount is payable within 15 days of the determination being communicated. Judicial review is available only with leave of the Divisional Court.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'A designated authority runs it.',
|
||||||
|
text: 'The Act empowers the Minister to designate an Authorized Nominating Authority, and Ontario Dispute Adjudication for Construction Contracts — ODACC — states on its own site that it is that authority.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'Prompt payment sets the clock.',
|
||||||
|
text: 'Part I.1 came into force on the same day. A proper invoice goes to the owner monthly unless the contract says otherwise; the owner pays within 28 days unless it delivers a notice of non-payment; and a contractor paid in full pays each subcontractor within seven days.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'And arbitration is where it lands.',
|
||||||
|
text: "The Act treats an adjudicator's determination as interim — binding until the matter is finally decided in a court proceeding, by written agreement, or by arbitration under the Arbitration Act, 1991. The Act creates no mediation process of its own. So the question a party is actually choosing between is which of those three finally resolves it, and how quickly.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: "Described so the process is legible, not applied to anyone's file. Everything above is sourced in docs/reference/ontario-construction-act.md against the Act itself; what it means for a particular contract is a question for each party's own counsel.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The context',
|
||||||
|
heading: 'Ontario is building, and building generates disputes.',
|
||||||
|
ground: 'alt',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'Ontario Power Generation holds a licence to construct a BWRX-300 small modular reactor at Darlington, granted by the Canadian Nuclear Safety Commission in April 2025, and applied in March 2026 for a licence to operate it. Bruce Power has a federal impact assessment under way for the Bruce C project, aimed at creating an option for up to 4,800 megawatts at the existing site, with reactor technology not yet selected.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Programmes on that scale run for years, through dozens of trade contracts. This practice is built to facilitate procurement and subcontract disputes on that pipeline. I am naming it as the shape of the market, not as a list of files — nothing here is a claim to be on any of these projects.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
technology: {
|
||||||
|
serviceType: ['Mediation', 'Commercial arbitration'],
|
||||||
|
serviceLabel: 'Mediation and arbitration',
|
||||||
|
title: 'Technology and Data Disputes · Pouya Lajevardi · Toronto',
|
||||||
|
description:
|
||||||
|
'Software contracts, SLA and MSA failures, data residency, AI vendor ' +
|
||||||
|
'diligence, IP and licensing — before a neutral who reads the system, ' +
|
||||||
|
'not only the contract.',
|
||||||
|
h1: 'I read the contract and the system.',
|
||||||
|
lede:
|
||||||
|
'This is the page the rest of the practice is built around. A technology ' +
|
||||||
|
'dispute usually turns on what a system actually did, and that question ' +
|
||||||
|
'is normally answered to a neutral second-hand, by whichever expert is ' +
|
||||||
|
'more fluent. I can read the primary material.',
|
||||||
|
disputeTypesLede:
|
||||||
|
'Commercial technology matters between businesses. Vendor, customer, ' +
|
||||||
|
'integrator and investor.',
|
||||||
|
disputeTypes: [
|
||||||
|
{
|
||||||
|
name: 'Software contracts',
|
||||||
|
body: 'Failed implementations, scope and acceptance disputes, and the argument about whether the product was ever capable of the thing that was demonstrated.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SLA and MSA breakdowns',
|
||||||
|
body: 'Availability and credit disputes, definitions of downtime that nobody checked against the monitoring, and termination-for-cause standoffs.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Data residency and processing',
|
||||||
|
body: 'Where data actually sits, which sub-processors touch it, and whether the processing terms match the architecture that was built.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AI vendor diligence',
|
||||||
|
body: 'Model performance against a warranted benchmark, training-data provenance, evaluation methodology, and what a model card does and does not say.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IP and licensing',
|
||||||
|
body: 'Ownership of work product, open-source obligations, scope-of-licence and field-of-use disputes, and derivative-work arguments.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cloud and sub-processor',
|
||||||
|
body: 'Shared-responsibility gaps, migration and egress disputes, and outages whose cause sits one layer below the contracting party.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Why me',
|
||||||
|
heading:
|
||||||
|
'The claim is engineering practice, so let me state it as one.',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'I work as a machine-learning and infrastructure engineer. Not "technologically literate", not "familiar with the sector" — I build and operate these systems, now, not formerly.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'What that buys a party is specific. An API trace, a set of monitoring dashboards, a model card, an evaluation harness, an architecture diagram and a data-processing addendum are all documents I can read directly. In a mediation that means the technical dispute can be tested in the room instead of deferred to an expert exchange that costs another quarter and often does not resolve it either.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'It also means I can tell which technical disagreements are real. Some technology disputes are contract disputes wearing technical costume, and a neutral who cannot tell the difference will let a party spend heavily proving something that was never in issue.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The backdrop',
|
||||||
|
heading: 'What is actually in force, as of this page.',
|
||||||
|
lede: 'Named precisely, because this is the area where a confident wrong statement is easiest to make.',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
lead: 'PIPEDA, still.',
|
||||||
|
text: 'The Personal Information Protection and Electronic Documents Act remains the federal private-sector privacy statute. Bill C-27, which would have enacted the Consumer Privacy Protection Act and the Artificial Intelligence and Data Act, died without royal assent when the session ended, and was not reinstated. A newer bill — C-36, for a Protecting Privacy and Consumer Data Act — was introduced in June 2026 and was at second reading when this page was written. Canada has no federal AI statute.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: "Ontario's AI-relevant statute has its artificial-intelligence obligations switched off.",
|
||||||
|
text: 'The Enhancing Digital Security and Trust Act, 2024 conditions the artificial-intelligence obligations in its section 5 on regulations prescribing which public sector entities they apply to and in what circumstances. Two regulations have been made under it — one on cyber security, one on digital technology affecting people under 18 — and neither is the AI one.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
/* THE LEAD WAS "And no federal or Ontario statute requires data to
|
||||||
|
be stored in Canada" — a universal over the FOUR instruments the
|
||||||
|
extract actually checked (PIPEDA, Ontario FIPPA, PHIPA and
|
||||||
|
O. Reg. 329/04), which is the shape §4 Forbidden's struck Q39
|
||||||
|
universal bars in both directions: this repository does not
|
||||||
|
conclude a proposition of law. Named instruments only. */
|
||||||
|
/* ⚠️ NAMED, AND THE FIRST CORRECTION ONLY NARROWED THE CLASS.
|
||||||
|
"Ontario's public-sector privacy statutes" is a class of two —
|
||||||
|
FIPPA and MFIPPA — and the extract records a residency finding for
|
||||||
|
FIPPA and none for MFIPPA, so the narrowed lead was still a
|
||||||
|
universal over an unchecked instrument. Same shape, smaller.
|
||||||
|
The lead names the three ACTS the extract searched. O. Reg. 329/04
|
||||||
|
is searched too and is deliberately not named: it is a regulation
|
||||||
|
under PHIPA, so naming the Act covers it without putting a
|
||||||
|
regulation number on a marketing page. */
|
||||||
|
lead: "And neither PIPEDA, nor Ontario's Freedom of Information and Protection of Privacy Act, nor its Personal Health Information Protection Act requires data to be stored in Canada.",
|
||||||
|
text: 'This is the one worth stating plainly, because data-residency clauses are often drafted against the opposite assumption. The federal Privacy Commissioner\'s own guidance says PIPEDA does not prohibit an organisation in Canada from transferring personal information to another jurisdiction for processing; what the Act requires instead is accountability — the organisation stays responsible for information it has transferred to a third party. Neither Ontario statute contains a storage-location rule either — FIPPA has no data-localisation provision, and PHIPA\'s "Disclosure outside Ontario" section is a disclosure permission rather than a rule about where records sit.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Which matters in a dispute because the parties are often arguing about a clause neither of them can point to a source for. Establishing what the obligation actually is, rather than what both sides assumed it was, frequently narrows the disagreement to something a mediation can close in a day.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
/* THE NOTE CLAIMED THE WHOLE RESIDENCY POINT WAS THE COMMISSIONER'S
|
||||||
|
WORDS. It is his words for PIPEDA and a reading of the Ontario
|
||||||
|
statutes for the rest — so the note disclaimed a conclusion the page
|
||||||
|
does in fact draw, which is worse than drawing it openly. */
|
||||||
|
note: "Described as the state of the instruments, not applied to anyone's file. On residency the PIPEDA half is the federal Privacy Commissioner's own words; the Ontario half is what FIPPA and PHIPA say, and all three are named rather than described as a class. All of it is sourced in docs/reference/canada-privacy-technology.md and all of it can change — a bill at second reading in August 2026 is not a bill at second reading forever. What any of it means for a particular contract is a question for each party's own counsel.",
|
||||||
|
ground: 'inverse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The shape',
|
||||||
|
heading: 'Why these disputes suit a private process.',
|
||||||
|
ground: 'alt',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
lead: 'Confidentiality is not a preference here.',
|
||||||
|
text: 'The evidence in a technology dispute is source code, architecture, security posture and customer data flows. That is material neither side wants in a public record, and it is a reason parties choose arbitration over litigation before any question of speed arises.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'The commercial relationship usually has to survive.',
|
||||||
|
text: 'A dispute with a vendor mid-implementation, or with a customer who is still live on the platform, is not a matter where either side can afford a two-year fight. Mediation, or med-arb with the switch agreed in advance, is built for exactly that shape.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'And the process has to be able to look at the system.',
|
||||||
|
text: 'A documents-only arbitration works well where the dispute is about what the contract says. Where it is about what the system did, the process needs a way to get at the artefacts — which is a matter for the first procedural order, not something to discover late.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
energy: {
|
||||||
|
serviceType: ['Mediation', 'Commercial arbitration'],
|
||||||
|
serviceLabel: 'Mediation and arbitration',
|
||||||
|
title: 'Energy and Grid Disputes · Pouya Lajevardi · Toronto',
|
||||||
|
description:
|
||||||
|
'Connection assessment, leave to construct, proponent and municipality ' +
|
||||||
|
'disputes, IESO market participation, and the new data-centre ' +
|
||||||
|
'connection regime in Ontario.',
|
||||||
|
h1: 'Grid disputes are engineering disputes with a regulator attached.',
|
||||||
|
lede:
|
||||||
|
/* "and new generation" was here and is struck: the extract establishes a
|
||||||
|
change for large loads (Electricity Act s. 28.1) and for what the Board
|
||||||
|
may weigh on a leave-to-construct application, and its one quotation on
|
||||||
|
generation runs the other way — the normal System Impact Assessment
|
||||||
|
"applies to the connection of all generation facilities, renewable or
|
||||||
|
non-renewable, equally". A class asserted from one instance. */
|
||||||
|
'Ontario has spent the last year rewriting how large loads ' +
|
||||||
|
'get connected. That produces commercial disputes between ' +
|
||||||
|
'proponents, distributors, transmitters and municipalities long before ' +
|
||||||
|
'anything reaches a regulator.',
|
||||||
|
disputeTypesLede:
|
||||||
|
'Commercial disputes around connection, construction and market ' +
|
||||||
|
'participation.',
|
||||||
|
disputeTypes: [
|
||||||
|
{
|
||||||
|
name: 'Connection assessment',
|
||||||
|
body: "Disputes arising out of the IESO's and transmitter's connection assessment and approval process — the system impact assessment, the transmitter's customer impact assessment, and the conditions attached to either.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Leave to construct',
|
||||||
|
body: 'Commercial disputes between proponents, landowners and affected parties around an Ontario Energy Board leave-to-construct application, as distinct from the application itself.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Proponent and municipality',
|
||||||
|
body: 'Siting, road use, access and community-benefit disputes between a proponent and the municipality it has to build through.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Market participation',
|
||||||
|
body: 'Disputes between registered market participants, and between a participant and a counterparty, arising out of the IESO-administered markets.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Large loads and data centres',
|
||||||
|
body: 'The connection assessment behind a data centre or other large load, and the contractual arrangements built on an assumption about when the power arrives.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EPC and equipment',
|
||||||
|
body: 'Construction and supply disputes on generation, storage and transmission projects, where the construction and the regulatory timetables are coupled.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Why me',
|
||||||
|
heading:
|
||||||
|
'A System Impact Assessment is a document, and documents can be read.',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'Grid connection disputes are usually argued through technical studies. My engineering practice is in infrastructure, so the study, the single-line diagram and the constraint that produced the condition are things I can work through with the parties rather than around them.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'The regulatory overlay is the other half. A commercial dispute about a connection sits next to a process at the Ontario Energy Board or the IESO with its own timetable, and a neutral who does not understand that coupling will schedule a mediation for a date at which nothing can yet be decided.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The machinery',
|
||||||
|
heading: 'Where the processes actually sit.',
|
||||||
|
lede: 'Named precisely, because two of these are routinely called something they are not.',
|
||||||
|
ground: 'inverse',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
lead: 'Leave to construct is section 92.',
|
||||||
|
text: 'Section 92 of the Ontario Energy Board Act, 1998 provides that no person may construct, expand or reinforce an electricity transmission or distribution line, or make an interconnection, without an order from the Board granting leave. The thresholds everyone actually argues about are not in that section — they are exemptions in a regulation under it, which carves out distribution lines outright and transmission lines of two kilometres or less. Section 90 is the separate provision for hydrocarbon lines. The test is the public interest, and as of December 2025 what the Board may consider on a section 92 application expressly includes supporting economic growth consistent with Government of Ontario policy.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'Connection runs through the IESO, and it is not a queue.',
|
||||||
|
text: 'The IESO operates a connection process of up to six stages. An application is assessed by system impact assessment, and the transmitter generally runs a customer impact assessment after the draft. The IESO states plainly that it does not use an interconnection queue — it works from a defined set of committed projects instead, so "our place in the queue" describes nothing.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'And large loads now have their own gate.',
|
||||||
|
/* ⚠️ THIS SENTENCE ASSERTED THE ABSENCE OF A REGULATION AND THE
|
||||||
|
EXTRACT FORBIDS ASSERTING IT. It read: "The regulation that would
|
||||||
|
set them had not been made as of August 2026". The source,
|
||||||
|
`docs/reference/ontario-energy-regulatory.md`, records the outcome
|
||||||
|
of exactly that question as **"NOT ESTABLISHED either way, and DO
|
||||||
|
NOT ASSERT ITS ABSENCE"** — because its 50-item e-Laws regulation
|
||||||
|
list may have been truncated by a page cap, and criteria could be
|
||||||
|
added to an existing regulation rather than a new one. It even
|
||||||
|
supplies safe wording, which is what this now uses.
|
||||||
|
|
||||||
|
Found 2026-08-31 by the compliance audit on a step-7c ARTICLE
|
||||||
|
DRAFT that had copied the same construction — so a defect in an
|
||||||
|
unpublished draft surfaced a shipped one. Neither of step 5's
|
||||||
|
review passes caught it, because both read the sentence against
|
||||||
|
the extract's *quotations* rather than against its adversarial
|
||||||
|
check. R18(b) tracks this fact as volatile; that is a different
|
||||||
|
problem from never having been established. */
|
||||||
|
text: 'Section 28.1 of the Electricity Act, 1998 came into force on 11 December 2025. It bars a transmitter or distributor from connecting a "specified load facility" unless it is satisfied that the connection requirements the regulations specify have been complied with. That category is defined to include data centres meeting criteria that may be set by regulation. The enabling section is in force; the Ministry\'s August 2026 consultation still described the connection-approval regulation as under consideration, and described it as something the province was considering drafting. That consultation, on an assessment framework for new data centres, ran a comment period to 12 September 2026.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: "Described so the process is legible, not applied to anyone's file — and the terms above are the ones these bodies actually use. Sourced in docs/reference/ontario-energy-regulatory.md.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The context',
|
||||||
|
heading: 'This one is a position, not a caseload.',
|
||||||
|
ground: 'alt',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'Bill 40 — the Protect Ontario by Securing Affordable Energy for Generations Act, 2025 — received Royal Assent on 11 December 2025. It added the large-load connection provision above and widened what the Board may weigh on a leave-to-construct application. Its own preamble names the responsible growth of energy-intensive industries like data centres.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'A statute that changes how things get connected changes what parties argue about, and the disputes that follow it have not been had yet. I am saying plainly that this is a position I am building into rather than a volume of work I have already done. The engineering and the regulatory reading are both real now; the file count is not the claim.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
insurance: {
|
||||||
|
/* MEDIATION ONLY. The page offers private mediation and states that
|
||||||
|
s. 280 of the Insurance Act gives the LAT exclusive jurisdiction over
|
||||||
|
these disputes; the word arbitration appears in its visible copy only
|
||||||
|
in the shared onward-links strip. §4 also scopes every arbitration row
|
||||||
|
to COMMERCIAL, and a SABS entitlement dispute is statutory. */
|
||||||
|
serviceType: 'Mediation',
|
||||||
|
serviceLabel: 'Mediation',
|
||||||
|
title: 'Accident Benefits and SABS · Pouya Lajevardi · Toronto',
|
||||||
|
description:
|
||||||
|
'Entitlement and quantum disputes under the Statutory Accident ' +
|
||||||
|
'Benefits Schedule, minor injury designations, and private mediation ' +
|
||||||
|
'retained by the parties.',
|
||||||
|
h1: "Private mediation, not the Tribunal's case conference.",
|
||||||
|
lede:
|
||||||
|
'Accident benefits disputes are high in volume, tightly regulated, and ' +
|
||||||
|
'unglamorous enough to be worth doing properly. The distinction in that ' +
|
||||||
|
'headline is the one to be clear about before anyone retains me.',
|
||||||
|
disputeTypesLede:
|
||||||
|
'Disputes between an insured person and an insurer under the Statutory ' +
|
||||||
|
'Accident Benefits Schedule.',
|
||||||
|
disputeTypes: [
|
||||||
|
{
|
||||||
|
name: 'Entitlement and quantum',
|
||||||
|
body: 'Whether a benefit is payable at all, and if so how much — the two questions the statutory scheme is built around.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Minor injury designation',
|
||||||
|
body: 'Whether an impairment falls inside the minor injury definition, and the monetary limit that follows if it does.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Medical and rehabilitation benefits',
|
||||||
|
body: 'Which treatment, services or goods are payable, and the conditions a guideline may attach to them.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Catastrophic impairment',
|
||||||
|
body: 'The determination itself, and the very different limits that turn on it.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Income replacement',
|
||||||
|
body: 'Eligibility, quantum, and the evidentiary disputes about pre-accident earnings and post-accident capacity.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Insurer repayment claims',
|
||||||
|
body: 'Overpayment and repayment disputes brought by an insurer rather than by the insured person.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'The forum',
|
||||||
|
heading: 'Where these disputes go, and what I am not.',
|
||||||
|
lede: 'Worth stating precisely, because the vocabulary invites a misunderstanding.',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
lead: 'The Tribunal has exclusive jurisdiction.',
|
||||||
|
text: "Section 280 of the Insurance Act sends disputes about an insured person's entitlement to statutory accident benefits, or the amount of them, to the Licence Appeal Tribunal, and bars a proceeding in any court other than an appeal or judicial review. The accident-benefits division is the Automobile Accident Benefits Service.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: "Its case conference is the Tribunal's own, and I am not appointed to it.",
|
||||||
|
/* "who is then disqualified from hearing the matter" was here and
|
||||||
|
overstated the rule. LAT Rule 14.3 disqualifies the Member
|
||||||
|
"except with the consent of the parties" — an absolute where the
|
||||||
|
rule is qualified. */
|
||||||
|
text: "The Tribunal's settlement step is a case conference led by one of its adjudicators, who does not then sit on the hearing panel unless the parties consent. It is sometimes called a pre-hearing, which is the Tribunal's own label for it. A privately retained neutral does not conduct it and cannot be appointed to it, and nothing on this page should be read as offering that.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'What I offer is private mediation.',
|
||||||
|
/* "The Tribunal's own materials point parties toward mediation" was
|
||||||
|
here: a plural class, and a direction, resting on one permissive
|
||||||
|
sentence on one page that pairs mediation with negotiation and
|
||||||
|
ranks neither. The extract's own adversarial check named both
|
||||||
|
over-reads; this is the same gloss one notch weaker, and it
|
||||||
|
survived the correction to the quotation beside it. */
|
||||||
|
text: 'Retained by the parties, on their own terms, under an agreement to mediate they sign. The Tribunal\'s accident-benefits page names mediation as something to consider before applying: under the heading "Consider other ways to resolve your dispute", it says that before you apply, you may want to consider negotiation or mediation services.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: 'That quotation is about mediation before an application is filed, and it is quoted no wider than it goes. Sourced in docs/reference/lat-case-conference.md, which carries the full passage and a correction to an earlier reading of it.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The scheme',
|
||||||
|
heading: 'Everything here runs off one regulation.',
|
||||||
|
ground: 'inverse',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
lead: 'The Schedule is the source.',
|
||||||
|
text: 'The Statutory Accident Benefits Schedule is O. Reg. 34/10 under the Insurance Act, and it sets both the benefits and their limits. "Minor injury" and "Minor Injury Guideline" are both defined terms in section 3 of the Schedule, and the monetary limit on medical and rehabilitation benefits for a predominantly minor injury is set by section 18 of the Schedule itself.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'Which is why these files reward a neutral who reads it.',
|
||||||
|
text: 'The arguments that actually move an accident-benefits mediation are about which provision governs, what the assessments say against it, and where the file sits on a two-year clock. That is a documentary exercise before it is a persuasion exercise.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: "Described so the scheme is legible, not applied to anyone's file. The Schedule was amended with effect from 1 July 2026; this page cites no figure, and how any provision bears on a particular claim is a question for each party's own counsel. Sourced in docs/reference/ontario-sabs-lat.md.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The context',
|
||||||
|
heading: 'The volume is the argument.',
|
||||||
|
ground: 'alt',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'Tribunals Ontario reported 16,002 accident-benefit appeals received by the Licence Appeal Tribunal in the fiscal year ending 31 March 2025, and 12,081 case conferences held. It also reported that the average time from application to an oral hearing fell from 437 to 332 days over that year.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Those are the Tribunal's numbers about its own docket, not mine about my practice. They are here because they describe the problem: a very large number of disputes moving through a process whose hearing dates still sat the better part of a year out after a year of improvement. Private mediation is one thing that changes that arithmetic for a particular file.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: 'Published figures for the fiscal year ending 31 March 2025, from the Tribunals Ontario annual report. A more recent report may exist — re-check before relying on these as current. Sourced in docs/reference/ontario-sabs-lat.md.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
shareholder: {
|
||||||
|
serviceType: ['Mediation', 'Commercial arbitration'],
|
||||||
|
serviceLabel: 'Mediation and arbitration',
|
||||||
|
title: 'Shareholder and Partnership Disputes · Pouya Lajevardi',
|
||||||
|
description:
|
||||||
|
'Oppression, deadlock, buy-out and valuation, partnership dissolution ' +
|
||||||
|
'and business succession in family-held companies — commercial ' +
|
||||||
|
'disputes, not family law.',
|
||||||
|
h1: 'The company still has to trade on Monday.',
|
||||||
|
lede:
|
||||||
|
'Shareholder disputes are the ones where the cost of the fight lands ' +
|
||||||
|
'on the asset both sides are fighting over. That is the whole argument ' +
|
||||||
|
'for resolving them privately, and quickly, and it is why the ' +
|
||||||
|
'commercial reality has to be in the room.',
|
||||||
|
disputeTypesLede:
|
||||||
|
'Commercial disputes between owners of closely held businesses, ' +
|
||||||
|
'including family-held ones.',
|
||||||
|
disputeTypes: [
|
||||||
|
{
|
||||||
|
name: 'Oppression',
|
||||||
|
body: 'Conduct said to be oppressive, unfairly prejudicial, or unfairly to disregard the interests of a shareholder, creditor, director or officer.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Deadlock',
|
||||||
|
body: 'Fifty-fifty splits and blocked boards, where the disagreement is not legal so much as structural.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Buy-out and valuation',
|
||||||
|
body: 'What the shares are worth, on what basis, at what date — usually the real dispute once the rest is stripped away.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Co-founder breakdowns',
|
||||||
|
body: 'Vesting, contribution, role and control disputes in businesses young enough that the paperwork was never finished.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Partnership dissolution',
|
||||||
|
body: 'Winding up and accounts between partners, and the disputes about what the partnership agreement displaced and what it did not.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Business succession',
|
||||||
|
body: 'Transitions between generations in family-held companies, where the shareholders are also relatives and the roles are not written down.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'What this area is',
|
||||||
|
heading:
|
||||||
|
'Commercial disputes among family shareholders. Not family law.',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: '"Family business" here means a company whose owners happen to be related — succession, control, buy-outs and the arguments that follow when one branch wants out and another wants to keep building. The disputes are corporate and commercial, and they are handled as such.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: 'I do not accept family law matters. Stating it saves an intake call rather than defending anything.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The alternative',
|
||||||
|
heading: 'What the parties are bargaining against.',
|
||||||
|
lede: 'A shareholder mediation works better when both sides know what the court route actually offers.',
|
||||||
|
ground: 'inverse',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
lead: 'The oppression remedy.',
|
||||||
|
text: "Section 248 of the Business Corporations Act (Ontario), and section 241 of the Canada Business Corporations Act, let a complainant apply to the court where the affairs of a corporation are carried on in a manner that is oppressive or unfairly prejudicial to, or that unfairly disregards, the interests of a security holder, creditor, director or officer. Both give the court a long list of orders, including an order that the corporation or another person purchase a shareholder's securities.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'And the end of the road.',
|
||||||
|
text: 'Both statutes also provide for the company to be wound up, or liquidated and dissolved, including on the ground that it is just and equitable, and the Ontario Partnerships Act lets a partner apply to the court to dissolve a partnership on grounds that include conduct by a partner other than the one suing, in matters relating to the partnership business, that makes it not reasonably practicable for the other partners to carry on the business in partnership with that partner.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lead: 'One provision points the other way.',
|
||||||
|
text: 'The Ontario Act mentions arbitration exactly once. It provides that a unanimous shareholder agreement may provide that, where the shareholders party to it are unable to agree on or resolve a matter pertaining to the agreement, the matter may be referred to arbitration on the procedures and conditions the agreement specifies. The federal Act says nothing of the kind. So the first thing worth checking in a shareholder dispute is whether the parties already wrote down how they would resolve one.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: "Described so the alternatives are legible, not applied to anyone's file. Sourced in docs/reference/ontario-shareholder-remedies.md; what any of it means for a particular company is a question for each party's own counsel.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Why me',
|
||||||
|
heading: 'I run a company alongside this practice.',
|
||||||
|
ground: 'alt',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'SML Company Ltd. operates alongside the practice, which means the operating consequences of a shareholder dispute are legible rather than abstract: what a deadlock does to a supplier relationship, what an information demand costs a small finance function to answer, what a stalled decision costs a business that still has to trade.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'It matters in the room because shareholder disputes are usually settled on structure rather than on liability — a price, a mechanism, a timetable, a set of undertakings about how the two sides deal with each other afterwards. Getting there needs someone who can hold the corporate law and the operating reality at the same time.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
'cross-cultural': {
|
||||||
|
serviceType: ['Mediation', 'Commercial arbitration'],
|
||||||
|
serviceLabel: 'Mediation and arbitration',
|
||||||
|
title: 'Cross-Border and Diaspora Disputes · Pouya Lajevardi',
|
||||||
|
description:
|
||||||
|
'Diaspora business succession, dual-jurisdiction shareholder disputes ' +
|
||||||
|
'and cross-cultural commercial matters, conducted in English or Farsi ' +
|
||||||
|
'in Toronto.',
|
||||||
|
h1: 'A session in the language the deal was made in.',
|
||||||
|
lede:
|
||||||
|
'Some commercial disputes are harder than they need to be because the ' +
|
||||||
|
'parties are working in a second language, in a business culture that ' +
|
||||||
|
'is not the one the agreement was struck in. That is a resolvable ' +
|
||||||
|
'problem and it is rarely treated as one.',
|
||||||
|
disputeTypesLede:
|
||||||
|
'Commercial matters where the parties, the business or the assets ' +
|
||||||
|
'cross a border or a culture.',
|
||||||
|
disputeTypes: [
|
||||||
|
{
|
||||||
|
name: 'Diaspora business succession',
|
||||||
|
body: 'Family-held companies passing between generations where one generation built the business abroad and the next runs it here.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Dual-jurisdiction shareholder',
|
||||||
|
body: 'Ownership disputes where the company, the shareholders or the assets sit in more than one country.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Partnership disputes',
|
||||||
|
body: 'Breakdowns between diaspora entrepreneurs, often built on arrangements that were trusted rather than documented.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cross-cultural commercial',
|
||||||
|
body: 'Contract and supply disputes where the disagreement is partly about what was actually agreed and partly about how each side expected the other to behave.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Informal arrangements',
|
||||||
|
body: 'Matters where the commercial substance is real and the paperwork is thin, and the process has to establish what the deal was before it can resolve it.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Interpreted proceedings',
|
||||||
|
body: "Matters that have been running through an interpreter, where a session in the parties' own language changes what gets said.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Language',
|
||||||
|
heading: 'English or Farsi, and the difference is not convenience.',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'I am bilingual in English and Farsi, so a session can run in either. That is not a service line; it changes what a mediation can do. A party working through an interpreter says less, says it more carefully, and loses the qualifications and the hesitations that a mediator is actually listening for.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'It matters most in caucus, which is where a mediation is usually decided. A party explaining to a neutral what they can really live with is doing something delicate, and doing it in a second language, through a third person, is a different and much worse conversation.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Culture',
|
||||||
|
heading:
|
||||||
|
'Some of these disputes are about the agreement behind the agreement.',
|
||||||
|
ground: 'inverse',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'I am Iranian-Canadian, and some commercial disputes are not separable from the relationship between the parties — family-held companies and diaspora businesses in particular. In matters of this kind a significant part of the disagreement is often not about the written contract at all — it is about obligations both sides genuinely believed were understood, and which one side never thought needed writing down.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'A neutral who does not recognise that reads the file as one party inventing terms after the fact. A neutral who does can get the real expectations on the table, which is usually the only route to a settlement either side will actually honour.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'The frame',
|
||||||
|
heading: 'The process is still an Ontario commercial process.',
|
||||||
|
ground: 'alt',
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text: 'These run as commercial mediations and arbitrations, from Toronto, on the rules the parties choose. Where assets or parties sit in another jurisdiction, that is a fact the process has to accommodate — in how an agreement is drafted to be useful in both places, and in who needs to be in the room for a settlement to hold.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "A question of another country's law can bear on what a settlement has to say. Each party brings their own advisers for that, here and wherever else the matter reaches, and I work from what they tell me rather than around it.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: 'This site is written in English by design. A page in Farsi would be a different commitment from a session in Farsi, and only the second is offered.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ANNOTATED, NEVER `as const satisfies`: `as const` makes `sections` a
|
||||||
|
heterogeneous tuple and the optional keys stop existing, which costs 10
|
||||||
|
`astro check` errors that `astro build` does not see. See the AGENTS.md
|
||||||
|
entry of 2026-08-29. */
|
||||||
+456
-53
@@ -16,15 +16,37 @@
|
|||||||
* 1. `LegalService` — NEVER. docs/04: schema.org defines it as a business
|
* 1. `LegalService` — NEVER. docs/04: schema.org defines it as a business
|
||||||
* providing legal advice and *representation*, which asserts in
|
* providing legal advice and *representation*, which asserts in
|
||||||
* machine-readable form exactly what D13 bars. `ProfessionalService`.
|
* machine-readable form exactly what D13 bars. `ProfessionalService`.
|
||||||
* 2. `worksFor` — omitted. Populating it either names the boutique (D16) or
|
* 2. `worksFor` — **OMITTED. Declined 2026-08-28 (Q49(b)) and CONFIRMED by
|
||||||
* misstates the employer. `jobTitle` carries the role on its own.
|
* Pouya 2026-08-29: "worksFor stays out."** Settled, not withheld pending
|
||||||
* 3. `priceRange` — omitted until `/fees/` exists (build step 9). docs/04
|
* anything. Two grounds, either sufficient. §4 rows "Operator of SML
|
||||||
* gates it on that page being real.
|
* Company Ltd **alongside** the practice"; "the entity the practice
|
||||||
|
* **operates through**" is a different structural relation with no row.
|
||||||
|
* And `ProfessionalService.provider` is this Person, so `provider →
|
||||||
|
* Person → worksFor → SML` asserts **transitively** a same-entity claim
|
||||||
|
* that `src/pages/about.astro` deliberately removed from visible prose as
|
||||||
|
* *"a corporate-structure claim"* — scoping the value to a bare name does
|
||||||
|
* not reach that. See `PRACTICE_JOB_TITLE` in `site.ts`.
|
||||||
|
* 3. `priceRange` — **omitted, and no longer "until `/fees/` exists".** That
|
||||||
|
* page exists as of build step 9, the field went in, and it came out the
|
||||||
|
* same day: its ends had different units and its floor was a quarter of the
|
||||||
|
* real entry price for a mediation. See `professionalServiceNode`.
|
||||||
*
|
*
|
||||||
* AND Q.Arb IS NOT IN `hasCredential`. It commenced August 2026 and is not
|
* AND `hasCredential` NOW CARRIES BOTH DESIGNATIONS. It was Q.Med-only until
|
||||||
* held. Q.Med is. That asymmetry is the whole point of the property.
|
* 2026-08-29 because Q.Arb was a commenced pathway and the property means
|
||||||
|
* *holds*. Q.Arb is held (§4), so it belongs there — and the field is built by
|
||||||
|
* mapping `CREDENTIALS.designations` rather than indexing it, so a designation
|
||||||
|
* added to §4 and to that constant cannot be silently omitted here.
|
||||||
*/
|
*/
|
||||||
import { CONTACT, CREDENTIALS, ROLE, SITE } from './site';
|
/* No `FEES` import. It was here for `priceRange`, which is gone — see
|
||||||
|
`professionalServiceNode`. Nothing in this file carries a number now, which is
|
||||||
|
the right shape: money is `/fees/`'s, with the conditions attached. */
|
||||||
|
import {
|
||||||
|
CONTACT,
|
||||||
|
CREDENTIALS,
|
||||||
|
MEMBERSHIP_ORGS,
|
||||||
|
PRACTICE_JOB_TITLE,
|
||||||
|
SITE,
|
||||||
|
} from './site';
|
||||||
|
|
||||||
/** Stable node ids, so pages cross-reference rather than duplicate. */
|
/** Stable node ids, so pages cross-reference rather than duplicate. */
|
||||||
export const PERSON_ID = `${SITE.url}/about/#person`;
|
export const PERSON_ID = `${SITE.url}/about/#person`;
|
||||||
@@ -41,48 +63,80 @@ export const SERVICE_ID = `${SITE.url}/#practice`;
|
|||||||
* counterpart row for a completed arbitration**, so "arbitrator" as a practised
|
* counterpart row for a completed arbitration**, so "arbitrator" as a practised
|
||||||
* role asserted something the register does not hold.
|
* role asserted something the register does not hold.
|
||||||
*
|
*
|
||||||
* AND IT CARRIES THE Q.Arb STAGE. Same finding, and it is the sharper half:
|
* A MACHINE-READABLE CLAIM IS STILL A CLAIM, which is why this description is
|
||||||
* §4 Offerings permits the arbitration offering only while the site states the
|
* audited like visible copy. Every clause traces: Q.Med and Q.Arb [verified], the JD [verified],
|
||||||
* stage of the arc plainly — "neither half may be dropped". The VISIBLE page
|
* engineering practice [verified], Toronto [verified]. It claims no licensure
|
||||||
* satisfied that with the fourth credential slot; this graph asserted
|
* and implies none — D13 bars implication as hard as assertion, and a crawler
|
||||||
* arbitration twice (here and in `serviceType`) and stated the stage nowhere.
|
* summary is a place where an implication travels unedited.
|
||||||
* A machine-readable claim is still a claim.
|
|
||||||
*
|
|
||||||
* `hasCredential` stays Q.Med-only regardless — the stage belongs in prose, not
|
|
||||||
* in a field that means "holds".
|
|
||||||
*
|
|
||||||
* Every clause traces: Q.Med [verified], the JD [verified], engineering
|
|
||||||
* practice [verified], Toronto [verified], the Q.Arb pathway commenced August
|
|
||||||
* 2026 [verified]. It claims no licensure and implies none — D13 bars
|
|
||||||
* implication as hard as assertion, and a crawler summary is a place where an
|
|
||||||
* implication travels unedited.
|
|
||||||
*/
|
*/
|
||||||
export function personNode(imageUrl?: string) {
|
export function personNode(
|
||||||
|
imageUrl?: string,
|
||||||
|
opts: { memberships?: boolean } = {},
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
'@id': PERSON_ID,
|
'@id': PERSON_ID,
|
||||||
name: SITE.name,
|
name: SITE.name,
|
||||||
url: `${SITE.url}/about/`,
|
url: `${SITE.url}/about/`,
|
||||||
jobTitle: ROLE.title,
|
/* PRACTICE_JOB_TITLE, not ROLE.title (Q47), and §4 now rows the value:
|
||||||
|
"Practised role — Mediator" [verified 2026-08-28 — Pouya, Q49, confirmed
|
||||||
|
2026-08-29]. Do not widen it to include arbitration — see the constant in site.ts. No
|
||||||
|
`worksFor` beside it: Q49(b) declined the row. */
|
||||||
|
jobTitle: PRACTICE_JOB_TITLE,
|
||||||
description:
|
description:
|
||||||
'Mediator in Toronto, accepting commercial arbitration appointments. ' +
|
'Mediator in Toronto, accepting commercial arbitration appointments. ' +
|
||||||
'Q.Med designation through ADRIC and ADRIO; the Q.Arb pathway commenced ' +
|
'Q.Med and Q.Arb designations through ADRIC and ADRIO. JD, Bond ' +
|
||||||
'in August 2026. JD, Bond University; practising machine-learning and ' +
|
'University; practising machine-learning and infrastructure engineer.',
|
||||||
'infrastructure engineer.',
|
|
||||||
knowsLanguage: ['en', 'fa'],
|
knowsLanguage: ['en', 'fa'],
|
||||||
alumniOf: { '@type': 'CollegeOrUniversity', name: 'Bond University' },
|
alumniOf: { '@type': 'CollegeOrUniversity', name: 'Bond University' },
|
||||||
// Q.Med only. See the header comment.
|
/* MAPPED, NOT INDEXED. This read `designations[0]` while Q.Arb was a
|
||||||
hasCredential: {
|
pathway; indexing is what would have left Q.Arb out of the graph
|
||||||
|
silently when §4 changed. */
|
||||||
|
hasCredential: CREDENTIALS.designations.map((name) => ({
|
||||||
'@type': 'EducationalOccupationalCredential',
|
'@type': 'EducationalOccupationalCredential',
|
||||||
name: CREDENTIALS.designations[0],
|
name,
|
||||||
credentialCategory: 'Professional designation',
|
credentialCategory: 'Professional designation',
|
||||||
recognizedBy: [
|
recognizedBy: [
|
||||||
{ '@type': 'Organization', name: 'ADR Institute of Canada' },
|
{ '@type': 'Organization', name: 'ADR Institute of Canada' },
|
||||||
{ '@type': 'Organization', name: 'ADR Institute of Ontario' },
|
{ '@type': 'Organization', name: 'ADR Institute of Ontario' },
|
||||||
],
|
],
|
||||||
},
|
})),
|
||||||
sameAs: [CONTACT.linkedin],
|
sameAs: [CONTACT.linkedin],
|
||||||
email: `mailto:${CONTACT.email}`,
|
email: `mailto:${CONTACT.email}`,
|
||||||
|
/* `memberOf` — OPT-IN, AND ONLY `/about/` OPTS IN. Q53, ruled by Pouya
|
||||||
|
2026-08-28: *"the memberships are already crawlable in /about/'s HTML, so
|
||||||
|
withholding the triple reduces no exposure and only makes the graph less
|
||||||
|
complete than the page."* **The reason he gave is page-specific**, so the
|
||||||
|
field is too: /about/ shows the list, `/` does not.
|
||||||
|
|
||||||
|
⚠️ DO NOT DELETE THE `opts` PARAMETER AS REDUNDANT. It looks redundant —
|
||||||
|
both pages serve the same `@id`, so a consumer that fetches both merges
|
||||||
|
them and gets `memberOf` either way. It was deleted on exactly that
|
||||||
|
reasoning and had to be put back: the argument is about what a *consumer*
|
||||||
|
can derive, and the scoping is about what a *page* asserts on its own.
|
||||||
|
Six records in this repository state the `/about/` scoping; the deletion
|
||||||
|
made all six false at once, silently.
|
||||||
|
|
||||||
|
THE NAMES COME FROM `MEMBERSHIP_ORGS`, NOT FROM THE DISPLAY LIST, and the
|
||||||
|
two differ on three of four lines — see that constant in site.ts, which
|
||||||
|
carries the reason and the D13 exclusions that govern both. Only their
|
||||||
|
LENGTH is asserted at compile time. **Content parity is manual.**
|
||||||
|
|
||||||
|
R10 fires on an event, not a date: re-confirm before any cutover or major
|
||||||
|
republish, and re-stamp §4. That covers this field as well as the visible
|
||||||
|
list. **Last fired 2026-09-02** — all four re-confirmed by Pouya on the day
|
||||||
|
of cutover. ⚠️ **The stamp lives on `CREDENTIALS.memberships` in site.ts
|
||||||
|
and THIS FIELD READS `MEMBERSHIP_ORGS`** — a different array, as the note
|
||||||
|
seven lines above says. Content parity between them is manual, so
|
||||||
|
re-stamping is not the same act as re-checking that the two agree. */
|
||||||
|
...(opts.memberships
|
||||||
|
? {
|
||||||
|
memberOf: MEMBERSHIP_ORGS.map((name) => ({
|
||||||
|
'@type': 'Organization',
|
||||||
|
name,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(imageUrl ? { image: imageUrl } : {}),
|
...(imageUrl ? { image: imageUrl } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -90,6 +144,41 @@ export function personNode(imageUrl?: string) {
|
|||||||
/**
|
/**
|
||||||
* `ProfessionalService` for the home page, with the Person as `provider`.
|
* `ProfessionalService` for the home page, with the Person as `provider`.
|
||||||
*
|
*
|
||||||
|
* THE MASTHEAD IS TWO FIELDS HERE, NOT ONE STRING — Q50, ruled by Pouya
|
||||||
|
* 2026-08-28: *"`ProfessionalService.name` takes the ratified masthead string.
|
||||||
|
* Q33 reaches it; one claim should not have two forms."*
|
||||||
|
*
|
||||||
|
* The masthead is not one string, and schema.org has a field for each half, so
|
||||||
|
* `name` + `slogan` is the mapping rather than a concatenation. Both read the
|
||||||
|
* constants, which is what kills the variant.
|
||||||
|
*
|
||||||
|
* ⚠️ AND ON `/` — THE ONLY PAGE THIS NODE SHIPS ON — THE MASTHEAD DOES NOT
|
||||||
|
* RENDER THE TAGLINE AT ALL. `SiteHeader.astro` gates it on `!isHome`,
|
||||||
|
* deliberately, so the header does not repeat the hero. `SITE.tagline` is
|
||||||
|
* visible on `/` as the **hero eyebrow** instead. That is the page-visible
|
||||||
|
* counterpart `slogan` mirrors; do not justify this mapping by the masthead.
|
||||||
|
*
|
||||||
|
* ⚠️ DO NOT PUT THE TAGLINE BACK INTO `name`. Two independent reasons.
|
||||||
|
* (1) On a `LocalBusiness`-family type `name` means the business's name; a name
|
||||||
|
* plus a service list plus a city is the name-stuffing pattern search engines
|
||||||
|
* discard a rich result for, and `areaServed` already carries Toronto and
|
||||||
|
* Ontario. (2) It put an **unscoped "Arbitration"** into the node's most
|
||||||
|
* prominent field — Q39's struck universal reappearing in machine-readable
|
||||||
|
* form.
|
||||||
|
*
|
||||||
|
* **Reason (2) is mitigated, not discharged**, and that is stated rather than
|
||||||
|
* glossed: the word is still in `slogan`. What makes it acceptable there is
|
||||||
|
* that the identical unscoped string already ships in `<title>`, `og:title`,
|
||||||
|
* `twitter:title` and the hero eyebrow, all ratified under Q33 — so excluding
|
||||||
|
* it from one name-like field alone would be incoherent. `serviceType` stays
|
||||||
|
* scoped because it **enumerates services**; a slogan and a title are names.
|
||||||
|
*
|
||||||
|
* ✅ RATIFIED 2026-08-29 (Q50). This was recorded here as a deviation from
|
||||||
|
* Pouya's literal ruling, awaiting his line. He gave it, and reversed his own
|
||||||
|
* ruling: *"name: 'Pouya Lajevardi' + slogan. My ruling was wrong... Your
|
||||||
|
* reading beat mine; record it as the decision, not as a deviation."* The
|
||||||
|
* two-field mapping IS the decision. The concatenation is struck, not pending.
|
||||||
|
*
|
||||||
* `serviceType` lists what §4 Offerings actually records as offered now —
|
* `serviceType` lists what §4 Offerings actually records as offered now —
|
||||||
* mediation, arbitration, med-arb. **Arbitration is scoped to commercial**
|
* mediation, arbitration, med-arb. **Arbitration is scoped to commercial**
|
||||||
* (Q39, 2026-08-27): family arbitration in Ontario carries prescribed training
|
* (Q39, 2026-08-27): family arbitration in Ontario carries prescribed training
|
||||||
@@ -99,18 +188,33 @@ export function personNode(imageUrl?: string) {
|
|||||||
*
|
*
|
||||||
* No `priceRange`, no `aggregateRating`, no `review` — the last two have no
|
* No `priceRange`, no `aggregateRating`, no `review` — the last two have no
|
||||||
* underlying data and §4 Forbidden bars the fabricated testimonial that the
|
* underlying data and §4 Forbidden bars the fabricated testimonial that the
|
||||||
* previous site carried.
|
* previous site carried. And no `availableLanguage`: it is not in this type's
|
||||||
|
* domain either (see `serviceGraph`), and the `Person` beside it in the graph
|
||||||
|
* carries `knowsLanguage`.
|
||||||
|
*
|
||||||
|
* ⚠️ **`serviceType` AND `provider` ARE STILL OUT OF DOMAIN ON THIS TYPE, AND
|
||||||
|
* THAT IS UNRESOLVED.** Measured 2026-08-28 against `validator.schema.org`:
|
||||||
|
* `/` returns `UNKNOWN_FIELD` for `serviceType`, `availableLanguage` and
|
||||||
|
* `provider` on `ProfessionalService`, which is a `LocalBusiness` and takes
|
||||||
|
* none of the three. `availableLanguage` is removed above because nothing is
|
||||||
|
* lost. The other two carry real information — the commercial scoping and the
|
||||||
|
* link to the Person — so removing them costs more than the warning does, and
|
||||||
|
* the modelled fix is a `Service` node with the business as `provider`, which
|
||||||
|
* is a change to the home page's structured data rather than a tidy-up. **The
|
||||||
|
* other four pages now validate with 0 warnings**; this node is the last one.
|
||||||
|
* Raised for the step-7 SEO pass, not left as folklore.
|
||||||
*/
|
*/
|
||||||
export function professionalServiceNode(imageUrl?: string) {
|
export function professionalServiceNode(imageUrl?: string) {
|
||||||
return {
|
return {
|
||||||
'@type': 'ProfessionalService',
|
'@type': 'ProfessionalService',
|
||||||
'@id': SERVICE_ID,
|
'@id': SERVICE_ID,
|
||||||
name: `${SITE.name} — Mediation & Arbitration`,
|
name: SITE.name,
|
||||||
|
slogan: SITE.tagline,
|
||||||
url: `${SITE.url}/`,
|
url: `${SITE.url}/`,
|
||||||
description:
|
description:
|
||||||
'Commercial mediation and arbitration for construction, technology, ' +
|
'Mediation and commercial arbitration for construction, technology, ' +
|
||||||
'energy, insurance, shareholder and cross-border disputes. Toronto, ' +
|
'energy, insurance, shareholder and cross-border disputes. Toronto, ' +
|
||||||
'by appointment. Q.Med held; the Q.Arb pathway commenced August 2026.',
|
'by appointment. Q.Med and Q.Arb designations held.',
|
||||||
provider: { '@id': PERSON_ID },
|
provider: { '@id': PERSON_ID },
|
||||||
areaServed: [
|
areaServed: [
|
||||||
{ '@type': 'City', name: 'Toronto' },
|
{ '@type': 'City', name: 'Toronto' },
|
||||||
@@ -121,8 +225,16 @@ export function professionalServiceNode(imageUrl?: string) {
|
|||||||
'Commercial arbitration',
|
'Commercial arbitration',
|
||||||
'Mediation-arbitration (med-arb)',
|
'Mediation-arbitration (med-arb)',
|
||||||
],
|
],
|
||||||
availableLanguage: ['en', 'fa'],
|
|
||||||
email: `mailto:${CONTACT.email}`,
|
email: `mailto:${CONTACT.email}`,
|
||||||
|
/**
|
||||||
|
* ⚠️ **NO `priceRange`, AND DO NOT ADD ONE.** `docs/04` gates the field on
|
||||||
|
* `/fees/` existing; the gate is met and the field is still declined. Any
|
||||||
|
* single range here mixes units — the hourly rate against a flat
|
||||||
|
* documents-only fee — and its floor understates a mediation, whose least
|
||||||
|
* cost is `halfDay.amount`. `/fees/` publishes the conditions that make one
|
||||||
|
* number misleading. No `Offer` node either, for the same reason.
|
||||||
|
* AGENTS.md entry (ah) records what the field said when it briefly shipped.
|
||||||
|
*/
|
||||||
...(imageUrl ? { image: imageUrl } : {}),
|
...(imageUrl ? { image: imageUrl } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -159,27 +271,318 @@ export function homeGraph(imageUrl?: string) {
|
|||||||
* no visible breadcrumb, and the header nav marks it as current — so
|
* no visible breadcrumb, and the header nav marks it as current — so
|
||||||
* emitting one would assert a navigation structure the page does not show.
|
* emitting one would assert a navigation structure the page does not show.
|
||||||
* Breadcrumbs begin at the two-level pages: `/practice/<area>/`, `/insights/<slug>/`.
|
* Breadcrumbs begin at the two-level pages: `/practice/<area>/`, `/insights/<slug>/`.
|
||||||
* 3. `memberOf` for the four memberships. Declined on R10 / **Q44** — and
|
* 3. `memberOf` — **EMITTED HERE, and only here.** Q53, 2026-08-28. It is
|
||||||
* the visible page reached the same answer one round later, which is worth
|
* opted into by this function and by no other; `personNode` carries the
|
||||||
* recording: `/about/` now publishes **no memberships group at all**,
|
* constraint and the reason.
|
||||||
* because R10 is a prohibition on shipping such a page and the
|
|
||||||
* re-confirmation was not obtained. So this field is not a stricter
|
|
||||||
* standard than the page; it is the same one.
|
|
||||||
*
|
|
||||||
* Two earlier versions of this comment were wrong on the facts. They said
|
|
||||||
* "the page publishes them visibly" (it does not, as of 2026-08-28) and
|
|
||||||
* "all four renew yearly" — §4 records yearly renewal for **the OBA
|
|
||||||
* sections and the CTF only** and says nothing about ADRIC or ADRIO. The
|
|
||||||
* widened form had propagated to four files.
|
|
||||||
*
|
|
||||||
* The reason a machine-readable membership claim is worse than a visible
|
|
||||||
* one stands regardless: a list on a page is corrected by editing the page,
|
|
||||||
* while a scraped claim is cached and re-served by systems that never
|
|
||||||
* re-read it. OCNI lapsed quietly once already. Add this when Q44 closes.
|
|
||||||
*/
|
*/
|
||||||
export function aboutGraph(imageUrl?: string) {
|
export function aboutGraph(imageUrl?: string) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [personNode(imageUrl, { memberships: true })],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Service` for the three process pages — `/mediation/`, `/arbitration/`,
|
||||||
|
* `/med-arb/` (build step 4). docs/04's structured-data table.
|
||||||
|
*
|
||||||
|
* THE PERSON NODE TRAVELS WITH IT, for the reason `homeGraph` gives: a `@graph`
|
||||||
|
* makes `provider: {'@id': …}` resolve inside this document rather than relying
|
||||||
|
* on a crawler fetching `/about/` and joining two. Same `@id` either way, so a
|
||||||
|
* consumer that does fetch both merges rather than duplicates.
|
||||||
|
*
|
||||||
|
* NO `BreadcrumbList`. These are one hop from the root and show no visible
|
||||||
|
* breadcrumb; docs/04 requires the markup to MATCH visible breadcrumbs, so
|
||||||
|
* emitting one would assert navigation the page does not show. Breadcrumbs
|
||||||
|
* begin at `/practice/<area>/` and `/insights/<slug>/`.
|
||||||
|
*
|
||||||
|
* NO `offers` AND NO `priceRange` — A DECISION, NOT A GATE. This read "until
|
||||||
|
* `/fees/` exists (build step 9)", which shipped, so it had become an
|
||||||
|
* instruction to add them — against the decision recorded on
|
||||||
|
* `professionalServiceNode` above, where `priceRange` went in at step 9 and came
|
||||||
|
* out the same day. `/fees/` publishes the conditions — session length, party
|
||||||
|
* count, format — that make any single machine-readable figure misleading, and
|
||||||
|
* schema.org's `Offer` models one price for one item. Found by
|
||||||
|
* `adversarial-reviewer` round 2.
|
||||||
|
*
|
||||||
|
* NO `availableLanguage` EITHER, AND THAT IS NOT AN OVERSIGHT. schema.org's
|
||||||
|
* `domainIncludes` for it is `ContactPoint`, `Course`, `LodgingBusiness`,
|
||||||
|
* `ServiceChannel`, `TouristAttraction` — **not `Service`**. It shipped here for
|
||||||
|
* one pass and `validator.schema.org` returned `UNKNOWN_FIELD` twice per page on
|
||||||
|
* all three, against **0 warnings** on `/about/`. The `Person` in the same
|
||||||
|
* `@graph` already carries `knowsLanguage: ['en','fa']`, so nothing is lost. The
|
||||||
|
* modelled alternative is `availableChannel → ServiceChannel → availableLanguage`,
|
||||||
|
* which is more machinery than the fact is worth.
|
||||||
|
*
|
||||||
|
* ⚠️ `serviceType` IS A UNION, NOT A STRING, AND THAT IS THE POINT. An
|
||||||
|
* unscoped `"Arbitration"` is Q39's struck universal — false as a universal,
|
||||||
|
* swept four times, reached a public page once — so it is not a value this
|
||||||
|
* function will accept. A comment asking a caller not to pass it is a warning; a
|
||||||
|
* union is a build error. Widening the union requires a §4 Offerings row, and
|
||||||
|
* the row is the only thing that should ever widen it.
|
||||||
|
*/
|
||||||
|
/** `docs/04`'s ratified `serviceType` strings — **not** §4's row labels, which
|
||||||
|
* read "Arbitration — sole arbitrator (commercial)" and "Med-Arb —
|
||||||
|
* mediation-arbitration". Adding a member needs a §4 Offerings row, but do not
|
||||||
|
* copy the row's wording: doing that "verbatim" yields the unscoped
|
||||||
|
* `Arbitration` this union exists to bar. */
|
||||||
|
export type PublishableServiceType =
|
||||||
|
'Mediation' | 'Commercial arbitration' | 'Mediation-arbitration (med-arb)';
|
||||||
|
|
||||||
|
export function serviceGraph(opts: {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
/** One value, or several. The union is what constrains it either way. */
|
||||||
|
serviceType: PublishableServiceType | readonly PublishableServiceType[];
|
||||||
|
description: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
/** Root-first and INCLUDING the current page — the same array the visible
|
||||||
|
* `<Breadcrumbs>` renders, because docs/04 requires the two to match. Omit
|
||||||
|
* on a page one hop from the root, which shows no visible trail. */
|
||||||
|
breadcrumbs?: ReadonlyArray<{ name: string; href: string }>;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [
|
||||||
|
{
|
||||||
|
'@type': 'Service',
|
||||||
|
'@id': `${SITE.url}${opts.path}#service`,
|
||||||
|
name: opts.name,
|
||||||
|
serviceType: opts.serviceType,
|
||||||
|
description: opts.description,
|
||||||
|
url: `${SITE.url}${opts.path}`,
|
||||||
|
provider: { '@id': PERSON_ID },
|
||||||
|
areaServed: [
|
||||||
|
{ '@type': 'City', name: 'Toronto' },
|
||||||
|
{ '@type': 'AdministrativeArea', name: 'Ontario' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
personNode(opts.imageUrl),
|
||||||
|
...(opts.breadcrumbs
|
||||||
|
? [breadcrumbNode(opts.path, opts.breadcrumbs)]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `BreadcrumbList`. docs/04: "All nested pages | Matches visible breadcrumbs" —
|
||||||
|
* so this is never called with an array the page does not also render, and
|
||||||
|
* `serviceGraph` takes the same array the `<Breadcrumbs>` component takes.
|
||||||
|
*
|
||||||
|
* `position` IS 1-BASED. schema.org's `ListItem.position` starts at 1, and a
|
||||||
|
* 0-based list is accepted by the validator while ranking the root second.
|
||||||
|
*/
|
||||||
|
function breadcrumbNode(
|
||||||
|
path: string,
|
||||||
|
trail: ReadonlyArray<{ name: string; href: string }>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
'@id': `${SITE.url}${path}#breadcrumbs`,
|
||||||
|
itemListElement: trail.map((crumb, i) => ({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: i + 1,
|
||||||
|
name: crumb.name,
|
||||||
|
item: `${SITE.url}${crumb.href}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `/practice/<area>/` — one `Service`, the `Person`, and a `BreadcrumbList`.
|
||||||
|
*
|
||||||
|
* ⚠️ **A SUBJECT-MATTER AREA IS NOT AN OFFERING (§4), AND `serviceType` MUST
|
||||||
|
* NOT MAKE IT ONE.** It carries the two rowed processes; the area belongs in
|
||||||
|
* `name` and `description`, where it reads as subject matter. A
|
||||||
|
* `serviceType: 'Construction dispute resolution'` would be an unrowed offering
|
||||||
|
* asserted in a field nobody reads — the defect `claims-auditor` caught on the
|
||||||
|
* Person node's `description`.
|
||||||
|
*
|
||||||
|
* Med-arb is left out deliberately: it is offered, and repeating it on six
|
||||||
|
* pages adds nothing `/med-arb/`'s own node does not already say.
|
||||||
|
*
|
||||||
|
* Arbitration is scoped commercial by the union, not by this function — and
|
||||||
|
* **which processes an area carries is the CALLER's**, not this function's.
|
||||||
|
*/
|
||||||
|
export function practiceAreaGraph(opts: {
|
||||||
|
slug: string;
|
||||||
|
/** The area's own name — §4's label, not a service name. */
|
||||||
|
areaName: string;
|
||||||
|
/**
|
||||||
|
* ⚠️ PER AREA. This was fixed at `['Mediation', 'Commercial arbitration']`
|
||||||
|
* for all six, which put a machine-readable offer of commercial arbitration
|
||||||
|
* on `/practice/insurance/` — whose `<h1>` reads "Private mediation, not the
|
||||||
|
* Tribunal's case conference", whose body recites Insurance Act s. 280
|
||||||
|
* exclusive jurisdiction, and whose visible copy offers arbitration nowhere.
|
||||||
|
* docs/04: **structured data represents the page it sits on.** Same family as
|
||||||
|
* the Person node's "Mediator and Commercial Arbitrator", struck 2026-08-27.
|
||||||
|
*/
|
||||||
|
serviceType: PublishableServiceType | readonly PublishableServiceType[];
|
||||||
|
/** Leads the `Service` name. Must describe what `serviceType` carries. */
|
||||||
|
serviceLabel: string;
|
||||||
|
description: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
}) {
|
||||||
|
const path = `/practice/${opts.slug}/`;
|
||||||
|
return serviceGraph({
|
||||||
|
path,
|
||||||
|
name: `${opts.serviceLabel} — ${opts.areaName}`,
|
||||||
|
serviceType: opts.serviceType,
|
||||||
|
description: opts.description,
|
||||||
|
imageUrl: opts.imageUrl,
|
||||||
|
breadcrumbs: [
|
||||||
|
{ name: 'Home', href: '/' },
|
||||||
|
{ name: 'Practice', href: '/practice/' },
|
||||||
|
{ name: opts.areaName, href: path },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A page that offers no `Service` of its own — the Person node alone.
|
||||||
|
*
|
||||||
|
* `/practice/` and `/process/` are both this shape: they describe how the
|
||||||
|
* practice works rather than offering something a crawler should lift as a
|
||||||
|
* service. `/process/` in particular must NOT emit one — the five timings are
|
||||||
|
* publishable only under Q43's framing sentence, and a `Service` node would
|
||||||
|
* carry the numbers into a field where no framing travels with them.
|
||||||
|
*
|
||||||
|
* No `BreadcrumbList` on either: one hop from the root, no visible trail.
|
||||||
|
*/
|
||||||
|
export function pageGraph(imageUrl?: string) {
|
||||||
return {
|
return {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@graph': [personNode(imageUrl)],
|
'@graph': [personNode(imageUrl)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `FAQPage` — built from the array the page renders, never a second copy. */
|
||||||
|
function faqNode(path: string, faq: ReadonlyArray<{ q: string; a: string }>) {
|
||||||
|
return {
|
||||||
|
'@type': 'FAQPage',
|
||||||
|
'@id': `${SITE.url}${path}#faq`,
|
||||||
|
mainEntity: faq.map((item) => ({
|
||||||
|
'@type': 'Question',
|
||||||
|
name: item.q,
|
||||||
|
acceptedAnswer: { '@type': 'Answer', text: item.a },
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `/for-parties/` — the Person node and a `FAQPage`. **No `Service` node.**
|
||||||
|
*
|
||||||
|
* docs/04 lists `FAQPage` for this page, under the condition that decides it:
|
||||||
|
* *"Only where the visible page genuinely is Q&A. Never fabricate questions to
|
||||||
|
* farm a rich result."* The page is written as questions a party actually asks,
|
||||||
|
* and this node is built from the same array it renders — so a question cannot
|
||||||
|
* enter the structured data without appearing on the page.
|
||||||
|
*
|
||||||
|
* The Service nodes for mediation live on `/mediation/`. This page explains a
|
||||||
|
* process to the party on the other side of it; it is not a second offer of it.
|
||||||
|
*/
|
||||||
|
export function forPartiesGraph(opts: {
|
||||||
|
faq: ReadonlyArray<{ q: string; a: string }>;
|
||||||
|
imageUrl?: string;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [personNode(opts.imageUrl), faqNode('/for-parties/', opts.faq)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `/med-arb/`'s graph — the Service, the Person, and a `FAQPage`.
|
||||||
|
*
|
||||||
|
* docs/04 lists `FAQPage` for this page and for `/for-parties/`, with the
|
||||||
|
* condition that matters: *"Only where the visible page genuinely is Q&A. Never
|
||||||
|
* fabricate questions to farm a rich result."* So the node is BUILT FROM THE
|
||||||
|
* SAME ARRAY THE PAGE RENDERS — a question cannot enter the structured data
|
||||||
|
* without appearing on the page, and the two cannot drift.
|
||||||
|
*/
|
||||||
|
export function medArbGraph(opts: {
|
||||||
|
faq: ReadonlyArray<{ q: string; a: string }>;
|
||||||
|
imageUrl?: string;
|
||||||
|
}) {
|
||||||
|
const base = serviceGraph({
|
||||||
|
path: '/med-arb/',
|
||||||
|
name: 'Med-arb (mediation-arbitration)',
|
||||||
|
serviceType: 'Mediation-arbitration (med-arb)',
|
||||||
|
description:
|
||||||
|
'Mediation that converts to binding arbitration if the mediation does ' +
|
||||||
|
'not resolve the dispute. One neutral, both phases, agreed in writing ' +
|
||||||
|
'in advance. Commercial matters.',
|
||||||
|
imageUrl: opts.imageUrl,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
'@graph': [...base['@graph'], faqNode('/med-arb/', opts.faq)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Article` — build step 7b. docs/04: *"`headline`, `description`,
|
||||||
|
* `datePublished`, `dateModified`, `author` → Person, `image`"*.
|
||||||
|
*
|
||||||
|
* `author` IS `{'@id': PERSON_ID}` AND THE PERSON NODE TRAVELS IN THE SAME
|
||||||
|
* `@graph` — `homeGraph`'s reasoning, applied a fifth time. A bare `@id` pointing
|
||||||
|
* at another document relies on a crawler fetching and joining two; inside one
|
||||||
|
* `@graph` it resolves in the document it arrives in.
|
||||||
|
*
|
||||||
|
* `dateModified` FALLS BACK TO `datePublished` RATHER THAN BEING OMITTED. An
|
||||||
|
* article with no `updatedDate` has not been modified since publication, which is
|
||||||
|
* a fact; omitting the field says nothing, and Google reads a missing
|
||||||
|
* `dateModified` as unknown rather than as "same as published".
|
||||||
|
*
|
||||||
|
* ⚠️ **NO `publisher`, AND NO `Organization` NODE ANYWHERE NEAR THIS.** The
|
||||||
|
* obvious shape for a blog is `publisher: { '@type': 'Organization', name: … }`,
|
||||||
|
* and on this site the only name available for it is SML Company Ltd — which
|
||||||
|
* would assert in machine-readable form that the company publishes the practice's
|
||||||
|
* writing. §4 rows *"Operator of SML Company Ltd **alongside** the practice"* and
|
||||||
|
* nothing more; `schema.ts` already declines `Person.worksFor` for the same
|
||||||
|
* reason (Q49(b)). A personal byline needs no publisher: `author` is the Person.
|
||||||
|
*
|
||||||
|
* ⚠️ **NO `wordCount`, NO `articleSection` KEYWORD STUFFING, AND NO
|
||||||
|
* `interactionStatistic`.** The first is derivable and adds nothing; the last is
|
||||||
|
* where a view count would go, and §4 Forbidden's reasoning about unverifiable
|
||||||
|
* numbers applies to a field nobody reads exactly as it applies to a page.
|
||||||
|
*/
|
||||||
|
export function articleGraph(opts: {
|
||||||
|
slug: string;
|
||||||
|
headline: string;
|
||||||
|
description: string;
|
||||||
|
datePublished: Date;
|
||||||
|
dateModified?: Date;
|
||||||
|
/** The article's own OG card, absolute. docs/04 lists `image` on `Article`. */
|
||||||
|
imageUrl: string;
|
||||||
|
/** The Person node's image — the portrait, not the card. Two different
|
||||||
|
* claims: this one is a photograph of a person. */
|
||||||
|
personImageUrl?: string;
|
||||||
|
}) {
|
||||||
|
const path = `/insights/${opts.slug}/`;
|
||||||
|
const iso = (d: Date) => d.toISOString().slice(0, 10);
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [
|
||||||
|
{
|
||||||
|
'@type': 'Article',
|
||||||
|
'@id': `${SITE.url}${path}#article`,
|
||||||
|
headline: opts.headline,
|
||||||
|
description: opts.description,
|
||||||
|
url: `${SITE.url}${path}`,
|
||||||
|
datePublished: iso(opts.datePublished),
|
||||||
|
dateModified: iso(opts.dateModified ?? opts.datePublished),
|
||||||
|
author: { '@id': PERSON_ID },
|
||||||
|
image: opts.imageUrl,
|
||||||
|
inLanguage: 'en-CA',
|
||||||
|
},
|
||||||
|
personNode(opts.personImageUrl),
|
||||||
|
breadcrumbNode(path, [
|
||||||
|
{ name: 'Home', href: '/' },
|
||||||
|
{ name: 'Insights', href: '/insights/' },
|
||||||
|
{ name: opts.headline, href: path },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
+438
-70
@@ -47,21 +47,45 @@ export const SITE = {
|
|||||||
* licence — and do not imply it either. See AGENTS.md §4 Forbidden.
|
* licence — and do not imply it either. See AGENTS.md §4 Forbidden.
|
||||||
*/
|
*/
|
||||||
export const CREDENTIALS = {
|
export const CREDENTIALS = {
|
||||||
designations: ['Q.Med (ADRIC / ADRIO)'],
|
/**
|
||||||
inProgress: ['Q.Arb — commenced August 2026'], // [verified 2026-08-26]
|
* BOTH ARE HELD. Q.Arb joined this list on 2026-08-29 (AGENTS.md §4,
|
||||||
goal: 'C.Med-Arb (Chartered Mediator-Arbitrator)',
|
* `[verified 2026-08-29 — Pouya]`); it was `inProgress` before that.
|
||||||
|
*
|
||||||
|
* ⚠️ NO ACQUISITION DATE IS PUBLISHED for either, and this file does not hold
|
||||||
|
* one. §4 is the only record of when Q.Arb was obtained — a second copy here
|
||||||
|
* is a copy that goes stale, and §4 currently carries an OPEN conflict on
|
||||||
|
* that date (Q55). Do not resolve it by writing a date into this file.
|
||||||
|
*
|
||||||
|
* There is NO `inProgress` KEY, and its absence is the mechanism. Nothing is
|
||||||
|
* in progress, and a key that exists is a key a page will eventually render —
|
||||||
|
* deleting it makes reaching for one a build error rather than a copy defect.
|
||||||
|
* Same pattern as the deleted `class` prop.
|
||||||
|
*/
|
||||||
|
designations: ['Q.Med (ADRIC / ADRIO)', 'Q.Arb (ADRIC / ADRIO)'],
|
||||||
education: ['JD, Bond University'],
|
education: ['JD, Bond University'],
|
||||||
certifications: [
|
certifications: [
|
||||||
'Kompass Arbitration Certificate Program',
|
'Kompass Arbitration Certificate Program',
|
||||||
'Stitt Feld Handy — negotiation and ADR workshop series',
|
/* "sequence", NOT "series" — §4: "Stitt Feld Handy negotiation and ADR
|
||||||
|
workshop **sequence** — completed". This list said "series" while the bio
|
||||||
|
prose on the same page said "sequence": one certification, two names, one
|
||||||
|
page. Found by `claims-auditor` 2026-08-28. Same shape as the C.Med-Arb
|
||||||
|
error — a third-party programme name that four review passes read without
|
||||||
|
checking it was right. */
|
||||||
|
'Stitt Feld Handy — negotiation and ADR workshop sequence',
|
||||||
],
|
],
|
||||||
languages: ['English', 'Farsi'],
|
languages: ['English', 'Farsi'],
|
||||||
/**
|
/**
|
||||||
* [verified 2026-08-26 — Pouya, AGENTS.md Q28 and the CTF addition of the same
|
* [verified 2026-09-02 — Pouya, R10's cutover fire] — re-confirmed on the day
|
||||||
* date] — and FOR NOW.
|
* of cutover: ADRIC, ADRIO, the three OBA sections and the CTF all current.
|
||||||
|
* Earlier stamps: 2026-08-28 (Q44), 2026-08-26 (Q28 plus the CTF addition of
|
||||||
|
* the same date). AND FOR NOW — the stamp is a snapshot, not a warranty.
|
||||||
*
|
*
|
||||||
* WHAT §4 ACTUALLY SAYS ABOUT RENEWAL, because a widened version of it reached
|
* WHAT §4 ACTUALLY SAYS ABOUT RENEWAL, because a widened version of it reached
|
||||||
* a public page. §4: *"Both the OBA sections and the CTF renew yearly."* It
|
* a public page. §4, quoted exactly: *"the OBA sections and the CTF renew
|
||||||
|
* yearly"* — NOT "Both the OBA sections…", which is how this comment quoted it
|
||||||
|
* until 2026-08-28. The substance was right and the quotation marks were not,
|
||||||
|
* in the comment written to stop a widened paraphrase of this exact sentence.
|
||||||
|
* Found by `claims-auditor`. It
|
||||||
* says **nothing** about ADRIC's or ADRIO's renewal period. An earlier form of
|
* says **nothing** about ADRIC's or ADRIO's renewal period. An earlier form of
|
||||||
* this comment read "Both the OBA sections and the Canadian Tax Foundation
|
* this comment read "Both the OBA sections and the Canadian Tax Foundation
|
||||||
* renew yearly, so every line below is a fact with a shelf life", which is two
|
* renew yearly, so every line below is a fact with a shelf life", which is two
|
||||||
@@ -70,12 +94,24 @@ export const CREDENTIALS = {
|
|||||||
* `/about/`, and into §9 Q44. Exactly the SES-DKIM duplication shape: the copy
|
* `/about/`, and into §9 Q44. Exactly the SES-DKIM duplication shape: the copy
|
||||||
* that goes stale is the one nobody re-reads, and this copy became public copy.
|
* that goes stale is the one nobody re-reads, and this copy became public copy.
|
||||||
*
|
*
|
||||||
* **NOT PUBLISHED AS OF 2026-08-28 — R10 / Q44.** R10 requires a
|
* **PUBLISHED FROM 2026-08-28 — Q44 CLOSED.** Pouya re-confirmed all four as
|
||||||
* re-confirmation *"before any page listing memberships ships"*, `/about/` is
|
* current, which discharges R10's prohibition, and `/about/` now renders a
|
||||||
* that page, and the re-confirmation is a fact only Pouya holds. It was not
|
* Memberships group from this array.
|
||||||
* obtained, so `/about/` ships its Credentials section WITHOUT a memberships
|
*
|
||||||
* group and carries a `TODO(pouya)`. Do not render this array on a public page
|
* **RE-CONFIRMED 2026-09-02 — R10's cutover event, and that is the stamp
|
||||||
* until Q44 closes.
|
* above.** R10 fires on an event and cutover is one of its two; a stamp is not
|
||||||
|
* a renewal receipt, so this was asked again rather than read again.
|
||||||
|
*
|
||||||
|
* ⚠️ **RENDER THE LIST; NEVER RENDER A CLAIM ABOUT ITS CURRENCY.** No
|
||||||
|
* "renewed annually", no "current as of", no "listed as current", no stamp
|
||||||
|
* date in the markup. Pouya's ruling: *"List the memberships; promise nothing
|
||||||
|
* about their future state. The list is a snapshot, and only §4's stamp says
|
||||||
|
* when it was taken."* He declined renewal-date tracking, so there is no date
|
||||||
|
* behind such a sentence and nothing in this repo could support one.
|
||||||
|
*
|
||||||
|
* R10 stays live and now fires on an **event** rather than a date: re-confirm
|
||||||
|
* before any cutover or major republish. OCNI is why — §4 carries it as lapsed
|
||||||
|
* and unpublishable, and that was found roughly a year late.
|
||||||
*
|
*
|
||||||
* NOT OCNI (lapsed — §4: "not current, do not publish") and NOT the Law
|
* NOT OCNI (lapsed — §4: "not current, do not publish") and NOT the Law
|
||||||
* Society: listing the LSO implies licensure, which D13 bars. Do not add
|
* Society: listing the LSO implies licensure, which D13 bars. Do not add
|
||||||
@@ -95,6 +131,38 @@ export const CREDENTIALS = {
|
|||||||
*/
|
*/
|
||||||
export const BOUTIQUE = 'a Toronto litigation and ADR boutique' as const;
|
export const BOUTIQUE = 'a Toronto litigation and ADR boutique' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GRAPH form of the memberships — `Person.memberOf` in `src/data/schema.ts`.
|
||||||
|
*
|
||||||
|
* ⚠️ SEPARATE FROM `CREDENTIALS.memberships` ON PURPOSE, AND THEY DIFFER ON
|
||||||
|
* THREE OF FOUR LINES: the display list carries the acronyms (`ADR Institute of
|
||||||
|
* Canada (ADRIC)`) and the OBA's three sections; this list names bare
|
||||||
|
* organisations. Only `Canadian Tax Foundation` is identical.
|
||||||
|
*
|
||||||
|
* **Why bare names:** `memberOf` points at an *organisation*, and nothing is
|
||||||
|
* named "Ontario Bar Association — Construction & Infrastructure, ADR, and
|
||||||
|
* Civil Litigation sections". A triple naming that cannot reconcile against the
|
||||||
|
* real entity gives up most of what the field buys. This is a **narrowing** of
|
||||||
|
* §4's row — §4 names the OBA as the body and then qualifies the sections — and
|
||||||
|
* not an inference beyond it. Do not read it the other way and "restore" the
|
||||||
|
* qualifiers into a name.
|
||||||
|
*
|
||||||
|
* ⚠️ THE SAME TWO EXCLUSIONS GOVERN THIS ARRAY, repeated here because this is
|
||||||
|
* where an addition would land: **NOT OCNI** (§4: not current, do not publish)
|
||||||
|
* and **NOT the Law Society**. Q51 is the distinction — the Law Society is the
|
||||||
|
* **regulator**, so membership is licensure, which D13 bars; the OBA is a
|
||||||
|
* **voluntary association**, which admits members it does not license.
|
||||||
|
*
|
||||||
|
* BOTH ARRAYS ARE THE SAME §4 ROW SEEN TWICE, so their lengths are asserted at
|
||||||
|
* compile time — see `_MembershipParity` below. **Their contents are not.**
|
||||||
|
*/
|
||||||
|
export const MEMBERSHIP_ORGS = [
|
||||||
|
'ADR Institute of Canada',
|
||||||
|
'ADR Institute of Ontario',
|
||||||
|
'Ontario Bar Association',
|
||||||
|
'Canadian Tax Foundation',
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The boutique role and the litigation line — the two highest-risk strings on
|
* The boutique role and the litigation line — the two highest-risk strings on
|
||||||
* the site, so they live here rather than being typed into a page.
|
* the site, so they live here rather than being typed into a page.
|
||||||
@@ -105,9 +173,16 @@ export const BOUTIQUE = 'a Toronto litigation and ADR boutique' as const;
|
|||||||
* these are the two where the wording IS the compliance.
|
* these are the two where the wording IS the compliance.
|
||||||
*/
|
*/
|
||||||
export const ROLE = {
|
export const ROLE = {
|
||||||
/** §4 verbatim. docs/04: this is `jobTitle` in the Person JSON-LD, and
|
/**
|
||||||
* `worksFor` is OMITTED — populating it either names the boutique (D16) or
|
* §4 verbatim, and it is the VISIBLE role only. Rendered as
|
||||||
* misstates the employer. */
|
* "I am {ROLE.title} at {ROLE.at}", always paired with BOUTIQUE (D16).
|
||||||
|
*
|
||||||
|
* NEVER the JSON-LD `jobTitle` — that takes PRACTICE_JOB_TITLE below (Q47).
|
||||||
|
* §4's row is one fact with two halves and visible copy always pairs them;
|
||||||
|
* a graph field carrying only "Director of Firm Operations", on a node whose
|
||||||
|
* `url` is this practice's `/about/`, lets a consumer attach the boutique
|
||||||
|
* title to this entity.
|
||||||
|
*/
|
||||||
title: 'Director of Firm Operations', // [verified 2026-08-25 — strategy brief §I]
|
title: 'Director of Firm Operations', // [verified 2026-08-25 — strategy brief §I]
|
||||||
/** Always rendered with BOUTIQUE, never with a firm name (D16). */
|
/** Always rendered with BOUTIQUE, never with a firm name (D16). */
|
||||||
at: BOUTIQUE,
|
at: BOUTIQUE,
|
||||||
@@ -133,6 +208,40 @@ export const ROLE = {
|
|||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Person node's `jobTitle`. §4 Verified: **Practised role — "Mediator"**
|
||||||
|
* `[verified 2026-08-28 — Pouya, Q49]`, supported by the Q.Med row and by
|
||||||
|
* "Has completed multiple sole mediations".
|
||||||
|
*
|
||||||
|
* NOT `ROLE.title`, and the separation is Q47's ruling: this field describes
|
||||||
|
* *this practice*, the boutique role stays in visible copy, and D16 keeps the
|
||||||
|
* boutique unnamed either way.
|
||||||
|
*
|
||||||
|
* ⚠️ DO NOT WIDEN IT TO INCLUDE ARBITRATION. "Arbitrator" as a practised role
|
||||||
|
* has **no §4 row** — the register verifies completed sole mediations, and for
|
||||||
|
* arbitration only that appointments are *accepted*. `jobTitle` is the most
|
||||||
|
* role-asserting field in the vocabulary and consumers render it beside `name`,
|
||||||
|
* so "Mediator and Commercial Arbitrator" there invites the reader to supply a
|
||||||
|
* track record for both halves. The arbitration **offering** is carried by
|
||||||
|
* `description` and `serviceType`, which is where an offering belongs.
|
||||||
|
*/
|
||||||
|
export const PRACTICE_JOB_TITLE = 'Mediator' as const;
|
||||||
|
|
||||||
|
/* NO `PRACTICE_ENTITY`, AND NO `Person.worksFor` — Q49(b), declined 2026-08-28.
|
||||||
|
*
|
||||||
|
* §4 rows "Operator of SML Company Ltd **alongside** the practice". "The entity
|
||||||
|
* the practice **operates through**" is a different structural relation and has
|
||||||
|
* no row. Two further reasons it must not come back on a tidy-up:
|
||||||
|
*
|
||||||
|
* - `ProfessionalService.provider` IS this Person, so
|
||||||
|
* `provider → Person → worksFor → SML` asserts **transitively** that the
|
||||||
|
* practice and the company are the same entity — the claim `about.astro`
|
||||||
|
* deliberately removed from visible prose as "a corporate-structure claim".
|
||||||
|
* Scoping the value to a bare name does not reach this.
|
||||||
|
* - `SITE.entity` already holds the string for the footer. A second constant
|
||||||
|
* for the same name is the drift this file keeps paying for.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* THE Q41(a) SENTENCE. It lives here for the reason `ROLE` above lives here:
|
* THE Q41(a) SENTENCE. It lives here for the reason `ROLE` above lives here:
|
||||||
* *"these are the two where the wording IS the compliance."*
|
* *"these are the two where the wording IS the compliance."*
|
||||||
@@ -160,6 +269,144 @@ export const ASYMMETRY_LINE =
|
|||||||
'honest part. A law degree on one side. A working engineering practice on ' +
|
'honest part. A law degree on one side. A working engineering practice on ' +
|
||||||
'the other. One is training I hold. The other is work I still do.';
|
'the other. One is training I hold. The other is work I still do.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE SENTENCE THAT ANSWERS THE CAPACITY QUESTION WITHOUT TAKING A SIDE OF IT.
|
||||||
|
*
|
||||||
|
* `docs/03` ratifies this as a reusable pattern and it took three attempts and
|
||||||
|
* two audits to get here:
|
||||||
|
*
|
||||||
|
* 1. "I do not give legal advice" — an ELECTION. Implies entitlement
|
||||||
|
* withheld by choice. Flagged by audit 1.
|
||||||
|
* 2. "I cannot give legal advice" — a DENIAL of capacity. Flagged by audit 2.
|
||||||
|
* 3. This one — makes no capacity claim at all.
|
||||||
|
*
|
||||||
|
* Both audits were right, and that is why the third version works: 1 and 2 are
|
||||||
|
* opposite answers to a question §4 records as `[unestablished]` and instructs
|
||||||
|
* this repository to answer neither way. The shipped sentence states the ROLE
|
||||||
|
* and the CONSEQUENCE for the reader, and stops.
|
||||||
|
*
|
||||||
|
* IT IS A CONSTANT BECAUSE `docs/03` NAMED WHERE IT WOULD RECUR AND WAS RIGHT:
|
||||||
|
* *"Where this will come up next: `/practice/` (step 5) and `/for-parties/`,
|
||||||
|
* both of which have to tell an unrepresented party what the neutral will and
|
||||||
|
* will not do for them — the exact place the 'cannot' phrasing feels most
|
||||||
|
* natural and is most wrong."* It was typed into `/mediation/` at step 4 and
|
||||||
|
* `/practice/` needs it at step 5, which is two copies of the sentence whose
|
||||||
|
* exact wording IS the compliance. Same argument as `ASYMMETRY_LINE`.
|
||||||
|
*
|
||||||
|
* Three tests before any variant of this ships. It fails if any is yes:
|
||||||
|
* 1. Could a reader infer he IS entitled to do the thing?
|
||||||
|
* 2. Could a reader infer he is NOT?
|
||||||
|
* 3. Does it contain a verb of capacity or permission attached to him at all?
|
||||||
|
*/
|
||||||
|
export const NEUTRAL_ROLE_LINE =
|
||||||
|
'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.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE CONDUCT UNDERTAKINGS — Q54, ANSWERED BY POUYA 2026-08-29, plus (g).
|
||||||
|
*
|
||||||
|
* ⚠️ **(a)–(f) ARE Q54's SIX. (g) IS NOT** — it was attested 2026-09-03 to
|
||||||
|
* close D20 finding 13 and carries its own stamp on the object below. The
|
||||||
|
* heading no longer states a count: this comment said "THE SIX" while the
|
||||||
|
* object held seven for exactly as long as it took to notice.
|
||||||
|
*
|
||||||
|
* A THIRD CLASS OF CLAIM, and the class is his: not a credential (a fact about
|
||||||
|
* him, §4 Verified) and not an offering (a process the practice conducts, §4
|
||||||
|
* Offerings) but **a commitment he has now made**, which binds because he made
|
||||||
|
* it. Under Q43 these are service commitments — publishable the moment he has
|
||||||
|
* said them. He has said them.
|
||||||
|
*
|
||||||
|
* THEY LIVE HERE FOR THE REASON `ROLE` AND `ASYMMETRY_LINE` LIVE HERE: the
|
||||||
|
* wording IS the substance. With a credential, a loose paraphrase overstates a
|
||||||
|
* fact. With an undertaking, a loose paraphrase **changes what was promised** —
|
||||||
|
* and it does so silently, because nothing in a build fails when a promise gets
|
||||||
|
* a little smaller. Pouya's instruction, recorded on the §4 rows: any later
|
||||||
|
* softening is a change to a published commitment, not a copy edit.
|
||||||
|
*
|
||||||
|
* So: render these, never retype them, never trim one to fit a layout, and
|
||||||
|
* never "tighten" one. If one should read differently, that is a decision for
|
||||||
|
* Pouya and a Change Log entry, and the diff on this constant is what makes it
|
||||||
|
* visible as one.
|
||||||
|
*
|
||||||
|
* (c) IS THE EXPENSIVE ONE AND IT SHIPS AS DRAFTED. Pouya's reasoning, kept
|
||||||
|
* because it is the part a future reader would otherwise have to reconstruct:
|
||||||
|
* it is the strongest available answer to the med-arb fairness objection, and
|
||||||
|
* cheaper in practice than it sounds — the arbitral phase runs on the
|
||||||
|
* evidentiary record, not the caucus, so the case where a neutral genuinely
|
||||||
|
* cannot decide without confidential material is uncommon. `/med-arb/` was
|
||||||
|
* raising the hardest question about med-arb and answering it only at the level
|
||||||
|
* of process design.
|
||||||
|
*
|
||||||
|
* ⚠️ (d) AND (e) SHIPPED FOR ONE PASS AT STEP 4 AND WERE REMOVED. `claims-auditor`
|
||||||
|
* found them: the gate was applied to `/med-arb/` in the same change set that
|
||||||
|
* wrote them and not applied one file over. They are here now because they are
|
||||||
|
* answered, not because the gate relaxed.
|
||||||
|
*/
|
||||||
|
export const CONDUCT_UNDERTAKINGS = {
|
||||||
|
/** (a) `/med-arb/` — the switch. */
|
||||||
|
medArbSwitch:
|
||||||
|
'The switch is agreed in writing before the mediation phase begins, or I ' +
|
||||||
|
'do not take the appointment. I will not convert a mediation into an ' +
|
||||||
|
'arbitration on the day because the room has run out of road.',
|
||||||
|
/** (b) `/med-arb/` — caucus material. */
|
||||||
|
medArbCaucus:
|
||||||
|
'If a party tells me something in caucus they are not prepared for me to ' +
|
||||||
|
'rely on as arbitrator, they say so at the time, and it does not enter ' +
|
||||||
|
'the arbitral record.',
|
||||||
|
/** (c) `/med-arb/` — the hard one. See the header. Ships as drafted. */
|
||||||
|
medArbStepOut:
|
||||||
|
'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.',
|
||||||
|
/** (d) `/mediation/` — caucus confidentiality. */
|
||||||
|
mediationCaucus:
|
||||||
|
'What a party tells me in caucus stays in that caucus until they tell me ' +
|
||||||
|
'I may use it, and I do not carry a number across the hall that I was not ' +
|
||||||
|
'given to carry.',
|
||||||
|
/** (e) `/arbitration/` — procedure. */
|
||||||
|
arbitrationProcedure:
|
||||||
|
'I will not run a process whose shape nobody agreed to in advance.',
|
||||||
|
/** (f) `/arbitration/` — the award date. */
|
||||||
|
arbitrationAwardDate:
|
||||||
|
'The date the award is due is fixed in the first procedural order rather ' +
|
||||||
|
'than left open.',
|
||||||
|
/**
|
||||||
|
* (g) `/legal/privacy/` — the conflicts check. **ATTESTED 2026-09-03 by
|
||||||
|
* Pouya, closing D20 finding 13.** It is NOT one of the Q54 six: its own
|
||||||
|
* date, its own ruling, and it is stamped separately below.
|
||||||
|
*
|
||||||
|
* The page was already stating a conflicts undertaking in prose, and §4's
|
||||||
|
* gate for this class is one line — he must have made it IN TERMS. He now
|
||||||
|
* has, so the sentence is rendered from here rather than typed there.
|
||||||
|
*
|
||||||
|
* ⚠️ **IT IS HIS WORDING, NOT A RENDERING OF IT, AND THAT IS THE WHOLE GATE.**
|
||||||
|
* The attestation is *"runs a conflicts check on every inquiry before
|
||||||
|
* engaging"*. This string shipped for one round as *"before I accept an
|
||||||
|
* appointment"* — the site's own vocabulary, defensible, and **a paraphrase of
|
||||||
|
* a commitment the page publishes as his**. §4's gate for this class is that
|
||||||
|
* he made it IN TERMS, and a substitution recorded in a code comment is not
|
||||||
|
* that. Do not smooth it back. If "engaging" turns out to be the wrong verb,
|
||||||
|
* the fix is a second attestation, never an edit here.
|
||||||
|
*/
|
||||||
|
conflictsCheck: 'I run a conflicts check on every inquiry before engaging.',
|
||||||
|
} as const; // (a)–(f) [verified 2026-08-29 — Pouya, Q54]; (g) [attested 2026-09-03 — Pouya]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE HELD-DESIGNATIONS SENTENCE, RENDERED AND NEVER RETYPED.
|
||||||
|
*
|
||||||
|
* `/about/` and `/med-arb/` both make this claim in prose, and in the change
|
||||||
|
* set that introduced both they had ALREADY diverged — "the Q.Med and Q.Arb
|
||||||
|
* designations through" versus "Q.Med and Q.Arb through". Two hand-typed forms
|
||||||
|
* of the highest-stakes claim on the site is the shape `ASYMMETRY_LINE` exists
|
||||||
|
* to prevent. Found by `adversarial-reviewer` 2026-08-30.
|
||||||
|
*
|
||||||
|
* It is a CLAUSE, not a sentence, because the two pages continue it
|
||||||
|
* differently. Each supplies its own tail; neither restates the head.
|
||||||
|
*/
|
||||||
|
export const DESIGNATIONS_HELD_LINE =
|
||||||
|
'I hold Q.Med and Q.Arb through the ADR Institute of Canada and the ADR ' +
|
||||||
|
'Institute of Ontario';
|
||||||
|
|
||||||
/** The three credential slots. Never matter counts — AGENTS.md §4. */
|
/** The three credential slots. Never matter counts — AGENTS.md §4. */
|
||||||
export const CREDENTIAL_ROW = [
|
export const CREDENTIAL_ROW = [
|
||||||
{ value: 'Q.Med', label: 'ADRIC / ADRIO designation' },
|
{ value: 'Q.Med', label: 'ADRIC / ADRIO designation' },
|
||||||
@@ -187,24 +434,22 @@ export const CREDENTIAL_ROW = [
|
|||||||
* The FOURTH credential slot — separate on purpose, so a three-slot layout
|
* The FOURTH credential slot — separate on purpose, so a three-slot layout
|
||||||
* cannot be handed four by accident and a page has to opt in.
|
* cannot be handed four by accident and a page has to opt in.
|
||||||
*
|
*
|
||||||
* docs/03: 'Fourth slot where the layout has one: Q.Arb — commenced August
|
* ⚠️ IT IS NO LONGER REQUIRED ANYWHERE. Until 2026-08-29 this slot carried
|
||||||
* 2026. Use that wording, not "in progress"' — the weaker form drifts toward
|
* `Q.Arb / Commenced August 2026` and §4's paired-disclosure condition made it
|
||||||
* 'nearly complete', which §4 Forbidden bars outright.
|
* mandatory on any page offering arbitration. Q.Arb is held, that condition is
|
||||||
|
* dissolved, and the slot reverts to what §4's substitution principle always
|
||||||
|
* wanted it for: a fourth credential where the layout has room.
|
||||||
*
|
*
|
||||||
* REQUIRED on any page that offers arbitration, not decorative. §4 Offerings
|
* The label is the ISSUING BODIES, matching `designations` above — not a date.
|
||||||
* carries a paired-disclosure condition: the site may make the offering only
|
* §4 records July 2026 and keeps it off the site.
|
||||||
* while 'stating the second plainly', and 'neither half may be dropped'. The
|
|
||||||
* footer's designation strip satisfies it site-wide; a page whose opening
|
|
||||||
* sentence says 'arbitrator' should not make the reader scroll to the footer
|
|
||||||
* for the stage.
|
|
||||||
*
|
|
||||||
* The em-dash in docs/03's string is carried by the layout (value over label),
|
|
||||||
* not by the text. Same wording, same pairing.
|
|
||||||
*/
|
*/
|
||||||
export const CREDENTIAL_ROW_ARB = {
|
export const CREDENTIAL_ROW_ARB = {
|
||||||
value: 'Q.Arb',
|
value: 'Q.Arb',
|
||||||
label: 'Commenced August 2026',
|
/* IDENTICAL TO `CREDENTIAL_ROW[0].label`. These two render side by side on `/`
|
||||||
} as const; // [verified 2026-08-26 — Pouya]
|
and `/arbitration/`; 'ADRIC / ADRIO' beside 'ADRIC / ADRIO designation'
|
||||||
|
reads as a distinction that is not being drawn. */
|
||||||
|
label: 'ADRIC / ADRIO designation',
|
||||||
|
} as const; // [verified 2026-08-29 — Pouya]
|
||||||
|
|
||||||
/** Analytics: privacy-first and cookieless (D15). No GA4, no consent banner. */
|
/** Analytics: privacy-first and cookieless (D15). No GA4, no consent banner. */
|
||||||
export const ANALYTICS = {
|
export const ANALYTICS = {
|
||||||
@@ -220,6 +465,16 @@ export const ANALYTICS = {
|
|||||||
*/
|
*/
|
||||||
provider: 'plausible' as 'plausible' | 'fathom',
|
provider: 'plausible' as 'plausible' | 'fathom',
|
||||||
domain: 'adr.smlcompany.ca',
|
domain: 'adr.smlcompany.ca',
|
||||||
|
/**
|
||||||
|
* ⚠️ **NOT INSTALLED — D15 decided the provider; deciding is not installing.**
|
||||||
|
* `/legal/privacy/` renders its analytics paragraph from this flag, so the
|
||||||
|
* policy states the fact rather than the intention: a policy naming a
|
||||||
|
* processor that processes nothing is a false disclosure, and a silent one.
|
||||||
|
*
|
||||||
|
* **Flipping this is a change to a published disclosure.** The policy's
|
||||||
|
* "last updated" date moves on the same build.
|
||||||
|
*/
|
||||||
|
installed: false, // [verified 2026-08-31 — no script on any built page]
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const CONTACT = {
|
export const CONTACT = {
|
||||||
@@ -264,13 +519,45 @@ export const FEES = {
|
|||||||
currency: 'CAD',
|
currency: 'CAD',
|
||||||
taxNote: 'All fees are plus HST.',
|
taxNote: 'All fees are plus HST.',
|
||||||
mediation: {
|
mediation: {
|
||||||
/** Prep is bundled AND stated on the page — [verified 2026-08-26].
|
/**
|
||||||
* Do not hide it: at these rates, saying preparation is included is the
|
* TWO SEPARATE ALLOWANCES, AND BOTH PUBLISH AS A CAP — Q58, 2026-08-31.
|
||||||
* point, not a detail. */
|
* `hours` is the **session**; `prepIncluded` is preparation, bundled but
|
||||||
halfDay: { amount: 2000, hours: 3.5, prepIncluded: 2 },
|
* capped. `docs/07` requires each published as a cap and with its noun:
|
||||||
fullDay: { amount: 4000, hours: 7, prepIncluded: 3 },
|
* *"up to 3 hours of session"*, *"including up to 2 hours of preparation"*.
|
||||||
|
* A flat "including 2 hours" sells an entitlement and a bare "preparation
|
||||||
|
* included" sells an uncapped allowance.
|
||||||
|
*/
|
||||||
|
halfDay: { amount: 2000, hours: 3, prepIncluded: 2 },
|
||||||
|
fullDay: { amount: 4000, hours: 6, prepIncluded: 3 },
|
||||||
additionalParty: 500, // each party beyond two
|
additionalParty: 500, // each party beyond two
|
||||||
overtimePerHour: 500, // [verified 2026-08-26]
|
overtimePerHour: 500, // [verified 2026-08-26]
|
||||||
|
/**
|
||||||
|
* ✅ **Q59 RULED — Pouya, 2026-08-31. OVERTIME RUNS FROM THE SESSION CAP**,
|
||||||
|
* i.e. from the fourth hour of a half day and the seventh of a full day —
|
||||||
|
* `hours` above, not the billed envelope.
|
||||||
|
*
|
||||||
|
* ⚠️ **THERE IS NO BOOLEAN FOR THAT, AND THERE WAS ONE FOR AN HOUR.**
|
||||||
|
* `overtimeStartsAfterSessionHours: true` sat here with a 21-line comment
|
||||||
|
* instructing that *"the page must say so wherever it publishes the overtime
|
||||||
|
* rate"* — and `grep -rn overtimeStartsAfterSessionHours src/ scripts/
|
||||||
|
* backend/` returned exactly one line: the declaration. Nothing read it.
|
||||||
|
* `/fees/` and `/bio/` both hardcode the session-cap wording in template
|
||||||
|
* strings, so reversing the flag would have changed nothing and failed
|
||||||
|
* nothing. **A flag that looks like a control and is not is `AGENTS.md` Q22
|
||||||
|
* at constant scope**, which is the defect this project has paid for most
|
||||||
|
* often. Deleted by `adversarial-reviewer`'s finding, 2026-08-31; the ruling
|
||||||
|
* lives in `docs/07` and in §9 Q59, which is where a ruling belongs.
|
||||||
|
*
|
||||||
|
* ⚠️ **`reservation` BELOW IS REAL AND MUST STAY.** It is interpolated into
|
||||||
|
* `/fees/`'s overtime row and into `/bio/`, and it is the half of Q59's
|
||||||
|
* ruling that answers the rate card's arithmetic anomaly: the full-day fee
|
||||||
|
* buys the **day**, so `2000 + 500 × 3 = 3500` against `4000` is not a
|
||||||
|
* penalty for booking properly. A reader who takes the number and skips this
|
||||||
|
* sentence has read a different offer — §12 R5 carries the anomaly.
|
||||||
|
*/
|
||||||
|
reservation:
|
||||||
|
'A full day reserves the day. Half-day overtime is subject to ' +
|
||||||
|
'availability.', // [verified 2026-08-31 — Pouya, Q59]
|
||||||
},
|
},
|
||||||
arbitration: {
|
arbitration: {
|
||||||
perHour: 500,
|
perHour: 500,
|
||||||
@@ -289,6 +576,40 @@ export const FEES = {
|
|||||||
* it, and do not price it.
|
* it, and do not price it.
|
||||||
*/
|
*/
|
||||||
hourly: 500, // [verified 2026-08-26]
|
hourly: 500, // [verified 2026-08-26]
|
||||||
|
/**
|
||||||
|
* MED-ARB IS BILLED BY PHASE, AND THAT IS WHY THERE IS NO NUMBER IN HERE.
|
||||||
|
*
|
||||||
|
* Pouya's ruling, 2026-09-03, closing D20 finding 10. `/fees/` opens *"Every
|
||||||
|
* figure is on this page"* while §4 Offerings carries a **Med-Arb** row that
|
||||||
|
* `docs/07-fees.md` priced nowhere — so the promise was wider than the card.
|
||||||
|
* The ruling closes it by pricing the offering out of the two rate sets that
|
||||||
|
* are already published rather than by narrowing the promise: each phase is
|
||||||
|
* charged at the rates for that process, so no third set of figures exists
|
||||||
|
* and the sentence becomes true as written.
|
||||||
|
*
|
||||||
|
* ⚠️ **THERE IS NO `amount` HERE ON PURPOSE. Do not add one.** A med-arb
|
||||||
|
* figure would be a fourth price for a process priced twice already, and the
|
||||||
|
* first thing it would do is disagree with one of them.
|
||||||
|
*
|
||||||
|
* ⚠️ **`termsApply` SAYS "as they apply to that process on its own", NOT
|
||||||
|
* "to both phases".** The additional-party fee is a MEDIATION row; the
|
||||||
|
* arbitration card has no equivalent. Saying the terms apply to each phase as
|
||||||
|
* they apply to that process invents nothing; saying they apply throughout
|
||||||
|
* would invent an additional-party charge in the arbitral phase.
|
||||||
|
*
|
||||||
|
* INTERIM, set 2026-09-03, reviewed at the §12 R5 twelve-month fee review.
|
||||||
|
* `docs/07` §Med-arb — billed by phase carries the rule and the same stamp.
|
||||||
|
*/
|
||||||
|
medArb: {
|
||||||
|
rule:
|
||||||
|
'Med-arb is billed by phase. The mediation phase is charged at the ' +
|
||||||
|
'mediation rates above. If the matter proceeds to arbitration, that ' +
|
||||||
|
'phase is charged at the arbitration rates above.',
|
||||||
|
noSeparateFee: 'There is no separate med-arb fee.',
|
||||||
|
termsApply:
|
||||||
|
'The additional-party and cancellation terms apply to each phase as ' +
|
||||||
|
'they apply to that process on its own.',
|
||||||
|
}, // [verified 2026-09-03 — Pouya, interim; R5]
|
||||||
cancellation: [
|
cancellation: [
|
||||||
{ window: 'More than 30 days before', fee: 'No fee. Disbursements only.' },
|
{ window: 'More than 30 days before', fee: 'No fee. Disbursements only.' },
|
||||||
{ window: '15 to 30 days before', fee: '50% of the booked fee.' },
|
{ window: '15 to 30 days before', fee: '50% of the booked fee.' },
|
||||||
@@ -358,47 +679,41 @@ export const PRACTICE_AREAS = [
|
|||||||
slug: 'energy',
|
slug: 'energy',
|
||||||
name: 'Energy, Grid & Regulatory',
|
name: 'Energy, Grid & Regulatory',
|
||||||
chip: 'Energy',
|
chip: 'Energy',
|
||||||
|
/**
|
||||||
|
* ⚠️ NOT "connection allocation" — the IESO uses no such term, and its
|
||||||
|
* connection pages contain zero occurrences of "allocation" of any kind.
|
||||||
|
* The real ones are connection assessment and approval (CAA), System Impact
|
||||||
|
* Assessment and Customer Impact Assessment, and Ontario has NO
|
||||||
|
* interconnection queue. Sourced:
|
||||||
|
* `docs/reference/ontario-energy-regulatory.md`. The OEB's Capacity
|
||||||
|
* Allocation Model is a different thing (housing connections). History in
|
||||||
|
* the AGENTS.md entry of 2026-08-29.
|
||||||
|
*/
|
||||||
blurb:
|
blurb:
|
||||||
'Grid connection and allocation, leave-to-construct, ' +
|
'Connection assessment and approval, leave to construct, ' +
|
||||||
'proponent–municipality disputes, IESO market participation.',
|
'proponent–municipality disputes, and IESO market participation.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'insurance',
|
slug: 'insurance',
|
||||||
name: 'Insurance, SABS & LAT',
|
name: 'Insurance, SABS & LAT',
|
||||||
chip: 'Insurance',
|
chip: 'Insurance',
|
||||||
/**
|
/**
|
||||||
* Q41(c) CLOSED 2026-08-27 — and the verification changed the wording again.
|
* ⚠️ NEVER `LAT pre-hearing mediation`, and never a phrasing in which a LAT
|
||||||
|
* proceeding appears to appoint or host the mediator. Rule 2.4 makes
|
||||||
|
* "Pre-Hearing Conference" the Tribunal's own name for a CASE CONFERENCE;
|
||||||
|
* Rule 14.3 puts a Member in the chair. A privately retained neutral cannot
|
||||||
|
* be appointed to it.
|
||||||
*
|
*
|
||||||
* `LAT pre-hearing mediation` (a SEARCH INTENT in `docs/01`, never an
|
* ⚠️ AND NEVER "before filing or after". The Tribunal names MEDIATION for
|
||||||
* offering) must never be published. Pouya's ruling: *"imprecise and must
|
* one moment only — "Before you apply" — and the "continuing after a claim
|
||||||
* not imply appointment by the tribunal. Verify against LAT's own materials
|
* has been filed" clause is expressly about NEGOTIATION. The blurb below
|
||||||
* how its case-conference process is conducted and who conducts it."*
|
* carries the distinction `docs/01` requires instead. Sourced:
|
||||||
*
|
* `docs/reference/lat-case-conference.md`, which holds the full passage and
|
||||||
* Verified 2026-08-28 against the LAT Rules and the LAT-AABS process page,
|
* the correction. History in the AGENTS.md entry of 2026-08-29.
|
||||||
* both extracted into `docs/reference/lat-case-conference.md`:
|
|
||||||
*
|
|
||||||
* - Rule 2.4: *"'Case Conference' has the same meaning as 'Pre-Hearing
|
|
||||||
* Conference' as defined in the SPPA."* **"Pre-hearing" is the
|
|
||||||
* Tribunal's own label**, and what it names is a case conference.
|
|
||||||
* - Rule 14.3: a **Member** presides, and is then disqualified from the
|
|
||||||
* hearing panel. Rule 14.6: parties must attend. The neutral is the
|
|
||||||
* Tribunal's, and a privately retained one cannot be appointed to it.
|
|
||||||
* - The Rules contain **zero** occurrences of `mediat` or `arbitrat`
|
|
||||||
* (0 in 66,593 characters). The concept is not in them.
|
|
||||||
*
|
|
||||||
* The interim read "private mediation of matters before the LAT", which is
|
|
||||||
* ambiguous in the one word that matters: `before` reads as *pending at* as
|
|
||||||
* easily as *prior to*. Replaced with the temporal frame the Tribunal's own
|
|
||||||
* page endorses — *"you may want to consider negotiation or mediation
|
|
||||||
* services... before filing at the LAT-AABS, and continuing... after a
|
|
||||||
* claim has been filed."*
|
|
||||||
*
|
|
||||||
* `/practice/insurance/` at step 5 must say the mediation is PRIVATE and is
|
|
||||||
* not the Tribunal's case conference.
|
|
||||||
*/
|
*/
|
||||||
blurb:
|
blurb:
|
||||||
'Accident benefits and SABS entitlement, MIG disputes, and private ' +
|
'Accident benefits and SABS entitlement, MIG disputes, and private ' +
|
||||||
'mediation alongside a LAT application, before filing or after.',
|
"mediation retained by the parties, not the Tribunal's case conference.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'shareholder',
|
slug: 'shareholder',
|
||||||
@@ -453,6 +768,59 @@ type _SlugsWithoutAnArea = Exclude<
|
|||||||
>;
|
>;
|
||||||
export type _SlugCoverage = _AssertNever<_SlugsWithoutAnArea>;
|
export type _SlugCoverage = _AssertNever<_SlugsWithoutAnArea>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two membership arrays must stay the same LENGTH — contents are not
|
||||||
|
* checked and cannot be. They are two views of ONE §4 row, and the failure mode
|
||||||
|
* is adding a membership to the page and not to the graph, which is otherwise
|
||||||
|
* silent.
|
||||||
|
*
|
||||||
|
* ⚠️ IT FAILS `npm run check`, NOT `npm run build` — measured. Both the deploy
|
||||||
|
* workflow and `scripts/deploy-local.sh` run `check` first, so the deploy path
|
||||||
|
* is covered; a bare `astro build` is not. And it depends on both arrays keeping
|
||||||
|
* `as const`: without it `length` widens to `number`, `number extends number` is
|
||||||
|
* true, and this assertion passes forever while checking nothing.
|
||||||
|
*/
|
||||||
|
type _AssertTrue<T extends true> = T;
|
||||||
|
export type _MembershipParity = _AssertTrue<
|
||||||
|
(typeof CREDENTIALS.memberships)['length'] extends (typeof MEMBERSHIP_ORGS)['length']
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ THE VISIBLE CREDENTIAL ROW HAND-TYPES THE DESIGNATIONS, AND THIS PINS IT.
|
||||||
|
*
|
||||||
|
* `CREDENTIALS.designations` feeds the footer strip, `/about/`'s designation
|
||||||
|
* line, `/about/`'s Designations list and `hasCredential`. `CREDENTIAL_ROW[0]`
|
||||||
|
* and `CREDENTIAL_ROW_ARB` carry the same two facts decomposed into
|
||||||
|
* `value`/`label` for `/` and `/arbitration/`, and nothing connected them —
|
||||||
|
* so a third designation added to §4 and to `designations` would reach four
|
||||||
|
* surfaces and silently miss the credential row.
|
||||||
|
*
|
||||||
|
* That was survivable while the fourth slot held a STAGE (`Commenced August
|
||||||
|
* 2026`), which was an independent fact. On 2026-08-29 it became a duplicate of
|
||||||
|
* `designations[1]`, which is what made the two constants drift-capable.
|
||||||
|
* `adversarial-reviewer` caught that the map-not-index fix celebrated in
|
||||||
|
* `schema.ts` and `docs/04` left this copy untouched.
|
||||||
|
*
|
||||||
|
* A type-level assertion rather than a refactor: the row's `value`/`label`
|
||||||
|
* shape is a LAYOUT decision (value over label, two-up on a phone) and
|
||||||
|
* collapsing it into the prose strings would be the wrong fix. This fails
|
||||||
|
* `npm run check` with a named type the moment the two disagree.
|
||||||
|
*/
|
||||||
|
type _DesignationValue<S> = S extends `${infer V} (${string})` ? V : never;
|
||||||
|
export type _DesignationRowParity = _AssertTrue<
|
||||||
|
_DesignationValue<
|
||||||
|
(typeof CREDENTIALS.designations)[0]
|
||||||
|
> extends (typeof CREDENTIAL_ROW)[0]['value']
|
||||||
|
? _DesignationValue<
|
||||||
|
(typeof CREDENTIALS.designations)[1]
|
||||||
|
> extends (typeof CREDENTIAL_ROW_ARB)['value']
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The five process steps. **HERE, NOT IN THE PAGE**, for the reason written
|
* The five process steps. **HERE, NOT IN THE PAGE**, for the reason written
|
||||||
* against PRACTICE_AREAS above and applied by `adversarial-reviewer` 2026-08-27:
|
* against PRACTICE_AREAS above and applied by `adversarial-reviewer` 2026-08-27:
|
||||||
@@ -490,11 +858,11 @@ export type _SlugCoverage = _AssertNever<_SlugsWithoutAnArea>;
|
|||||||
* "Binding conclusion" alone would read as though a mediation binds, which it
|
* "Binding conclusion" alone would read as though a mediation binds, which it
|
||||||
* does not until the parties sign.
|
* does not until the parties sign.
|
||||||
*
|
*
|
||||||
* NO FEE CLAIM IN ANY BODY. `docs/07` bundles a CAPPED preparation allowance
|
* NO FEE CLAIM IN ANY BODY. `docs/07` §All parameters confirmed requires the
|
||||||
* (2 h in the half day, 3 h in the full day) and says in terms that it "must be
|
* bundled preparation allowance published **in hours and as a cap**, and a
|
||||||
* stated on the page... Do not quietly fold it into the hours figure." A
|
* five-word strip cannot do that. Stating it improperly misdescribes money.
|
||||||
* five-word strip cannot state it properly, and stating it improperly
|
* `/fees/` at step 9. (Cited, not quoted — the quotation that stood here went
|
||||||
* misdescribes money. `/fees/` at step 9.
|
* stale when Q58 corrected the section it came from.)
|
||||||
*/
|
*/
|
||||||
export const PROCESS = [
|
export const PROCESS = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ const { preloadSerifItalic = false, ...seo } = Astro.props;
|
|||||||
/* No SVG favicon. The mark is a shaded ribbon, not flat vector paths, so
|
/* No SVG favicon. The mark is a shaded ribbon, not flat vector paths, so
|
||||||
there is no honest SVG of it to serve — see InfinityMark.astro and
|
there is no honest SVG of it to serve — see InfinityMark.astro and
|
||||||
AGENTS.md Q38. The .ico carries 16/32/48, and is what crawlers request
|
AGENTS.md Q38. The .ico carries 16/32/48, and is what crawlers request
|
||||||
at the root regardless of what is declared here. */
|
at the root regardless of what is declared here.
|
||||||
|
|
||||||
|
The .ico is transparent and the touch icon is opaque cream ON PURPOSE —
|
||||||
|
do not change either to match the other:
|
||||||
|
docs/reference/brand-assets.md §The icon set. */
|
||||||
}
|
}
|
||||||
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
|
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
@@ -77,7 +81,7 @@ const { preloadSerifItalic = false, ...seo } = Astro.props;
|
|||||||
a fallback first, and a serif-to-fallback swap inside a 96px headline
|
a fallback first, and a serif-to-fallback swap inside a 96px headline
|
||||||
moves the whole last line.
|
moves the whole last line.
|
||||||
|
|
||||||
GEIST MONO IS DELIBERATELY NOT. It sets the eyebrow — 12px, uppercase,
|
GEIST MONO IS DELIBERATELY NOT. It sets the eyebrow — 13px, uppercase,
|
||||||
0.18em tracking — and the credential labels. A swap there costs one short
|
0.18em tracking — and the credential labels. A swap there costs one short
|
||||||
line of reflow at a size where the fallback is metrically close, and
|
line of reflow at a size where the fallback is metrically close, and
|
||||||
preloading it would put 95,688 B of font on the critical path instead of
|
preloading it would put 95,688 B of font on the critical path instead of
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
/**
|
||||||
|
* The Open Graph card generator. Spec: docs/04-seo-spec.md §Metadata —
|
||||||
|
* *"the site's own type and palette: display headline on cream, infinity mark,
|
||||||
|
* designation line"*. Discharges `AGENTS.md` R15.
|
||||||
|
*
|
||||||
|
* WHY THE CARDS MATTER AND WHY NOBODY HERE WOULD EVER NOTICE THEM. R15's own
|
||||||
|
* reasoning: a link preview is rendered by LinkedIn, Slack and Teams for a
|
||||||
|
* reader who is not us. Nineteen pages with unique titles previewing
|
||||||
|
* identically is the defect, and it is invisible from inside the repo.
|
||||||
|
*
|
||||||
|
* ⚠️ **TEXT BAKED INTO AN IMAGE IS UNREACHABLE BY `npm run check:claims`.**
|
||||||
|
* That script greps `dist/`'s HTML; a claim rendered into a JPEG is a claim no
|
||||||
|
* mechanical control on this project can see, and under D20 `check:claims` is
|
||||||
|
* the only per-step claims control there is. So the rule for card copy is
|
||||||
|
* structural rather than editorial:
|
||||||
|
*
|
||||||
|
* **A card renders strings that already exist elsewhere in the repo.** The
|
||||||
|
* kicker is `CREDENTIALS.designations`, rendered. The eyebrow and headline
|
||||||
|
* come from `src/data/og-cards.ts`, whose entries are short subject labels
|
||||||
|
* for pages that already ship — not new prose, and never a claim that is not
|
||||||
|
* already made in auditable HTML on the page the card is for.
|
||||||
|
*
|
||||||
|
* COLOURS ARE PARSED OUT OF `tokens.css`, NOT COPIED. `CLAUDE.md` requires
|
||||||
|
* every colour to come from a token, and this file is not CSS — so the choice
|
||||||
|
* was a duplicated hex table or a parse. A duplicated hex table is the SES-DKIM
|
||||||
|
* shape: two copies of one fact, and the stale one is the copy nobody re-reads.
|
||||||
|
* A missing token throws rather than falling back, because a silent fallback
|
||||||
|
* would render a card in the wrong palette and look deliberate.
|
||||||
|
*
|
||||||
|
* FONTS ARE THE STATIC `@fontsource` CUTS, NOT `public/fonts/`, AND THAT IS
|
||||||
|
* FORCED. Measured 2026-08-31: satori parses TTF/OTF/WOFF and not WOFF2, and
|
||||||
|
* decompressing `public/fonts/geist-latin-wght-normal.woff2` to TTF then
|
||||||
|
* **throws** inside satori's `opentype.js` fork —
|
||||||
|
* `parseFvarAxis: Cannot read properties of undefined` — because Fontsource's
|
||||||
|
* subsetting drops the `name` records that the variable font's `fvar` table
|
||||||
|
* points at. `@fontsource/geist` and `@fontsource/instrument-serif` ship static
|
||||||
|
* 400 cuts as `.woff`, which satori reads directly. Same typefaces, same
|
||||||
|
* upstream version (5.3.0) as `docs/reference/fonts-provenance.md` records for
|
||||||
|
* the site's own files, same weight. Build-time only: no visitor fetches these.
|
||||||
|
*/
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import satori from 'satori';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { CREDENTIALS } from '../data/site';
|
||||||
|
|
||||||
|
export const OG_WIDTH = 1200;
|
||||||
|
export const OG_HEIGHT = 630;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ PATHS ARE RESOLVED FROM `process.cwd()`, NOT FROM `import.meta.url`, AND
|
||||||
|
* THAT IS NOT A STYLE CHOICE. Measured: with `import.meta.url` the build fails
|
||||||
|
* with `ENOENT ... /dist/.prerender/chunks/../styles/tokens.css`, because Astro
|
||||||
|
* bundles this module into `dist/.prerender/chunks/` and `import.meta.url` is
|
||||||
|
* the CHUNK's location, not this file's. It works under `astro dev`, where the
|
||||||
|
* module is served from source — the same dev-passes / build-fails shape as the
|
||||||
|
* `animation-timeline` minifier defect, and the reason `/build` Phase 5 checks
|
||||||
|
* the built output rather than the dev server.
|
||||||
|
*
|
||||||
|
* `astro build` runs with the project root as cwd. Every read below is
|
||||||
|
* build-time only and throws with the path if it is wrong, so a future runner
|
||||||
|
* with a different cwd fails loudly rather than shipping a blank card.
|
||||||
|
*/
|
||||||
|
const fromRoot = (...parts: string[]) => join(process.cwd(), ...parts);
|
||||||
|
const TOKENS_CSS = fromRoot('src', 'styles', 'tokens.css');
|
||||||
|
const MARK_PNG = fromRoot('src', 'assets', 'brand', 'sml-infinity-mark.png');
|
||||||
|
const SERIF_WOFF = fromRoot(
|
||||||
|
'node_modules',
|
||||||
|
'@fontsource',
|
||||||
|
'instrument-serif',
|
||||||
|
'files',
|
||||||
|
'instrument-serif-latin-400-normal.woff',
|
||||||
|
);
|
||||||
|
const SANS_WOFF = fromRoot(
|
||||||
|
'node_modules',
|
||||||
|
'@fontsource',
|
||||||
|
'geist',
|
||||||
|
'files',
|
||||||
|
'geist-latin-400-normal.woff',
|
||||||
|
);
|
||||||
|
|
||||||
|
/** The tokens this card uses, by their `tokens.css` names. */
|
||||||
|
const NEEDED = ['cream', 'ink', 'ink-soft', 'maroon', 'gold'] as const;
|
||||||
|
type TokenName = (typeof NEEDED)[number];
|
||||||
|
|
||||||
|
async function loadPalette(): Promise<Record<TokenName, string>> {
|
||||||
|
const css = await readFile(TOKENS_CSS, 'utf8');
|
||||||
|
const palette = {} as Record<TokenName, string>;
|
||||||
|
for (const name of NEEDED) {
|
||||||
|
// Only the literal hex declarations in the palette block, never an alias
|
||||||
|
// like `--bg: var(--cream)` — resolving one level of indirection here would
|
||||||
|
// invite resolving two, and this generator has no cascade.
|
||||||
|
const match = new RegExp(`--${name}:\\s*(#[0-9a-fA-F]{3,8})\\s*;`).exec(
|
||||||
|
css,
|
||||||
|
);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(
|
||||||
|
`src/styles/tokens.css has no literal --${name} hex declaration. ` +
|
||||||
|
'The OG card generator reads the palette from that file so the card ' +
|
||||||
|
'and the site cannot drift; add the token there rather than a hex ' +
|
||||||
|
'value here.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
palette[name] = match[1];
|
||||||
|
}
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mark, resized once and inlined as a data URI. satori resolves no URLs, so
|
||||||
|
* a data URI is the only way in — and `CLAUDE.md`'s rule against base64-inlining
|
||||||
|
* an image is about bytes shipped to a visitor in HTML. Nothing here reaches a
|
||||||
|
* visitor: this string exists for the few milliseconds before sharp flattens
|
||||||
|
* the SVG to a JPEG.
|
||||||
|
*
|
||||||
|
* 132 px wide at the mark's own 2668 × 1704 (1.5657:1), so it renders at its
|
||||||
|
* true proportion — the ratio `InfinityMark.astro` records as measured, and the
|
||||||
|
* one a hand-traced path got wrong (Q32).
|
||||||
|
*/
|
||||||
|
const MARK_W = 132;
|
||||||
|
const MARK_H = Math.round((MARK_W * 1704) / 2668);
|
||||||
|
|
||||||
|
type Assets = {
|
||||||
|
palette: Record<TokenName, string>;
|
||||||
|
serif: Buffer;
|
||||||
|
sans: Buffer;
|
||||||
|
mark: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Read once per build, not once per card — seventeen pages plus every article
|
||||||
|
* go through here in one `astro build`. */
|
||||||
|
let assets: Promise<Assets> | null = null;
|
||||||
|
|
||||||
|
function loadAssets(): Promise<Assets> {
|
||||||
|
assets ??= (async () => {
|
||||||
|
const [palette, serif, sans, markPng] = await Promise.all([
|
||||||
|
loadPalette(),
|
||||||
|
readFile(SERIF_WOFF),
|
||||||
|
readFile(SANS_WOFF),
|
||||||
|
readFile(MARK_PNG),
|
||||||
|
]);
|
||||||
|
const markResized = await sharp(markPng)
|
||||||
|
.resize({ width: MARK_W * 2, withoutEnlargement: true })
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
return {
|
||||||
|
palette,
|
||||||
|
serif,
|
||||||
|
sans,
|
||||||
|
mark: `data:image/png;base64,${markResized.toString('base64')}`,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headline size, chosen from length rather than measured. satori does not
|
||||||
|
* shrink text to fit and silently overflows its container instead, so a card
|
||||||
|
* with a long headline would crop — the exact class of defect nobody on this
|
||||||
|
* project would ever see. The bands are set so the longest entry in
|
||||||
|
* `og-cards.ts` renders on three lines at most. **`npm run og:proof -- --sheet`
|
||||||
|
* writes a contact sheet of every card to `dist/og-proof.jpg`**, which is how
|
||||||
|
* that claim is checked by looking — and the `--sheet` flag is required, because
|
||||||
|
* a plain `npm run og:proof` produces no images at all. *(This sentence named
|
||||||
|
* `dist/og-proof/` and omitted the flag, so the one documented mitigation for
|
||||||
|
* this file's own stated hazard was wrong in both the path and the command.
|
||||||
|
* Found by `adversarial-reviewer` round 2.)*
|
||||||
|
*/
|
||||||
|
function headlineSize(headline: string): number {
|
||||||
|
if (headline.length > 62) return 58;
|
||||||
|
if (headline.length > 42) return 68;
|
||||||
|
return 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OgCard = {
|
||||||
|
/** Short, uppercased on the card. The page's own eyebrow where it has one. */
|
||||||
|
eyebrow: string;
|
||||||
|
/** The card's display line. A subject label, not new prose — see the header. */
|
||||||
|
headline: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function renderOgCard(card: OgCard): Promise<Buffer> {
|
||||||
|
const { palette, serif, sans, mark } = await loadAssets();
|
||||||
|
const pad = 72;
|
||||||
|
|
||||||
|
const svg = await satori(
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: `${OG_WIDTH}px`,
|
||||||
|
height: `${OG_HEIGHT}px`,
|
||||||
|
backgroundColor: palette.cream,
|
||||||
|
padding: `${pad}px`,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
fontFamily: 'Geist',
|
||||||
|
fontSize: 22,
|
||||||
|
letterSpacing: 4,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: palette.maroon,
|
||||||
|
},
|
||||||
|
children: card.eyebrow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: 44,
|
||||||
|
fontFamily: 'Instrument Serif',
|
||||||
|
fontSize: headlineSize(card.headline),
|
||||||
|
lineHeight: 1.06,
|
||||||
|
letterSpacing: -1,
|
||||||
|
color: palette.ink,
|
||||||
|
},
|
||||||
|
children: card.headline,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Pushes the footer to the bottom edge whatever the headline does.
|
||||||
|
{ type: 'div', props: { style: { display: 'flex', flexGrow: 1 } } },
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
height: '1px',
|
||||||
|
backgroundColor: palette.gold,
|
||||||
|
marginBottom: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
fontFamily: 'Geist',
|
||||||
|
color: palette['ink-soft'],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: { display: 'flex', fontSize: 30 },
|
||||||
|
children: 'Pouya Lajevardi',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'div',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: 8,
|
||||||
|
fontSize: 21,
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
// Rendered from §4's own designation strings, never
|
||||||
|
// retyped. `Q.Arb (ADRIC / ADRIO)` is the publishable
|
||||||
|
// form and no acquisition date appears anywhere.
|
||||||
|
children: CREDENTIALS.designations.join(' · '),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'img',
|
||||||
|
props: { src: mark, width: MARK_W, height: MARK_H },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: OG_WIDTH,
|
||||||
|
height: OG_HEIGHT,
|
||||||
|
fonts: [
|
||||||
|
{ name: 'Instrument Serif', data: serif, weight: 400, style: 'normal' },
|
||||||
|
{ name: 'Geist', data: sans, weight: 400, style: 'normal' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// JPEG, for the reason SEO.astro already gives for the portrait: link-preview
|
||||||
|
// crawlers are not browsers and several still do not decode WebP at all.
|
||||||
|
// 4:4:4 because the card is type on a flat ground, where chroma subsampling
|
||||||
|
// is visible on the letterforms rather than free.
|
||||||
|
return sharp(Buffer.from(svg))
|
||||||
|
.jpeg({ quality: 88, chromaSubsampling: '4:4:4', mozjpeg: true })
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* The 404 page. `docs/04-seo-spec.md`: "Real, styled, with search-intent links
|
||||||
|
* out. CloudFront must return it with a genuine 404 status."
|
||||||
|
*
|
||||||
|
* ⚠️ THE PAGE IS HALF OF THE FIX AND THE DISTRIBUTION IS THE OTHER HALF. Astro
|
||||||
|
* emits this as `dist/404.html`; nothing in the build can make CloudFront serve
|
||||||
|
* it. Until the custom error response exists, a missing URL returns S3's
|
||||||
|
* `AccessDenied` XML — measured 2026-09-01, not assumed: `/about/` and
|
||||||
|
* `/definitely-not-a-page/` both answered **403, `application/xml`, 111 bytes**
|
||||||
|
* on the live distribution. `docs/06` carries the two commands and the reason
|
||||||
|
* the mapping is on 404 rather than 403.
|
||||||
|
*
|
||||||
|
* `noindex`, because a 404 that invites indexing is a 404 that gets indexed.
|
||||||
|
* `robots` is `noindex,follow` so the links out are still crawled, which is the
|
||||||
|
* whole point of a page with links out.
|
||||||
|
*
|
||||||
|
* NO CLAIM ABOUT PRACTICE OR CREDENTIALS APPEARS IN THE VISIBLE COPY, and the
|
||||||
|
* omission is the design rather than an oversight. An error page has no reader
|
||||||
|
* who came for a credential, so a sentence it adds is a sentence `AGENTS.md` §4
|
||||||
|
* has to carry for no return. It names pages and nothing else.
|
||||||
|
*
|
||||||
|
* ⚠️ THE JSON-LD IS A DIFFERENT MATTER AND THIS COMMENT USED TO DENY IT. The
|
||||||
|
* `pageGraph()` above is the shared graph and it DOES emit the §4 Person node —
|
||||||
|
* `jobTitle`, the `description`, and `hasCredential` for Q.Med and Q.Arb. Every
|
||||||
|
* one of those is registered, so it is not a §4 breach; the false statement was
|
||||||
|
* this comment, which invited the next editor to treat the 404 page as outside
|
||||||
|
* the register's blast radius. It is not: this is machine-readable credential
|
||||||
|
* assertion served on every unmatched URL, to exactly the reader `robots.txt`
|
||||||
|
* names — "an assistant that counsel is using to shortlist a neutral". Found by
|
||||||
|
* `adversarial-reviewer`, 2026-09-01.
|
||||||
|
*/
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import Button from '../components/Button.astro';
|
||||||
|
import Eyebrow from '../components/Eyebrow.astro';
|
||||||
|
import { getImage } from 'astro:assets';
|
||||||
|
import ogDefault from '../assets/og-portrait.jpg';
|
||||||
|
import { pageGraph } from '../data/schema';
|
||||||
|
import { CONTACT } from '../data/site';
|
||||||
|
|
||||||
|
const ldImage = await getImage({
|
||||||
|
src: ogDefault,
|
||||||
|
format: 'jpeg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
});
|
||||||
|
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
|
||||||
|
|
||||||
|
/* The routes worth offering, in the order a lost reader is most likely to want
|
||||||
|
them. Not the full sitemap — the footer on this page already carries that. */
|
||||||
|
const routes = [
|
||||||
|
{ href: '/mediation/', label: 'Mediation' },
|
||||||
|
{ href: '/arbitration/', label: 'Arbitration' },
|
||||||
|
{ href: '/med-arb/', label: 'Med-Arb' },
|
||||||
|
{ href: '/practice/', label: 'Practice areas' },
|
||||||
|
{ href: '/fees/', label: 'Fees' },
|
||||||
|
{ href: '/about/', label: 'About' },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Page Not Found · Dispute Resolution · Pouya Lajevardi"
|
||||||
|
description="That page is not here. Mediation, arbitration and med-arb each have a page, the practice areas are listed, and an inquiry can be sent from contact."
|
||||||
|
jsonLd={graph}
|
||||||
|
noindex
|
||||||
|
>
|
||||||
|
<section class="section hero">
|
||||||
|
<div class="wrap">
|
||||||
|
<Eyebrow dot>Not found</Eyebrow>
|
||||||
|
<h1 class="display hero-h">That page is not here.</h1>
|
||||||
|
<div class="prose">
|
||||||
|
<p class="statement">
|
||||||
|
The address may have changed, or it may never have existed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you were looking for something specific, email <a
|
||||||
|
href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a
|
||||||
|
> and say what it was.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="routes" aria-label="Main pages">
|
||||||
|
<ul role="list">
|
||||||
|
{
|
||||||
|
routes.map((route) => (
|
||||||
|
<li>
|
||||||
|
<a href={route.href}>{route.label}</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="cta">
|
||||||
|
<Button href="/">Start at the beginning</Button>
|
||||||
|
<Button href="/contact/" variant="ghost">Send an inquiry →</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
padding-block: var(--space-9) var(--space-11);
|
||||||
|
}
|
||||||
|
.hero-h {
|
||||||
|
margin-block: var(--space-4) var(--space-5);
|
||||||
|
font-size: var(--text-5xl);
|
||||||
|
}
|
||||||
|
.statement {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.routes {
|
||||||
|
margin-block-start: var(--space-7);
|
||||||
|
padding-block-start: var(--space-5);
|
||||||
|
border-block-start: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
.routes ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2) var(--space-6);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.routes a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
/* BOTH AXES. `min-block-size` alone left "Fees" at 37 x 44 px — measured, and
|
||||||
|
under `docs/02`'s 44 x 44 floor on the inline axis while the comment beside
|
||||||
|
it claimed compliance. WCAG 2.5.8's 24 x 24 AA minimum was still met via the
|
||||||
|
`--space-6` gap; this is the project's own stricter floor.
|
||||||
|
`padding-inline` as well as the minimum, so a short label is a wide target
|
||||||
|
rather than a narrow one centred in a wide box. */
|
||||||
|
min-block-size: 44px;
|
||||||
|
min-inline-size: 44px;
|
||||||
|
padding-inline: var(--space-2);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
color: var(--link);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.routes a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3) var(--space-4);
|
||||||
|
margin-block-start: var(--space-8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+250
-258
@@ -11,21 +11,39 @@
|
|||||||
* 1 Portrait, name, designation line → the hero
|
* 1 Portrait, name, designation line → the hero
|
||||||
* 2 Narrative biography, 400–600 words → §Background
|
* 2 Narrative biography, 400–600 words → §Background
|
||||||
* 3 Credentials, structured → §Credentials
|
* 3 Credentials, structured → §Credentials
|
||||||
* 4 The credentialing arc → §The arc
|
* 4 The credentialing arc → REMOVED 2026-08-29, see below
|
||||||
* 5 Languages and cross-cultural → §Language
|
* 5 Languages and cross-cultural → §Language
|
||||||
* 6 Speaking and publications → OMITTED, per the spec itself
|
* 6 Speaking and publications → OMITTED, per the spec itself
|
||||||
* 7 Person JSON-LD + PDF bio → JSON-LD ships; the PDF does not
|
* 7 Person JSON-LD + PDF bio → JSON-LD ships; the PDF does not
|
||||||
*
|
*
|
||||||
|
* ⚠️ ITEM 4 IS GONE, AND IT WAS DELETED RATHER THAN REWRITTEN. Pouya, 2026-08-29:
|
||||||
|
* *"Two held designations, no journey… An arc invites 'where are you on it';
|
||||||
|
* two designations don't."* Q.Arb is held and C.Med-Arb is off the site
|
||||||
|
* entirely, so the section had no subject left — three pills reading Held,
|
||||||
|
* Held, and nothing. What it used to carry now sits in item 3, which is a list
|
||||||
|
* of held things and was always the right shape for it. **Do not restore it
|
||||||
|
* from docs/01's seven-item outline**; that item is struck there too.
|
||||||
|
*
|
||||||
* ITEM 6 IS OMITTED ON THE SPEC'S OWN INSTRUCTION, not by oversight: "Omit the
|
* ITEM 6 IS OMITTED ON THE SPEC'S OWN INSTRUCTION, not by oversight: "Omit the
|
||||||
* section entirely until there is something in it. An empty 'Speaking' heading
|
* section entirely until there is something in it. An empty 'Speaking' heading
|
||||||
* is worse than no heading." Nothing to list.
|
* is worse than no heading." Nothing to list.
|
||||||
*
|
*
|
||||||
* ITEM 7'S PDF IS NOT SHIPPED, and the omission is stated rather than silent —
|
* ITEM 7'S PDF IS NOT SHIPPED, and the omission is stated rather than silent.
|
||||||
* AGENTS.md Q45. No such file exists, and a link to a missing file on the page
|
* **Q45 CLOSED 2026-08-28 — deferred by Pouya to build step 9, alongside
|
||||||
* an appointing body reads is worse than its absence. It is also not a
|
* `/fees/`, and tracked as AGENTS.md §12 R16.** His reasoning: it is a derived
|
||||||
* formatting job: a one-page bio is a credential document circulated DETACHED
|
* artefact, so building it before this page and the fee card are final means
|
||||||
* from the site, where no build and no reviewer ever re-checks it. Two
|
* building it twice. No such file exists yet, and a link to a missing file on
|
||||||
* decisions there are Pouya's.
|
* the page an appointing body reads is worse than its absence.
|
||||||
|
*
|
||||||
|
* It is also not a formatting job: a one-page bio is a credential document
|
||||||
|
* circulated DETACHED from the site, where no build and no reviewer ever
|
||||||
|
* re-checks it. Two decisions travel with R16 and are NOT settled by the
|
||||||
|
* deferral — generated-at-build vs authored once, and whether it carries
|
||||||
|
* anything the site does not.
|
||||||
|
*
|
||||||
|
* This paragraph also comes out at step 9 (R16), which now lists this file.
|
||||||
|
* *(It pointed at Q45 after Q45 closed; `adversarial-reviewer` found that
|
||||||
|
* `grep -rn R16 src` returned nothing at all.)*
|
||||||
*
|
*
|
||||||
* THE PARENT/CHILD SCOPE TRAP, because this page uses <SectionHeading> four
|
* THE PARENT/CHILD SCOPE TRAP, because this page uses <SectionHeading> four
|
||||||
* times. A parent CANNOT style a child component's root element — the rule
|
* times. A parent CANNOT style a child component's root element — the rule
|
||||||
@@ -35,21 +53,30 @@
|
|||||||
* components have had their `class` props deleted so passing one is a build
|
* components have had their `class` props deleted so passing one is a build
|
||||||
* error rather than a silent no-op.
|
* error rather than a silent no-op.
|
||||||
*
|
*
|
||||||
* R10 / Q44 — THE MEMBERSHIPS GROUP IS NOT ON THIS PAGE. R10 is written as a
|
* R10 / Q44 — THE MEMBERSHIPS GROUP SHIPS, AND IT CARRIES NO CURRENCY
|
||||||
* prohibition — re-confirm *before* any page listing memberships ships — and
|
* WARRANTY. Q44 closed 2026-08-28: Pouya re-confirmed all four as current, so
|
||||||
* `/about/` is the page it names. The re-confirmation is a fact only Pouya holds
|
* R10's prohibition is discharged and the group renders. Two conditions came
|
||||||
* and was not obtained, so the group is withheld and a `TODO(pouya)` sits on
|
* with that ruling and both are load-bearing on this page:
|
||||||
* CREDENTIAL_GROUPS below with the exact question. §4 is NOT re-stamped:
|
|
||||||
* nothing was re-checked. Q44.
|
|
||||||
*
|
*
|
||||||
* A first version of this page published all four and disclosed the gap instead.
|
* 1. **Nothing here promises the memberships stay current.** The struck
|
||||||
* Both review agents rejected that; the reasoning is on CREDENTIAL_GROUPS.
|
* sentence ("Memberships are renewed annually and are listed as current")
|
||||||
|
* stays struck and nothing replaces it. His words: *"List the memberships;
|
||||||
|
* promise nothing about their future state."* The list is a snapshot, and
|
||||||
|
* only §4's stamp says when it was taken.
|
||||||
|
* 2. He declined renewal-date tracking, so **R10 no longer fires on a date —
|
||||||
|
* it fires on an event**: re-confirm before any cutover or major
|
||||||
|
* republish. That is why R10 stays live with the group shipped.
|
||||||
|
*
|
||||||
|
* The page held all three positions in three days — published-with-a-disclosure
|
||||||
|
* (rejected by both review agents), withheld-behind-a-TODO (correct while R10
|
||||||
|
* was undischarged), now published-with-no-warranty. The middle state was not a
|
||||||
|
* detour; it is what "a build that fails on an unanswered question is a correct
|
||||||
|
* build" looks like from the inside.
|
||||||
*/
|
*/
|
||||||
import { Picture, getImage } from 'astro:assets';
|
import { Picture, getImage } from 'astro:assets';
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import ContactBand from '../components/ContactBand.astro';
|
import ContactBand from '../components/ContactBand.astro';
|
||||||
import Eyebrow from '../components/Eyebrow.astro';
|
import Eyebrow from '../components/Eyebrow.astro';
|
||||||
import Pill from '../components/Pill.astro';
|
|
||||||
import SectionHeading from '../components/SectionHeading.astro';
|
import SectionHeading from '../components/SectionHeading.astro';
|
||||||
import portrait from '../assets/pouya-lajevardi.jpg';
|
import portrait from '../assets/pouya-lajevardi.jpg';
|
||||||
import ogDefault from '../assets/og-portrait.jpg';
|
import ogDefault from '../assets/og-portrait.jpg';
|
||||||
@@ -57,7 +84,9 @@ import { aboutGraph } from '../data/schema';
|
|||||||
import {
|
import {
|
||||||
ASYMMETRY_LINE,
|
ASYMMETRY_LINE,
|
||||||
CREDENTIALS,
|
CREDENTIALS,
|
||||||
|
DESIGNATIONS_HELD_LINE,
|
||||||
PORTRAIT,
|
PORTRAIT,
|
||||||
|
PRACTICE_AREAS,
|
||||||
ROLE,
|
ROLE,
|
||||||
SITE,
|
SITE,
|
||||||
} from '../data/site';
|
} from '../data/site';
|
||||||
@@ -109,112 +138,47 @@ const ldImage = await getImage({
|
|||||||
});
|
});
|
||||||
const graph = aboutGraph(new URL(ldImage.src, Astro.site).href);
|
const graph = aboutGraph(new URL(ldImage.src, Astro.site).href);
|
||||||
|
|
||||||
|
/* THE BIOGRAPHY ENUMERATES THE PRACTICE AREAS IN PROSE, which means it is the
|
||||||
|
one place on the site that names them without rendering `PRACTICE_AREAS`.
|
||||||
|
Card labels ("Insurance, SABS & LAT") do not read as prose, and a joined list
|
||||||
|
of them in a paragraph was worse than the duplication. So the count is
|
||||||
|
asserted instead: add a seventh area and this throws, which is the prompt to
|
||||||
|
rewrite the sentence. Same mechanism as the ground-alternation check in
|
||||||
|
`practice/[slug].astro`. */
|
||||||
|
/* `: number`, NOT the inferred literal. `PRACTICE_AREAS` is `as const`, so
|
||||||
|
`.length` is the literal type `6`; with `AREAS_NAMED_IN_BIO` also literal `6`,
|
||||||
|
adding a seventh area makes the comparison `7 !== 6` — non-overlapping
|
||||||
|
literals — and `astro check` fails first with `ts(2367) This comparison
|
||||||
|
appears to be unintentional`, which reads like lint noise. Both deploy paths
|
||||||
|
run `npm run check` BEFORE the build, so the implementer would never see the
|
||||||
|
message below, and the message is the whole point of the guard. Widening the
|
||||||
|
left side keeps the comparison live at runtime. Measured by
|
||||||
|
`adversarial-reviewer` 2026-08-30. */
|
||||||
|
const AREAS_NAMED_IN_BIO: number = 6;
|
||||||
|
if (PRACTICE_AREAS.length !== AREAS_NAMED_IN_BIO) {
|
||||||
|
throw new Error(
|
||||||
|
`/about/'s biography names ${AREAS_NAMED_IN_BIO} practice areas in prose, ` +
|
||||||
|
`but PRACTICE_AREAS now has ${PRACTICE_AREAS.length}. Rewrite the ` +
|
||||||
|
`"I accept appointments in six areas" paragraph, then update this count.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The designation line — docs/01 item 1. Assembled from constants so it cannot
|
* The designation line — docs/01 item 1. Assembled from constants so it cannot
|
||||||
* drift from §4, and ordered held-first.
|
* drift from §4.
|
||||||
*
|
*
|
||||||
* Q.Arb IS NOT IN IT, deliberately. It is not held (§4: "Describe as newly
|
* BOTH DESIGNATIONS, because a designation line is a list of things held and
|
||||||
* commenced, never as held or nearing completion"), and a designation line is
|
* as of 2026-08-29 Q.Arb is one of them. It was excluded while it was a stage.
|
||||||
* precisely a list of things held. The arc section states the stage plainly,
|
* The issuing bodies repeat across the two entries; that is what the strings in
|
||||||
* which is what §4's paired-disclosure condition requires — this page offers
|
* `CREDENTIALS.designations` say, and spelling them once across a joined pair
|
||||||
* arbitration, so the stage appears on this page and not only in the footer.
|
* would be this file re-typing §4 rather than rendering it.
|
||||||
*/
|
*/
|
||||||
const designationLine = [
|
const designationLine = [
|
||||||
'Mediator',
|
'Mediator',
|
||||||
CREDENTIALS.designations[0],
|
...CREDENTIALS.designations,
|
||||||
CREDENTIALS.education[0],
|
CREDENTIALS.education[0],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* The credentialing arc — docs/01 item 4, and docs/03: "the credentialing
|
|
||||||
* pathway from Q.Med through Q.Arb to C.Med-Arb is stated openly as in
|
|
||||||
* progress. The brief treats that arc as part of the story rather than
|
|
||||||
* something to obscure."
|
|
||||||
*
|
|
||||||
* `state` is the load-bearing column. "Commenced August 2026" is §4's exact
|
|
||||||
* wording and the ONLY permitted wording — docs/03: not "in progress", because
|
|
||||||
* the weaker form drifts toward "nearly complete", which §4 Forbidden bars
|
|
||||||
* outright.
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
* FIVE CLAIMS CAME OUT OF THIS BLOCK, and the first was the worst thing in the
|
|
||||||
* step-3 diff. BOTH review agents found it independently, which is the strongest
|
|
||||||
* signal this loop produces.
|
|
||||||
*
|
|
||||||
* 1. ⚠️ "arbitral appointments are not gated behind it, which is why I accept
|
|
||||||
* them now" — **the false universal Q39 struck, on a public page.**
|
|
||||||
* Unscoped ("arbitral", not commercial), asserted as flat fact in the first
|
|
||||||
* person, and it publishes a proposition of Ontario law that §4 holds only
|
|
||||||
* in scoped form and deliberately does NOT stamp `[verified]`. Family
|
|
||||||
* arbitration is an arbitral appointment and it IS gated
|
|
||||||
* (`docs/reference/ontario-family-arbitration-training.md`). Q39 swept
|
|
||||||
* three instances of this universal on 2026-08-27; this was the fourth and
|
|
||||||
* the first outside a comment. §4 requires the STAGE be stated — never the
|
|
||||||
* register's gating rationale. Deleted rather than rescoped: this page has
|
|
||||||
* no business carrying the argument at all.
|
|
||||||
* 2. "on the same institutional pathway" and
|
|
||||||
* 3. "Three designations on one institutional pathway" (the section lede) —
|
|
||||||
* §4 attaches ADRIC / ADRIO to **Q.Med only**. Neither the Q.Arb row nor
|
|
||||||
* the C.Med-Arb row names a body.
|
|
||||||
* 4. "The senior hybrid designation" — a ranking claim about a third party's
|
|
||||||
* credential structure, with no row and no source.
|
|
||||||
* 5. The expansions — "Qualified Mediator", "Qualified Arbitrator",
|
|
||||||
* "Chartered Mediator-Arbitrator". Flagged as being in §11 Glossary but
|
|
||||||
* not in §4 Verified.
|
|
||||||
*
|
|
||||||
* ⚠️ ITEM 5 WAS REMOVED AND IS NOW RESTORED, AND IT IS THE ONE PLACE THIS
|
|
||||||
* SESSION WENT AGAINST A REVIEW FINDING. The reason is a SECOND finding, from
|
|
||||||
* the next audit pass, and it is a consistency point rather than a claim point:
|
|
||||||
* this page also publishes "Provincial Offences Act", "Statutory Accident
|
|
||||||
* Benefits Schedule" (as SABS) and "the ADR Institute of Canada and the ADR
|
|
||||||
* Institute of Ontario" — every one of them a §11 Glossary expansion, on exactly
|
|
||||||
* the ground the designation names were struck. *"One standard or the other."*
|
|
||||||
*
|
|
||||||
* The standard chosen is: **§11 Glossary is the source for DEFINITIONAL
|
|
||||||
* expansions** — what an abbreviation stands for — while §4 Verified remains the
|
|
||||||
* only source for claims ABOUT POUYA. Expanding `Q.Med` says nothing about him;
|
|
||||||
* "he holds it" is the claim, and that has a row. The alternative standard would
|
|
||||||
* have required stripping POA, SABS and the institute names from the prose and
|
|
||||||
* `recognizedBy` from the JSON-LD, which makes the page materially worse for a
|
|
||||||
* reader who does not already know the acronyms, in exchange for no reduction in
|
|
||||||
* risk. **Q46 asks Pouya to ratify that standard** and it is the only thing
|
|
||||||
* holding it up; if he declines, all four classes come out together.
|
|
||||||
*
|
|
||||||
* Sourcing them externally was tried first and failed: `adric.ca/designations/`
|
|
||||||
* redirects to `/designations-cee/` and its HTML contains **zero** occurrences
|
|
||||||
* of "Q.Med", "Qualified Mediator" or "Chartered Mediator" in 114,985 bytes —
|
|
||||||
* navigation only, body assembled client-side. So R14 cannot be met from the
|
|
||||||
* obvious URL, which is why this rests on §11 and on Q46 rather than on a
|
|
||||||
* committed extract.
|
|
||||||
*/
|
|
||||||
const ARC = [
|
|
||||||
{
|
|
||||||
name: 'Q.Med',
|
|
||||||
state: 'Held',
|
|
||||||
/* NOT "the designation I mediate under". That imported a PERMISSION framing
|
|
||||||
onto what §4 records as a voluntary credential, in a register that says
|
|
||||||
no designation is required to be appointed as a mediator — the
|
|
||||||
credential-as-licence slip §4 says produced a wrong answer twice. */
|
|
||||||
body:
|
|
||||||
'Qualified Mediator, held through the ADR Institute of Canada and the ' +
|
|
||||||
'ADR Institute of Ontario. The designation I hold as a mediator today.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Q.Arb',
|
|
||||||
state: 'Commenced August 2026',
|
|
||||||
body:
|
|
||||||
'Qualified Arbitrator. Newly commenced — not held, and not nearing ' +
|
|
||||||
'completion.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'C.Med-Arb',
|
|
||||||
state: 'The endpoint',
|
|
||||||
body:
|
|
||||||
'Chartered Mediator-Arbitrator. The designation this practice is built ' +
|
|
||||||
'toward.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The structured credentials — docs/01 item 3: "designations, education,
|
* The structured credentials — docs/01 item 3: "designations, education,
|
||||||
* certifications, memberships. Every line from AGENTS.md §4 Verified."
|
* certifications, memberships. Every line from AGENTS.md §4 Verified."
|
||||||
@@ -228,49 +192,69 @@ const ARC = [
|
|||||||
* NOT PRESENT, AND EACH IS A §4 DIRECTIVE RATHER THAN A GAP:
|
* NOT PRESENT, AND EACH IS A §4 DIRECTIVE RATHER THAN A GAP:
|
||||||
* - The Law Society. Listing it implies licensure, which D13 bars. §4:
|
* - The Law Society. Listing it implies licensure, which D13 bars. §4:
|
||||||
* "Excluded deliberately, not by oversight."
|
* "Excluded deliberately, not by oversight."
|
||||||
* - OCNI. Not current (§4), so it is not published.
|
* - OCNI. Not current (§4), so it is not published. **It is the precedent** —
|
||||||
|
* a membership lapsed quietly and the register now reads "not current, do
|
||||||
|
* not publish". Nothing told anyone when.
|
||||||
* - Any licence status, in either direction. §4 records it `[unestablished]`.
|
* - Any licence status, in either direction. §4 records it `[unestablished]`.
|
||||||
*
|
*
|
||||||
* TODO(pouya): Are ADRIC, ADRIO, the three OBA sections (Construction &
|
* ⚠️ MEMBERSHIPS RENDER, AND THE PAGE SAYS NOTHING ABOUT THEIR FUTURE STATE.
|
||||||
* Infrastructure, ADR, Civil Litigation) and the Canadian Tax Foundation all
|
* That second clause is the whole ruling, so it is written here beside the
|
||||||
* current TODAY, and in which month does each renew? — AGENTS.md Q44.
|
* array rather than in a change log nobody reads at edit time.
|
||||||
*
|
*
|
||||||
* ⚠️ MEMBERSHIPS ARE DELIBERATELY NOT RENDERED, AND THIS IS A REVERSAL.
|
* **Q44 closed 2026-08-28.** Pouya re-confirmed all four as current — ADRIC,
|
||||||
|
* ADRIO, the three OBA sections, and the Canadian Tax Foundation — which
|
||||||
|
* discharges R10's prohibition and puts the group back on the page.
|
||||||
*
|
*
|
||||||
* The first version of this page published all four on §4's 2026-08-26 stamp and
|
* **RE-CONFIRMED AGAIN 2026-09-02 — R10's cutover fire.** All four current;
|
||||||
* disclosed the outstanding re-confirmation in a note, in this comment, in
|
* §4 and `CREDENTIALS.memberships` re-stamped `[verified 2026-09-02 — Pouya]`.
|
||||||
* `schema.ts`, in §9 Q44 and on the cutover checklist. Both review agents
|
* ⚠️ **THIS COMMENT IS A FOURTH STAMP-BEARING SITE and it carried the
|
||||||
* rejected that, and they are right. §12 **R10** is written as a PROHIBITION —
|
* 2026-08-28 date in the present tense after the re-stamp** — `docs/06`'s R10
|
||||||
* *"Re-confirm at each renewal, **and before any page listing memberships
|
* instruction named three files and not this one, so the next fire would have
|
||||||
* ships** — `/about/` at build step 3 is the first one that will"* — and
|
* missed it again (`adversarial-reviewer`, round 2). The instruction now names
|
||||||
* documenting a prohibition is not discharging it. `CLAUDE.md` gives the
|
* four.
|
||||||
* procedure for a fact you do not have, and it is this one: leave
|
|
||||||
* `TODO(pouya)`, log the question, let the gap be visible. *"A build that fails
|
|
||||||
* on an unanswered question is a correct build."*
|
|
||||||
*
|
*
|
||||||
* The alternative was to ship them and call it disclosed. It was taken once and
|
* **DO NOT ADD A CURRENCY SENTENCE.** Not "renewed annually", not "current as
|
||||||
* is recorded here as the decision it was, not as an oversight — and it came
|
* of", not "listed as current", not a stamp date in the markup. His ruling is
|
||||||
* with a second defect on top: the note asserted *"Memberships are renewed
|
* explicit: *"List the memberships; promise nothing about their future state.
|
||||||
* annually and are listed as current"*, which (a) warranted currency the
|
* The list is a snapshot, and only §4's stamp says when it was taken."* The
|
||||||
* register cannot vouch for and (b) widened §4, which records yearly renewal for
|
* struck sentence is quoted in the comment further down this file with the
|
||||||
* the **OBA sections and the CTF only** and says nothing about ADRIC or ADRIO.
|
* three defects it carried; the second of those is now the live one.
|
||||||
*
|
*
|
||||||
* OCNI is the precedent and it is in §4: a membership lapsed, quietly, and the
|
* **WHY THE ABSENCE OF A WARRANTY IS THE HONEST FORM AND NOT AN EVASION.**
|
||||||
* register now reads "not current, do not publish". Nothing tells you when.
|
* Pouya declined renewal-date tracking. Without renewal months there is no date
|
||||||
|
* on which anyone could re-check, so any sentence asserting continued currency
|
||||||
|
* would be asserting something nothing in this repo can support — which is the
|
||||||
|
* OCNI shape exactly: §4 carries OCNI as lapsed and unpublishable, and that was
|
||||||
|
* found roughly a year late. R10 therefore fires on an **event** now (before any
|
||||||
|
* cutover or major republish), not on a date. That is recorded in §12 R10 with
|
||||||
|
* this reasoning attached, because a stamp with no trigger behind it goes stale
|
||||||
|
* silently.
|
||||||
*
|
*
|
||||||
* The other three groups ship. Restoring this one is one array entry, the moment
|
* "Canadian Tax Foundation" ships WITHOUT a "(CTF)" abbreviation, unlike ADRIC
|
||||||
* Q44 closes — and re-stamp §4 and `CREDENTIALS.memberships` that day.
|
* and ADRIO. Pouya's ruling wrote "(CTF)" when listing what to confirm, but the
|
||||||
|
* abbreviation appears nowhere else on this site — ADRIC and ADRIO earn their
|
||||||
|
* parentheses because "Q.Med (ADRIC / ADRIO)" uses them in the Designations
|
||||||
|
* group two entries above. An unused abbreviation is noise, so it is omitted;
|
||||||
|
* one word to reverse if he wants it.
|
||||||
|
*
|
||||||
|
* `memberOf` IS EMITTED FROM THIS PAGE'S JSON-LD (Q53), AND FROM A DIFFERENT
|
||||||
|
* ARRAY. `src/data/schema.ts` builds it from `MEMBERSHIP_ORGS`, not from the
|
||||||
|
* `CREDENTIALS.memberships` this group renders: the graph names the bodies, the
|
||||||
|
* page also names the OBA's three sections. **Only their length is enforced, by
|
||||||
|
* a compile-time assertion — content parity is manual.** So editing this list
|
||||||
|
* means editing that one. They are the same §4 row seen twice.
|
||||||
*/
|
*/
|
||||||
const CREDENTIAL_GROUPS = [
|
const CREDENTIAL_GROUPS = [
|
||||||
{ title: 'Designations', items: CREDENTIALS.designations },
|
{ title: 'Designations', items: CREDENTIALS.designations },
|
||||||
{ title: 'Education', items: CREDENTIALS.education },
|
{ title: 'Education', items: CREDENTIALS.education },
|
||||||
{ title: 'Certifications', items: CREDENTIALS.certifications },
|
{ title: 'Certifications', items: CREDENTIALS.certifications },
|
||||||
|
{ title: 'Memberships', items: CREDENTIALS.memberships },
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="About · Pouya Lajevardi · Mediator, Q.Med · Toronto"
|
title="About · Pouya Lajevardi · Mediator, Q.Med, Q.Arb · Toronto"
|
||||||
description="Pouya Lajevardi, JD, Q.Med — a Toronto mediator who also practises as a machine-learning and infrastructure engineer. Credentials, background, designations."
|
description="Pouya Lajevardi, JD, Q.Med, Q.Arb — a Toronto mediator who also practises as a machine-learning and infrastructure engineer. Credentials and background."
|
||||||
ogType="profile"
|
ogType="profile"
|
||||||
imageAlt={PORTRAIT.alt}
|
imageAlt={PORTRAIT.alt}
|
||||||
jsonLd={graph}
|
jsonLd={graph}
|
||||||
@@ -325,14 +309,20 @@ const CREDENTIAL_GROUPS = [
|
|||||||
|
|
||||||
EXPLICITLY INTERIM — AGENTS.md R1, surfaced again 2026-08-28
|
EXPLICITLY INTERIM — AGENTS.md R1, surfaced again 2026-08-28
|
||||||
precisely because this page is where the framing now does its
|
precisely because this page is where the framing now does its
|
||||||
heaviest work. */
|
heaviest work.
|
||||||
|
|
||||||
|
THE MEDIATION HALF CARRIES NO SCOPE, AND THE ASYMMETRY IS
|
||||||
|
DELIBERATE — Q56, ruled 2026-08-30. Arbitration is scoped commercial
|
||||||
|
because of a legal gate (Q39: family arbitration in Ontario requires
|
||||||
|
prescribed training). Mediation has no such gate and §4's row is
|
||||||
|
unscoped. Do not restore "commercial" to the mediation half to make
|
||||||
|
the two read as a pair. */
|
||||||
}
|
}
|
||||||
<p class="hero-lede">
|
<p class="hero-lede">
|
||||||
I am {ROLE.title} at {ROLE.at}, with {ROLE.litigationLine} across{' '}
|
I am {ROLE.title} at {ROLE.at}, with {ROLE.litigationLine} across{' '}
|
||||||
{ROLE.litigationAreas.slice(0, -1).join(', ')} and{' '}
|
{ROLE.litigationAreas.slice(0, -1).join(', ')} and{' '}
|
||||||
{ROLE.litigationAreas.at(-1)}. I mediate commercial disputes and I
|
{ROLE.litigationAreas.at(-1)}. I mediate. I accept arbitration
|
||||||
accept arbitration appointments in commercial matters; the Q.Arb
|
appointments in commercial matters. I also work as a machine-learning
|
||||||
pathway commenced in August 2026. I also work as a machine-learning
|
|
||||||
and infrastructure engineer.
|
and infrastructure engineer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -457,9 +447,8 @@ const CREDENTIAL_GROUPS = [
|
|||||||
EVERY CLAIM TRACES TO §4 Verified: the JD, the boutique role, active
|
EVERY CLAIM TRACES TO §4 Verified: the JD, the boutique role, active
|
||||||
litigation exposure and its four matter types, Q.Med, multiple
|
litigation exposure and its four matter types, Q.Med, multiple
|
||||||
completed sole mediations, arbitration appointments (§4 Offerings,
|
completed sole mediations, arbitration appointments (§4 Offerings,
|
||||||
scoped to commercial), the Q.Arb pathway commenced August 2026,
|
scoped to commercial), Q.Arb, engineering practice, SML Company Ltd,
|
||||||
C.Med-Arb as the goal, engineering practice, SML Company Ltd, Farsi,
|
Farsi, Iranian-Canadian. Nothing here asserts or implies licensure. */
|
||||||
Iranian-Canadian. Nothing here asserts or implies licensure. */
|
|
||||||
}
|
}
|
||||||
<div class="prose bio-prose">
|
<div class="prose bio-prose">
|
||||||
<p>
|
<p>
|
||||||
@@ -492,21 +481,18 @@ const CREDENTIAL_GROUPS = [
|
|||||||
drift. */
|
drift. */
|
||||||
}
|
}
|
||||||
<p>{ASYMMETRY_LINE}</p>
|
<p>{ASYMMETRY_LINE}</p>
|
||||||
|
{
|
||||||
|
/* A SECOND PARAGRAPH HERE WAS CANDOUR ABOUT AN INCOMPLETE CREDENTIAL
|
||||||
|
— "I would rather say where I am on the arc". Q.Arb is held; do not
|
||||||
|
write another. `check:claims` q-arb-as-a-stage catches the form. */
|
||||||
|
}
|
||||||
<p>
|
<p>
|
||||||
Mediation is where they meet. I hold the Q.Med designation through the
|
Mediation is where they meet. {DESIGNATIONS_HELD_LINE}, and I have
|
||||||
ADR Institute of Canada and the ADR Institute of Ontario, and I have
|
|
||||||
completed multiple sole mediations. I accept arbitration appointments
|
completed multiple sole mediations. I accept arbitration appointments
|
||||||
in commercial matters — as sole arbitrator, as a party-appointed
|
in commercial matters — as sole arbitrator, as a party-appointed
|
||||||
arbitrator, and in co-arbitration. Where a matter turns on a technical
|
arbitrator, and in co-arbitration. Where a matter turns on a technical
|
||||||
question, I read the technical material myself.
|
question, I read the technical material myself.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
My Q.Arb pathway commenced in August 2026, and C.Med-Arb is the
|
|
||||||
designation I am working toward. I state the stage openly because an
|
|
||||||
appointing body will establish it anyway, and because a reader can do
|
|
||||||
more with the fact than with a hedge. I would rather say where I am on
|
|
||||||
the arc than leave it to be inferred.
|
|
||||||
</p>
|
|
||||||
{
|
{
|
||||||
/* BOTH ADDITIONS TO THIS SENTENCE CAME BACK OUT. §4 verifies exactly
|
/* BOTH ADDITIONS TO THIS SENTENCE CAME BACK OUT. §4 verifies exactly
|
||||||
one relation — *"Operator of SML Company Ltd. alongside the
|
one relation — *"Operator of SML Company Ltd. alongside the
|
||||||
@@ -523,7 +509,36 @@ const CREDENTIAL_GROUPS = [
|
|||||||
practice, the jurisdiction of incorporation and the place of
|
practice, the jurisdiction of incorporation and the place of
|
||||||
business — not which work runs through which vehicle. */
|
business — not which work runs through which vehicle. */
|
||||||
}
|
}
|
||||||
<p>I run SML Company Ltd alongside both.</p>
|
{
|
||||||
|
/* §4 VERBATIM: "Operator of SML Company Ltd. **alongside the
|
||||||
|
practice**." This read "alongside both", where "both" resolved to the
|
||||||
|
law track and the engineering track — a concurrency claim §4 does not
|
||||||
|
make, flagged by `claims-auditor` 2026-08-28. Two neighbouring
|
||||||
|
additions were deleted from this section for the same reason (see
|
||||||
|
below), and the wording had propagated from docs/03, now corrected
|
||||||
|
too. NOT a corporate-structure claim either — see schema.ts on why
|
||||||
|
`worksFor` is withheld. */
|
||||||
|
}
|
||||||
|
<p>I run SML Company Ltd alongside the practice.</p>
|
||||||
|
{
|
||||||
|
/* ⚠️ NO SCOPE CLAIM AND NO COVERAGE CLAIM. §4 has no row for the
|
||||||
|
subject matter of his ADR work, and `/practice/insurance/` ships
|
||||||
|
"Disputes between an insured person and an insurer" — an
|
||||||
|
individual-versus-insurer SABS dispute is not commercial, so this
|
||||||
|
paragraph must not declare the six areas a commercial class.
|
||||||
|
What Q35 grants is the frame used here: an area may be named where
|
||||||
|
he can competently accept an appointment, framed as positioning
|
||||||
|
rather than claimed history. "I accept appointments in" is that
|
||||||
|
frame; "the subject matter runs across" is not, because §Background
|
||||||
|
is where a reader defaults to reading history. */
|
||||||
|
}
|
||||||
|
<p>
|
||||||
|
I accept appointments in six areas: construction and infrastructure;
|
||||||
|
technology, AI and data; energy and the grid; insurance and accident
|
||||||
|
benefits; shareholder and family-business disputes; and cross-border
|
||||||
|
matters. What those have in common is a record somebody has to read
|
||||||
|
closely, and a dispute that turns on what is actually in it.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
I have also completed the Kompass Arbitration Certificate Program and
|
I have also completed the Kompass Arbitration Certificate Program and
|
||||||
the Stitt Feld Handy negotiation and ADR workshop sequence. Neither is
|
the Stitt Feld Handy negotiation and ADR workshop sequence. Neither is
|
||||||
@@ -558,11 +573,19 @@ const CREDENTIAL_GROUPS = [
|
|||||||
|
|
||||||
Both readings are correct and they point in opposite directions,
|
Both readings are correct and they point in opposite directions,
|
||||||
because both sentences make a claim about CAPACITY. So this one does
|
because both sentences make a claim about CAPACITY. So this one does
|
||||||
not: it states the ROLE and its consequence for the reader, which is
|
not: it states the ROLE and its consequence for the reader, and it is
|
||||||
the form `docs/03` actually sanctions on `/for-parties/` (*"the
|
the only form that asserts nothing and denies nothing. The underlying
|
||||||
mediator is not your lawyer"* — role, not capacity) and the only one
|
question is R1's.
|
||||||
that asserts nothing and denies nothing. The underlying question is
|
|
||||||
R1's. */
|
AN EARLIER VERSION OF THIS COMMENT CITED `docs/03`'s
|
||||||
|
`/for-parties/` directive as the sanctioned example — *"the mediator
|
||||||
|
is not your lawyer" — role, not capacity*. That was wrong twice:
|
||||||
|
"not your lawyer" PRESUPPOSES lawyer status, which §4 Forbidden bars
|
||||||
|
of Pouya and D13 bars by implication; and the half of that sentence
|
||||||
|
this comment did not quote was "cannot give you legal advice", which
|
||||||
|
is the denial the comment itself says was struck twelve lines above.
|
||||||
|
`docs/03` has since been corrected. Do not re-import an example from
|
||||||
|
a spec without reading the whole sentence it sits in. */
|
||||||
}
|
}
|
||||||
<p>
|
<p>
|
||||||
I act as a neutral. I do not act for a party in a matter I take, and
|
I act as a neutral. I do not act for a party in a matter I take, and
|
||||||
@@ -572,64 +595,48 @@ const CREDENTIAL_GROUPS = [
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---- 4. The credentialing arc -------------------------------------- */}
|
{/* ---- 3b. The one-page PDF ------------------------------------------- */}
|
||||||
{
|
{
|
||||||
/* SECTION 4 BEFORE SECTION 3, and the reorder is deliberate. docs/01 lists
|
/* ✅ **R16 / Q45 DISCHARGED — build step 9.** `docs/01` §`/about/` item 7 has
|
||||||
credentials (item 3) then the arc (item 4). The arc is the part a reader
|
carried a pending note since step 3: the PDF did not exist, and a link to a
|
||||||
is likely to have a question about — it is the thing this practice is
|
file that does not exist is a broken link on the page an appointing body
|
||||||
candid about that others are not — and burying it under a scannable list
|
reads. It exists now, it is committed, and this is the link.
|
||||||
of things already held reads as a footnote to them. §4's paired-disclosure
|
**`/bio/` is the source and the PDF is a rendering of it** — so every line
|
||||||
condition also wants the stage stated where the offering is made, and the
|
of the document circulated with an appointment proposal is on a page that
|
||||||
offering is made in the narrative directly above. The list follows. */
|
`check:claims`, the adversarial review and the cutover claims pass all see.
|
||||||
|
That was R16's actual objection: *"a PDF circulated with an appointment
|
||||||
|
proposal is read once, by the reader who matters most, and never seen by a
|
||||||
|
reviewer again."*
|
||||||
|
It carries NOTHING the site does not — R16's second open sub-decision, and
|
||||||
|
the answer that avoids the §4 question it flagged. No matter list, no
|
||||||
|
referees, no figure that is not on `/fees/`. */
|
||||||
}
|
}
|
||||||
<section class="section section-inverse arc-section reveal">
|
<section class="section bio-download reveal">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="section-head">
|
<p class="download-line">
|
||||||
<SectionHeading
|
<a href="/pouya-lajevardi-bio.pdf" download>
|
||||||
eyebrow="The arc"
|
Download a one-page PDF of this record
|
||||||
level={2}
|
</a>
|
||||||
lede="Three designations. Two of them are ahead of me, and saying so is the point."
|
<span class="download-note">
|
||||||
>
|
— designations, education, memberships, the processes offered and the
|
||||||
<span slot="heading">Where the credentials sit.</span>
|
rates, on one sheet. The same page is at <a href="/bio/">/bio/</a>.
|
||||||
</SectionHeading>
|
</span>
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
{
|
|
||||||
/* `role="list"` RESTORED, AND THE REASON I REMOVED IT WAS WRONG ABOUT
|
|
||||||
ARIA. The comment here claimed that on an <ol> the role "re-announces
|
|
||||||
an ordered list as an unordered one". It does not: **both <ul> and <ol>
|
|
||||||
map to the `list` role**, so `role="list"` on an <ol> is a no-op for
|
|
||||||
ordering, not a downgrade. What it is actually for is the WebKit
|
|
||||||
heuristic that strips list semantics from a list with
|
|
||||||
`list-style-type: none` — and `.arc` sets exactly that.
|
|
||||||
|
|
||||||
It also left the two <ol>s on this two-page site DISAGREEING, with
|
|
||||||
`/`'s `.process-strip` keeping the role. That is the state that gets
|
|
||||||
copied seventeen times.
|
|
||||||
|
|
||||||
Not verified here: whether WebKit's heuristic covers <ol> as well as
|
|
||||||
<ul>. There is no Safari instrument in this environment, so the role
|
|
||||||
stays on the precautionary side, which costs nothing. Chrome's AX tree
|
|
||||||
exposes `.arc` as `list` with three `listitem` children either way. */
|
|
||||||
}
|
|
||||||
<ol class="arc" role="list">
|
|
||||||
{
|
|
||||||
ARC.map((stage) => (
|
|
||||||
<li class="arc-item">
|
|
||||||
<h3 class="arc-name">{stage.name}</h3>
|
|
||||||
<p class="arc-state">
|
|
||||||
<Pill>{stage.state}</Pill>
|
|
||||||
</p>
|
|
||||||
<p class="arc-body">{stage.body}</p>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---- 3. Credentials, structured ------------------------------------ */}
|
{/* ---- 3. Credentials, structured ------------------------------------ */}
|
||||||
<section class="section section-alt creds reveal">
|
{
|
||||||
|
/* ⚠️ `section-inverse`, NOT `section-alt` — approved by Pouya at build step
|
||||||
|
6 and applied at step 7b. The arc section struck on 2026-08-29 was this
|
||||||
|
page's only dark band, so removing it left `/about/` with four cream
|
||||||
|
sections and the accent contact band, and the alternating rhythm
|
||||||
|
`docs/02` sets went with it.
|
||||||
|
Exactly ONE rule had to change — `.cred-title`. The measured ratios are on
|
||||||
|
that rule below, which is where a future editor changing a colour will be
|
||||||
|
looking. Everything else inherits cream from `.section-inverse`. */
|
||||||
|
}
|
||||||
|
<section class="section section-inverse creds reveal">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<SectionHeading eyebrow="Credentials" level={2}>
|
<SectionHeading eyebrow="Credentials" level={2}>
|
||||||
@@ -661,10 +668,13 @@ const CREDENTIAL_GROUPS = [
|
|||||||
the OBA sections and the CTF only and says nothing about ADRIC
|
the OBA sections and the CTF only and says nothing about ADRIC
|
||||||
or ADRIO. The widened form had already propagated to four places.
|
or ADRIO. The widened form had already propagated to four places.
|
||||||
(b) "listed as current" was an affirmative public WARRANTY of
|
(b) "listed as current" was an affirmative public WARRANTY of
|
||||||
currency stacked on top of an undischarged R10 — the reminder
|
currency. **THIS IS NOW THE OPERATIVE DEFECT.** Q44 closed and
|
||||||
whose entire purpose is that no such warranty be made without a
|
the group is back on the page, so the sentence has something to
|
||||||
re-confirmation. The memberships group is now off the page
|
warrant again — and it still must not return. Pouya, 2026-08-28:
|
||||||
(Q44), so the sentence has nothing left to warrant either.
|
*"NO CURRENCY WARRANTY on the page. The sentence you struck
|
||||||
|
stays struck, and nothing may replace it."* He declined renewal
|
||||||
|
tracking, which is exactly why the warranty cannot be made:
|
||||||
|
there is no date behind it. R10 fires on an event instead.
|
||||||
(c) "Nothing above asserts a licence to practise law, in either
|
(c) "Nothing above asserts a licence to practise law, in either
|
||||||
direction" READS AS A DENIAL. §4 on licence status: "Do not
|
direction" READS AS A DENIAL. §4 on licence status: "Do not
|
||||||
assert it, do not deny it, do not infer it from anything else
|
assert it, do not deny it, do not infer it from anything else
|
||||||
@@ -872,49 +882,22 @@ const CREDENTIAL_GROUPS = [
|
|||||||
page. "Provincial Offences Act" is set in roman. If a statute name ever
|
page. "Provincial Offences Act" is set in roman. If a statute name ever
|
||||||
needs italics here, load a face for it first. */
|
needs italics here, load a face for it first. */
|
||||||
|
|
||||||
/* --- 4. The arc ------------------------------------------------------ */
|
/* --- 3b. The one-page PDF ------------------------------------------- */
|
||||||
|
|
||||||
.arc {
|
/* A quiet band between the bio and the credentials, not a call to action: the
|
||||||
display: grid;
|
reader an appointing body sends here is looking for the record, and a
|
||||||
/* `min(18rem, 100%)` rather than a bare 18rem floor: a bare floor cannot
|
download button styled like the contact CTA would compete with it. */
|
||||||
shrink below itself and overflows at a large default font size. The
|
.bio-download {
|
||||||
credential row on `/` is the measured instance of that mistake. */
|
padding-block: var(--space-7);
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
|
border-block: 1px solid var(--border);
|
||||||
gap: var(--space-6);
|
|
||||||
/* NO `padding: 0` OR `list-style: none` HERE — `global.css`'s
|
|
||||||
`ul[role='list'], ol[role='list']` reset already supplies both, and this
|
|
||||||
block was re-implementing it by hand. Two rules for one job, and the
|
|
||||||
hand-written copy is the one that drifts. `margin: 0` also comes from the
|
|
||||||
global `* { margin: 0 }` reset. */
|
|
||||||
}
|
}
|
||||||
.arc-item {
|
.download-line {
|
||||||
display: flex;
|
max-inline-size: var(--width-prose);
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
padding-block-start: var(--space-4);
|
|
||||||
border-block-start: 1px solid var(--line-dark);
|
|
||||||
}
|
|
||||||
/* <h3>, not <p>. These are the headings of the three arc items and they set
|
|
||||||
at --text-2xl serif, so marking them up as paragraphs was the fake-heading
|
|
||||||
pattern: a screen-reader user got no heading navigation for the one section
|
|
||||||
on this page a reader is most likely to jump to. `ProcessStep` on `/` uses
|
|
||||||
<h3> for exactly this shape. Outline stays h1 -> h2 -> h3 with no skips. */
|
|
||||||
.arc-name {
|
|
||||||
font-family: var(--font-serif);
|
|
||||||
font-size: var(--text-2xl);
|
|
||||||
line-height: var(--leading-tight);
|
|
||||||
}
|
|
||||||
.arc-state {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.arc-body {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
line-height: var(--leading-body);
|
line-height: var(--leading-body);
|
||||||
/* NOT --text-secondary. On an inverse ground `--ink-soft` measures ~1.4:1
|
}
|
||||||
against `--ink` — the inherited `--text-inverse` (cream, 16.81:1) is what
|
.download-note {
|
||||||
carries body copy here, so the colour is deliberately left alone rather
|
color: var(--text-secondary);
|
||||||
than set to a token that is correct only on cream. */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- 3. Credentials -------------------------------------------------- */
|
/* --- 3. Credentials -------------------------------------------------- */
|
||||||
@@ -930,7 +913,8 @@ const CREDENTIAL_GROUPS = [
|
|||||||
not a heading and never carries the <h*>." `Eyebrow.astro` restates it.
|
not a heading and never carries the <h*>." `Eyebrow.astro` restates it.
|
||||||
|
|
||||||
The measurable consequence was worse than the rule breach: at 11px these
|
The measurable consequence was worse than the rule breach: at 11px these
|
||||||
<h3>s were SMALLER than the 12px eyebrow above them and 5px smaller than
|
<h3>s were SMALLER than the eyebrow above them (12px at the time, 13px
|
||||||
|
since 2026-08-31) and 5px smaller than
|
||||||
the 16px list items they head, so "MEMBERSHIPS" was the least legible text
|
the 16px list items they head, so "MEMBERSHIPS" was the least legible text
|
||||||
on the page an appointing body reads.
|
on the page an appointing body reads.
|
||||||
|
|
||||||
@@ -942,7 +926,15 @@ const CREDENTIAL_GROUPS = [
|
|||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
font-weight: var(--weight-medium);
|
font-weight: var(--weight-medium);
|
||||||
letter-spacing: var(--tracking-tight);
|
letter-spacing: var(--tracking-tight);
|
||||||
color: var(--text-secondary);
|
/* THE ONE COLOUR THAT HAD TO MOVE WITH THE BAND. This was
|
||||||
|
`--text-secondary`, which is `--ink-soft` — **1.43:1** on the ink ground
|
||||||
|
this section now has, i.e. worse than the gold-on-cream 2.10:1 this
|
||||||
|
project treats as the defect that must never ship. `--text-inverse-2` is
|
||||||
|
gold-l: 11.09:1 on ink (docs/02). The list items below inherit cream from
|
||||||
|
`.section-inverse` at 16.81:1 and are untouched.
|
||||||
|
The gold border is a 1px divider, which tokens.css sanctions gold for on
|
||||||
|
any ground, and gold on ink measures 8.00:1 regardless. */
|
||||||
|
color: var(--text-inverse-2);
|
||||||
padding-block-end: var(--space-3);
|
padding-block-end: var(--space-3);
|
||||||
border-block-end: 1px solid var(--rule);
|
border-block-end: 1px solid var(--rule);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* `/arbitration/` — build step 4. Spec: docs/01 §`/arbitration/`, docs/03.
|
||||||
|
*
|
||||||
|
* ⚠️ THE PAIRED-DISCLOSURE CONDITION IS GONE, AND SO IS THE SECTION IT
|
||||||
|
* REQUIRED. Until 2026-08-29 this page was built around §4's condition that the
|
||||||
|
* arbitration offering ship beside the STAGE of the Q.Arb pathway — an h1
|
||||||
|
* reading "Available now, and open about the stage", and a whole section headed
|
||||||
|
* "Where I am in the arc". Q.Arb is held; the condition dissolved with its
|
||||||
|
* subject; the section was deleted rather than rewritten.
|
||||||
|
*
|
||||||
|
* ⚠️ AND DO NOT REPRODUCE IT FROM MEMORY. The characteristic failure now is a
|
||||||
|
* page that offers arbitration and then reaches for something qualifying to
|
||||||
|
* say. There is nothing to qualify: §4 Offerings rows all three forms, the
|
||||||
|
* designations are held, and the hero's `CredentialRow` names them. What
|
||||||
|
* remains of the old framing is the commercial scope — which was never part of
|
||||||
|
* the condition and is Pouya's own choice (Q39).
|
||||||
|
*
|
||||||
|
* ⚠️ AND NOTHING HERE MAY SAY OR IMPLY THAT ARBITRATION IS UNGATED IN ONTARIO.
|
||||||
|
* Q39's struck universal — "anyone may be appointed an arbitrator in Ontario;
|
||||||
|
* nothing in law gates the role behind a designation" — was FALSE as a
|
||||||
|
* universal, has been swept four times, and reached a public page once. The
|
||||||
|
* page states what is offered. It makes no claim about what the law requires of
|
||||||
|
* anyone.
|
||||||
|
*/
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import Button from '../components/Button.astro';
|
||||||
|
import ContactBand from '../components/ContactBand.astro';
|
||||||
|
import DefinitionGrid from '../components/DefinitionGrid.astro';
|
||||||
|
import CredentialRow from '../components/CredentialRow.astro';
|
||||||
|
import Eyebrow from '../components/Eyebrow.astro';
|
||||||
|
import SectionHeading from '../components/SectionHeading.astro';
|
||||||
|
import Undertaking from '../components/Undertaking.astro';
|
||||||
|
import { getImage } from 'astro:assets';
|
||||||
|
import ogDefault from '../assets/og-portrait.jpg';
|
||||||
|
import { serviceGraph } from '../data/schema';
|
||||||
|
import {
|
||||||
|
CONDUCT_UNDERTAKINGS,
|
||||||
|
CREDENTIAL_ROW,
|
||||||
|
CREDENTIAL_ROW_ARB,
|
||||||
|
} from '../data/site';
|
||||||
|
|
||||||
|
const ldImage = await getImage({
|
||||||
|
src: ogDefault,
|
||||||
|
format: 'jpeg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
});
|
||||||
|
const graph = serviceGraph({
|
||||||
|
path: '/arbitration/',
|
||||||
|
name: 'Commercial arbitration',
|
||||||
|
serviceType: 'Commercial arbitration',
|
||||||
|
description:
|
||||||
|
'Sole-arbitrator, party-appointed and co-arbitration appointments in ' +
|
||||||
|
'commercial matters. Documents-only, expedited and full hearing tracks.',
|
||||||
|
imageUrl: new URL(ldImage.src, Astro.site).href,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* §4 Offerings rows all three, each `[verified 2026-08-26 — Pouya, Q33/Q36]`,
|
||||||
|
each scoped commercial. Do not add a fourth without a row. */
|
||||||
|
const APPOINTMENTS = [
|
||||||
|
{
|
||||||
|
name: 'Sole arbitrator',
|
||||||
|
body: 'One arbitrator, appointed by agreement or by the mechanism the contract names.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Party-appointed',
|
||||||
|
body: 'Appointed by one side to a three-member tribunal, deciding with the other two.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Co-arbitration',
|
||||||
|
body: 'Sitting with co-arbitrators, usually where the matter spans more than one discipline.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/* Tracks are docs/01 §`/arbitration/` item 2. The flat fees behind the first
|
||||||
|
two are D14's card and live on `/fees/`; no figure appears here. */
|
||||||
|
const TRACKS = [
|
||||||
|
{
|
||||||
|
name: 'Documents only',
|
||||||
|
body: 'No hearing. Written submissions, the documentary record, and an award. The right track where the dispute is about what the contract says rather than about what happened.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Expedited',
|
||||||
|
body: 'A compressed timetable fixed at the outset, with page limits and a short hearing. Chosen when the commercial cost of the dispute staying open exceeds the value of a full process.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Full hearing',
|
||||||
|
body: 'Pleadings, disclosure, witnesses, experts, oral argument. Where the facts are genuinely contested and someone has to hear them tested.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Commercial Arbitration · Pouya Lajevardi · Toronto"
|
||||||
|
description="Sole, party-appointed and co-arbitration appointments in commercial matters, Toronto. Tracks, rules, and how an award is written and when it is due."
|
||||||
|
jsonLd={graph}
|
||||||
|
>
|
||||||
|
{/* ---- 1. Hero -------------------------------------------------------- */}
|
||||||
|
<section class="section hero">
|
||||||
|
<div class="wrap">
|
||||||
|
<Eyebrow dot>Arbitration</Eyebrow>
|
||||||
|
<h1 class="display hero-h">Sole, party-appointed, co-arbitration.</h1>
|
||||||
|
<p class="hero-lede">
|
||||||
|
I accept all three in commercial matters. This page covers how much
|
||||||
|
process the dispute needs, whose rules it runs under, and how the award
|
||||||
|
gets written.
|
||||||
|
</p>
|
||||||
|
<div class="hero-creds">
|
||||||
|
<CredentialRow slots={[...CREDENTIAL_ROW, CREDENTIAL_ROW_ARB]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---- 2. Appointments ------------------------------------------------ */}
|
||||||
|
{
|
||||||
|
/* INVERSE, and it inherited the ground rather than choosing it. The deleted
|
||||||
|
credentialing-stage section was this page's only `section-inverse`; left
|
||||||
|
cream, this section would have sat cream-on-cream against the hero and
|
||||||
|
rendered as one doubled block — the defect `/practice/[slug].astro` now
|
||||||
|
throws on. */
|
||||||
|
}
|
||||||
|
<section class="section section-inverse reveal">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-head">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Appointments"
|
||||||
|
level={2}
|
||||||
|
lede="Commercial matters. I do not accept family arbitration."
|
||||||
|
>
|
||||||
|
<span slot="heading">Three forms.</span>
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
<DefinitionGrid items={APPOINTMENTS} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---- 3. Tracks ------------------------------------------------------ */}
|
||||||
|
<section class="section section-alt reveal">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-head">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Tracks"
|
||||||
|
level={2}
|
||||||
|
lede="Settled with the parties before the first procedural order, not defaulted to."
|
||||||
|
>
|
||||||
|
<span slot="heading">How much process the dispute needs.</span>
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
<DefinitionGrid items={TRACKS} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---- 4. Rules ------------------------------------------------------- */}
|
||||||
|
<section class="section reveal">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-head">
|
||||||
|
<SectionHeading eyebrow="Rules" level={2}>
|
||||||
|
<span slot="heading">Under whose rules.</span>
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
<div class="prose">
|
||||||
|
{
|
||||||
|
/* Sourced: docs/reference/adric-rules.md.
|
||||||
|
|
||||||
|
⚠️ ADR CHAMBERS IS NOT NAMED HERE, AND MUST NOT BE ADDED BACK.
|
||||||
|
Struck by Pouya 2026-08-30 from this page and from `docs/01` item 3
|
||||||
|
in the same ruling. `docs/reference/adr-institution-names.md`
|
||||||
|
establishes what the firm publishes — it does not establish that an
|
||||||
|
outside neutral can be appointed under its rules, and its own model
|
||||||
|
clause reads "at ADR Chambers". Naming it implies a relationship
|
||||||
|
this repository does not source. ADRIC and ad hoc are enough. */
|
||||||
|
}
|
||||||
|
<p>
|
||||||
|
<strong>The ADRIC Arbitration Rules.</strong> The ADR Institute of Canada
|
||||||
|
adopted a new edition effective 1 March 2025, alongside an arbitrator appointment
|
||||||
|
protocol and a set of forms — notice to arbitrate, request to administer,
|
||||||
|
request for the appointment of an arbitrator, urgent interim measures, challenge
|
||||||
|
to an arbitrator, notice of appeal.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Or ad hoc, or whatever the contract names.</strong> Where a contract
|
||||||
|
names a rule set, a seat and a language and leaves the rest to the tribunal,
|
||||||
|
that works.
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
/* Q54(e), rowed in §4 as a conduct undertaking. It REPLACED the
|
||||||
|
third-person sentence that made the same point as an observation;
|
||||||
|
do not restore that sentence beside it. */
|
||||||
|
}
|
||||||
|
<Undertaking>{CONDUCT_UNDERTAKINGS.arbitrationProcedure}</Undertaking>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---- 5. Awards ------------------------------------------------------ */}
|
||||||
|
<section class="section section-alt reveal">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-head">
|
||||||
|
<SectionHeading eyebrow="Awards" level={2}>
|
||||||
|
<span slot="heading">In writing, with reasons.</span>
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
<div class="prose">
|
||||||
|
<p>
|
||||||
|
An award should be in writing and give reasons — what was in dispute,
|
||||||
|
what the record showed, and why the conclusion follows. An award that
|
||||||
|
announces a result without the reasoning is not much use to the party
|
||||||
|
that lost, and it is no use at all to the relationship that has to
|
||||||
|
survive it.
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
/* THE DATE IS A COMMITMENT ABOUT PROCESS, NOT A PUBLISHED TURNAROUND.
|
||||||
|
§4 Forbidden bars a time-to-award statistic outright; no figure
|
||||||
|
appears here and none may be added. */
|
||||||
|
}
|
||||||
|
{
|
||||||
|
/* Q54(f), answered by Pouya 2026-08-29 and rowed in §4 as a conduct
|
||||||
|
undertaking. Same replacement as in the Rules section above: this
|
||||||
|
paragraph opened "The date an award is due belongs in the first
|
||||||
|
procedural order", the observation form of the same commitment. The
|
||||||
|
sentence that follows it is unchanged and is the one doing the §4
|
||||||
|
Forbidden work. */
|
||||||
|
}
|
||||||
|
<Undertaking>{CONDUCT_UNDERTAKINGS.arbitrationAwardDate}</Undertaking>
|
||||||
|
<p>
|
||||||
|
No number is published here: a turnaround time advertised in advance
|
||||||
|
of a record is a guess dressed as a commitment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---- 6. Fees --------------------------------------------------------- */}
|
||||||
|
<section class="section reveal">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-head">
|
||||||
|
<SectionHeading eyebrow="Fees" level={2}>
|
||||||
|
<span slot="heading">Published in full.</span>
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
<div class="prose">
|
||||||
|
<p>
|
||||||
|
Hourly, hearing day, and flat fees for documents-only and expedited
|
||||||
|
matters at two levels of complexity. The cancellation schedule is on
|
||||||
|
the same page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cta">
|
||||||
|
<Button href="/fees/" variant="ghost">The rate card →</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ContactBand />
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
padding-block-start: var(--space-9);
|
||||||
|
}
|
||||||
|
.hero-h {
|
||||||
|
margin-block: var(--space-4) var(--space-5);
|
||||||
|
font-size: var(--text-5xl);
|
||||||
|
max-inline-size: 20ch;
|
||||||
|
}
|
||||||
|
.hero-lede {
|
||||||
|
max-inline-size: 58ch;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.hero-creds {
|
||||||
|
margin-block-start: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
margin-block-start: var(--space-6);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* `/bio/` — the one-page bio, and the SOURCE of the PDF. Build step 9.
|
||||||
|
* Discharges `AGENTS.md` R16 / Q45.
|
||||||
|
*
|
||||||
|
* ⚠️ **R16 LEFT TWO DECISIONS OPEN AND BOTH ARE TAKEN HERE, UNDER STANDING
|
||||||
|
* AUTHORISATION. Read them before changing anything.**
|
||||||
|
*
|
||||||
|
* **(a) Generated at build, or authored once as a designed artefact?** Neither,
|
||||||
|
* exactly — and the third option is better than both. The bio is a PAGE in this
|
||||||
|
* repository, so every line of it is reviewed by the same apparatus that reviews
|
||||||
|
* every other page: `astro check`, `npm run check:claims` on the built HTML, the
|
||||||
|
* adversarial review, and the cutover claims pass. The PDF is then RENDERED from
|
||||||
|
* this page by `npm run bio:pdf`, deterministically, with no new dependency —
|
||||||
|
* `chrome-launcher` is already a devDependency because Lighthouse needs it.
|
||||||
|
*
|
||||||
|
* That answers R16's actual worry, which was never about tooling: *"It is the
|
||||||
|
* one artefact class this project's review apparatus cannot reach. A web page is
|
||||||
|
* re-reviewed by every audit; a PDF circulated with an appointment proposal is
|
||||||
|
* read once, by the reader who matters most, and never seen by a reviewer
|
||||||
|
* again."* Making the PDF a rendering of a reviewed page puts it back inside the
|
||||||
|
* apparatus. **It is not generated during `astro build`** — CI has no Chrome, and
|
||||||
|
* a build step that cannot run in CI is the Q22 shape again.
|
||||||
|
*
|
||||||
|
* **(b) Does it carry anything the site does not? NO.** Every line here renders
|
||||||
|
* from the same constants as the pages: `CREDENTIALS`, `ROLE`, `BOUTIQUE`,
|
||||||
|
* `PRACTICE_AREAS`, `FEES`, `CONTACT`. R16 flagged that both open sub-decisions
|
||||||
|
* were "each a §4 question of its own, and the matter list would collide with §4
|
||||||
|
* Forbidden directly" — so the answer that avoids both is a bio that adds
|
||||||
|
* nothing. No matter list, no referees, no figure that is not on `/fees/`. The
|
||||||
|
* fee summary IS here, because R16's own reasoning says an appointment proposal
|
||||||
|
* needs the rate card as much as the bio, and every figure in it is `/fees/`'s.
|
||||||
|
*
|
||||||
|
* `noindex`, and excluded from the sitemap in `astro.config.mjs`: it is a
|
||||||
|
* condensed duplicate of `/about/` and `/fees/`, and two URLs competing on the
|
||||||
|
* same content is the one thing `docs/04` is most concerned with.
|
||||||
|
*
|
||||||
|
* PRINT LAYOUT. `global.css`'s `@media print` block already hides the header,
|
||||||
|
* the footer and the skip link, neutralises the inverse grounds, and disables
|
||||||
|
* the reveal — all of it added because `/about/` is printed by people evaluating
|
||||||
|
* an appointment. This page adds only what makes it fit ONE sheet, and
|
||||||
|
* `scripts/bio-pdf.mjs` ASSERTS the page count rather than trusting it.
|
||||||
|
*/
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import Eyebrow from '../components/Eyebrow.astro';
|
||||||
|
import { getImage } from 'astro:assets';
|
||||||
|
import ogDefault from '../assets/og-portrait.jpg';
|
||||||
|
import { pageGraph } from '../data/schema';
|
||||||
|
import {
|
||||||
|
BOUTIQUE,
|
||||||
|
CONTACT,
|
||||||
|
CREDENTIALS,
|
||||||
|
FEES,
|
||||||
|
PRACTICE_AREAS,
|
||||||
|
ROLE,
|
||||||
|
SITE,
|
||||||
|
} from '../data/site';
|
||||||
|
|
||||||
|
const ldImage = await getImage({
|
||||||
|
src: ogDefault,
|
||||||
|
format: 'jpeg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
});
|
||||||
|
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
|
||||||
|
|
||||||
|
const money = (amount: number) =>
|
||||||
|
new Intl.NumberFormat('en-CA', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: FEES.currency,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
|
|
||||||
|
const { halfDay, fullDay } = FEES.mediation;
|
||||||
|
|
||||||
|
/* The processes, each with a §4 Offerings row. Arbitration is scoped commercial
|
||||||
|
because Q39's gate is a legal one; mediation is unscoped because it has no
|
||||||
|
such gate (Q56). The asymmetry is designed — do not tidy it. */
|
||||||
|
const PROCESSES = [
|
||||||
|
'Mediation — sole mediator',
|
||||||
|
'Commercial arbitration — sole, party-appointed, co-arbitration',
|
||||||
|
'Med-arb — mediation converting to binding arbitration, agreed in advance',
|
||||||
|
'Early neutral evaluation — delivered to both parties together',
|
||||||
|
'Dispute-system design',
|
||||||
|
'Pre-dispute technical advisory',
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="One-Page Bio · Pouya Lajevardi · Mediator · Toronto"
|
||||||
|
description="A one-page record for circulation with an appointment proposal: designations, education, memberships, the processes offered, the practice areas, and the rates."
|
||||||
|
jsonLd={graph}
|
||||||
|
noindex
|
||||||
|
>
|
||||||
|
<section class="section bio-sheet">
|
||||||
|
<div class="wrap">
|
||||||
|
{
|
||||||
|
/* The download sits above the sheet and is `.no-print`, so the printed
|
||||||
|
copy does not carry a link to itself. */
|
||||||
|
}
|
||||||
|
<p class="no-print sheet-note">
|
||||||
|
This page is the source of the one-page PDF.
|
||||||
|
<a href="/pouya-lajevardi-bio.pdf" download>Download the PDF</a>, or
|
||||||
|
print this page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<header class="sheet-head">
|
||||||
|
{
|
||||||
|
/* THE EYEBROW IS `.no-print`, AND IT IS HERE BECAUSE `og:proof` ASKED
|
||||||
|
FOR IT. That check compares each card's eyebrow against its page's
|
||||||
|
first `.eyebrow`, and this page had none — so the card said "Bio"
|
||||||
|
against nothing. The options were to weaken the check or to give the
|
||||||
|
page the element every other page has; weakening a check to match an
|
||||||
|
artefact is how a control stops controlling. On paper the sheet leads
|
||||||
|
with the name, so the eyebrow prints away. */
|
||||||
|
}
|
||||||
|
<div class="no-print">
|
||||||
|
<Eyebrow dot>Bio</Eyebrow>
|
||||||
|
</div>
|
||||||
|
<h1 class="sheet-name">{SITE.name}</h1>
|
||||||
|
<p class="sheet-desigs">{CREDENTIALS.designations.join(' · ')}</p>
|
||||||
|
<p class="eyebrow sheet-strap">{SITE.tagline}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="sheet-grid">
|
||||||
|
<section class="block block-wide">
|
||||||
|
<h2 class="eyebrow">The practice</h2>
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
/* ⚠️ NO LEADING SCOPE. This sentence read "I act as a neutral in
|
||||||
|
commercial disputes — as a mediator, as an arbitrator in
|
||||||
|
commercial matters, and in med-arb…", and the leading clause
|
||||||
|
scoped ALL THREE, mediation included. §4's mediation row is
|
||||||
|
unscoped deliberately (Q56) and says in terms: "do not scope it
|
||||||
|
on a page." It is the `/practice/` shape exactly — the two words
|
||||||
|
never appear in the same element, so no proximity grep reaches
|
||||||
|
it — and it was found by reading the rendered PDF. The scope
|
||||||
|
belongs on the arbitration clause alone, where Q39's legal gate
|
||||||
|
puts it.
|
||||||
|
|
||||||
|
⚠️ AND THE VERB IS `accept appointments`, NOT `act as`. §4
|
||||||
|
verifies exactly one practised role — "Mediator" — and says in
|
||||||
|
terms that "Arbitrator" as a practised role is NOT a row; what it
|
||||||
|
verifies is that appointments are ACCEPTED. `/` and `/about/`
|
||||||
|
carry the same construction. */
|
||||||
|
}
|
||||||
|
I act as a neutral. I accept appointments as a mediator, as an arbitrator
|
||||||
|
in commercial matters, and in med-arb where the parties want one neutral
|
||||||
|
across both phases. I read the contract and the technical record underneath
|
||||||
|
it rather than either side's summary of them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I am {ROLE.title} at {BOUTIQUE}, with {ROLE.litigationLine} across
|
||||||
|
{' '}{ROLE.litigationAreas.join(', ')} matters. I am also a practising
|
||||||
|
machine-learning and infrastructure engineer, which is what lets me work
|
||||||
|
through a technical record at first hand.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2 class="eyebrow">Designations</h2>
|
||||||
|
<ul role="list">
|
||||||
|
{CREDENTIALS.designations.map((d) => <li>{d}</li>)}
|
||||||
|
</ul>
|
||||||
|
<h2 class="eyebrow">Education</h2>
|
||||||
|
<ul role="list">
|
||||||
|
{CREDENTIALS.education.map((d) => <li>{d}</li>)}
|
||||||
|
</ul>
|
||||||
|
<h2 class="eyebrow">Certifications</h2>
|
||||||
|
<ul role="list">
|
||||||
|
{CREDENTIALS.certifications.map((d) => <li>{d}</li>)}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2 class="eyebrow">Memberships</h2>
|
||||||
|
<ul role="list">
|
||||||
|
{CREDENTIALS.memberships.map((d) => <li>{d}</li>)}
|
||||||
|
</ul>
|
||||||
|
<h2 class="eyebrow">Languages</h2>
|
||||||
|
<ul role="list">
|
||||||
|
<li>
|
||||||
|
{CREDENTIALS.languages.join(' and ')}, without an interpreter
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2 class="eyebrow">Processes</h2>
|
||||||
|
<ul role="list">
|
||||||
|
{PROCESSES.map((p) => <li>{p}</li>)}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2 class="eyebrow">Subject matter</h2>
|
||||||
|
<ul role="list">
|
||||||
|
{PRACTICE_AREAS.map((area) => <li>{area.name}</li>)}
|
||||||
|
</ul>
|
||||||
|
<p class="fine">
|
||||||
|
Family arbitration under the <em>Family Law Act</em> is not offered.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block block-wide">
|
||||||
|
<h2 class="eyebrow">Rates</h2>
|
||||||
|
<ul role="list" class="rates-list">
|
||||||
|
<li>
|
||||||
|
Half day, up to {halfDay.hours} hours of session — {
|
||||||
|
money(halfDay.amount)
|
||||||
|
}. Fee includes up to {halfDay.prepIncluded} hours of preparation.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Full day, up to {fullDay.hours} hours of session — {
|
||||||
|
money(fullDay.amount)
|
||||||
|
}. Fee includes up to {fullDay.prepIncluded} hours of preparation.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Each party beyond two — {money(FEES.mediation.additionalParty)}.
|
||||||
|
Overtime beyond the session hours the fee covers —
|
||||||
|
{' '}{money(FEES.mediation.overtimePerHour)} an hour.
|
||||||
|
{' '}{FEES.mediation.reservation}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Arbitration — {money(FEES.arbitration.perHour)} an hour,
|
||||||
|
{' '}{money(FEES.arbitration.hearingDay)} a hearing day, or a flat fee
|
||||||
|
for documents-only and expedited references.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{FEES.taxNote} The full card, the cancellation schedule and the terms
|
||||||
|
are published at {SITE.url}/fees/.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block block-wide sheet-contact">
|
||||||
|
<h2 class="eyebrow">Contact</h2>
|
||||||
|
<p>
|
||||||
|
{CONTACT.email} · {CONTACT.phoneFallback} · {CONTACT.location}
|
||||||
|
<br />
|
||||||
|
{CONTACT.responseTime} · {SITE.url} · {CONTACT.linkedin}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sheet-note {
|
||||||
|
margin-block-end: var(--space-7);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bio-sheet {
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding-block-end: var(--space-5);
|
||||||
|
border-block-end: 2px solid var(--rule);
|
||||||
|
}
|
||||||
|
.sheet-name {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
}
|
||||||
|
.sheet-desigs {
|
||||||
|
margin-block-start: var(--space-3);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
/* Type comes from the global `.eyebrow` class on the element; only the margin is
|
||||||
|
here. This rule and `.block h2` below were byte-for-byte copies of
|
||||||
|
`.eyebrow`'s declarations at `--text-2xs` — the same escape the footer's
|
||||||
|
column headings were. The print block below sets both to 7pt, so the screen
|
||||||
|
size never reached the PDF. */
|
||||||
|
.sheet-strap {
|
||||||
|
margin-block-start: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
|
||||||
|
gap: var(--space-6);
|
||||||
|
margin-block-start: var(--space-6);
|
||||||
|
}
|
||||||
|
.block-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* See `.sheet-strap` above: type from `.eyebrow`, only the rule under it here. */
|
||||||
|
.block h2 {
|
||||||
|
padding-block-end: var(--space-2);
|
||||||
|
border-block-end: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.block h2:not(:first-child) {
|
||||||
|
margin-block-start: var(--space-5);
|
||||||
|
}
|
||||||
|
.block ul {
|
||||||
|
/* `global.css` strips the marker and padding from `ul[role='list']`. */
|
||||||
|
margin-block-start: var(--space-3);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
}
|
||||||
|
.block p {
|
||||||
|
margin-block-start: var(--space-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-inline-size: var(--width-prose);
|
||||||
|
}
|
||||||
|
.block p + p {
|
||||||
|
margin-block-start: var(--space-3);
|
||||||
|
}
|
||||||
|
/* `anywhere`, and the cause is ONE STRING: the last row prints the fee-card URL,
|
||||||
|
which has no break opportunity and sized this single-column grid track. The
|
||||||
|
URL has to stay — printed sheet, the reader cannot click it. `docs/02`
|
||||||
|
§Reflow, instrument finding 1. */
|
||||||
|
.rates-list {
|
||||||
|
max-inline-size: none;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.fine {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
.sheet-contact p {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
max-inline-size: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- One sheet of paper ------------------------------------------------ */
|
||||||
|
|
||||||
|
/* `global.css`'s print block already hides the header, footer and skip link,
|
||||||
|
neutralises the inverse grounds and disables the reveal. This is only what
|
||||||
|
makes the content FIT, and `scripts/bio-pdf.mjs` asserts the page count
|
||||||
|
rather than this comment claiming it. */
|
||||||
|
@media print {
|
||||||
|
.bio-sheet {
|
||||||
|
padding-block: 0;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-inline-size: none;
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
.sheet-grid {
|
||||||
|
/* Two fixed columns rather than auto-fit: on paper there is no viewport
|
||||||
|
to fit to, and a print UA resolves `auto-fit` against the sheet width
|
||||||
|
inconsistently. */
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10pt 18pt;
|
||||||
|
margin-block-start: 10pt;
|
||||||
|
}
|
||||||
|
.sheet-name {
|
||||||
|
font-size: 22pt;
|
||||||
|
}
|
||||||
|
.sheet-desigs {
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
.sheet-strap {
|
||||||
|
font-size: 7pt;
|
||||||
|
}
|
||||||
|
.sheet-head {
|
||||||
|
padding-block-end: 8pt;
|
||||||
|
}
|
||||||
|
/* ⚠️ `font-weight` IS FROZEN AT 400 BY RULING, 2026-09-01. The circulated
|
||||||
|
PDF's typography changes only when its CONTENT is deliberately revised,
|
||||||
|
never as a side effect of a screen refactor — so print keeps 400 while
|
||||||
|
screen takes the 500 every other eyebrow has. `docs/02` §Accessibility
|
||||||
|
floor carries the reasoning and the byte figures. */
|
||||||
|
.block h2,
|
||||||
|
.sheet-strap {
|
||||||
|
font-weight: var(--weight-normal);
|
||||||
|
}
|
||||||
|
.block h2 {
|
||||||
|
font-size: 7pt;
|
||||||
|
padding-block-end: 3pt;
|
||||||
|
}
|
||||||
|
.block h2:not(:first-child) {
|
||||||
|
margin-block-start: 9pt;
|
||||||
|
}
|
||||||
|
.block ul,
|
||||||
|
.block p {
|
||||||
|
margin-block-start: 5pt;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.block ul {
|
||||||
|
gap: 2pt;
|
||||||
|
}
|
||||||
|
.sheet-contact p {
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
/* A block must not be split across a page break — on a one-sheet document
|
||||||
|
that would mean a second sheet carrying two lines. */
|
||||||
|
.block {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* `/contact/` — build step 8. Spec: docs/01 §`/contact/`, docs/05-backend-spec.md.
|
||||||
|
*
|
||||||
|
* ⚠️ **THE FORM USES NO JAVASCRIPT, AND THAT IS NOT A CONSTRAINT WORKED AROUND —
|
||||||
|
* IT IS THE DESIGN.** A plain `<form method="post">` to a same-origin path; the
|
||||||
|
* handler answers `303 See Other` to `/contact/received/`. So it works with
|
||||||
|
* script disabled, cannot double-submit on refresh, and never shows the visitor a
|
||||||
|
* raw JSON response. `backend/intake/handler.mjs` carries the reasoning in full.
|
||||||
|
*
|
||||||
|
* Consequences that shape the markup:
|
||||||
|
* - **Validation errors land on `/contact/could-not-send/`**, because a static
|
||||||
|
* page cannot read a query string without script. In practice the browser's
|
||||||
|
* own `required` / `type="email"` / `maxlength` handling catches the real
|
||||||
|
* cases and announces them natively, which is what `docs/05`'s
|
||||||
|
* "errors announced with `role="alert"`" asks for; a server rejection is
|
||||||
|
* almost always a bot, and a bot gets the success page (see the handler).
|
||||||
|
* - **No booking embed — R6.** Parked by Pouya 2026-08-26. `docs/01` asks for a
|
||||||
|
* "reserved slot for an embed", so the slot is `CONTACT.bookingUrl` being
|
||||||
|
* `null`: nothing renders, and when a URL exists the block appears without a
|
||||||
|
* rebuild of this page. **Nothing on this page mentions booking**, because a
|
||||||
|
* page that says "book a call" with no way to book it is worse than one that
|
||||||
|
* says to email.
|
||||||
|
*
|
||||||
|
* ⚠️ **THE RESPONSE-TIME SENTENCE IS A PUBLIC COMMITMENT (§4, Q27) AND MUST READ
|
||||||
|
* IDENTICALLY HERE, IN THE CONFIRMATION EMAIL, AND IN ANY BIO.** It is rendered
|
||||||
|
* from `CONTACT.responseTime`; the handler takes the same string from its
|
||||||
|
* environment. Never retype it, and never soften it to "usually".
|
||||||
|
*
|
||||||
|
* ⚠️ **`NO_RETAINER_NOTICE` AND `CONSENT_TEXT` BOTH SHIP, AND THAT IS NOT
|
||||||
|
* DUPLICATION.** `docs/01` requires the page to carry the notice; `docs/05`
|
||||||
|
* requires the consent the inquirer TICKS to carry it too. One is a statement the
|
||||||
|
* page makes, the other is a thing the inquirer agrees to. Neither is retyped.
|
||||||
|
*
|
||||||
|
* ⚠️ **NO PHONE NUMBER — Q3. §4 verifies "no public phone number".** Render
|
||||||
|
* `CONTACT.phoneFallback` wherever a number would go rather than leaving the slot
|
||||||
|
* visually empty.
|
||||||
|
*/
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import Button from '../components/Button.astro';
|
||||||
|
import Undertaking from '../components/Undertaking.astro';
|
||||||
|
import ContactBand from '../components/ContactBand.astro';
|
||||||
|
import Eyebrow from '../components/Eyebrow.astro';
|
||||||
|
import SectionHeading from '../components/SectionHeading.astro';
|
||||||
|
import { getImage } from 'astro:assets';
|
||||||
|
import ogDefault from '../assets/og-portrait.jpg';
|
||||||
|
import { pageGraph } from '../data/schema';
|
||||||
|
import {
|
||||||
|
CONDUCT_UNDERTAKINGS,
|
||||||
|
CONTACT,
|
||||||
|
NO_RETAINER_NOTICE,
|
||||||
|
} from '../data/site';
|
||||||
|
import {
|
||||||
|
CONSENT_TEXT,
|
||||||
|
DECOY_CHECKBOX_FIELD,
|
||||||
|
HONEYPOT_FIELD,
|
||||||
|
INTAKE_ACTION,
|
||||||
|
INTAKE_FIELDS,
|
||||||
|
} from '../data/intake';
|
||||||
|
|
||||||
|
const ldImage = await getImage({
|
||||||
|
src: ogDefault,
|
||||||
|
format: 'jpeg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* No `Service` node. `/contact/` offers nothing — it is the way in to what the
|
||||||
|
other pages offer, and a `Service` here would duplicate an `@id` that already
|
||||||
|
resolves on `/mediation/`. Person alone, the `/practice/` and `/process/`
|
||||||
|
shape. No `BreadcrumbList`: one hop from the root, no visible trail. */
|
||||||
|
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
|
||||||
|
|
||||||
|
const hintId = (name: string) => `${name}-hint`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Contact · Request a Consultation · Pouya Lajevardi"
|
||||||
|
description="Request a confidential intake call about a mediation, arbitration or med-arb appointment in Ontario. Inquiries are answered within two business days."
|
||||||
|
jsonLd={graph}
|
||||||
|
>
|
||||||
|
{/* ---- 1. Hero -------------------------------------------------------- */}
|
||||||
|
<section class="section hero">
|
||||||
|
<div class="wrap">
|
||||||
|
<Eyebrow dot>Contact</Eyebrow>
|
||||||
|
<h1 class="display hero-h">Start with a confidential call.</h1>
|
||||||
|
<p class="hero-lede">
|
||||||
|
The first step is a scheduled call to scope the matter, identify the
|
||||||
|
parties, and run conflicts. Send the form below, or email me directly.
|
||||||
|
</p>
|
||||||
|
<dl class="direct">
|
||||||
|
<div>
|
||||||
|
<dt>Email</dt>
|
||||||
|
<dd><a href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Phone</dt>
|
||||||
|
{
|
||||||
|
/* Q3: no public number. The fallback fills the slot rather than
|
||||||
|
leaving a labelled row visually empty. */
|
||||||
|
}
|
||||||
|
<dd>{CONTACT.phoneFallback}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Location</dt>
|
||||||
|
<dd>{CONTACT.location}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Response</dt>
|
||||||
|
<dd>{CONTACT.responseTime}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---- 2. What an inquiry does and does not do ------------------------ */}
|
||||||
|
<section class="section section-inverse reveal">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-head">
|
||||||
|
<SectionHeading eyebrow="Before you write" level={2}>
|
||||||
|
<span slot="heading">What an inquiry is, and what it is not.</span>
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
<div class="prose">
|
||||||
|
<p class="statement">{NO_RETAINER_NOTICE}</p>
|
||||||
|
<p>I ask for the other parties and their counsel for one reason.</p>
|
||||||
|
{
|
||||||
|
/* RENDERED FROM `CONDUCT_UNDERTAKINGS`, NEVER TYPED — undertaking (g).
|
||||||
|
⚠️ THIS PAGE HAND-TYPED THE SAME PROPOSITION AS *"I cannot accept an
|
||||||
|
appointment before conflicts are checked"* UNTIL 2026-09-04, and it
|
||||||
|
survived the change set that struck the identical sentence from
|
||||||
|
`/legal/privacy/` — one file swept, its sibling missed, which is the
|
||||||
|
shape R8 exists for. §4 row (g) lists BOTH surfaces. */
|
||||||
|
}
|
||||||
|
<Undertaking>{CONDUCT_UNDERTAKINGS.conflictsCheck}</Undertaking>
|
||||||
|
<p>
|
||||||
|
That check needs names, and the call above is where it happens. Please
|
||||||
|
keep the summary short and leave privileged or confidential detail out
|
||||||
|
of it — the call is for that.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
What is collected, where it is stored, how long it is kept, and how to
|
||||||
|
have it deleted are set out in the <a href="/legal/privacy/"
|
||||||
|
>privacy policy</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---- 3. The intake form -------------------------------------------- */}
|
||||||
|
<section class="section reveal">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-head">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Intake"
|
||||||
|
level={2}
|
||||||
|
lede="Required fields are marked. Nothing here is a retainer or an appointment."
|
||||||
|
>
|
||||||
|
<span slot="heading">Tell me about the matter.</span>
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
/* `novalidate` IS DELIBERATELY ABSENT. The browser's own validation is
|
||||||
|
the only client-side validation on this page, and with no script it is
|
||||||
|
also the only thing that can announce an error inline — which is what
|
||||||
|
`docs/05`'s `role="alert"` item is really asking for. The Lambda
|
||||||
|
re-validates everything regardless; see `src/data/intake.ts`. */
|
||||||
|
}
|
||||||
|
<form class="intake" method="post" action={INTAKE_ACTION}>
|
||||||
|
{
|
||||||
|
INTAKE_FIELDS.map((field) => (
|
||||||
|
<div class={`field field-${field.type}`}>
|
||||||
|
{field.type === 'radio' ? (
|
||||||
|
<fieldset>
|
||||||
|
<legend>{field.label}</legend>
|
||||||
|
<div class="radios">
|
||||||
|
{field.options?.map((option) => (
|
||||||
|
<label class="radio">
|
||||||
|
{/* No default selection. `preferredContact` is
|
||||||
|
optional, and pre-checking "Email" would submit a
|
||||||
|
preference the inquirer never expressed. */}
|
||||||
|
<input type="radio" name={field.name} value={option} />
|
||||||
|
<span>{option}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<label for={field.name}>
|
||||||
|
{field.label}
|
||||||
|
{field.required && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<span class="req" aria-hidden="true">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span class="visually-hidden">(required)</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{field.type === 'select' ? (
|
||||||
|
<select
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
required={field.required || undefined}
|
||||||
|
aria-describedby={
|
||||||
|
field.hint ? hintId(field.name) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* An empty first option, so a required select cannot be
|
||||||
|
satisfied by whichever value happened to be first. */}
|
||||||
|
<option value="">Choose one</option>
|
||||||
|
{field.options?.map((option) => (
|
||||||
|
<option value={option}>{option}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : field.type === 'textarea' ? (
|
||||||
|
<textarea
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
rows="6"
|
||||||
|
maxlength={field.max}
|
||||||
|
required={field.required || undefined}
|
||||||
|
aria-describedby={
|
||||||
|
field.hint ? hintId(field.name) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type={field.type}
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
maxlength={field.max}
|
||||||
|
autocomplete={field.autocomplete}
|
||||||
|
required={field.required || undefined}
|
||||||
|
aria-describedby={
|
||||||
|
field.hint ? hintId(field.name) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{field.hint && (
|
||||||
|
<p class="hint" id={hintId(field.name)}>
|
||||||
|
{field.hint}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
/* THE HONEYPOT. Hidden from sighted users by `display: none` on the
|
||||||
|
wrapper, from assistive technology by `aria-hidden`, and from the
|
||||||
|
keyboard by `tabindex="-1"` — all three, because any one alone leaves
|
||||||
|
a real visitor able to reach a field that silently discards their
|
||||||
|
inquiry. `autocomplete="off"` matters more here than anywhere else on
|
||||||
|
the form: a browser that helpfully fills a plausible-looking field
|
||||||
|
would make a human look like a bot. */
|
||||||
|
}
|
||||||
|
{
|
||||||
|
/* `hidden` ADDED 2026-09-04, for the reason spelled out on the decoy
|
||||||
|
below: a class-only rule leaves this field on screen wherever author
|
||||||
|
styles do not apply, and a visitor who fills it loses their inquiry
|
||||||
|
behind a success page. */
|
||||||
|
}
|
||||||
|
<div class="honeypot" hidden aria-hidden="true">
|
||||||
|
<label for={HONEYPOT_FIELD}>Company website</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={HONEYPOT_FIELD}
|
||||||
|
name={HONEYPOT_FIELD}
|
||||||
|
tabindex="-1"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
/* ⚠️ THE PRIVACY LINK MUST STAY OUT OF THIS LABEL. Two reasons, both
|
||||||
|
about the one REQUIRED control on the form: a focusable element
|
||||||
|
inside a `<label>` for another control behaves inconsistently across
|
||||||
|
engines, and the checkbox's accessible name becomes the whole
|
||||||
|
paragraph plus "Privacy policy link" — re-announced on every
|
||||||
|
validation failure. The consent wording itself must be verbatim from
|
||||||
|
`CONSENT_TEXT`, so it stays in the label; the link is DESCRIBED
|
||||||
|
instead, via `aria-describedby`. */
|
||||||
|
}
|
||||||
|
<div class="field field-consent">
|
||||||
|
<label class="consent">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="consent"
|
||||||
|
value="on"
|
||||||
|
required
|
||||||
|
aria-describedby="consent-privacy"
|
||||||
|
/>
|
||||||
|
<span>{CONSENT_TEXT}</span>
|
||||||
|
</label>
|
||||||
|
<p class="consent-note" id="consent-privacy">
|
||||||
|
How that information is handled, and how to have it deleted: <a
|
||||||
|
href="/legal/privacy/">privacy policy</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
/* THE SECOND HONEYPOT — a decoy CHECKBOX. The mechanism, and the
|
||||||
|
limits of what the observed spam supports, are in `src/data/intake.ts`
|
||||||
|
and are not restated here. Four properties of the MARKUP, each of
|
||||||
|
which is what stops this field costing a real inquiry:
|
||||||
|
|
||||||
|
· its own CLASS NAME, not `.honeypot` — one selector must not
|
||||||
|
match both traps. They share a declaration block below, which is
|
||||||
|
presentation; what matters is that `.honeypot` does not select
|
||||||
|
this one;
|
||||||
|
· placed after the consent block, not beside the other honeypot;
|
||||||
|
· `hidden` as well as the CSS rule, so it stays hidden where
|
||||||
|
author styles do not apply;
|
||||||
|
· a label that tells a human not to tick it. With `hidden` in
|
||||||
|
place a human essentially cannot see it, so this is the last
|
||||||
|
line rather than the first — and it costs almost nothing,
|
||||||
|
because the PLAUSIBLE NAME is what a bot matches on and the name
|
||||||
|
is unchanged.
|
||||||
|
|
||||||
|
⚠️ NO `required`, AND NO `checked`. An unchecked box sends nothing,
|
||||||
|
so absence is the pass — and the handler tests for a NON-EMPTY value,
|
||||||
|
so an empty one passes too. */
|
||||||
|
}
|
||||||
|
<div class="optin-decoy" hidden aria-hidden="true">
|
||||||
|
<label for={DECOY_CHECKBOX_FIELD}>Leave this box unticked.</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={DECOY_CHECKBOX_FIELD}
|
||||||
|
name={DECOY_CHECKBOX_FIELD}
|
||||||
|
value="on"
|
||||||
|
tabindex="-1"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
/* ⚠️ `<Button type="submit">`, NOT a hand-written `<button class="btn">`.
|
||||||
|
`.btn` and `.btn-primary` are SCOPED TO `Button.astro`, so a raw
|
||||||
|
button carrying those class names compiles against this page's cid,
|
||||||
|
matches nothing, and renders as an unstyled default button — the
|
||||||
|
parent-scope trap `CLAUDE.md` records, arrived at from the other
|
||||||
|
direction. The first version of this file did exactly that.
|
||||||
|
AND IT IS WRAPPED IN A DIV THIS PAGE OWNS, for the same rule read
|
||||||
|
forwards: `.submit` on `<Button>` itself would compile to
|
||||||
|
`.submit[cid-of-this-page]` and never match the rendered element. */
|
||||||
|
}
|
||||||
|
<div class="submit">
|
||||||
|
<Button type="submit">Send the inquiry</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ContactBand />
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
padding-block-start: var(--space-9);
|
||||||
|
}
|
||||||
|
.hero-h {
|
||||||
|
margin-block: var(--space-4) var(--space-5);
|
||||||
|
font-size: var(--text-6xl);
|
||||||
|
}
|
||||||
|
.hero-lede {
|
||||||
|
max-inline-size: 58ch;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The direct-contact block. A `<dl>` because each row is genuinely a term and
|
||||||
|
its value, which is what gives the labels their semantics without spending a
|
||||||
|
heading level on them. */
|
||||||
|
.direct {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
|
||||||
|
gap: var(--space-5);
|
||||||
|
margin-block-start: var(--space-8);
|
||||||
|
}
|
||||||
|
/* `--text-eyebrow`, not `--text-2xs`: these `<dt>`s are label text on the page
|
||||||
|
that collects inquiries, so they move with the `<label>`s below rather than
|
||||||
|
sitting a step behind them. Pouya's ruling, 2026-08-31. */
|
||||||
|
.direct dt {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-eyebrow);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
letter-spacing: var(--tracking-eyebrow);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-meta);
|
||||||
|
}
|
||||||
|
.direct dd {
|
||||||
|
margin-block-start: var(--space-2);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
/* The email address has no break opportunity and overflowed at a 200% default
|
||||||
|
font size. It must stay selectable and correct, so it breaks rather than
|
||||||
|
being truncated. `docs/02` §Reflow carries the measurement. */
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The no-retainer sentence, set larger than the paragraphs under it. On an
|
||||||
|
inverse ground it inherits cream (16.81:1) from `.section-inverse`. */
|
||||||
|
.statement {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- The form -------------------------------------------------------- */
|
||||||
|
|
||||||
|
.intake {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
|
||||||
|
gap: var(--space-5) var(--space-6);
|
||||||
|
max-inline-size: 56rem;
|
||||||
|
}
|
||||||
|
/* The two long fields span the whole form rather than sitting in a column
|
||||||
|
20rem wide. `1 / -1` works at every column count the auto-fit produces. */
|
||||||
|
.field-textarea,
|
||||||
|
.field-consent,
|
||||||
|
.submit {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NOT the `.eyebrow` class, deliberately: `--text-secondary` (11.75:1) rather
|
||||||
|
than `.eyebrow`'s `--text-meta` (5.47:1), because a form label is operative
|
||||||
|
text. Everything else matches it, `font-weight` included — without that these
|
||||||
|
rendered at 400 under a `p.eyebrow` of the same size and colour.
|
||||||
|
`overflow-wrap` because at a 200% default font size "Firm or organisation"
|
||||||
|
ran 38px outside its own box at 320px: `docs/02` §Reflow, instrument
|
||||||
|
finding 2. */
|
||||||
|
label,
|
||||||
|
legend {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-eyebrow);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
letter-spacing: var(--tracking-eyebrow);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
/* Maroon on cream is 12.29:1, so the asterisk is legible — but it is
|
||||||
|
`aria-hidden` and paired with a visually-hidden "(required)", because
|
||||||
|
colour and a glyph must never be the only carrier of meaning (docs/02). */
|
||||||
|
.req {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
inline-size: 100%;
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
/* 1rem, not smaller. iOS Safari zooms the viewport on focus for any font
|
||||||
|
size under 16px, which on a form this long throws the layout sideways. */
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
/* 44px minimum target (WCAG 2.5.8) comes from the padding plus this
|
||||||
|
line-height; measured rather than set with a fixed height, so a longer
|
||||||
|
label or a zoomed root does not crush it. */
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
}
|
||||||
|
input:focus-visible,
|
||||||
|
select:focus-visible,
|
||||||
|
textarea:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: var(--focus-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
color: var(--text-meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.radios {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-block-start: var(--space-2);
|
||||||
|
}
|
||||||
|
.radio,
|
||||||
|
.consent {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
/* The label text next to a control is sentence case and normal size — the
|
||||||
|
mono uppercase treatment above is for the field's own name, and applying
|
||||||
|
it to a paragraph of consent text would be unreadable. */
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
letter-spacing: var(--tracking-normal);
|
||||||
|
text-transform: none;
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
/* 44px IS THE FLOOR (`docs/02` §Accessibility floor) and this row was 25.6px:
|
||||||
|
an 18.4px control plus one line of body text, with no `::after { inset: 0 }`
|
||||||
|
overlay to enlarge it. `min-block-size` rather than padding, so the label
|
||||||
|
grows to the floor and no further — padding would push the two radios apart
|
||||||
|
at every width. */
|
||||||
|
.radio {
|
||||||
|
align-items: center;
|
||||||
|
min-block-size: 44px;
|
||||||
|
}
|
||||||
|
.consent {
|
||||||
|
align-items: flex-start;
|
||||||
|
max-inline-size: var(--width-prose);
|
||||||
|
}
|
||||||
|
/* Indented to the label's text column so it reads as belonging to the
|
||||||
|
checkbox — 1.15rem control plus the flex gap. */
|
||||||
|
.consent-note {
|
||||||
|
margin-block-start: var(--space-3);
|
||||||
|
margin-inline-start: calc(1.15rem + var(--space-3));
|
||||||
|
max-inline-size: var(--width-prose);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
color: var(--text-meta);
|
||||||
|
}
|
||||||
|
.radio input,
|
||||||
|
.consent input {
|
||||||
|
inline-size: 1.15rem;
|
||||||
|
block-size: 1.15rem;
|
||||||
|
flex: none;
|
||||||
|
padding: 0;
|
||||||
|
/* The checkbox sits on the first line of its own label text rather than at
|
||||||
|
the top of the box, which is where `flex-start` alone would put it. */
|
||||||
|
margin-block-start: 0.25em;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THE HONEYPOT. `display: none` is what keeps it out of the layout AND out of
|
||||||
|
the accessibility tree; `aria-hidden` on the wrapper and `tabindex="-1"` on
|
||||||
|
the input are belt and braces for the case where a future stylesheet
|
||||||
|
un-hides it. Do not swap this for `visibility` or an off-screen position:
|
||||||
|
an off-screen input is still focusable and still announced. */
|
||||||
|
.honeypot,
|
||||||
|
.optin-decoy {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user