From 02739adac996be86634a9d925b26ce2996f6b9ba Mon Sep 17 00:00:00 2001 From: Pouya Lajevardi Date: Thu, 3 Sep 2026 17:23:20 -0400 Subject: [PATCH] fix: refute (ar)'s intake finding; fix the D20 gloss class; add X-Robots-Tag on *.pdf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5 --- AGENTS.md | 161 +++++++++- CLAUDE.md | 18 +- docs/01-architecture.md | 21 +- docs/06-deployment.md | 165 ++++++++-- docs/09-cutover-runbook.md | 67 +++- docs/reference/ontario-energy-regulatory.md | 4 +- infra/cloudfront/configure.mjs | 300 +++++++++++++++++- infra/cloudfront/router.js | 9 +- public/pouya-lajevardi-bio.pdf | Bin 89496 -> 89549 bytes .../bill-40-grid-connection-disputes.mdx | 2 +- ...ontario-data-centre-build-out-disputes.mdx | 6 +- ...t-a-system-impact-assessment-evaluates.mdx | 6 +- src/data/practice-pages.ts | 20 +- src/pages/bio.astro | 16 +- src/pages/fees.astro | 4 +- src/pages/legal/privacy.astro | 41 ++- src/pages/legal/terms.astro | 8 +- src/pages/mediation.astro | 17 +- 18 files changed, 764 insertions(+), 101 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5f86d0c..a05c06f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -782,7 +782,7 @@ Nothing below can be invented. Each needs an answer from Pouya. | ~~**Q63**~~ | ✅ **CLOSED 2026-09-02 — ALL THREE LIMBS ANSWERED BY RULING, and the answer to (c) changed the sentence (a) had just approved.** **(a) WORDING APPROVED WITH TWO TRIMS:** the editorial closing sentence (*"I would rather tell you that than give you the tidier answer"*) is struck, and the mailbox clause is rewritten per (b). **(b) `info@smlcompany.ca` IS A DELEGATED MAILBOX — Pouya and administrative staff read it.** The page said *"anyone who can reach that mailbox"*; it now says who. ⚠️ **That answer reached FOUR sentences, not the one the question named** — §Where it is stored twice (*"a notification to me"*, *"my own mail is on Google Workspace"*), §How long it is kept once (*"the notification sits in my mailbox"*) and §Who can see it once — because the mailbox had been written as a personal one throughout the page. **Fifth partial sweep of this page's who-can-see-it set. The section comment names the places by opening phrase and gives NO COUNT** — it carried "eight" for one round after excluding a paragraph its own change set had edited, then "nine", and the set is now ten (`adversarial-reviewer`, rounds 1 and 2, on successive counts). **(c) ROOT IS HELD BY POUYA** `[verified 2026-09-02 — Pouya]`, and the page now states that it has no programmatic key and that he holds it. ⚠️ **THE CONSEQUENCE NOBODY ASKED FOR AND IT IS THE MOST IMPORTANT LINE IN THIS ROW: THE HUMAN HEADCOUNT CAME OFF THE PAGE.** Pouya's attestation: *"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 was exhaustive and the inference off it was not**: every read path terminates at two IAM identities, which is a **lower bound** on the number of people who can reach them, and `/legal/privacy/` published it as an exact count. It then read *"the account's administrators — me, and the small number of people who administer it with me"*. ⚠️ **THAT IS NOT THE SHIPPED SENTENCE EITHER, AND NOR WAS ITS REPLACEMENT.** Pouya ruled again the same day that the section states who and not how; the measurement paragraph, the root sentence and the summary were deleted outright, and the first sentence was then rewritten **twice more under review** — *"The people who run this practice can"* was struck for asserting an unregistered claim about who runs the practice, and *"administer this practice's systems"* for naming a set neither the measurement nor the attestation supports. **It ships as *"The record in the table: me, and the small number of people who administer the account it sits in with me"*, and §4 has the row it rests on.** Each limb of Q63 still stands; only the text it was applied to moved — **four times in one day, which is why nothing outside `dist/` is a safe source for this quote.** **No numeric human headcount ships**, the identity count stays in §7 and in the reference extract, and the extract's own inference sentence (*"The count of people is two"*) is corrected at source, because a false inference left in the evidence file re-supplies itself to the next reader. ⚠️ **AND THE `33` WAS DELIBERATELY NOT PUBLISHED** — the ruling permits the identity count on the page *"if useful"*; a role total moves when AWS creates a service-linked role by itself, so publishing it would put a second R21-governed number on a legal page that can go stale with no human acting. *"Every user and every role in the account"* carries the exhaustiveness and survives the count moving. **One line to overrule.** Original question follows. 🛑 **TWO THINGS `/legal/privacy/` STILL NEEDED FROM POUYA, ANSWERED IN THE SAME READ-THROUGH.** **(a) APPROVE THE §Who can see it WORDING.** Q62 settled what it must say and he reserved the wording in terms: *"Draft it; Pouya gives final approval on wording during his page read-through."* The draft is shipped in `dist/` and quoted in `docs/reference/intake-table-access-verification.md`. **This is a gate that existed only inside records marked closed until 2026-09-02** — the `TODO(pouya)` had been deleted, Q62 struck, and `docs/06`'s blocker ticked, so when Q60's TTL test passes **nothing mechanical or visual would have stopped unapproved copy publishing.** `adversarial-reviewer`, round 1. **(b) WHO ELSE CAN READ `info@smlcompany.ca`?** Nothing in this repository establishes it — §7 records the mail host (Google Workspace) and the SES identities, **not the mailbox's access list**, and a Workspace super-admin can reach any mailbox in the tenancy. Given that this project's AWS account, its Gitea instance and §10 are all jointly administered, the plausible case is that it is not only him. The copy is now written to assert **no** access list (*"anyone who can reach that mailbox"*), so nothing false is published either way — but a privacy policy that answers the table half with a measured number and the mail half with a shrug is answering the same question on two standards, and the reader is entitled to the specific on both. **(c) WHO HOLDS THE ROOT CREDENTIAL FOR THE AWS ACCOUNT?** The page's headline answer is a **count of people**, and root is the one path no policy constrains and no simulation can reach — it is not an IAM principal and does not appear in `list-users`. Two facts bound it: `AccountAccessKeysPresent: 0`, so there is no programmatic root credential, and MFA is on, so console access needs the root password and its device `[verified 2026-09-02]`. **If those are held by anyone other than the two administrators, "Two people can" is short by one.** The second paragraph is scoped to *"every user and every role"* and to who has been **granted** access, so it is unaffected either way — this reaches the first sentence only. Raised by `adversarial-reviewer`, round 2, which is also where the honest form of the objection came from: the artefact says in terms that this is not established, and a page was resting a count on it. **If he answers: put the fact in §7, make the sentence specific like the table sentence above it, and arm it with §12 R21's trigger.** Raised by `claims-auditor` and `adversarial-reviewer`, D20 cutover pass round 1, 2026-09-02 | *(closed)* | | ~~**Q62**~~ | ✅ **CLOSED 2026-09-02 — RULED *state the truth*, NOT *remove the access*. Pouya:** *"Rewrite the `/legal/privacy/` sentence to say exactly who can access the submissions table… the true number of people and their roles, stated specifically — not 'authorised administrators' or any other vacancy."* **Applied.** The page now opens §Who can see it with *"Two people can"*, states that the AWS account also runs systems unrelated to this practice and has two administrators, and says administrative access carries the ability to read the table. It then published the two facts the false sentence had been crowding out and which are **stronger** than what it claimed: the function that receives the form can only add a record and **cannot read the table back**, and the credential that publishes this website has **no access to the table at all**. ⚠️ **OF THOSE TWO, ONLY THE FIRST STILL SHIPS** — the mechanics ruling of 2026-09-02 took the deploy-credential sentence off the page along with the rest of the method. It is unretracted and still measured; it lives in §7 and in `docs/reference/intake-table-access-verification.md`. ⚠️ **THE FIX TOUCHED THREE PLACES, NOT ONE, AND THE OTHER TWO ARE THE POINT.** A vocabulary sweep — `grep -rniE "no (team\|assistant\|outside\|external) [a-z]*\|nobody else\|no one else" src/` — found the same falsehood in different words **two sections up the same page**: §Where it is stored ended *"and no assistant or outside administrator"*, which the Q62 pattern could not see because it was anchored on the two sentences under the other heading. And the summary paragraph closed *"the honest answer to 'who can see this' is: me, and Google"* — which would have survived the correction directly above it and re-asserted the struck number. All three now change together and the page's own comment says so. **THE TRIPWIRE STAYS PERMANENTLY — his ruling in terms:** *"it bars the false-claim shape from returning, which is exactly what the freeze's breach exception exists for."* Extended from two alternatives to **five** — round 1 of the closing audit found a third published surface unbarred, and found the first extension had widened one alternative to four phrasings where one was published. Every alternative is now a single string that reached `dist/`, so nothing speculative entered a frozen script. **Proven both ways, on real published bytes and not on fixtures alone:** against the pre-correction page rebuilt from `bd282aa` it exits **1** with **5 matches** at `dist/legal/privacy/index.html:54, 67, 67, 68, 72` — 54 is the clause the first form missed and 72 the surface it could not see at all — and against the corrected page it exits **0**. ⚠️ **THE APPROVED-STRING AND FIXTURE COUNTS ARE NO LONGER RESTATED HERE, DELIBERATELY.** They said four/four/33/three, then 36/six, then were stale again within one review round when the Q63 rewrite changed the copy the fixtures quote — three times in two days, on a row whose own point is that the record of what a frozen script bars is its maintenance surface. **`npm run check:claims` prints both numbers on every run; read them there.** **Both numbers are deliberately not repeated here** — they moved again on 2026-09-02 when the mechanics cut retired six fixtures and added four, and a row that had just ruled the printed numbers authoritative was still carrying its own stale pair one sentence later (`adversarial-reviewer`, round 1). The script prints the pattern count, the approved-string count and which of them are live page copy; that is the record. ⚠️ **The counts in this row said four/four/33/three until 2026-09-02**, describing the script as it stood before round 1's fix; on a frozen script the record of what it bars is the maintenance surface, so `adversarial-reviewer` round 2 was right to treat a stale count as a defect. ⚠️ **AND THE FIX AS FIRST WRITTEN INTRODUCED THREE DEFECTS OF ITS OWN, ALL FOUND BY THE D20 PASS ROUND 1 AND ALL NOW CORRECTED — see entry (an).** The replacement asserted *"No third party has access to it"* (an absolute negative that excludes a **disclosed processor**, which is the exact sentence struck from this page on 2026-08-31 as *"the most serious thing found in the step 7–10 review"*); it claimed *"every account and role in this infrastructure… that is checked rather than assumed"* over an artefact that had screened five users and **one** role; and it said *"the one other place a copy exists"* when the handler puts the whole submission into the confirmation it sends the inquirer, so a **third** copy sits with the reader's own provider — which this page already says two sections up. **Q62's own fix recreated Q62's shape twice.** **Two things are NOT resolved and are now Q63 rather than a footnote here** — the wording approval Pouya reserved, and the `info@smlcompany.ca` access list. The earlier form of this row called the mailbox point non-gating *"because the copy is true either way"*, which was **a guess about a fact nobody checked**; the copy is now written so it asserts no access list at all, and the question gates the page through Q63 with a `TODO(pouya)` beside it. Original finding follows. 🛑 **`/legal/privacy/` TELLS THE PUBLIC SOMETHING FALSE ABOUT WHO CAN READ THE INTAKE TABLE, AND IT IS A PRIVACY POLICY.** The page says: *"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 account has an IAM group `admins` carrying `AdministratorAccess` with TWO members**, and `iam simulate-principal-policy` returns **allowed** for `dynamodb:GetItem`, `dynamodb:Query` and `dynamodb:Scan` on the table for both of them — identical access, by the same route, the other user's own attachments being only `IAMUserChangePassword` `[verified 2026-09-01 — the five users, the group, and a five-row simulation, all commands in `docs/reference/intake-table-access-verification.md`]`. So **both halves of the sentence are wrong**: a second account has access, and it belongs to a second administrator of a shared account (§10). The three deploy users are `implicitDeny`; two CDK bootstrap roles carry `AdministratorAccess` but are assumable only by the same two administrators; the writing role holds **`PutItem` only and cannot read the table**, which is a stronger fact than the page currently claims and is the part of the sentence that is true. **THE QUESTION, AND IT IS ONE OF TWO THINGS.** (1) **Remove the access** — take that user out of `admins`, or deny DynamoDB on this table — after which the sentence becomes true as written. ⚠️ Note the likely collision: **Q23 records the Gitea instance as jointly administered and blocked on "its second administrator"**, so that access is probably not only for this account and removing it may cost something elsewhere. (2) **State the true number** — how many people hold administrative access, and that the function which 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 describing a live dispute is entitled to the specific. **Why this was invisible until now:** it is the only claim on the site whose subject lives entirely outside the repository, so R14 applies — *"unverifiable by construction"* — and there was no committed artefact to compare it against. There is now. Raised by `claims-auditor`, D20 cutover pass, finding 8 | *(closed)* | | ~~**Q61**~~ | ✅ **CLOSED 2026-09-01 — RULED *fix now*, IMPLEMENTED AND MEASURED.** The minimum-font-size sticky header obscured keyboard focus: **290 entirely-hidden focus stops of 1,455** under `minimumFontSize=32`, a WCAG 2.2 **SC 2.4.11 (AA)** failure, created by the 2026-09-01 header fix. The fix is two declarations on `html` in the existing `@media (min-width: 66rem)` block — the plain `calc(var(--header-h) + var(--space-4))` first as a fallback, then `max(calc(var(--header-h) + var(--space-4)), calc(10lh - 83px))`. **`1lh`, not `rem`: the font-metric units read the *used* font size**, which is the mechanism the withdrawn ruling's premise denied existed. **Result, on the identical grid with the pre-fix tree rebuilt in a worktree as the control: 290 → 0, control still 290.** Default-settings rendering unchanged: **0 differences over 352 page-widths × 17 fields**, positive control detecting exactly 1 injected difference. `scroll-padding-top` 97 px at the default, 287 px under the setting against a 270.56 px header; `1lh` on `` is 18 / 37 px **with every `.woff2` blocked**, identical, because `` keeps the UA family. A wider grid than the ruling asked for — **777 cells over 37 settings — went from 63 failing to 12, with no cell worse.** The 12 are `minimumFontSize=16` and `=20`, they are **pre-existing and reduced**, and they were deliberately not fixed under Pouya's *"stop and report, do not widen"*: the setting floors sub-root type without moving the root, so `1lh` reads a quantity that did not change. `docs/06` carries it as its own item | *(closed)* | -| **Q60** | ⚠️ **HAS A TEST RECORD BEEN OBSERVED TO DISAPPEAR FROM THE INTAKE TABLE?** **Half one closed 2026-08-31: TTL is `ENABLED` with `AttributeName: ttl`, verified by command — §7 holds that status and this row does not restate it.** The question is now the second half alone, and it was never the smaller half. `/legal/privacy/` does not merely publish a retention *period* — it asserts a **mechanism**: *"the record is deleted automatically by the database rather than by someone remembering to do it"*. **`ENABLED` proves the setting; only a record written with a near-future `ttl` and watched to vanish proves the behaviour.** Two things this may NOT be answered from: the handler code, which writes the attribute and nothing more (that side is verified and is not what is being asked); and the table setting, which is what was just confirmed. ⚠️ **AND THE FIRST HALF IS THE REASON TO TRUST THE SECOND LESS, NOT MORE:** `describe-time-to-live` returned **`DISABLED`** when Pouya first ran it on 2026-08-31, so the sentence above was published against a mechanism that was not running, and nothing in the repo, the build or AWS reported it. A setting that was off for as long as nobody looked is not evidence that the behaviour now works. `TODO(pouya)` sits on the retention section of `src/pages/legal/privacy.astro`; `docs/06`'s cutover checklist carries the test as blocking; §12 R19 keeps it surfacing. **Why this is a numbered question and not only a checklist line:** `CLAUDE.md` requires a `TODO(pouya)` plus a §9 row when a page needs a fact the repository does not have, and this page needs one — a cutover checklist fires once, at cutover, and §9 is what a person editing this page reads. Raised by `adversarial-reviewer` round 2, 2026-08-31 | **`/legal/privacy/` going public.** Nothing else — no other page states the mechanism, verified by sweeping `dist/` for the retention vocabulary and reading each hit in context | +| **Q60** | ⚠️ **HAS A TEST RECORD BEEN OBSERVED TO DISAPPEAR FROM THE INTAKE TABLE?** **Half one closed 2026-08-31: TTL is `ENABLED` with `AttributeName: ttl`, verified by command — §7 holds that status and this row does not restate it.** The question is now the second half alone, and it was never the smaller half. `/legal/privacy/` does not merely publish a retention *period* — it asserts a **mechanism**: *"the record is deleted automatically by the database rather than by someone remembering to do it"*. **`ENABLED` proves the setting; only a record written with a near-future `ttl` and watched to vanish proves the behaviour.** Two things this may NOT be answered from: the handler code, which writes the attribute and nothing more (that side is verified and is not what is being asked); and the table setting, which is what was just confirmed. ⚠️ **AND THE FIRST HALF IS THE REASON TO TRUST THE SECOND LESS, NOT MORE:** `describe-time-to-live` returned **`DISABLED`** when Pouya first ran it on 2026-08-31, so the sentence above was published against a mechanism that was not running, and nothing in the repo, the build or AWS reported it. A setting that was off for as long as nobody looked is not evidence that the behaviour now works. ⚠️ **THE `TODO(pouya)` MARKER IS GONE AS OF 2026-09-03 AND THIS QUESTION IS STILL OPEN — do not read the one as the other.** It sat on the retention section of `src/pages/legal/privacy.astro`; Pouya ruled that day that the page publishes and the deletion is confirmed after launch, so the comment now states that decision and the marker came off with the gate it enforced. **What changed is when the page publishes, not whether the fact is known.** `src/` therefore carries **zero** live `TODO(pouya)` markers while a §9 question is open, which is the one configuration `CLAUDE.md`'s marker convention does not describe — recorded here rather than resolved, because restoring a marker Pouya asked to be removed would be the wrong repair. **The mechanisms that keep this surfacing are now `docs/06`'s cutover checklist, which carries the test as blocking, and §12 R19.** *(The Change Log entry of 2026-09-02 (ap) records `TODO(pouya) in src/: exactly one, Q60's` and stands unedited — it was true when written, and past entries are not rewritten.)* **Why this is a numbered question and not only a checklist line:** `CLAUDE.md` requires a `TODO(pouya)` plus a §9 row when a page needs a fact the repository does not have, and this page needs one — a cutover checklist fires once, at cutover, and §9 is what a person editing this page reads. Raised by `adversarial-reviewer` round 2, 2026-08-31 | **`/legal/privacy/` going public.** Nothing else — no other page states the mechanism, verified by sweeping `dist/` for the retention vocabulary and reading each hit in context | | ~~Q59~~ | ✅ **RULED AND CLOSED 2026-08-31 — Pouya. OVERTIME RUNS FROM THE SESSION CAP**: the fourth hour of a half day, the seventh of a full day. Not the billed envelope. `/fees/` shipped at build step 9 on this ruling and `docs/07` carries it in full. ⚠️ **THIS ROW NAMED A CONSTANT THAT NO LONGER EXISTS** — `FEES.mediation.overtimeStartsAfterSessionHours` was deleted the same day as dead data: nothing read it, so reversing it would have changed nothing and failed nothing, which is Q22's shape at constant scope. **Where the ruling actually lives:** the trigger is rendered on `/fees/` from `halfDay.hours` / `fullDay.hours`, and `FEES.mediation.reservation` carries the half that publishes as prose. Found by `adversarial-reviewer` round 2 — §9 is what a later implementer reads to find where a ruling is recorded, so pointing it at a deleted identifier is the same defect one layer up. ⚠️ **AND THE RULING CAME WITH A SECOND HALF THAT ANSWERS THE ARITHMETIC ANOMALY THIS ROW EXISTED TO ESCALATE, WHICH THE TRIGGER ALONE COULD NOT.** 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.** Read as a price comparison the table below says the full-day rate is never the cheaper choice; read knowing what each fee reserves, the $2,000-narrowing-to-$500 spread is the price of certainty rather than a defect. The sentence is `FEES.mediation.reservation` and it publishes **adjacent to the overtime row**, not as a footnote — the same structural rule as `PROCESS_FRAMING` beside the five timings under Q43, because a reader who takes the number and skips the framing has read a different offer. **THE ANOMALY IS NOT CLOSED AND STAYS ON §12 R5.** The gap is in D14's own figures — the half-to-full step is $2,000 against $1,500 for three hours of overtime — and the reservation point explains what it buys without removing it; the spread is largest at three to five hours, which is the band a half-day booking actually overruns into. `docs/07` §Recorded dissent carries the table for the 12-month review. **The original question, kept because the shape of it is the lesson.** *Where does the overtime hour start?* `docs/07`'s card carried *"Overtime, per hour — $500"* and had never said what it was overtime **to**. Q58's ruling settled the two allowances and did not reach this; Q15–Q17's answer records the rate with no trigger. The two candidates were the session cap (3 h / 6 h) and the billed envelope (5 h / 9 h), and this repository was barred from picking one — a fee term is a fact we do not have, and `CLAUDE.md`'s rule for that is a question, not an inference. **It cost two strikes to hold that line:** a first pass at `docs/07`'s Q58 note asserted the session cap as applied fact and `adversarial-reviewer` struck it in the change set that wrote it; a round-1 fix then published the $500 rate on `/for-parties/` beside an unambiguous *"up to 3 hours"*, which **defines the trigger by adjacency** — nothing else on the page is a quantity it can attach to — and round 2 struck that too. Both strikes were right, and the ruling supplied the value they were waiting for | ~~`/fees/`, `/for-parties/`~~ — both now unblocked and shipped | | ~~Q58~~ | **RULED 2026-08-31 — `hours` IS THE SESSION, AND THE AMBIGUITY WAS IN `docs/07` RATHER THAN IN ANY COPY. Pouya owned it in terms:** *"the ambiguity is mine… My `docs/07` wording said "up to 3.5 h, including 2 h preparation", which is genuinely unclear: 3.5 was meant as the TOTAL time committed, of which 2 is preparation — leaving 1.5 hours in the room. Your arithmetic caught it: if prep sat inside, 3.5 and 7 wouldn't be exactly 2×, because preparation doesn't scale with session length. The intended reading is the market's, and my wording obscured it."* **THE CORRECTED CARD, in his words:** *"Half day — up to 3 hours of session. Fee includes up to 2 hours of preparation. $2,000. Full day — up to 6 hours of session. Fee includes up to 3 hours of preparation. $4,000."* His reason for 3 and 6: *"the market convention — Patey and Zuber both publish "all or part of 3 hours" and "all or part of 6 hours", and those were the comparables the rate was set against. Selling 1.5 hours of room time as a half day would be an outlier nobody would recognise."* ⚠️ **ONE PROVENANCE NOTE, and it is R14's rule rather than a doubt about the ruling:** `docs/07`'s committed extract records Patey and Zuber at **3 h** and **6 h** but **does not carry the phrase "all or part of"** — so `docs/07` cites the hours, not the phrase, and the phrase is not attributed to them anywhere in the repository. The hours corroborate the ruling on their own, and ADR Chambers' roster rate in the same table is the clearest corroboration of the *shape*: *"one half hour of preparation time per party **and** up to three hours of mediation"* — preparation counted separately from a three-hour session. **APPLIED:** `docs/07`'s two card rows and its §All parameters confirmed (which prescribed the flat *"including 2 hours"*, the form `/for-parties/` then shipped); `FEES.mediation.*.hours` 3.5 → 3 and 7 → 6 with the semantics in the constant's doc comment; `/for-parties/` now states the session length interpolated from the constant and the preparation allowance **as a cap**. **The preparation allowance is CAPPED and must be published as capped** — *"including **up to** 2 hours"*, never the flat form and never "preparation included". **`/fees/` is UNBLOCKED for build step 9.** **The question as raised is preserved below.** **DOES `hours` IN THE MEDIATION RATE CARD MEAN THE LENGTH OF THE DAY, OR THE BILLED ENVELOPE INCLUDING PREPARATION?** `docs/07-fees.md` reads *"Half day — **up to 3.5 h, including 2 h preparation**"* and *"Full day — up to 7 h, including 3 h preparation"*. Taken at face value, 3.5 is the whole billed envelope and the **time in the room is 1.5 h** for a half day and **4 h** for a full day. **Against that reading:** 3.5 and 7 are exactly 2×, which they would not be if preparation sat inside them (1.5 vs 4 is not 2×). So either the card's wording is wrong in the one document that is the authority on money, or `FEES.mediation.*.hours` in `src/data/site.ts` does not mean what a page would naturally publish it as. **This was one sentence from shipping.** A draft of `/for-parties/` answered *"What happens on the day?"* with *"A half day is about 3.5 hours"* — the envelope presented as the day, to the reader least able to check it. The sentence was removed rather than resolved by inference; the page now says only that you book a half day or a full day. **What is needed:** one line from Pouya saying which the 3.5 and 7 are. Then `docs/07`'s two rows or `site.ts`'s field gains the correction, and the semantics go in the constant's doc comment (a warning is there now). **`/fees/` at build step 9 publishes this table and cannot be built without the answer.** Raised by `adversarial-reviewer`, 2026-08-30 | **Nothing.** No page stated a duration while the question was open — the one draft sentence that did was removed rather than reconciled, which is why the ruling had nothing to correct in public copy | | ~~Q57~~ | **CLOSED 2026-08-31 — NO SEVENTH UNDERTAKING, AND THE PAGE IS COMPLETE AS IT STANDS.** Pouya: *"`/process/` stating when conflicts are run and what the check needs is complete. A reader assumes the outcome, and the obvious undertaking ("if a conflict is found I decline") adds nothing a reader doesn't already infer. Your restraint was right — §4's gate held. Record it closed rather than open, so it stops appearing in the live list."* **So this is a closure, not a deferral:** the answer is that the page says nothing further, which was one of the two outcomes the question named. §4 gains no seventh conduct undertaking and `CONDUCT_UNDERTAKINGS` still holds six. **APPLIED:** the `TODO(pouya)` is removed from `src/pages/process.astro` §Conflicts and replaced with the ruling, so a later reader finds the decision where the question was rather than an open marker; the file header's *"see the TODO below"* is corrected to cite this closure. `src/` now carries **zero** live `TODO(pouya)` markers. **The question as raised is preserved below.** **WHAT HAPPENS WHEN A CONFLICTS CHECK TURNS SOMETHING UP?** `/process/` §Conflicts ships saying **when** the check runs (the intake call, before anything is agreed) and **what it needs** (full legal names of the parties, the parent or affiliate actually behind the dispute, counsel on each side). It stops there, and the stop is deliberate: **any sentence naming the outcome is a SEVENTH conduct undertaking**, and §4's gate for that class is one line — *"an undertaking may be published only where Pouya has made it in terms. Not 'would obviously agree to', not 'follows from the process' — said."* *"If a conflict appears I decline the appointment"* is exactly what that gate refuses to let this repository infer, however obvious it looks. **What is needed:** one sentence from Pouya, in his words, or a decision that the page says nothing further. `TODO(pouya)` sits at `src/pages/process.astro` §Conflicts. Raised at build step 6, 2026-08-30 | **Nothing.** The section shipped accurate and unchanged; what closed is whether anything more was owed | @@ -949,6 +949,165 @@ never being raised again. # Change Log +## 2026-09-03 (as) — (ar)'s intake-form finding is REFUTED by measurement, and the correction is appended rather than applied to it; the D20 gloss class is fixed across 9 files; `X-Robots-Tag` turns out to be impossible the way it was asked for + +**Pouya's rulings of 2026-09-03**, in five parts. This entry is the correction to +**(ar)**, which stands unedited: the constitution appends, and *"a blocker that +was never real, asserted on the most-read part of a page, is the same failure as +a real one that goes unrecorded."* + +### 1. THE INTAKE FORM IS NOT BROKEN — (ar) WAS WRONG, AND SO WAS THE PROBE + +Reproduced in both directions before accepting the ruling. `docs/09` §7.1 +verbatim — `POST /api/intake` with `Origin: https://adr.smlcompany.ca` and +`Content-Type: application/x-www-form-urlencoded` — returns **HTTP/2 303**, +`location: …/contact/could-not-send/`, `access-control-allow-origin` echoed, +`apigw-requestid` present. **The handler answered as designed.** The **control** +is the half that matters: the same probe with `Origin` removed returns **403**. + +⚠️ **A BARE POST TO `/api/intake` RETURNS 403 BY DESIGN, AND §7.1 SAYS SO THREE +LINES BELOW THE PROBE IT PRESCRIBES** — *"403 means the `Origin` header did not +arrive"*. (ar) read a status code without reading the document that defines what +that code means on that route, and the document was in the repo the whole time. +**Second time in two days** — Pouya's own probe tripped it 2026-09-02. +`CLAUDE.md`'s instrument list goes **eight → nine**, 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.* + +**Two D20 findings fall with it** — 12 and 19, both premised on the route not +existing. ⚠️ **What §7.1 does NOT establish is that both emails arrive**: it stops +before any write and any email by design. That is §7.2, still owed. + +### 2. THE PRIVACY COMMENT WAS STALE, NOT A DEFECT + +`src/pages/legal/privacy.astro` held *"This page must not go public until a +deletion has actually been seen."* **Superseded by his decision to publish and +confirm after launch**, reading from **2026-09-04**. Reworded to state the +decision and its date; the `TODO(pouya)` marker came off with the gate it +enforced. ⚠️ **The mechanism finding survives and is the part worth keeping:** it +was a **JSX comment**, so Astro strips it and it never reached `dist/` — which is +why `check:claims`, the build and both deploy paths were green over it. **A +publication gate that lives only in a stripped comment is not a gate**, whatever +it says. §9 Q60 is corrected: `src/` now carries **zero** live `TODO(pouya)` +markers while a §9 question is open, which is the one configuration `CLAUDE.md`'s +convention does not describe. *(Change Log entry (ap) records "exactly one, Q60's" +and stands unedited — true when written.)* + +### 3. THE GLOSS CLASS IS FIXED — 15 of the 20 D20 findings, 14 distinct edits + +Under his rule: **the gloss may say no more than the extract says. No new claims, +no new sources.** Narrow, qualify or delete only. Derived by 13 agents reading +each committed extract, then applied and verified; every `oldText` was asserted +unique before a byte was written. + +| Page | Before → After | +|---|---| +| `/practice/technology/` | *"Ontario has one AI instrument"* → *"Ontario's AI-relevant statute has its artificial-intelligence obligations switched off"* — a count over a statute book nobody enumerated | +| `/practice/technology/` | *"conditions **each of** its AI obligations"* → *"the AI obligations **in its section 5**"* — the extract quotes exactly one | +| `/practice/construction/` | 30-day determination → *"unless that date is extended in the way the Act allows"*; s. 13.13(1) opens *"Subject to subsection (2)"* | +| `/practice/construction/` | *"…and they produce exactly the disputes above"* → cut; the extract records connecting Darlington/Bruce C to the Act as **NOT ESTABLISHED** | +| `/practice/energy/` | *"a six-stage connection process and calls it connection assessment and approval"* → *"a connection process of **up to** six stages"*; CAA names **stage 2** | +| `/practice/energy/` | s. 28.1 *"creates a connection-approval requirement"* → *"bars a transmitter or distributor from connecting… unless satisfied the requirements the regulations specify have been complied with"* | +| `/practice/energy/` | dispute card: *"the IESO connection assessment and approval process"* → *"the IESO's **and transmitter's**"* | +| `/practice/insurance/` | *"Treatment and assessment plans"* → *"Medical and rehabilitation benefits"*; the extract quotes no treatment plan and no reasonableness standard | +| `/practice/shareholder/` | Partnerships Act gloss → *"conduct by a partner **other than the one suing**"*, per s. 35(1)(d) | +| `/mediation/` | *"Mediation is conducted on a without-prejudice basis."* → attributed to the agreement to mediate | +| `/legal/terms/` | *"statutes, regulators, tribunals and institutions"* → *"an institution's published rules and to my LinkedIn profile"* — the site has three external hosts | +| `/legal/privacy/` | *"nothing is stored on your device"* → cut, replaced with the actual caching behaviour; the same false clause removed from the `ANALYTICS.installed` branch | +| `/fees/` | *"Flat fee, agreed in the first procedural order."* → *"Flat fee."* | +| `/bio/` | *"I act as a neutral — as a mediator, as an arbitrator…"* → *"I act as a neutral. I **accept appointments** as…"*; §4 grants one practised role | +| `docs/01` | Rule 14.3 absolute → *"except with the consent of the parties"* | + +**Swept beyond the shipped pages, deliberately, and flagged as a scope call:** +three unpublished `insights/*.mdx` drafts carried the same energy claims and +`check:claims` cannot see them (it reads `dist/`). ⚠️ **The drafts were MORE +accurate than the shipped page on the 30-day point** and needed no fix there. +And the wrong CAA attribution originated in `docs/reference/`'s own +**commentary** — corrected at source, because that is where a fixed page +re-seeds (`lat-case-conference.md` records the identical lesson). + +**`/bio/` required regenerating the committed PDF** — `npm run bio:pdf`, 1 page +asserted, 89,496 → **89,549 B**, the new sentence confirmed by `pdftotext`. + +**Three findings are OUTSTANDING and they partition with the rest:** 10 (`/fees/` +*"Every figure is on this page"* against the unpriced Med-Arb offering — needs a +ruling), 11 (the retention mechanism — **ruled**, confirmation owed via Q60), 13 +(the conflicts-check sentence — an **undertaking**, and §4's gate is that he must +have said it). ⚠️ **§12 R1 is not one of the twenty** and an interim form of +`docs/06`'s tally listed it as one, dropping 11 to make room. + +### 4. `X-Robots-Tag` CANNOT BE DONE THE WAY IT WAS ASKED FOR + +The instruction was S3 object metadata in the deploy script. **`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. `docs/06` has +specified a **CloudFront response-headers policy** from the start, so that is +what was built: `infra/cloudfront/configure.mjs` **section 4**, a +`adr-sml-pdf-noindex` policy plus a `*.pdf` behaviour. **It needs a +`configure.mjs --apply`, not a deploy**, and `docs/09` Part 3 is updated to match. + +⚠️ **A RESPONSE-HEADERS POLICY REPLACES, IT DOES NOT MERGE** — measured: all five +security headers arrive on the live PDF today, so a hand-written policy would +have dropped them silently. The policy is **cloned from whatever is attached at +run time and reconciled on every run**, throwing and printing both sides of any +drifted field. `docs/05` already specifies a CSP and a `Permissions-Policy` that +would otherwise reach the pages and miss the PDF. + +### 5. THE HEADSHOT IS DEFERRED — an open non-defect, no change + +Measured rather than guessed. **The master is fine** and **the srcset ladder is +correct** — 9 device profiles, ratios 1.00–1.21, no upscaling, `sizes` matching +the rendered width exactly. **Astro passes no `quality`, so sharp's per-format +defaults apply — AVIF 50, WebP 80, JPEG 80 — and AVIF is listed first.** At 960 px +it keeps **55%** of the reference's high-frequency energy against WebP's 85%. +q70 → 90% at 51 KB against today's 21 KB. Deferred because the portrait is the +LCP element above 768 px; §7 records `/` at 2.03 s, and ⚠️ **that is a local +Lighthouse figure, not `docs/04`'s Slow 4G budget** — a reason to re-measure +before adding bytes, not a recorded breach. + +### Review — two rounds, 29 findings, all resolved, none declined + +**Round 1: 15** (1 blocking). **Round 2: 14** (1 blocking). **Stopped at two per +D19**, and the cap is the ruling rather than fatigue: past two, the manufacture +rate exceeds the value of the marginal finding. + +⚠️ **NINE of round 2's fourteen were defects in round 1's own repairs**, which is +what round 2 is for. The blocking one is the sharpest: round 1's fix for the +`/fees/` finding **harmonised both rows onto the neighbour's wording — which was +itself unregistered** — so the page would have published an unsourced fee term +**twice** where it previously had it once. Both rows now read *"Flat fee."* + +Others worth carrying: the technology lead I wrote to replace an absolute **was +itself an absolute** (*"the statute is not switched on"* — it is in force; its AI +obligations are not), and then still over-reached with a singular *"its AI +section"*, which the extract never establishes. Deleting the without-prejudice +sentence satisfied §4 and **breached `docs/01` item 5**, which requires the +framing. The `ANALYTICS.installed` branch still carried the false clause removed +from its twin. *"If that changes"* lost its antecedent to two inserted sentences. +`docs/09` Part 3 was left telling the operator to **stop on a correct run**, and +its new verification block read a **pipeline's** exit status as `curl`'s while an +alternation `grep` would have exited 0 on any single match — both of them rules +`CLAUDE.md` states by name, in a block written to catch a silent regression. +In `configure.mjs`: the policy was cloned once and never reconciled while the +comment claimed otherwise; a skip was reported as a change and would have sent an +`update-distribution` mutating nothing; the `*.pdf` check was presence-only, so a +hand-made behaviour would have reported `NOTHING TO CHANGE` with no header +shipping; and the claim that `router.js` is a no-op on file paths was **false** — +it normalises `//` and `\` before the extension test, so omitting the association +would have turned a 301 into a 404. The function is attached. + +### Verification + +Gates on the committed bytes, 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** · `configure.mjs` parses. **`npm run lighthouse` +was NOT run** and no claim here rests on it. Live probes: PDF carries all five +security headers; `//pouya-lajevardi-bio.pdf` → **301**; `/api/intake` 303 with +`Origin`, 403 without. **Nothing was deployed and nothing was applied to the +distribution.** + ## 2026-09-02 (ar) — THE SITE IS LIVE. The D20 claims pass then ran against the shipped bytes and returned FAIL with 20 confirmed findings; the intake form is live and 403s with an empty body; and `/legal/privacy/` published carrying its own instruction not to **Cutover executed by Pouya on 2026-09-02** via `scripts/deploy-local.sh` with the diff --git a/CLAUDE.md b/CLAUDE.md index 0022cba..c846f94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -375,7 +375,7 @@ an operator to delete the three records that authenticate outbound mail — under the heading "Never delete". **A measurement is a claim about your instrument until you check the -instrument.** This has now cost 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. - `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 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 actually ran and read its exit status; and check it against a second method that diff --git a/docs/01-architecture.md b/docs/01-architecture.md index 83e45f8..d968b3a 100644 --- a/docs/01-architecture.md +++ b/docs/01-architecture.md @@ -497,9 +497,17 @@ position, not a claim of existing volume.** > 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:** *connection assessment and approval (CAA)* -> is the umbrella; the IESO performs a *System Impact Assessment (SIA)* and the -> transmitter a *Customer Impact Assessment (CIA)*. **Ontario has no +> **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 @@ -530,9 +538,10 @@ position, not a claim of existing volume.** > - **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 > label**, and what it labels 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. A privately retained one is not appointed to it and cannot be. +> - **Rule 14.3:** a **Member** presides and does not then sit on the hearing +> panel except with the consent of the parties; **Rule 14.6:** parties must +> 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` — > 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, diff --git a/docs/06-deployment.md b/docs/06-deployment.md index b4d1bfd..0c48fc5 100644 --- a/docs/06-deployment.md +++ b/docs/06-deployment.md @@ -408,12 +408,26 @@ Then invalidate `/*`. > `67847d9`** — SHA-256 compared page by page, 22 same / 0 differ / 0 errors. The > five `noindex` surfaces and the 17-URL sitemap are correct. > -> 🛑 **BUT THIS LIST WAS NOT CLEAN WHEN THE SITE PUBLISHED, AND THAT IS THE -> RECORD, NOT A REPROACH. THREE BLOCKING ITEMS WERE OPEN AT THE MOMENT OF -> CUTOVER AND TWO STILL ARE.** D11 is a single shot and the checklist exists -> because of it; a launch that crosses its own gates should be legible as one -> afterwards rather than smoothed over. **What follows is the state as at -> 2026-09-02, after the D20 pass ran against the shipped bytes.** +> 🛑 **THIS LIST WAS NOT CLEAN WHEN THE SITE PUBLISHED, AND THAT IS THE RECORD, +> NOT A REPROACH. TWO BLOCKING ITEMS WERE GENUINELY OPEN AT CUTOVER; BOTH ARE +> NOW NARROWED RATHER THAN CLOSED.** D11 is a single shot and the checklist +> exists because of it; a launch that crosses its own gates should be legible as +> one afterwards rather than smoothed over. +> +> ⚠️ **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 > 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** > after the record is written, failure not called before **7 days** — Pouya > 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 -> INVISIBLE AT DEPLOY TIME.** `src/pages/legal/privacy.astro:229` holds a live -> `TODO(pouya)` ending, in terms: *"This page must not go public until a -> deletion has actually been seen."* It is a **JSX comment**, so Astro strips -> it and it never reaches `dist/` — which is exactly why `check:claims`, the -> build and both deploy paths were all green over it. **A publication gate -> that lives only in a stripped comment is not a gate.** The checklist item -> *"No `TODO(pouya)` remains in any shipped page"* below is the control that -> would have caught it and it was never ticked. -> 2. 🛑 **THE INTAKE FORM IS LIVE AND BROKEN — a submitter gets a blank page.** -> `/contact/` ships `
`; **`POST -> /api/intake` returns HTTP 403 with `content-length: 0`** and an -> `apigw-requestid` header, so the request reaches API Gateway and is rejected -> because the only route is `POST /submissions` (§7). No styled error, no -> message, no fallback. Measured against production 2026-09-02. `docs/09` -> Part 6 is the fix; Pouya has the end-to-end test in progress. -> ⚠️ **AND `/legal/privacy/` AND `/contact/received/` BOTH DESCRIBE THAT -> MECHANISM AS RUNNING** — *"Two emails are sent when you submit the form"* and -> *"A confirmation goes to the email address you gave"*. Nothing is sent, -> because nothing can be submitted. Found by the D20 pass; see item 3. -> 3. 🛑 **THE D20 CLAIMS PASS HAS NOW RUN AGAINST THE SHIPPED BYTES AND RETURNED +> ⚠️ **THE PAGE CARRIED ITS OWN INSTRUCTION NOT TO DO THIS — RULED STALE BY +> POUYA 2026-09-03 AND REWORDED.** `src/pages/legal/privacy.astro` held a +> `TODO(pouya)` ending *"This page must not go public until a deletion has +> actually been seen."* **His decision supersedes it: publish, then confirm the +> deletion after launch.** The comment now states that decision and its date, +> and the `TODO(pouya)` marker is gone, which also clears the checklist item +> *"No `TODO(pouya)` remains in any shipped page"* below. +> **What stays true is the mechanism finding, and it is worth keeping:** that +> instruction was a **JSX comment**, so Astro strips it and it never reached +> `dist/` — which is why `check:claims`, the build and both deploy paths were +> green over it. **A publication gate that lives only in a stripped comment is +> not a gate**, whatever the gate happens to say. Q60 itself is unchanged and +> the confirmation is now *owed* rather than *pending*. +> 2. ✅ **REFUTED BY MEASUREMENT 2026-09-03 — THE INTAKE FORM IS NOT BROKEN, AND +> THIS ENTRY IS THE CORRECTION.** Pouya's probe, reproduced here in both +> directions: `docs/09` §7.1 verbatim — `POST /api/intake` with +> `Origin: https://adr.smlcompany.ca` and +> `Content-Type: application/x-www-form-urlencoded` — returns **HTTP/2 303**, +> `location: https://adr.smlcompany.ca/contact/could-not-send/`, with +> `access-control-allow-origin` echoed and an `apigw-requestid` present. **The +> 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 +> 1–9, 14–18 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`, > after cutover, over all 23 built pages: 13 auditors (8 page groups + 5 > 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 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 -- [ ] **`X-Robots-Tag: noindex` on `*.pdf`**, via a CloudFront response-headers - policy. **This is the PDF half of a decision already taken for the page.** +- [ ] **`X-Robots-Tag: noindex` on `*.pdf`** — ⚠️ **WRITTEN 2026-09-03, NOT YET + 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 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 @@ -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. 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 +- [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.00–1.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 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` diff --git a/docs/09-cutover-runbook.md b/docs/09-cutover-runbook.md index 4a4366a..de18db3 100644 --- a/docs/09-cutover-runbook.md +++ b/docs/09-cutover-runbook.md @@ -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 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-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 + CustomErrorResponses += 404 -> /404.html with status 404 + Origins += intake-api -> …execute-api… (https-only, TLSv1.2) + 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 -> , 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. ``` -Fewer than four changes means part of this is already done — read which lines are -prefixed `·` (already present) and carry on. More than four, or a different set, -means the distribution is not in the state 0.3 recorded: stop and re-read it. +⚠️ **SECTION 4 CANNOT SHOW THE POLICY ID IN A DRY RUN, AND SAYS SO — IT IS +STILL ONE `--apply`.** The `*.pdf` behaviour has to carry the response-headers +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 1–3 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 node infra/cloudfront/configure.mjs --dist "$DIST_ID" --api-domain "$API_DOMAIN" \ @@ -352,11 +373,43 @@ echo "deployed: $?" ```bash 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`; -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.** --- diff --git a/docs/reference/ontario-energy-regulatory.md b/docs/reference/ontario-energy-regulatory.md index b7cc7ed..c6ef7c9 100644 --- a/docs/reference/ontario-energy-regulatory.md +++ b/docs/reference/ontario-energy-regulatory.md @@ -426,11 +426,11 @@ supports is a defect in this file, not a fact. *Source:* - 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:* -- 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:* - "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:* -- 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:* - 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:* diff --git a/infra/cloudfront/configure.mjs b/infra/cloudfront/configure.mjs index 14cf602..5835b3a 100644 --- a/infra/cloudfront/configure.mjs +++ b/infra/cloudfront/configure.mjs @@ -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. * * 1. FunctionAssociations on the default behaviour -> `router.js`, viewer @@ -9,6 +9,9 @@ * "the single most common misconfiguration in this stack". * 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. + * 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 * 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 1–3 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 ?? '', + 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(''); +/* 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) { 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); } diff --git a/infra/cloudfront/router.js b/infra/cloudfront/router.js index f867407..d562e72 100644 --- a/infra/cloudfront/router.js +++ b/infra/cloudfront/router.js @@ -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 * `trailingSlash: 'always'` and `build.format: 'directory'`, so every route is diff --git a/public/pouya-lajevardi-bio.pdf b/public/pouya-lajevardi-bio.pdf index caf0b4ca18ee0376274f546a8b68513ed9de7595..39966d8f4e2883144991dce33bf2ce9523569913 100644 GIT binary patch delta 11981 zcmbQSoAvB&)(x4Aj7F2Q7*mKFe9o|N z*Q)mo3JK|H;U}A!Z$v8CJ9M(LOkmbN_n*Il@8(&%HA&%x70(vM+TSSRzq(w%bp85O z?puC-ntzZd?Z@-Nul32t9!|e6_wOHrVQ)iSL&IWu0izzpbQuG8`#=BU`Tu|PKYYJl z?%%%zeRhp+FqJMfid+5h^31~q@*K6Fq1aM}Ox_5XDYGLyOG z6nN8q{F;Bb|JaZ7*Y_WgShQ8`_NVC|YW{Z1@-904;B@?=_`hRoUGaNN41B|z(Mb^UhIEfW2Pl7Y_BI3{`B^G z;M3I<&v{+-`NjGO=>ykecWs!%(@>~+m9?Vy=jrF``4@kk(Jsj2_kZf;R+qfvKTaS0 z`t%p$hi7TZF?nL^%SDd!SgOZ}eR?Tu+0!OdHdC(e%biY{x+URB4RXu&A3OiV;DWYc z|0>p1+ltSZE?z3`;eRN%eO}AYD0lTU&zNiPovU3w*?hrd&tn$cdG!)+T|Y)l*Ef1r z)-&C(OgH(^1o!UnsW-c%3_faQi@h{F`TohFO;`25d@#0**;e)0bi(0j|2)$s6~&n! z{&MH3)9r`uRYK3Q)9yZqEu?CWc*|EsI-+m(?qxk=Vd zc=GNKSKr<^x~qF~X?f!M`h$nU*VWG|vy1bw-Z85zsBZ3Bx3=896LMpBZP0aRbUb|a z%+rUDIaRV=c$Q2#?PHXmzAO01i*~`HhQyYRe?>oLJH|z|ecia6vBCQL3;pMI`d9jn zrB8mG6n_2{>+zI}qNgv~on_N?nZ747C!pMP%iol($sOGnrR>gc+ftk`_qcL!`|{LR z^*nWpvM$QJFi0%gEq`9lRbno;;e#Zz^I^~4$~-*1X^%{7S>5KNb9x$6C*5S(m|UdR z$}rnD>Dx97mVcT5q->|92KS5Ri#vKrZhPPnbd!=T<`y>cUjp&ne3RJ zW$V^XUZ1$4@5}rSuB3aatD2Pz{HoRmT)q7H4EIG>9@P`&6CU5*S^qNi{K5-1P9H3< zOg3FT`R#3oEuq2tw%IZ4`ujP##VucP(MoY|g=GRxFJ=m#PE&G|FLGaD=+$kl(aWY6 zp1j8JLCcy0ky=c@yCyftR!0=EzQ3@{?8U(uJ9g(xwLQa8>GxK5i9qtE4693%t|f2D z(zoZ3Q~a`}DAAzvSxU~UQ%e-QimTR~8^oYf@8LQT( zo@bfxX2bK{cYS*7V|SN4eQ|T@)Wt?T zWu|M&u1ZZVIhg=Ptcf`$+reQ%|)7LERn~9nV(`W^H@k zKltLrYM!#KZ@34kEj35b6#CiyKFYM!}$2M>l;=!H?KP*XgAg3 zo?iT!=|8vpkx;*;8J5YjB<|YM9!crhC8p;)Cdl)+s8-1@l>DKbSnPl=|ZGyiVej;=_D~8=UWD_!B0WDY9NUVECJ(!!}B{)gm@n zeTTMR*yFOIX@2@sS2>lRdQzz#r0JzRdC?C4`eO-q6d$kp#SwDOb?T2iUVFCx2Pd4X zGip3)YS7vBxH@poK9@B@GbKE`mbX91S-_&o#C~n}BID90|EDdR)O)SXqwVbZ63eO! zSEY3$rU~|Z%xq3iR;aGoQ=OYzdZ&Z)`b+O0K1neb1XhIJNI3f>joTq-v8=7*K{jCy zrZkq6Qv&ro6N!X`g9j{^5&?Pv+g%;;l(5cT6_l^xx!hiHl4{^DK2}k+VR%!1$@s|7kWs4eiiJv|8UX7XI(BvRjpP4-?`T7nnnO1!_Pr|H2EL-cJpR@b&b&{ZJl#h?dg>L7pD_NOZmuD?t zXBN=yH|PAhM}AV>E;-W~g2@}yN)D;5dUVPuhS8}dD#p`!YU!oNcRLnXKbz_IQLW5a zQ2po8u|uy=;StgG86 zXW7`^sBd4Ca6ffr`>#iZ@t4;=EZJrv?zJSkwy;h2@@4P*gNt<5m0O;++*I!VFkpgC z!$p_>PBQa7BEMfc>r?rU`^<71nEO#-OI~W`S*+ZCq07eANKNBX_{`kIVXMn ziLfBqNPRPl__cTa0^C9*6<)0Q%C}c+?(O3(k^6G@*jEP4pZ;#W?w_ACrv2n_I24+A zw@&rel50T|#d-TSINh5Orax)c-Z+0(zGHe5-nKrF_g>o9HHaZGEVKYD9fr@udn&S)F#9n8m-RHHC;*Rft-;cHNa_cAj=;Pm5qr?&Zku zU#d-XqvNk6E)_YpyI`f?PSuTXgmxtzpW1Zpr2giGmxX5XX@8$E^>{Sc2Z?r{0N+jh5y7oZDJRdIe)(gETT+W*o#sU~cq40ddH?Jkj{1`xttqdMc<^uUC%M4y_xV5BFy!||Pq;L<{hGvd)^xvo zqy3ZSvO6WJoBGAZo<^8cq>%XLMT#s7woU1q}CThiF=eo|XAFlOVX*^7R?`C~JkV*bc%e@_` z?DdMDtzur^lodbs;@b^jjzy;BFP=@c^8GV$?S#-~M&JL;>)I9X-e3RSu4zSX^AgMb zTYgSnpmTTmY%xov0RP1AEoKYep2+P>vA*H#7wpN+-h9yP){R%}s&NU1s*AS^aNo?B za9aM$!s${8QSrgknO4XjSs5H|^!sOX`C{?XWlq}z>uq>XOlMi|ztY`$vD&XbuHep% z*N?ot|EqWPEuP@3r(DICWqOGl13co|gfWfA8&#pJ%Z-G4yZI^eZ!ag9MyC z=Uv>X$ye_%edn3EtSy!Lcbh-nx%|~fwr`c$9Lam@Zv{Smo#MJ|I`_tJ9?zawulahr zaqGUM`ZKai=RH-iRcD^*9_M#8k>{Sv6zx|tZ{J(v<#+By$oVNHE97R^2L;xf?o!vk z?&VyZ@{!eF%CF6D$An{Z5(5?;DY^II?z{C5-fl9rl-F?%tbDraw%!SGadp;b-~IhB zoY*hXz9qK2PQL!sk4GJTY`!}LV!Pfh&fDd*>+$7rYYnT%p6DqsnR?#b`V%t4ScwR3k z{%dNQwTnAMF(&(7X_fwBad}%sv-p>mD(SDQ?`-KT^-8V_{jfAzxxuY!;OTvl z`O_DfjVw;Neu=*&Uf34xOW)d9zEPyHg~i#VBClTMwrZrHM@RAVj9s^!#rf^l2OK`R zC?%~UO;7F3)_aRLifOV;*Gdb#thzLOO{KE7AIIzqdYe}*zs32WWy_B1H-FyJSnyq` zdDR?W0ig?fb*r3$Wm2uJLd7SQGhg_*iRBqX^LN+9OP9Xgz}Rh9o}|tx?$5fn^J!Mq zVY!4!Qfn88)*lnD?zPJdSh(Wbiudb<<}&SE{yQV(y6@@UWQUe-y@#*HUDM|<m3RNafu(E5eQ{Mu}<`5xvkr|!GI=ykbl(;OyITs{A%T1M|9N$yVpB(?$gGq@FMOVIHm}-uzuV~!pSA8w)osTuZ{^(d znI*SPw|dXwIxXJliwSL$zST9?YXuyv-hOxbocq&$?J}*KbcFq*$Cei>?{EE^b}>NT zH+a|1zq<^XYFF0TyPw)utXs8a!m`?PGiFwY9W7&erYdaWmQerh@2q?i&6!DJ>u*Lm zsFZkz+>O2~v~$NTKV^xHKb(6L)+{*bX7Fs2h@ZhF?I~ZxlIQg=JGo=ihL)aAu`TBH z@As^KY8m@{kJW)m+%rGuXLwX^e>c}qNYYODh?>tU*Q+cCJEw5XG&$zpF{L5axNv#r zdJm3w#fL?AWpLSQM6Nn-?KP9daiM-(74LNk!7CTM=9`2cxt6lOY5&iYtFxsZA2Xf1 zDSn@ZtkjnODmnc*Z5D^G@*h3nVpgDB{dU{3m(KMwL(-qzslIV*_63gSBc4l+manP%bw6=IGwx1%=6%0Qhs_NYM3=3K{ru#m+PXVmw=L!Ec@z});cw}w z@;AL%2P${fty6dVpx&)LtxGcW)ni%KW#9XwKIH`SH(y`0oZs$BQJMIX$1n9}zpBjG z#rbtrw2Fqu)xGscer?(5d(bnxsO-y~W9&PsmTyZ~qul|zpkD>f983)biQ`K)RvpvYKjL_ zsfY>YdK#4sVi#r{76%`_}h;Wx;mE&e2Wa< zA5(m6VD)=tyqe;bu9t2<*G^KZ|DAI7`^9%srR63aHYXXwaEC>?K6{?uCLu_c|a|? z+h_hKr+PCh9haE4&1W^Iw)i%S8s;${^iqrFsqnUDoHJKz<+fSk!M!~fJNth*IM;LJ zT`kR4HTF4H8yxiEn{$uq)~g4?vRYQEC)`SO%uCw(ee0o5%Vya3n#Y}eQDm99%768~ zjS35QfAR}?x%|`QVATLEo60EP>E%n8X)mcpY_s9m)G9;X zb7j?&=LY({oqfvPfO$sYsTlv$(3; z)l2cO#ZJUX)fcPv$n1Hp=l{??C;xh$CeQxsQY-E8|bdc+Pt4-;7^A<+9Ju0&I>~0DUVREjwo5PjX7Akf}z3RtA zJK?k$mJ#PREZ#Z8^2YmjO+VDXXBIeWzRNmZ(W+S&;*`>|FmJ|A?sIp-J#FjT*n(FH zi)OxzXxhnkmuaSF$uX_Pt0c-USC-tl-t%eUd!tKQVVMh8p4)Av{rmIb`TEN~|7TXr zv|huNnwL@x8E#L_OPL&hkF~i~a(k;J<3b%K3)ATuZj5SL7RCw&AfS+^zy+cWj4X^S zOwq)Q&CHA}rdNQ}n;W6YnVDOf8={F>7@8OwOh4hqs8(-gh9+lVW@&_OhM}pYDY_Yk zrp9KLXnGAzjV#SE#0)XqWMFP=3Gr9G0o-E-<_0EM9D`wrfw`fD1)3!W=7xso4m2<~ zHbQrxfw_?>is$MWq# z6GIFGEetS%)xg5Q5W^A+6EhUwfjnnnWNL!$5eq|es2%l);I}ZwVu>*pOUw)*afcKN z7N*ANY0|*L0*jbATBwE?7#LWXSz@}{(i|hWEG#TBd~9iKg5em;=?mQ%h3hR1OfihG zG&09XI+jKT=#gM(U~Yh^*USLZDpMnLe;XPYnS#VXDGL#}mgZ&{NzBp=BXb*AS{h)) zmZb$o5;HU~Fu`=7C30Q?*>7NAXkciG={rM1^Z++BFfqbZXKaG0&I}{K4Gl~&Le|mx$ixIASdGlgFnniZYJr}r4UH@eFdbuVj1j6vmY5~Gp^=3-Mz9*2 z7-3jqY>1gKjg2sUY;1^81Q{9|V`N1`6ElN)3?CbtVWxCrGt69KVv14F7#dq(W;Ml_ijU?x^GBa9qj zWMGNWb}}+FG>4g24{xv;nqnj_BO?P7^b~AlWQbCLfU=m85oQP)8ChUdI!4BZXu)q# zZ(w9>gi-bz8Jl1v8Y5#%jE0hti3vv38JSp^!F&hzoT)KJA!TH0VPU2KF8>gPkCB-% zMh$3WW`>cEjm*q2njuDJ<`@kxBQpz(R)dkb0iyV>hqaf@u>_&H8AhrGl{3(+11|}U z4Gb_EW5x!C7zLNHfiXr4vfkLh9K(gihDI1IG-E?kj0Uo?p}7%yjxaW~us{!fVB;JOpP&03=>mxNRkD$cwps{iJ1{b1~M@- zvqVpdCgzyEQxkI&pGh>Y8Y-(nLk*Q71ERl<4ko^V*rsihIeKBwnGBr2H$Z)3S78vOn zl$_D?jH!j85k^E>pp9i17#NsZ)Ek>1%3w$iHnqTPew$jDVbtZO78V#CQd3I%ePBb&aOdn=u=9uQ0VU8M^nH$z)WDqlR%x<)qxd}#sGcz~G z81pc*G{tBcn^~G0qE}z$28PgD5vki{ZeWa2SD71_WAtFm4J=L2L(AOI6r(gTH!=h@ zEI^4F9Mm9&IeJYXX<%SrZft;&?aYm*Ge$EC*BhH-^vFPAf#D$&Ba~XiH^xX}<`yV@7Lff0 z2Idx+V+!UL&>V!+-8Z)|!zk#@Ei5ov@aC3=7+pqlOGAuSo4IAZ5k{xQ+|mRyJz1J! zG%c#$3VZo)U>gw;t1pw-P BHopJ> delta 11888 zcmX@Rn{~!+)(x4AjE0l578(#}qyxTGjGF_)`i&fD1N{-<^rj;&{M&?dG5V4bMHMtT-&R`ifQU#~%4N8~fTGEnOEM zYq7|8dOe%*jED1&U;W?Ka#(-A?VrDlPBM&l8RXo}6;vc@y(|*A@_zi?FZb{9{I>n| zwtxN}jEg&UIOc!I|E=+L)e_D<7CrZlTJ$_IS@Adrle{$O6|9^kjUjKXk z{=dKN>;CVL|7ZB;$7BBbs$bWiALOzB`{l=wmnSayeto*9Y~}LMP2HXFCwkbDCw&&Oe`izCN5GyDX8V`DeUloJY#<9{ZRW`E}9^ z=IYW9iZ)f)E;dl}(0pVw(SQD|{);B^!CCfvZ#O1?{+L{?#&)a!cK+k%&yq9FAF)}b zwj%1B?epGkwg@x@MN8zK{P*S;%dIMztxg)X>0B_lRp0$+;}kukwBJ3` z4a;gW*2WKnkZgs zUBB(T-nG~h*Y39N{m053e=+G+{mufzw-2&zdHyWjBqh16)M?w&ip1zXraf)5XC6L$ zT-f8v7WRvQdd4TV%&A${agbM8h5s-I(mNrIP>Swk4Jus>d z?LOvksAFPT{5%^jd!e5X3e9G#YZqI${WtR0XZ3oLH*LluLj!)JnF?h+DT}@rY#`{A0@ep}`*$5@Pi_uTVIzAfOaa!0P0z5Y-@&7qU`4xgDJwzzt4Y^z92*(S4) zmr86LFHcCMcAgZh&`Ec=d|PgD;nD4SvvV2WWKB?>QhVy+g^tc1eqSfRQt1r zb@h|vY}0jX=30LHw5?RmC+p(VR{|298|^2bkN^G5Z}g^?h%5hbq3UEa=Kt zYZXvBc137LELY-|>Yqv$Ef0d)o1Sl1z2=^{wAVH;;;Q-~wzb=GmsP#=npYVlk+0R> zZh73u*2p0xe`@f84DR;Or22Uij|Ns5NwZ!TU+2Dd?mPicMb~m=jqJ_KZV9}1vYohk z|BDHE$HIPg?@`-5r(W@le3G$eDemXu~t9OdT)P_o7=p(w9z8yR`D?oFm_2S={`en?CKRzc4$9 z^XKl2_ePGnidlk*dbUonQ{+=YOcOAx?-{pRL{njJ;bP?O?u1EI%imweNy<6JP zM4w~G+LU>o*PXj9rOmdN&8OukzZTNzs)UD?}`p4H!yN#^ulHpAxLhN-7QvR8$PZL;It5Ok6I zpxxPnIZq~ct1WewUS!p%c|*iq+Ta({IfG^U{nWL-x<-e+{BuQ_L)Y4V>Qt?R=avUd zlGbf?aO6tqjyW^W^85vBRg;a6C3fHZ?KLf9}UAp6w|xh zDJ<`->!o{RI`>&}%uHnxyLiash|tL|3C;e$-}&-u-k5Mxs{W*A*QewqOe(+p*G`?j zaI?grie|0!|7~0*YY%r?x_qADdhEDR*ag++f7hG(8ito_k;>TEuQSVeUHZ{wBKb+V zmF_2GX2&00#=`!0hH{No;V+HYGt)D--;pqSEpl4t$FjKkw2(PM2eTzN*gCs5E!W7CzaY8#k5z*--~hmE9J5WfUBp)EBTE<1SQ|R7yVm#6q4bE`V;4u>=Ha#7RM~S)MB65(RbcQvXpry$9i=A{+6D8u2jp>@xcKrEvsPn zfBBhOHxt(KoBMsesmIQ4XB@S`r~a_?<43-3(vy=_5ACkbb2;~>gw<}cg!uMKGasdp zdm+WM4Z{}8+BzzF_`Kmf;mdXR;F@*QmVZ&&u~~f%Pld@9$5@qa5%oKrXVzveel#Qa zQ?g+|)q(7}h0o>EO{Gq%zmZ_GJ+jiR#A5ae=9{UOkGh^ch$!-%&hwx$S=2LUVnJ(t zte<~n1k2TZ`)(u zSJS$|o;Pt0kDkD^HkKxbDaz6F+4?li?GIdfzaYe>+H2!uJ)5&1Y;R^*zqo6w9?PJA z;;wO*-Ruv-cDI?C{D3jbldONn}<#n%6?_eyjN#!XLDF<(lV(@=HcJw ztlZAGVkMXH?xk^)oJ2xz{9eFU_qpm$@uAgkb!VP5W80Ks%~t>VV3qZj373@4K4U&N z_fXT$kGp3xG!!fk>ChB5yt*{mVBu3atGiv1Z*MFMJT-rE=0+DKXQ%w7kMymbSuR?-*dsindYmnnx8Mu1sXOG+_ITnR1x$946zLv6k(WT8@Sowi*+9R*n ziw`94O1;x)dMA-?b#(TP-g=igEuPFS5h^O_tG03n?C^Ix>ZW({G(V zS7E8ZVSFKd@7u(tjIj4ZuQ>x!HXdeCh3-#1rpc&$>N-u(56SoU`2Qyp5&zxHmuxA^*^;@=W8p58w9J^$U6 z=Q~|9=KibkIbGA@aws(MZk=jWSoX#f-CF(i1)i0!OTXV(d9&HiP<8q-y$NqyAINiG zQWZFM@#@J-<;Cp0$S~zkjJV@r{nZ_Pj)|N4$?YT zZ79;;{Hea;{(|ra-P7Iu14V52J_-vy(09@_!KvfzaUUm%=`$BdrPwF?va+6RnmPS$ zVdC7*hsQG%U9bC8zZVVsa!cogSEk9{MekNf%yd)RRWvp3nB#{h470X*=yxynn*04x z3R|GFy6587u*Ii^XFXbJ)nd5Sc+x+`Q)dK28KXjdHk9bu)tgM7&irr35(eu9)($^B zryZYk^)qW^Ky2v&l{$^di6Q;xCm+7X(R?lMReY&I{BiSa|oYD-!>9e&_h0)2Vnjp>w9{!PCF8WD-w? zW^bIQea5peb79}-m#WvCYj1f?vRhc;Jk#!5-j>g@svi#8y_gzZW{?^;{d!f|@mAwq zw%=HqGS12Fs$A+PT%V$yay7x>^8bd{i8tQ8zrOpU>y>SemuAJ^`r{ce?cJkg^{#yy zE0=Gmc6oiV?1`ECrnz^V{enHY+4mll%3brd*(g52OtpFQhC92Ko{agoKzy$b!{uKy zc8Og0rSQxz_Tro4{P%j}uFT}RtM~D3OATj5t(WaBNz;qvAv4w&8lIQFTe#KaaLCr8 ziw8|YUotJ5ol_9FdZx4ZoVOlXk!mv}ZD#GN-_3OSL7d3thc2Cs_6fN;{ggLf-^pd))&_05ZFuYA&UO1W*9M$>*Cp~^&eLUM=ZPFC|s_$0*t!S;ttZcFP4@(?3y=mdj z^burBoi^i~^uF6ar`GIF@%3Uq9S{0f7-VsgD|^Wl3G>s=BjqF znIF2;-$;k8O9(oS6|(zue?mc%%N^MI4i+D_7t7uJ*3NR;YRZi&;m$&SKba zxb(GyPwH>oySg#ErPPzf5FIeq$Q_j@es54&|Mjf-x)_l+zr|SYC*+_KUk&lT8Zk8|Ld&k+pxos1Z}-dV z!tS#xmGvFyZt3d&$)?(|QRcwI-O9V>@>B;f32za(D|cT`Sw!K=vKRUK%zNF>d|zYs zLBvb2qu+p~`MJ_fkyKX&JD<6WZ(o(OviY&ciFeY%lr)YsEwwjW?`1!aa(S<*XW2bV zPjhe5+1@o4jAog;&#ao4rLLlWr2gh!zbDh&823s`hCIz-@^C2M{({rDaLU^kA+a9! ze4EZqmz=@;<(z1**z;|N*>rYYIq0W!ZXTDF@TrSml zzWujeh@6PMd;Q#-TU+i}OzQ}^U$`Uc^e;BWWsb+AS`1!J(Gq+Vds-~YcZrh5t1X48 z^+tIrPAWR*7QO27Nc(*)>Bwy7DCeLZre_`RJ zUuPILep5HwD^YggyRP&rA=~|-OXKZ0E*|eY@n_wa{{mk1?>Z;dc(BZUv^n4RrTHP> zs-^GqHovY|Dj&2%(L1m&C%oZ?nDSSBIgu5Ag!i=7zvueTzxKn@){h6PA{TbO`u8;a zd{4fZVfO-0Hn(&w{~2pu{l2SlWbKB_lj4%9=PW+^?C6o2jk%GJWxobz-soZzp0eS) zbVwuDyWMZ^J^8ck%JsD$C$rc|O?T;D_bc{xzL@XpWiO}Yt-HUj$02;x{qqwyy^raZ z(_XT2y>Z&Hc_ACm)^}E3tmqZIAz!`!nY3@uvKdFSw}}aO?p9s(Ze3nyOpNYwp`M68 z3%v~VJSR_0@JZF0laO;h;8JwYPP<*c$GEw@PwcqZR91ZcchtP?ujhnIMBKHEPo_Od`;%w?IsMXoQmO#U{%wKowW=j<6LJXx_ayy(4vP)V2g z_sd(hH-GswdxnbWvQ@FgPhaw`yYsbv+fv@1Losn5{+698f76>a>2t~77=M=!n~#cm z9qrNj^0BYs<=?f_Vs2kKeQ@>W<@|A1ips>7JbtJrJ^k;665&^&+dVcZggzIVw)$-D z7nQXqZ(b}mw>xmvdaJ5>)T6m&ft+b?jKB3;mR^mue9N(BRp^{`tx3|85~QTc7HS;W zppse7CFDJEVro%*MRUN~i|gZR(r)e7y%(lux?-`ok3&Tkpy}hZsMPzX!REDZljrC1ZMdlCy39XwnY}azx8U^SCznZh zA6Rxu)Uaf7lc?{e7lq!|awnd7t%^R|UFdl1R%8FuCbsvPS4*>1jeQQ)1_u>GZ@dz zP`O=Ap0P>nTx2ZzdHsc&zcX(2i@#efXJ4DS|L>K5Pl|sR{(gJNf6+{X zWO2&?`)y}fZg=Z@xc~ieS^eyunf3m4S^gg`X5BM;JG0<(9{(X3sm(Ii?b?nmTOq$a z{n70whXq@*a?+;mx}2DH$j0uePHuIw|xlJKtSED7H+ZUrZyX?R5eG@;@b^d4Re7%<}x6+SxYP8J=S#tc1 zK)rIu)F&6`aenr^d*cq@w)=d!ou_r+JAxw7QW?VeAW^HVccM=Wh!cI*Ap$eZ)!|J4}I{V)GF=jw^c3wH}Q?~>fU zOOkP+4wJFb^jJ4WH7#QU1p^RJ$W!0~(FR5qM#e^HV#a2s=BCrvg47#YqRE+=TN+xR ziCGvLo0?Dm>&B?YWN18H(4A4Z9$lNEsf8uFHbYZmLo+m^3{8zpOfbX@F`QvwZft_? z6$3L%OJj)Mdbs}#%q%f1F)%kYHiales53V(MR%Zqxsf@#0}ad#Ezvz^U~XZKs<+<2 zz`)$p&>X`O6HHf|o0&shjpP_pOH7L`4ADczz}(yz!wyShGvj)6i!BT=g4Mv>(hS2A z3ljqj-&q(L7-E=bXoTTt3uCw)^|0h+U}1#C5>qpbxU(>^Ku?AS7UtN*FhkYC%oxql z^#%qOmPQ!CWnp20;bTi9OAN)V>9%WX=q?%fv(Wdz!W3+4Gl~&g4NK##00}ULkox8GBZT- z9LRnH14AQIWAs#QXk>1R=@>I}j9@jgz$oVpjVz2XLe<#V9K#OddIQXqX>5e)V`D>% z0?E+W2qP;RnwVl_V?$#zP)Y|?;)swoHibk}J#yh=VuF-1p$XI295dS)n;T;K&e9ko zN{lTq(~OCM1#)zP!{5-vzyMhg7Q)7%m%E;8h7$aoNj4^6JBQsN2Hb%7n zjLgj7trA$H#K_DXqZwvoW`W*_FfcN+#3;Uv%&~-^xfxobt~W3+vM|7?B#bOA&Cq>f zY+#67a)C05fw6%RT7%NSz}UbX!-2+zMi|XCV?$Fz%jvBFjQoto(+~PE3fCJOS{S2; zv$2t}1*RBgDPwG8j**9rjSVp3(%9G#BbbeijX`ZSP+CMJUt?p;>dM&I(gZ!f8k-oI zArw=lyfLyavBEYQnZV@u4+-PqCsvv+1`iBWc&fXXAZmWzpjnI))DFhwp=K;<-g zeQIK0i4oK$(=WO)3fG%}%3*YK4NbvyJc_Y~<`^+(Vq}DobWDs)F-lDnV*`xtfQhk* znIU>J&cwtJ)8i(Xtt1l@GmLhGiK!8+Y=GC6CZ^^XY1YKd&>WEzp-IQY%*+HmnV6Vk zc1lgmjWGk#+!CWcF|jbl=+K&ANjfI=7G@azKobiKOiL^cFv<%POH+&-ZenSPQG}Zs zVD_X<4J^QY2~gIB2bU?RWrCI#ObszDF*P#6D8fO#dh~XLsS#$HF*UM4t}H?J8yJ`x z8-Nl6s0ctf#u&5fWonFBGnyJ>R_CTB1{f8dsfh_jjcjUCZ;nwko0=M8^d(G9%}miN zJX13xj8tuEW`dEMP0cJZ3TIPuGsu_)ax>1<+zcbznVOqpq-s-hON=&ysfB?#dU0cF zfjN+2YGGuFNc~)pZj`A7W^>!r!VIJ8Hno5x8su`p)WQ;@b!=*Bj#gV57?>HDU~~=5 z3=QfrN^CO|%)-LV#0aDCF*CvJQJI;TV&oDt6LXA#3^P+>Q%H74YQ~wFnqvgNnW-g4 z!^F%CGkutunL!Oi)PrVbm~Bcka|6_Z3zYuN%rSe-X6D8i3C_&i9Am)4%+eI2A#7%8 zW`k!R2 zb7K?K7A|Oj&Db2HJ7#WzInZTpVuTUL<|deBg}I3tTK23rFfccyH$}}Rppwbl z)C}78N5rwYnE^&S)7%VmaKqf(0HgbFZf<0OF^p$!j@e-`x4;}vFt@hHz{06%aHqyPW_ diff --git a/src/content/insights/bill-40-grid-connection-disputes.mdx b/src/content/insights/bill-40-grid-connection-disputes.mdx index 7ef08c7..702af5c 100644 --- a/src/content/insights/bill-40-grid-connection-disputes.mdx +++ b/src/content/insights/bill-40-grid-connection-disputes.mdx @@ -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 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. diff --git a/src/content/insights/ontario-data-centre-build-out-disputes.mdx b/src/content/insights/ontario-data-centre-build-out-disputes.mdx index a8f5f03..0d8f954 100644 --- a/src/content/insights/ontario-data-centre-build-out-disputes.mdx +++ b/src/content/insights/ontario-data-centre-build-out-disputes.mdx @@ -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. -## 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/). diff --git a/src/content/insights/what-a-system-impact-assessment-evaluates.mdx b/src/content/insights/what-a-system-impact-assessment-evaluates.mdx index 029e2c9..dd6149b 100644 --- a/src/content/insights/what-a-system-impact-assessment-evaluates.mdx +++ b/src/content/insights/what-a-system-impact-assessment-evaluates.mdx @@ -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 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. The umbrella name is the connection -assessment and approval process, CAA in the IESO's usage, and each application -is given a unique CAA ID. +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 diff --git a/src/data/practice-pages.ts b/src/data/practice-pages.ts index 38e60c8..f77dc68 100644 --- a/src/data/practice-pages.ts +++ b/src/data/practice-pages.ts @@ -151,7 +151,7 @@ export const PRACTICE_PAGES: Record = { 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, 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.', @@ -177,7 +177,7 @@ export const PRACTICE_PAGES: Record = { 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 = { 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.', - 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.', + 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 @@ -337,7 +337,7 @@ export const PRACTICE_PAGES: Record = { disputeTypes: [ { 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', @@ -386,7 +386,7 @@ export const PRACTICE_PAGES: Record = { }, { 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.', @@ -407,7 +407,7 @@ export const PRACTICE_PAGES: Record = { 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 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.", @@ -460,8 +460,8 @@ export const PRACTICE_PAGES: Record = { body: 'Whether an impairment falls inside the minor injury definition, and the monetary limit that follows if it does.', }, { - name: 'Treatment and assessment plans', - body: 'Denied or partially approved plans, competing assessments, and disputes about the reasonableness and necessity of proposed treatment.', + 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', @@ -608,7 +608,7 @@ export const PRACTICE_PAGES: Record = { }, { 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.', diff --git a/src/pages/bio.astro b/src/pages/bio.astro index c1d3883..ade8a43 100644 --- a/src/pages/bio.astro +++ b/src/pages/bio.astro @@ -136,12 +136,18 @@ const PROCESSES = [ 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. */ + 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, - 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. + 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.

I am {ROLE.title} at {BOUTIQUE}, with {ROLE.litigationLine} across diff --git a/src/pages/fees.astro b/src/pages/fees.astro index db52e21..d37ed50 100644 --- a/src/pages/fees.astro +++ b/src/pages/fees.astro @@ -131,12 +131,12 @@ const ARBITRATION_ROWS = [ }, { item: 'Documents-only or expedited — simple', - detail: 'Flat fee, agreed in the first procedural order.', + detail: 'Flat fee.', fee: money(FEES.arbitration.documentsOnlySimple), }, { 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), }, ]; diff --git a/src/pages/legal/privacy.astro b/src/pages/legal/privacy.astro index 5a92519..e7c7b6c 100644 --- a/src/pages/legal/privacy.astro +++ b/src/pages/legal/privacy.astro @@ -44,8 +44,12 @@ * 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 * "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 - * is public.** See the TODO(pouya) on the retention section below, and §9 Q60. + * names this page as what that item protects. ⚠️ **This read "both halves + * 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 * 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 * 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` and the honeypot are absent from `INTAKE_FIELDS` deliberately and are @@ -226,15 +230,15 @@ const COLLECTED = INTAKE_FIELDS.map((field) => field.label);

How long it is kept

{ - /* TODO(pouya): has a test record been written to the intake table with a - near-future `ttl` and OBSERVED TO DISAPPEAR? AGENTS.md §9 Q60. The - sentence below asserts a MECHANISM, not just a period, and the - setting being on does not prove the mechanism runs. The table - setting is confirmed — §7 holds that status and this comment does - not restate it, because it did restate it once and went stale within - the day (§12 R19). Do not answer this from the handler code, which - only writes the attribute. This page must not go public until a - deletion has actually been seen. */ + /* The sentence below asserts a MECHANISM, not just a period, and the + mechanism is still unobserved — AGENTS.md §9 Q60, open. **Pouya + ruled 2026-09-03 that the page publishes now and the deletion is + confirmed after launch**; the observation window opened 2026-09-02 + and the earliest useful reading is 2026-09-04 (`docs/09` Part 10). + That decision is why this is no longer a `TODO(pouya)`. The table + setting lives in §7 and is deliberately not restated here — it was + once, and went stale within the day (§12 R19). Do not answer Q60 + from the handler code, which only writes the attribute. */ }

{RETENTION_MONTHS} months from the date you send it, @@ -307,15 +311,18 @@ const COLLECTED = INTAKE_FIELDS.map((field) => field.label); {ANALYTICS.provider === 'plausible' ? 'Plausible' : 'Fathom'}, which is cookieless and collects no personal information 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.

) : (

This site sets no cookies and runs no analytics. - There is no tracking script on any page, nothing is stored on your - device, and there is therefore nothing to consent to and no - banner. If that changes, this page changes on the same day and its - last updated date moves with it. + There is no tracking script on any page, and there is therefore + nothing to consent to and no banner. If cookies or analytics are + ever introduced, this page changes on the same day and its last + 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.

) } diff --git a/src/pages/legal/terms.astro b/src/pages/legal/terms.astro index 3ad960f..a22f333 100644 --- a/src/pages/legal/terms.astro +++ b/src/pages/legal/terms.astro @@ -52,7 +52,7 @@ const ldImage = await getImage({ const graph = pageGraph(new URL(ldImage.src, Astro.site).href); /** Bump on any substantive edit. See the note on the privacy page. */ -const LAST_UPDATED = '31 August 2026'; +const LAST_UPDATED = '3 September 2026'; ---

- Links out go to sources — statutes, regulators, tribunals and - institutions. I do not control those sites and am not responsible for - what they say. + Links out go to an institution's published rules and to my LinkedIn + profile. I do not control those sites and am not responsible for what + they say.

Changes

diff --git a/src/pages/mediation.astro b/src/pages/mediation.astro index efc17d2..8944a4d 100644 --- a/src/pages/mediation.astro +++ b/src/pages/mediation.astro @@ -220,15 +220,18 @@ const FORMATS = [ } {CONDUCT_UNDERTAKINGS.mediationCaucus} { - /* The without-prejudice question is answered by pointing, not by - characterising legal effect. AGENTS.md §4 bars this repository from - concluding a proposition of law, and docs/03's `[unestablished]` - pattern says to write around the capacity question. */ + /* WITHOUT PREJUDICE IS ATTRIBUTED TO THE AGREEMENT, NEVER ASSERTED + AS LAW — and it may be narrowed but NOT deleted. §4 bars this + repository from concluding a proposition of law, and no extract + 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. */ }

- Mediation is conducted on a without-prejudice basis. What that means - for a particular file, and what survives it, is a question for each - party's own counsel rather than for the neutral. + Whether the session is without prejudice, and what that covers, is + settled by the agreement to mediate. What being without prejudice + means for a particular file, and what survives the session, is a + question for each party's own counsel rather than for the neutral.