fix: refute (ar)'s intake finding; fix the D20 gloss class; add X-Robots-Tag on *.pdf
Build and deploy / build-and-deploy (push) Failing after 4s

Pouya's rulings of 2026-09-03, in five parts.

1. THE INTAKE FORM IS NOT BROKEN. (ar) was wrong. docs/09 §7.1 verbatim —
POST /api/intake with an Origin header — returns 303 to /contact/could-not-send/
with access-control-allow-origin echoed; the same probe without Origin returns
403. A bare POST 403s BY DESIGN and §7.1 says so three lines below the probe it
prescribes: "403 means the Origin header did not arrive". The earlier finding
read a status code without reading the document that defines it. Second time in
two days. CLAUDE.md's instrument list goes eight to nine. D20 findings 12 and 19
fall with it; §7.2 (that both emails arrive) is still owed.

The correction is APPENDED as entry (as); (ar) stands unedited.

2. The privacy retention comment was stale, not a defect — superseded by his
decision to publish and confirm after launch, reading from 2026-09-04. Reworded;
the TODO(pouya) came off with the gate it enforced. The mechanism finding
survives: it was a JSX comment, stripped by Astro, so no build or deploy path
could see it. A publication gate that lives only in a stripped comment is not a
gate. §9 Q60 corrected.

3. The gloss class is fixed — 15 of the 20 D20 findings, 14 distinct edits across
9 files, under the rule "the gloss may say no more than the extract says; no new
claims, no new sources". Swept three unpublished insights drafts too, and
corrected the wrong CAA attribution at its source in docs/reference/, which is
where a fixed page re-seeds. /bio/ changed, so the committed PDF is regenerated
(89,549 B, 1 page asserted). Three findings outstanding: 10 needs a ruling, 11 is
ruled and owed via Q60, 13 needs him to have said it. R1 is not one of the twenty.

4. X-Robots-Tag cannot be done with S3 object metadata — --metadata writes user
metadata, returned as x-amz-meta-x-robots-tag, which no crawler reads. Built as
the CloudFront response-headers policy docs/06 has specified all along:
configure.mjs section 4. It needs a --apply run, not a deploy. The policy is
cloned from whatever is attached at run time and reconciled on every run, because
a response-headers policy replaces rather than merges.

5. Headshot deferred as an open non-defect. The master and the srcset ladder are
both fine; Astro passes no quality, so AVIF encodes at sharp's default 50 and is
served first.

Two review rounds, 29 findings, all resolved, none declined; stopped at two per
D19. NINE of round 2's fourteen were defects in round 1's own repairs — including
a fix that harmonised both /fees/ rows onto wording that was itself unregistered,
publishing an unsourced fee term twice where it had been once.

Gates, exit status read for each: check 0 (0 errors, 0 warnings, 0 hints),
build 0 (23 pages), check:claims 0, check:intake 0, og:proof 0, lint 0, minifier
grep exit 1, router.test.mjs 30/30. Lighthouse NOT run. Nothing deployed and
nothing applied to the distribution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-09-03 17:23:20 -04:00
co-authored by Claude Opus 5
parent b9523817e2
commit 02739adac9
18 changed files with 764 additions and 101 deletions
+160 -1
View File
File diff suppressed because one or more lines are too long
+17 -1
View File
@@ -375,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 eight 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
@@ -417,6 +417,22 @@ time: a number that looks like a finding, from a probe nobody validated.
about a colour that is never painted.** Composite against the actual ground about a colour that is never painted.** Composite against the actual ground
before measuring contrast, and take "the ink colour" only from fully opaque before measuring contrast, and take "the ink colour" only from fully opaque
pixels. 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
+15 -6
View File
@@ -497,9 +497,17 @@ position, not a claim of existing volume.**
> occurrences of "allocation" of any kind across the connection process. It also > 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. > reached `src/data/site.ts` and shipped in the six-card grid on three pages.
> >
> **Use the terms these bodies use:** *connection assessment and approval (CAA)* > **Use the terms these bodies use:** the IESO's own words are *"the IESO's and
> is the umbrella; the IESO performs a *System Impact Assessment (SIA)* and the > transmitter's connection assessment and approval (CAA) process"*, within which
> transmitter a *Customer Impact Assessment (CIA)*. **Ontario has no > 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 > interconnection queue** — the IESO says so in terms and works from "committed
> projects" instead, so "our place in the queue" describes nothing. The > projects" instead, so "our place in the queue" describes nothing. The
> genuinely adjacent term, the OEB's *Capacity Allocation Model* in the > genuinely adjacent term, the OEB's *Capacity Allocation Model* in the
@@ -530,9 +538,10 @@ position, not 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,
+136 -29
View File
@@ -408,12 +408,26 @@ Then invalidate `/*`.
> `67847d9`** — SHA-256 compared page by page, 22 same / 0 differ / 0 errors. The > `67847d9`** — SHA-256 compared page by page, 22 same / 0 differ / 0 errors. The
> five `noindex` surfaces and the 17-URL sitemap are correct. > five `noindex` surfaces and the 17-URL sitemap are correct.
> >
> 🛑 **BUT THIS LIST WAS NOT CLEAN WHEN THE SITE PUBLISHED, AND THAT IS THE > 🛑 **THIS LIST WAS NOT CLEAN WHEN THE SITE PUBLISHED, AND THAT IS THE RECORD,
> RECORD, NOT A REPROACH. THREE BLOCKING ITEMS WERE OPEN AT THE MOMENT OF > NOT A REPROACH. TWO BLOCKING ITEMS WERE GENUINELY OPEN AT CUTOVER; BOTH ARE
> CUTOVER AND TWO STILL ARE.** D11 is a single shot and the checklist exists > NOW NARROWED RATHER THAN CLOSED.** D11 is a single shot and the checklist
> because of it; a launch that crosses its own gates should be legible as one > exists because of it; a launch that crosses its own gates should be legible as
> afterwards rather than smoothed over. **What follows is the state as at > one afterwards rather than smoothed over.
> 2026-09-02, after the D20 pass ran against the shipped bytes.** >
> ⚠️ **THE COUNT SAID THREE FOR ONE DAY AND THREE WAS WRONG — corrected
> 2026-09-03.** The third, *"the intake form is live and broken"*, was **a false
> alarm from a malformed probe** and is refuted in item 2 below. It is corrected
> here rather than deleted because a blocker that was never real, asserted on the
> most-read part of this page, is the same failure as a real one that goes
> unrecorded — and because **this is the first time the count moved for a reason
> the earlier notes did not anticipate: not closed, not deleted, not moot, but
> WRONG.** That is a fourth way off this list, and it looks identical to the
> other three in a tally.
>
> **The state as at 2026-09-03:** **Q60** is owed rather than pending — Pouya
> ruled the page publishes and the deletion is confirmed after launch, reading
> from **2026-09-04**. **The D20 pass** returned 20 confirmed findings, of which
> **15 are fixed, 2 refuted and 3 need a ruling from him** rather than an edit.
> >
> ✅ **THE READ-THROUGH IS COMPLETE — Pouya, 2026-09-02, and it returned ONE > ✅ **THE READ-THROUGH IS COMPLETE — Pouya, 2026-09-02, and it returned ONE
> FINDING WHICH WAS NOT COPY.** `public/favicon.ico` shipped with no > FINDING WHICH WAS NOT COPY.** `public/favicon.ico` shipped with no
@@ -455,27 +469,73 @@ Then invalidate `/*`.
> period. `docs/09` Part 10 is the test; earliest useful reading **48 hours** > period. `docs/09` Part 10 is the test; earliest useful reading **48 hours**
> after the record is written, failure not called before **7 days** — Pouya > after the record is written, failure not called before **7 days** — Pouya
> started the window 2026-09-02, so **check from 2026-09-04**. > started the window 2026-09-02, so **check from 2026-09-04**.
> ⚠️ **THE PAGE CARRIED ITS OWN INSTRUCTION NOT TO DO THIS AND IT WAS > ⚠️ **THE PAGE CARRIED ITS OWN INSTRUCTION NOT TO DO THIS — RULED STALE BY
> INVISIBLE AT DEPLOY TIME.** `src/pages/legal/privacy.astro:229` holds a live > POUYA 2026-09-03 AND REWORDED.** `src/pages/legal/privacy.astro` held a
> `TODO(pouya)` ending, in terms: *"This page must not go public until a > `TODO(pouya)` ending *"This page must not go public until a deletion has
> deletion has actually been seen."* It is a **JSX comment**, so Astro strips > actually been seen."* **His decision supersedes it: publish, then confirm the
> it and it never reaches `dist/` — which is exactly why `check:claims`, the > deletion after launch.** The comment now states that decision and its date,
> build and both deploy paths were all green over it. **A publication gate > and the `TODO(pouya)` marker is gone, which also clears the checklist item
> that lives only in a stripped comment is not a gate.** The checklist item > *"No `TODO(pouya)` remains in any shipped page"* below.
> *"No `TODO(pouya)` remains in any shipped page"* below is the control that > **What stays true is the mechanism finding, and it is worth keeping:** that
> would have caught it and it was never ticked. > instruction was a **JSX comment**, so Astro strips it and it never reached
> 2. 🛑 **THE INTAKE FORM IS LIVE AND BROKEN — a submitter gets a blank page.** > `dist/` — which is why `check:claims`, the build and both deploy paths were
> `/contact/` ships `<form method="post" action="/api/intake">`; **`POST > green over it. **A publication gate that lives only in a stripped comment is
> /api/intake` returns HTTP 403 with `content-length: 0`** and an > not a gate**, whatever the gate happens to say. Q60 itself is unchanged and
> `apigw-requestid` header, so the request reaches API Gateway and is rejected > the confirmation is now *owed* rather than *pending*.
> because the only route is `POST /submissions` (§7). No styled error, no > 2. ✅ **REFUTED BY MEASUREMENT 2026-09-03 — THE INTAKE FORM IS NOT BROKEN, AND
> message, no fallback. Measured against production 2026-09-02. `docs/09` > THIS ENTRY IS THE CORRECTION.** Pouya's probe, reproduced here in both
> Part 6 is the fix; Pouya has the end-to-end test in progress. > directions: `docs/09` §7.1 verbatim — `POST /api/intake` with
> ⚠️ **AND `/legal/privacy/` AND `/contact/received/` BOTH DESCRIBE THAT > `Origin: https://adr.smlcompany.ca` and
> MECHANISM AS RUNNING** — *"Two emails are sent when you submit the form"* and > `Content-Type: application/x-www-form-urlencoded` — returns **HTTP/2 303**,
> *"A confirmation goes to the email address you gave"*. Nothing is sent, > `location: https://adr.smlcompany.ca/contact/could-not-send/`, with
> because nothing can be submitted. Found by the D20 pass; see item 3. > `access-control-allow-origin` echoed and an `apigw-requestid` present. **The
> 3. 🛑 **THE D20 CLAIMS PASS HAS NOW RUN AGAINST THE SHIPPED BYTES AND RETURNED > handler answered as designed**: it validated, found an empty submission and
> redirected to the failure page before any write and any email. The **same
> probe with the `Origin` header removed returns 403**, which is the control.
> ⚠️ **A BARE POST TO `/api/intake` RETURNS 403 BY DESIGN, AND §7.1 SAYS SO
> THREE LINES BELOW THE PROBE** — *"403 means the `Origin` header did not
> arrive"*. The earlier finding read a status code without reading the document
> that defines what that code means on that route. **This false alarm has now
> fired twice in two days** — Pouya's own probe tripped it 2026-09-02 — and it
> is recorded in `CLAUDE.md`'s instrument list, which stands at nine.
> **The only valid route probe is `docs/09` §7.1 verbatim, `Origin` included.**
>
> ⚠️ **AND THE TWO "BACKEND NOT DEPLOYED" CLAIMS FINDINGS FALL WITH IT.**
> `/legal/privacy/` §Where it is stored (*"Two emails are sent when you submit
> the form"*) and `/contact/received/` (*"A confirmation goes to the email
> address you gave"*) were both premised on the route not existing. It exists.
> **What is NOT settled by this probe is that both emails actually arrive** —
> §7.1 stops before any write and any email by design, and that is `docs/09`
> §7.2, the real-submission test Pouya has in progress. The disclosures are
> unblocked; the end-to-end confirmation is still owed.
> 3. ⚠️ **THE D20 CLAIMS PASS RETURNED FAIL WITH 20 CONFIRMED FINDINGS; 15 ARE
> NOW FIXED, 2 REFUTED, 3 OUTSTANDING — updated 2026-09-03, and the three
> numbers partition the twenty.** Fixed under Pouya's rule *"the gloss may say
> no more than the extract says; no new claims, no new sources"*: findings
> 19, 1418 and 20 — the whole gloss class, plus `/bio/`'s role verb.
> ⚠️ **15 FINDINGS, 14 DISTINCT EDITS: findings 4 and 15 quote the same
> sentence** on `/practice/energy/`, so one edit closed both. **REFUTED:**
> findings 12 and 19, the two backend disclosures, with item 2 above.
> **OUTSTANDING — findings 10, 11 and 13, and each is outstanding for a
> different reason:**
> **(10) NEEDS A RULING.** `/fees/`'s *"Every figure is on this page"* against
> §4's **Med-Arb** offering, which `docs/07-fees.md` prices nowhere. Either a
> med-arb fee term or a scoped promise; it cannot be closed by narrowing.
> **(11) IS RULED, AND THE CONFIRMATION IS OWED.** The retention *mechanism*
> sentence on `/legal/privacy/` is unchanged and still ships, deliberately —
> that is blocker 1 above and §9 Q60, reading from 2026-09-04. It is listed so
> the twenty account for themselves, not because it is unresolved.
> **(13) NEEDS HIM TO HAVE SAID IT.** `/legal/privacy/`'s *"if a conflicts
> check has already been run I will tell you what its outcome was"* is an
> **undertaking**, and §4's gate for that class is one line: Pouya must have
> made it **in terms**. It is not in `CONDUCT_UNDERTAKINGS`.
> ⚠️ **§12 R1 IS NOT ONE OF THE TWENTY.** An earlier form of this item named it
> as the third outstanding finding and dropped 11 to make room — a tally that
> did not partition its own set. R1 is a standing reminder on licensure that a
> completeness critic reached independently from the copy; it is live, and it
> is counted nowhere. The original entry follows.
>
> 🛑 **THE D20 CLAIMS PASS HAS NOW RUN AGAINST THE SHIPPED BYTES AND RETURNED
> FAIL — 20 CONFIRMED FINDINGS ON LIVE PAGES.** Run 2026-09-02 at `67847d9`, > FAIL — 20 CONFIRMED FINDINGS ON LIVE PAGES.** Run 2026-09-02 at `67847d9`,
> after cutover, over all 23 built pages: 13 auditors (8 page groups + 5 > after cutover, over all 23 built pages: 13 auditors (8 page groups + 5
> cross-cutting lenses) → 41 raw findings → 31 distinct → each adversarially > cross-cutting lenses) → 41 raw findings → 31 distinct → each adversarially
@@ -1081,8 +1141,25 @@ the decision is re-readable rather than re-litigated.
byte-reproducible** — Chrome stamps a `/CreationDate`, so two runs of byte-reproducible** — Chrome stamps a `/CreationDate`, so two runs of
identical content differ in digest and every re-render is a binary diff. identical content differ in digest and every re-render is a binary diff.
Re-commit it when something actually changed, and say what in the message Re-commit it when something actually changed, and say what in the message
- [ ] **`X-Robots-Tag: noindex` on `*.pdf`**, via a CloudFront response-headers - [ ] **`X-Robots-Tag: noindex` on `*.pdf`** — ⚠️ **WRITTEN 2026-09-03, NOT YET
policy. **This is the PDF half of a decision already taken for the page.** APPLIED. It needs a `configure.mjs --apply` run, not a deploy.**
`infra/cloudfront/configure.mjs` §4 creates a response-headers policy
`adr-sml-pdf-noindex` and a `*.pdf` cache behaviour carrying it. ⚠️ **S3
OBJECT METADATA CANNOT DO THIS, which is the natural first reach and was
the instruction this was implemented against.** `aws s3 sync --metadata`
writes USER metadata, which S3 returns as `x-amz-meta-x-robots-tag` — a
header no crawler reads. Only a literal `X-Robots-Tag` counts and S3's REST
endpoint will not emit one, so the mechanism is the response-headers policy
this line has specified from the start. ⚠️ **THE POLICY CLONES THE
SECURITY HEADERS AT RUN TIME RATHER THAN RETYPING THEM** — a
response-headers policy REPLACES rather than merges, and all five
(`strict-transport-security`, `x-content-type-options`, `x-frame-options`,
`x-xss-protection`, `referrer-policy`) were measured arriving on the live
PDF 2026-09-03, so a hand-written policy would have silently dropped them.
Verify after applying with `docs/09` Part 3's header block, which counts
each of the six separately — an alternation `grep` exits 0 on any one match
and would call a partial clone a pass. **This is the PDF half of a decision
already taken for the page.**
`/bio/` is `noindex` and excluded from the sitemap because it is a condensed `/bio/` is `noindex` and excluded from the sitemap because it is a condensed
duplicate of `/about/` and `/fees/`, and *"two URLs competing on the same duplicate of `/about/` and `/fees/`, and *"two URLs competing on the same
content is the one thing `docs/04` is most concerned with."* The committed content is the one thing `docs/04` is most concerned with."* The committed
@@ -1092,6 +1169,36 @@ the decision is re-readable rather than re-litigated.
instead. A `Disallow` will not do it: a blocked URL can still be listed. instead. A `Disallow` will not do it: a blocked URL can still be listed.
Found by `adversarial-reviewer`, 2026-08-31 Found by `adversarial-reviewer`, 2026-08-31
- [ ] Booking link works, including the no-JavaScript fallback — **conditional on R6**; booking is parked and `CONTACT.bookingUrl` is `null`, so nothing renders and this passes vacuously until a tool is chosen. **Nothing on `/contact/` mentions booking**, deliberately - [ ] Booking link works, including the no-JavaScript fallback — **conditional on R6**; booking is parked and `CONTACT.bookingUrl` is `null`, so nothing renders and this passes vacuously until a tool is chosen. **Nothing on `/contact/` mentions booking**, deliberately
- [x] ⚠️ **THE HEADSHOT SHIPS SOFT, AND IT IS A DEFERRED DECISION RATHER THAN A
DEFECT — Pouya, 2026-09-03. NO CHANGE.** ✅ **Ticked because the decision is
taken, not because anything was done** — an item recording a decision *not*
to act can never be ticked on completion, and leaving it open would stop
this checklist ever reading clean. He raised it on the live site;
measured 2026-09-03 and the cause is not the master and not the delivery.
**The master is fine** (1600×1600, 4:4:4, real detail at full size — a
1/2-scale round trip is visibly softer than it is) and **the srcset ladder
is correct** (9 device profiles in Chrome: ratios **1.001.21, no
upscaling anywhere**, `sizes` 476 px matching the measured rendered width
exactly). **The cause is that Astro passes no `quality`, so sharp's
per-format defaults apply — AVIF 50, WebP 80, JPEG 80 — and
`formats={['avif','webp']}` puts AVIF first, so every modern browser gets
the quality-50 encode.** At 960 px it retains **55%** of the reference's
high-frequency energy; WebP retains 85% and JPEG 95%, and neither is
served. Sweep at 960 px: q60 → 76% at 33 KB, q65 → 80% at 39 KB, **q70 →
90% at 51 KB**, q80 → 94% at 77 KB, against today's **21 KB**.
⚠️ **IT IS DEFERRED BECAUSE IT IS A REAL TRADE, NOT BECAUSE IT IS SMALL:**
the portrait is the LCP element from 768 px up, and **§7's Lighthouse row
records `/` at LCP 2.03 s** `[verified 2026-08-31 — lcp-breakdown-insight]`,
so +30 KB needs a fresh `npm run lighthouse` before it ships.
⚠️ **THAT IS NOT A MEASUREMENT AGAINST `docs/04`'s BUDGET AND MUST NOT BE
READ AS ONE.** `docs/04`'s < 2.0 s is a **Slow 4G field** figure; 2.03 s is
a local run under loopback throttling, which is why `npm run lighthouse`
*reports* LCP and does not assert it (§7). The two are close enough to look
comparable and are not the same measurement — so this is a reason to
re-measure before adding bytes, not a recorded budget breach. **Three call sites would be affected
and none sets `quality`** — `src/pages/index.astro`, `src/pages/about.astro`
and `src/components/InfinityMark.astro`; the mark is line art and would
want a different number from the portrait, so this is not one edit.
- [x] ✅ **Favicon set complete, and REGENERATED 2026-09-02 — it had shipped with - [x] ✅ **Favicon set complete, and REGENERATED 2026-09-02 — it had shipped with
no transparency at all.** Pouya's read-through finding. All three frames no transparency at all.** Pouya's read-through finding. All three frames
(16/32/48) declared a 32-bit alpha channel and then carried `alpha = 255` (16/32/48) declared a 32-bit alpha channel and then carried `alpha = 255`
+60 -7
View File
@@ -309,7 +309,7 @@ status, not the absence of an error.
--- ---
## Part 3 — Apply the three distribution changes ## Part 3 — Apply the four distribution changes
One script, `infra/cloudfront/configure.mjs`, because the alternative is One script, `infra/cloudfront/configure.mjs`, because the alternative is
hand-editing a 300-line JSON document and posting it back with an `IfMatch` ETag. hand-editing a 300-line JSON document and posting it back with an `IfMatch` ETag.
@@ -328,18 +328,39 @@ Part 0.3 records is exactly:
resolved Managed-CachingDisabled = 4135ea2d-6df8-44a3-9df3-4b5a84be39ad resolved Managed-CachingDisabled = 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
resolved Managed-AllViewerExceptHostHeader = b689b0a8-53d0-40ab-baf2-68738e2966ac resolved Managed-AllViewerExceptHostHeader = b689b0a8-53d0-40ab-baf2-68738e2966ac
4 change(s) to distribution E1OK7G98KNKUTA (ETag …): 6 change(s) to distribution E1OK7G98KNKUTA (ETag …):
+ DefaultCacheBehavior.FunctionAssociations viewer-request -> arn:…:function/adr-sml-router + DefaultCacheBehavior.FunctionAssociations viewer-request -> arn:…:function/adr-sml-router
+ CustomErrorResponses += 404 -> /404.html with status 404 + CustomErrorResponses += 404 -> /404.html with status 404
+ Origins += intake-api -> …execute-api… (https-only, TLSv1.2) + Origins += intake-api -> …execute-api… (https-only, TLSv1.2)
+ CacheBehaviors += /api/* -> intake-api, CachingDisabled, AllViewerExceptHostHeader, POST allowed + CacheBehaviors += /api/* -> intake-api, CachingDisabled, AllViewerExceptHostHeader, POST allowed
+ create response-headers policy adr-sml-pdf-noindex (SecurityHeadersConfig cloned from … + X-Robots-Tag: noindex)
+ CacheBehaviors += *.pdf -> <s3-origin>, default cache policy, adr-sml-pdf-noindex (policy id created in the same --apply pass)
DRY RUN — nothing was sent. Re-run with --apply to write it. DRY RUN — nothing was sent. Re-run with --apply to write it.
``` ```
Fewer than four changes means part of this is already done — read which lines are ⚠️ **SECTION 4 CANNOT SHOW THE POLICY ID IN A DRY RUN, AND SAYS SO — IT IS
prefixed `·` (already present) and carry on. More than four, or a different set, STILL ONE `--apply`.** The `*.pdf` behaviour has to carry the response-headers
means the distribution is not in the state 0.3 recorded: stop and re-read it. policy's id, and on a first run that policy does not exist yet, so the dry run
prints the behaviour it *would* add with `(policy id created in the same --apply
pass)` where the id goes. **A single `--apply` creates the policy and adds the
behaviour in one call — do not run it twice.** The dry run reports both changes
either way; one that listed only the policy would hide the half that touches a
distribution serving 23 pages.
Fewer than six changes means part of this is already done — read which lines are
prefixed `·` (already present) and carry on. **On the live distribution as at
2026-09-03, changes 13 are applied and you should see exactly the last two.**
More than six, or a different set, means the distribution is not in the state 0.3
recorded: stop and re-read it.
⚠️ **AND `adr-sml-pdf-noindex` IS RECONCILED ON EVERY RUN, NOT ONLY CREATED.** A
response-headers policy **replaces** rather than merges, so the PDF policy has to
carry everything the default behaviour's policy carries. If they have diverged —
someone adds the `Content-Security-Policy` or `Permissions-Policy` that
`docs/05` specifies to one and not the other — the script **throws and names the
diff** rather than passing. That is deliberate: the failure it guards is the PDF
being served different headers from the pages, which is silent.
```bash ```bash
node infra/cloudfront/configure.mjs --dist "$DIST_ID" --api-domain "$API_DOMAIN" \ node infra/cloudfront/configure.mjs --dist "$DIST_ID" --api-domain "$API_DOMAIN" \
@@ -352,11 +373,43 @@ echo "deployed: $?"
```bash ```bash
aws cloudfront get-distribution-config --id "$DIST_ID" \ aws cloudfront get-distribution-config --id "$DIST_ID" \
--query 'DistributionConfig.{Fn:DefaultCacheBehavior.FunctionAssociations.Items[].EventType,Err:CustomErrorResponses.Items[].{Code:ErrorCode,Page:ResponsePagePath,Status:ResponseCode},Beh:CacheBehaviors.Items[].{P:PathPattern,O:TargetOriginId,Methods:AllowedMethods.Items},Origins:Origins.Items[].Id}' --query 'DistributionConfig.{Fn:DefaultCacheBehavior.FunctionAssociations.Items[].EventType,Err:CustomErrorResponses.Items[].{Code:ErrorCode,Page:ResponsePagePath,Status:ResponseCode},Beh:CacheBehaviors.Items[].{P:PathPattern,O:TargetOriginId,RHP:ResponseHeadersPolicyId,Fn2:FunctionAssociations.Items[].EventType,Methods:AllowedMethods.Items},Origins:Origins.Items[].Id}'
``` ```
**Expect:** `Fn: ["viewer-request"]`; one error response `404 → /404.html → 404`; **Expect:** `Fn: ["viewer-request"]`; one error response `404 → /404.html → 404`;
one behaviour `/api/*``intake-api` with POST in its method list; two origins. **two** behaviours — `/api/*``intake-api`, POST in its method list, **no
`RHP` and `Fn2: null`** (the association is withheld there deliberately: a 301
would turn the form's POST into a GET and drop the body), and `*.pdf` → the S3
origin **with an `RHP` id and `Fn2: ["viewer-request"]`**; two origins.
**Then verify the header actually arrives, because the config landing is not the
same fact:**
```bash
curl -D /tmp/pdf.h -o /dev/null "$SITE/pouya-lajevardi-bio.pdf"
echo "curl_exit=$?" # curl's OWN status, on its own line
for h in x-robots-tag strict-transport-security x-content-type-options \
x-frame-options x-xss-protection referrer-policy; do
printf '%-28s %s\n' "$h" "$(grep -ic "^$h:" /tmp/pdf.h)"
done
```
**Expect** `curl_exit=0` and **`1` against every one of the six** — the five
security headers *and* `x-robots-tag`.
⚠️ **THE SHAPE OF THIS BLOCK IS THE POINT, and its first version got all three
wrong.** It piped `curl -sI` into one `grep -E` with six alternatives and read
`$?`. That reports **grep's** status, not curl's, so a DNS failure, a TLS failure
and a 5xx all read as `exit=1` — indistinguishable from "the headers are
missing", with `-s` deleting the message that would have told them apart. And an
alternation exits **0 if ANY ONE** matches, so `exit=0` would not have meant the
five arrived, which is the only regression the block exists to catch. Counting
each header separately is what makes a partial clone visible. (`CLAUDE.md`: never
suppress stderr, never read a pipeline's status as its first command's, and a
uniform pass is the result that ends a check rather than starting one.)
⚠️ **If any of the five security headers reads `0`, the policy did not clone them
and the PDF has LOST headers it had before this change.**
--- ---
+2 -2
View File
@@ -426,11 +426,11 @@ supports is a defect in this file, not a fact.
*Source:* <https://www.ontario.ca/laws/statute/98e15> *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)". - 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> *Source:* <https://www.ieso.ca/en/Sector-Participants/Connection-Process/Authorize-Market-and-Program-Participation>
- CONNECTION PROCESS — the IESO runs a six-stage connection process: (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." - 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> *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. - "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> *Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Overview>
- 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)". - ⚠️ **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> *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. - 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> *Source:* <https://www.ieso.ca/Sector-Participants/Connection-Process/Obtain-Approval>
+298 -2
View File
@@ -1,5 +1,5 @@
/** /**
* Applies the three distribution changes the site needs, as one reviewable * Applies the four distribution changes the site needs, as one reviewable
* transaction. `docs/09-cutover-runbook.md` Part 3 is what calls it. * transaction. `docs/09-cutover-runbook.md` Part 3 is what calls it.
* *
* 1. FunctionAssociations on the default behaviour -> `router.js`, viewer * 1. FunctionAssociations on the default behaviour -> `router.js`, viewer
@@ -9,6 +9,9 @@
* "the single most common misconfiguration in this stack". * "the single most common misconfiguration in this stack".
* 3. A `/api/*` cache behaviour on a new origin pointing at the HTTP API, so * 3. A `/api/*` cache behaviour on a new origin pointing at the HTTP API, so
* the intake form's same-origin POST reaches the handler. * the intake form's same-origin POST reaches the handler.
* 4. A `*.pdf` cache behaviour carrying a response-headers policy that adds
* `X-Robots-Tag: noindex`, so the bio PDF is not indexed as a duplicate of
* `/bio/`. `docs/06`'s checklist item carries the reasoning.
* *
* ⚠️ DRY RUN BY DEFAULT. It prints what it would change and exits 0 without * ⚠️ DRY RUN BY DEFAULT. It prints what it would change and exits 0 without
* calling `update-distribution`. `--apply` is the only thing that writes, and it * calling `update-distribution`. `--apply` is the only thing that writes, and it
@@ -291,10 +294,303 @@ if (catchAll !== -1 && catchAll < apiIndex) {
); );
} }
/* ---- 4. X-Robots-Tag: noindex on the bio PDF ----------------------------
⚠️ S3 OBJECT METADATA CANNOT DO THIS. `aws s3 sync --metadata` writes USER
metadata, which S3 returns as `x-amz-meta-x-robots-tag` — a header no crawler
reads. Only a literal `X-Robots-Tag` counts and the REST endpoint will not
emit one, so the mechanism is a response-headers policy. `docs/06`'s checklist
item carries why the PDF needs it at all; this comment carries only what the
next implementer needs in order not to break it.
⚠️ THE ONE LIVE CONSTRAINT: A RESPONSE-HEADERS POLICY REPLACES, IT DOES NOT
MERGE. Attaching a policy to `*.pdf` means the default behaviour's policy no
longer applies there, so this one must carry everything that policy carries —
hence the clone below, and hence the drift check that follows it. Measured
2026-09-03: all five security headers arrive on the live PDF today. */
const PDF_PATTERN = '*.pdf';
const PDF_POLICY_NAME = 'adr-sml-pdf-noindex';
const XRT = { Header: 'X-Robots-Tag', Value: 'noindex', Override: true };
const defaultRhpId = cfg.DefaultCacheBehavior.ResponseHeadersPolicyId;
function getResponseHeadersPolicy(id) {
return aws([
'cloudfront',
'get-response-headers-policy',
'--id',
id,
'--output',
'json',
]);
}
/* Only `custom` is listed: `adr-sml-pdf-noindex` is a name this script creates,
so a managed hit is impossible and listing them would be a wasted call that
reads as if one were possible. */
function findPdfPolicy() {
const res = aws([
'cloudfront',
'list-response-headers-policies',
'--type',
'custom',
'--output',
'json',
]);
const items = res?.ResponseHeadersPolicyList?.Items ?? [];
return (
items.find(
(i) =>
i.ResponseHeadersPolicy.ResponseHeadersPolicyConfig.Name ===
PDF_POLICY_NAME,
)?.ResponseHeadersPolicy ?? null
);
}
/* ⚠️ SKIP, DO NOT THROW. Sections 13 have already staged their mutations, and
throwing here would make the script unusable for re-applying the router
function or the 404 mapping — which is the re-run contract this file promises
at the top, and `router.js` is what keeps 22 of 23 pages off S3's
AccessDenied. A missing policy on the default behaviour is section 4's
problem alone. */
/* ⚠️ A SKIP IS NOT A CHANGE AND MUST NOT ENTER `changes`. That array is printed
under "N change(s)", `docs/09` Part 3 tells the operator to COUNT those lines,
and the `NOTHING TO CHANGE` guard exits on its length — so a skip in there
would both miscount and send an `update-distribution` carrying a config
nothing mutated. Skips get their own list and their own heading. */
const skipped = [];
if (!defaultRhpId) {
skipped.push(
`${PDF_PATTERN} / ${PDF_POLICY_NAME} — the default behaviour has no ResponseHeadersPolicyId, so there is nothing to clone the security headers from`,
);
} else {
const existingPdfPolicy = findPdfPolicy();
let pdfPolicyId = existingPdfPolicy?.Id ?? null;
/* ⚠️ RECONCILE ON EVERY RUN, NEVER ONLY AT CREATION. The clone is a copy of a
fact that lives somewhere else, so it goes stale the moment the default
behaviour's policy changes — and it would go stale silently, as a uniform
pass. `docs/05` already specifies a Content-Security-Policy (a field OF
SecurityHeadersConfig) and a Permissions-Policy (which can only be a CUSTOM
header) that the site does not ship yet; adding either to the default
behaviour would reach the pages and not the PDF. This check fails loudly
instead, naming the diff. */
const source = getResponseHeadersPolicy(defaultRhpId);
const srcCfg = source?.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig;
/* ⚠️ SKIP, NOT THROW — same rule as the missing-id case above, and it was
inconsistent for one round. A policy carrying only `CorsConfig` is legal;
an ABSENT source is section 4's problem alone and must not stop sections
1-3 from re-applying `router.js`. The DRIFT throw below is different: that
is a divergence, not an absence, and `docs/09` Part 3 argues for it. */
if (!srcCfg?.SecurityHeadersConfig) {
skipped.push(
`${PDF_PATTERN} / ${PDF_POLICY_NAME} — response-headers policy ${defaultRhpId} has no SecurityHeadersConfig to clone`,
);
} else {
const wanted = {
SecurityHeadersConfig: srcCfg.SecurityHeadersConfig,
...(srcCfg.CorsConfig ? { CorsConfig: srcCfg.CorsConfig } : {}),
...(srcCfg.RemoveHeadersConfig
? { RemoveHeadersConfig: srcCfg.RemoveHeadersConfig }
: {}),
...(srcCfg.ServerTimingHeadersConfig
? { ServerTimingHeadersConfig: srcCfg.ServerTimingHeadersConfig }
: {}),
CustomHeadersConfig: {
Quantity: (srcCfg.CustomHeadersConfig?.Items ?? []).length + 1,
Items: [...(srcCfg.CustomHeadersConfig?.Items ?? []), XRT],
},
};
if (existingPdfPolicy) {
const have = existingPdfPolicy.ResponseHeadersPolicyConfig;
const norm = (o) => JSON.stringify(o ?? null);
const drift = [
'SecurityHeadersConfig',
'CorsConfig',
'RemoveHeadersConfig',
'ServerTimingHeadersConfig',
]
.filter((k) => norm(have[k]) !== norm(wanted[k]))
.concat(
norm(have.CustomHeadersConfig?.Items) !==
norm(wanted.CustomHeadersConfig.Items)
? ['CustomHeadersConfig']
: [],
);
if (drift.length) {
/* Print BOTH SIDES of every drifted key. Naming the field alone does not
tell the operator which header moved, nor which direction to reconcile
in — the same message fires whether the source gained a header or the
PDF policy lost its X-Robots-Tag, and those need opposite repairs. */
const detail = drift
.map(
(k) =>
` ${k}\n pdf policy : ${norm(
k === 'CustomHeadersConfig'
? have.CustomHeadersConfig?.Items
: have[k],
)}\n default : ${norm(
k === 'CustomHeadersConfig'
? wanted.CustomHeadersConfig.Items
: wanted[k],
)}`,
)
.join('\n');
throw new Error(
`${PDF_POLICY_NAME} has DRIFTED from the default behaviour's policy ` +
`${defaultRhpId} on ${drift.length} field(s). The PDF is being served ` +
`different headers from the pages — read which way before repairing:\n` +
`${detail}\n` +
`Reconcile with update-response-headers-policy (it needs the policy's ` +
`own ETag), then re-run. This script will not silently paper over it.`,
);
}
console.log(
`· response-headers policy ${PDF_POLICY_NAME} exists and matches the default behaviour`,
);
} else if (!APPLY) {
console.log(`· would CREATE response-headers policy ${PDF_POLICY_NAME}`);
changes.push(
`create response-headers policy ${PDF_POLICY_NAME} (SecurityHeadersConfig cloned from ${defaultRhpId} + X-Robots-Tag: noindex)`,
);
} else {
const created = aws([
'cloudfront',
'create-response-headers-policy',
'--response-headers-policy-config',
JSON.stringify({
Name: PDF_POLICY_NAME,
Comment:
'Cloned from the default behaviour, plus X-Robots-Tag: noindex for *.pdf. See infra/cloudfront/configure.mjs section 4.',
...wanted,
}),
'--output',
'json',
]);
pdfPolicyId = created?.ResponseHeadersPolicy?.Id;
if (!pdfPolicyId) {
throw new Error('create-response-headers-policy returned no Id');
}
console.log(
`created response-headers policy ${PDF_POLICY_NAME} = ${pdfPolicyId}`,
);
changes.push(
`created response-headers policy ${PDF_POLICY_NAME} = ${pdfPolicyId}`,
);
}
const pdfBehaviours = cfg.CacheBehaviors?.Items ?? [];
const foundPdf = pdfBehaviours.find((b) => b.PathPattern === PDF_PATTERN);
if (foundPdf) {
/* ⚠️ PRESENCE IS NOT CORRECTNESS. This checked only that a `*.pdf`
behaviour existed, so one added by hand — while chasing the
`aws s3 sync --metadata` route this file's header records as the original
instruction — would report `already present`, push nothing, and print
NOTHING TO CHANGE while the PDF served no `X-Robots-Tag` at all. Section 1
compares the FunctionARN before declaring a match; so does this now. */
const wrong = [];
if (foundPdf.ResponseHeadersPolicyId !== pdfPolicyId) {
wrong.push(
`ResponseHeadersPolicyId is ${foundPdf.ResponseHeadersPolicyId ?? '(none)'}, expected ${pdfPolicyId ?? '(the policy this script manages)'}`,
);
}
if (foundPdf.TargetOriginId !== cfg.DefaultCacheBehavior.TargetOriginId) {
wrong.push(
`TargetOriginId is ${foundPdf.TargetOriginId}, expected ${cfg.DefaultCacheBehavior.TargetOriginId}`,
);
}
const hasViewerRequest = (
foundPdf.FunctionAssociations?.Items ?? []
).some((i) => i.EventType === 'viewer-request');
if (!hasViewerRequest) {
wrong.push(
'no viewer-request FunctionAssociation — router.js normalises `//` and `\\` on file paths, so `//pouya-lajevardi-bio.pdf` would 404 instead of 301',
);
}
if (wrong.length) {
throw new Error(
`a ${PDF_PATTERN} cache behaviour already exists but is NOT the one this ` +
`script manages:\n - ${wrong.join('\n - ')}\n` +
`Reconcile or remove it before re-running; this script will not adopt ` +
`a behaviour it cannot account for.`,
);
}
console.log(
`· cache behaviour ${PDF_PATTERN} already present and correctly configured`,
);
} else {
const d = cfg.DefaultCacheBehavior;
const behaviour = {
PathPattern: PDF_PATTERN,
TargetOriginId: d.TargetOriginId,
ViewerProtocolPolicy: d.ViewerProtocolPolicy,
AllowedMethods: d.AllowedMethods,
CachePolicyId: d.CachePolicyId,
/* Placeholder only in a dry run — the real id exists by the time --apply
reaches this line, because the branch above created it. */
ResponseHeadersPolicyId: pdfPolicyId ?? '<created on --apply>',
Compress: d.Compress,
SmoothStreaming: false,
FieldLevelEncryptionId: '',
/* ⚠️ THE ROUTER FUNCTION IS ATTACHED, AND IT IS NOT A NO-OP ON FILE PATHS.
`router.js` normalises `\` to `/` and collapses a leading `//` run
BEFORE it tests for an extension, and 301s when normalisation changed
anything — so `//pouya-lajevardi-bio.pdf` redirects to the canonical
path today. Omitting the association here would silently drop that and
hand S3 the doubled key instead. The `/api/*` reason for omitting it —
a 301 turning a POST into a GET and losing the body — does not apply to
a GET-only PDF. */
FunctionAssociations: d.FunctionAssociations ?? { Quantity: 0 },
LambdaFunctionAssociations: { Quantity: 0 },
TrustedKeyGroups: { Enabled: false, Quantity: 0 },
};
/* ⚠️ STAGE THE REPORT EVEN WHEN THE ID IS NOT KNOWN YET. The dry run's whole
job is to show what would touch a distribution serving 23 pages; reporting
only the harmless policy creation and staying silent about the behaviour
would mean the first sight of it is `update-distribution` writing it. The
`cfg` mutation stays gated on a real id; the REPORT does not. */
changes.push(
`CacheBehaviors += ${PDF_PATTERN} -> ${d.TargetOriginId}, default cache policy, ${PDF_POLICY_NAME}` +
(pdfPolicyId
? ` (${pdfPolicyId})`
: ' (policy id created in the same --apply pass)'),
);
if (!APPLY && !pdfPolicyId) {
console.log(
`· would ADD cache behaviour ${PDF_PATTERN}:\n` +
JSON.stringify(behaviour, null, 2)
.split('\n')
.map((l) => ' ' + l)
.join('\n'),
);
} else {
pdfBehaviours.push(behaviour);
cfg.CacheBehaviors = {
Quantity: pdfBehaviours.length,
Items: pdfBehaviours,
};
}
}
}
}
console.log(''); console.log('');
/* Skips print under their own heading and are NOT counted as changes — see the
comment on `skipped`. A skip means section 4 did nothing and the PDF is
probably not noindexed; that is louder than a silent omission and quieter
than a false change. */
if (skipped.length) {
console.log(`${skipped.length} thing(s) SKIPPED, not changed:`);
for (const k of skipped) console.log(` ! ${k}`);
console.log(' Sections 1-3 are unaffected. Investigate before relying on');
console.log(` ${PDF_PATTERN} carrying X-Robots-Tag.`);
console.log('');
}
if (changes.length === 0) { if (changes.length === 0) {
console.log( console.log(
'NOTHING TO CHANGE — the distribution already carries all three.', skipped.length
? 'NOTHING TO CHANGE — but see the skips above; the distribution does NOT carry all four.'
: 'NOTHING TO CHANGE — the distribution already carries all four.',
); );
process.exit(0); process.exit(0);
} }
+8 -1
View File
@@ -1,5 +1,12 @@
/** /**
* CloudFront Function, VIEWER REQUEST, on the default cache behaviour only. * 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 * ⚠️ THE SITE DOES NOT WORK WITHOUT THIS. `astro.config.mjs` sets
* `trailingSlash: 'always'` and `build.format: 'directory'`, so every route is * `trailingSlash: 'always'` and `build.format: 'directory'`, so every route is
Binary file not shown.
@@ -56,7 +56,7 @@ So one date sorts a pipeline into two regimes, and the requirements the later on
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 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 umbrella name is connection assessment and approval. 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. 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. 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.
@@ -19,11 +19,11 @@ There is a connection: an assessment run by the Independent Electricity System O
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. 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.
## What the connection assessment and approval process is ## 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. The umbrella is connection assessment and approval, or CAA. 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 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 published 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". 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/). 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/).
@@ -24,9 +24,9 @@ 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 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. 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 The transmitter conducts the CIA. Treating the pair as one exhibit loses the
distinction most of these disputes turn on. The umbrella name is the connection distinction most of these disputes turn on. Both sit in the IESO's and
assessment and approval process, CAA in the IESO's usage, and each application transmitter's connection assessment and approval process, CAA in the IESO's
is given a unique CAA ID. usage, and each application is given a unique CAA ID.
## What the assessment is actually of ## What the assessment is actually of
+10 -10
View File
@@ -151,7 +151,7 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
paragraphs: [ paragraphs: [
{ {
lead: 'Interim adjudication.', 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, and a determined amount is payable within 15 days of the determination being communicated. Judicial review is available only with leave of the Divisional Court.', 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.', lead: 'A designated authority runs it.',
@@ -177,7 +177,7 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
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: '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, and they produce exactly the disputes above. 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.', 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.',
}, },
], ],
}, },
@@ -255,8 +255,8 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
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.', 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 has one AI instrument, and it is mostly not switched on.', lead: "Ontario's AI-relevant statute has its artificial-intelligence obligations switched off.",
text: 'The Enhancing Digital Security and Trust Act, 2024 conditions each of its artificial-intelligence obligations on regulations prescribing who they apply to and when. 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.', 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 /* THE LEAD WAS "And no federal or Ontario statute requires data to
@@ -337,7 +337,7 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
disputeTypes: [ disputeTypes: [
{ {
name: 'Connection assessment', name: 'Connection assessment',
body: "Disputes arising out of the IESO connection assessment and approval process — the system impact assessment, the transmitter's customer impact assessment, and the conditions attached to either.", 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', name: 'Leave to construct',
@@ -386,7 +386,7 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
}, },
{ {
lead: 'Connection runs through the IESO, and it is not a queue.', lead: 'Connection runs through the IESO, and it is not a queue.',
text: 'The IESO operates a six-stage connection process and calls it connection assessment and approval. 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.', 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.', lead: 'And large loads now have their own gate.',
@@ -407,7 +407,7 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
the extract's *quotations* rather than against its adversarial the extract's *quotations* rather than against its adversarial
check. R18(b) tracks this fact as volatile; that is a different check. R18(b) tracks this fact as volatile; that is a different
problem from never having been established. */ problem from never having been established. */
text: 'Section 28.1 of the Electricity Act, 1998 came into force on 11 December 2025 and creates a connection-approval requirement for a "specified load facility", a category 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.', 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.", 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.",
@@ -460,8 +460,8 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
body: 'Whether an impairment falls inside the minor injury definition, and the monetary limit that follows if it does.', body: 'Whether an impairment falls inside the minor injury definition, and the monetary limit that follows if it does.',
}, },
{ {
name: 'Treatment and assessment plans', name: 'Medical and rehabilitation benefits',
body: 'Denied or partially approved plans, competing assessments, and disputes about the reasonableness and necessity of proposed treatment.', body: 'Which treatment, services or goods are payable, and the conditions a guideline may attach to them.',
}, },
{ {
name: 'Catastrophic impairment', name: 'Catastrophic impairment',
@@ -608,7 +608,7 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
}, },
{ {
lead: 'And the end of the road.', 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 making it not reasonably practicable to carry on business together.', 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.', lead: 'One provision points the other way.',
+11 -5
View File
@@ -136,12 +136,18 @@ const PROCESSES = [
never appear in the same element, so no proximity grep reaches never appear in the same element, so no proximity grep reaches
it — and it was found by reading the rendered PDF. The scope it — and it was found by reading the rendered PDF. The scope
belongs on the arbitration clause alone, where Q39's legal gate belongs on the arbitration clause alone, where Q39's legal gate
puts it. */ 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 as a mediator, as an arbitrator in commercial matters, I act as a neutral. I accept appointments as a mediator, as an arbitrator
and in med-arb where the parties want one neutral across both phases. in commercial matters, and in med-arb where the parties want one neutral
I read the contract and the technical record underneath it rather than across both phases. I read the contract and the technical record underneath
either side's summary of them. it rather than either side's summary of them.
</p> </p>
<p> <p>
I am {ROLE.title} at {BOUTIQUE}, with {ROLE.litigationLine} across I am {ROLE.title} at {BOUTIQUE}, with {ROLE.litigationLine} across
+2 -2
View File
@@ -131,12 +131,12 @@ const ARBITRATION_ROWS = [
}, },
{ {
item: 'Documents-only or expedited — simple', item: 'Documents-only or expedited — simple',
detail: 'Flat fee, agreed in the first procedural order.', detail: 'Flat fee.',
fee: money(FEES.arbitration.documentsOnlySimple), fee: money(FEES.arbitration.documentsOnlySimple),
}, },
{ {
item: 'Documents-only or expedited — complex', item: 'Documents-only or expedited — complex',
detail: 'Flat fee. Which band applies is settled before the appointment.', detail: 'Flat fee.',
fee: money(FEES.arbitration.documentsOnlyComplex), fee: money(FEES.arbitration.documentsOnlyComplex),
}, },
]; ];
+24 -17
View File
@@ -44,8 +44,12 @@
* record is ever deleted.** Only a record written with a near-future `ttl` * record is ever deleted.** Only a record written with a near-future `ttl`
* and watched to vanish proves that. docs/05's definition of done carries * and watched to vanish proves that. docs/05's definition of done carries
* "TTL set and verified by test record" and `docs/06`'s cutover checklist * "TTL set and verified by test record" and `docs/06`'s cutover checklist
* names this page as what that item protects. **Both halves before this page * names this page as what that item protects. ⚠️ **This read "both halves
* is public.** See the TODO(pouya) on the retention section below, and §9 Q60. * before this page is public" and the page went public first — Pouya's
* ruling of 2026-09-03: publish, then confirm the deletion, reading from
* 2026-09-04.** So the second half is now owed rather than pending, which is
* a weaker position and is recorded as one. See the comment on the retention
* section below, and §9 Q60.
* *
* ⚠️ **NO LICENSURE CLAIM AND NO ANSWER TO THE CAPACITY QUESTION.** A privacy * ⚠️ **NO LICENSURE CLAIM AND NO ANSWER TO THE CAPACITY QUESTION.** A privacy
* policy is where "legal advice" phrasing arrives by convention. §4 records * policy is where "legal advice" phrasing arrives by convention. §4 records
@@ -81,7 +85,7 @@ const RETENTION_MONTHS = 24;
/** Bump this on ANY substantive edit. A privacy policy with a stale date is a /** Bump this on ANY substantive edit. A privacy policy with a stale date is a
* policy a reader cannot tell they are reading an old version of. */ * policy a reader cannot tell they are reading an old version of. */
const LAST_UPDATED = '2 September 2026'; const LAST_UPDATED = '3 September 2026';
/* Rendered from the form's own field list, so the two cannot drift. `consent` /* Rendered from the form's own field list, so the two cannot drift. `consent`
and the honeypot are absent from `INTAKE_FIELDS` deliberately and are and the honeypot are absent from `INTAKE_FIELDS` deliberately and are
@@ -226,15 +230,15 @@ const COLLECTED = INTAKE_FIELDS.map((field) => field.label);
<h2>How long it is kept</h2> <h2>How long it is kept</h2>
{ {
/* TODO(pouya): has a test record been written to the intake table with a /* The sentence below asserts a MECHANISM, not just a period, and the
near-future `ttl` and OBSERVED TO DISAPPEAR? AGENTS.md §9 Q60. The mechanism is still unobserved — AGENTS.md §9 Q60, open. **Pouya
sentence below asserts a MECHANISM, not just a period, and the ruled 2026-09-03 that the page publishes now and the deletion is
setting being on does not prove the mechanism runs. The table confirmed after launch**; the observation window opened 2026-09-02
setting is confirmed — §7 holds that status and this comment does and the earliest useful reading is 2026-09-04 (`docs/09` Part 10).
not restate it, because it did restate it once and went stale within That decision is why this is no longer a `TODO(pouya)`. The table
the day (§12 R19). Do not answer this from the handler code, which setting lives in §7 and is deliberately not restated here — it was
only writes the attribute. This page must not go public until a once, and went stale within the day (§12 R19). Do not answer Q60
deletion has actually been seen. */ from the handler code, which only writes the attribute. */
} }
<p> <p>
<strong>{RETENTION_MONTHS} months from the date you send it</strong>, <strong>{RETENTION_MONTHS} months from the date you send it</strong>,
@@ -307,15 +311,18 @@ const COLLECTED = INTAKE_FIELDS.map((field) => field.label);
{ANALYTICS.provider === 'plausible' ? 'Plausible' : 'Fathom'}, {ANALYTICS.provider === 'plausible' ? 'Plausible' : 'Fathom'},
which is cookieless and collects no personal information and no which is cookieless and collects no personal information and no
cross-site identifiers. There is nothing to consent to and no cross-site identifiers. There is nothing to consent to and no
banner, because nothing is stored on your device. banner, because it sets no cookies and stores no identifier on
your device.
</p> </p>
) : ( ) : (
<p> <p>
<strong>This site sets no cookies and runs no analytics.</strong> <strong>This site sets no cookies and runs no analytics.</strong>
There is no tracking script on any page, nothing is stored on your There is no tracking script on any page, and there is therefore
device, and there is therefore nothing to consent to and no nothing to consent to and no banner. If cookies or analytics are
banner. If that changes, this page changes on the same day and its ever introduced, this page changes on the same day and its last
last updated date moves with it. updated date moves with it. Your browser does cache this site's
fonts, stylesheets and images for up to a year so a return visit
loads faster, and those are the same files for every visitor.
</p> </p>
) )
} }
+4 -4
View File
@@ -52,7 +52,7 @@ const ldImage = await getImage({
const graph = pageGraph(new URL(ldImage.src, Astro.site).href); const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
/** Bump on any substantive edit. See the note on the privacy page. */ /** Bump on any substantive edit. See the note on the privacy page. */
const LAST_UPDATED = '31 August 2026'; const LAST_UPDATED = '3 September 2026';
--- ---
<BaseLayout <BaseLayout
@@ -152,9 +152,9 @@ const LAST_UPDATED = '31 August 2026';
belong to that institution and are marked as quotations. belong to that institution and are marked as quotations.
</p> </p>
<p> <p>
Links out go to sources — statutes, regulators, tribunals and Links out go to an institution's published rules and to my LinkedIn
institutions. I do not control those sites and am not responsible for profile. I do not control those sites and am not responsible for what
what they say. they say.
</p> </p>
<h2>Changes</h2> <h2>Changes</h2>
+10 -7
View File
@@ -220,15 +220,18 @@ const FORMATS = [
} }
<Undertaking>{CONDUCT_UNDERTAKINGS.mediationCaucus}</Undertaking> <Undertaking>{CONDUCT_UNDERTAKINGS.mediationCaucus}</Undertaking>
{ {
/* The without-prejudice question is answered by pointing, not by /* WITHOUT PREJUDICE IS ATTRIBUTED TO THE AGREEMENT, NEVER ASSERTED
characterising legal effect. AGENTS.md §4 bars this repository from AS LAW — and it may be narrowed but NOT deleted. §4 bars this
concluding a proposition of law, and docs/03's `[unestablished]` repository from concluding a proposition of law, and no extract
pattern says to write around the capacity question. */ establishes the effect. But docs/01 §/mediation/ item 5 requires the
without-prejudice framing and docs/03 keeps the term as permitted,
so removing it breaches the spec that requires it. */
} }
<p> <p>
Mediation is conducted on a without-prejudice basis. What that means Whether the session is without prejudice, and what that covers, is
for a particular file, and what survives it, is a question for each settled by the agreement to mediate. What being without prejudice
party's own counsel rather than for the neutral. means for a particular file, and what survives the session, is a
question for each party's own counsel rather than for the neutral.
</p> </p>
</div> </div>
</div> </div>