fix: regenerate favicon.ico with a transparent ground; tick Pouya's read-through
public/favicon.ico shipped with no transparency: all three frames declared a 32-bit alpha channel and then carried alpha=255 on every one of their 256/1024/ 2304 pixels, ground opaque cream rgba(250,247,242,255). Pouya's read-through finding, confirmed by parsing the ICO container directly. The render source carries true alpha (2,272,386 transparent px, 20,795 partial), so this is an export, not a mask derived from the cream ground — R13's harder branch did not fire and R13 is unchanged on its own terms. New scripts/icons.mjs + npm run icons re-derives the icon from the committed master: asserts the source is still the documented crop (R14), verifies a candidate file and renames on success so a rejected build cannot replace a good favicon, and runs a boundary-colour halo test. Composition is unchanged — ink bbox and pixel count identical at all three sizes. apple-touch-icon.png is byte-identical and stays opaque cream deliberately; the reason lives in docs/reference/brand-assets.md §The icon set, with the bar and a pointer in BaseLayout.astro, docs/06 and R13. docs/06: the read-through is ticked, and the cutover callout drops to ONE blocker — Q60's waiting period. Two adversarial review rounds, 15 findings, all resolved, none declined; stopped at two per D19. claims-auditor correctly deferred to cutover per D20. Gates on the committed bytes, exit status read: check 0, build 0 (23 pages), check:claims 0, check:intake 0, og:proof 0, lint 0, lighthouse 0 (worst of 23 99/100/100/100). Nothing deployed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
co-authored by
Claude Opus 5
parent
4735989f0b
commit
67847d94fa
@@ -932,7 +932,7 @@ never being raised again.
|
|||||||
| R10 | ✅ **FIRED AND SATISFIED 2026-09-02 — THE CUTOVER EVENT AND THE CONFIRMATION, IN THAT ORDER. THE ROW STAYS LIVE.** Asked of Pouya on 2026-09-02 as a one-line question — *are ADRIC, ADRIO, the three OBA sections and the CTF all still current?* — and **answered the same day: all current.** It was asked rather than looked up, which is the entire content of this reminder: **a stamp is not a renewal receipt, and re-reading an old one is not re-confirming.** It was open for part of the day, alongside R18 which fired and was satisfied the same day. **Re-stamped on all three surfaces, because `memberOf` is emitted and the constant feeds both:** §4's memberships row, `CREDENTIALS.memberships` in `src/data/site.ts` and the R10 note in `src/data/schema.ts`. ⚠️ **AND THERE ARE TWO ARRAYS, NOT ONE — re-stamping is not the same act as checking they still agree.** `CREDENTIALS.memberships` feeds `/about/`'s visible list and `/bio/`; **`MEMBERSHIP_ORGS` feeds `/process/` §Confidentiality and the `memberOf` triples**, and `site.ts` records that the two differ on three of four lines. `_MembershipParity` compares their **`['length']` only**, so a substitution passes `npm run check` in silence. An earlier form of this row said one constant fed both surfaces, which would have left `/process/` and the JSON-LD publishing a lapsed membership after a correct-looking edit — the OCNI failure with a green build (`adversarial-reviewer`, round 1). `docs/06`'s item is ticked and **re-armed for the next republish** — the trigger is an event and events recur, which is why this row does not close on being satisfied. Original text follows. ⚠️ **A THIRD SURFACE, 2026-08-30: `/process/` §Confidentiality renders `MEMBERSHIP_ORGS[0]` ("I am a member of the ADR Institute of Canada").** It is rendered from the constant rather than typed, so the sweep this reminder prescribes reaches it — that was `adversarial-reviewer`'s finding and the fix, in that order. **DISCHARGED AS WRITTEN 2026-08-28 — AND RE-ARMED WITH AN EVENT TRIGGER INSTEAD OF A DATE. STILL LIVE.** Pouya re-confirmed all four memberships as current (Q44), which discharges the prohibition this row carried, and `/about/` now publishes the Memberships group. **The row does not close, because he declined renewal-date tracking**, and that was his instruction for what to do about it: *"Without renewal months it cannot fire on a date, so make it fire on an event: re-confirm memberships before any cutover or major republish, and re-stamp §4 when confirmed."* **THE TRIGGER: re-confirm before any cutover, and before any major republish. Then re-stamp §4 the same day.** **His reason, kept verbatim because it is the general principle and not a membership detail:** *"§4 already carries OCNI as lapsed and unpublishable, and that was found roughly a year late. A stamp with no trigger behind it goes stale silently, which is exactly how OCNI got onto a list of things to feature."* **Two things the discharge did NOT license.** (1) **No currency warranty on the page** — list the memberships, promise nothing about their future state; the struck sentence stays struck and nothing replaces it. (2) ~~`memberOf` stays out of the JSON-LD~~ — **SUPERSEDED. Q53, ruled 2026-08-28: EMIT IT.** `/about/`'s Person node now carries the four memberships as `Organization` nodes. Pouya took `adversarial-reviewer`'s argument: they are already crawlable in `/about/`'s HTML, so withholding the triple reduced no exposure and only made the graph less complete than the page. **The consequence for THIS reminder is that it now covers two surfaces** — re-confirming before a cutover means `src/data/schema.ts` as well as the visible list, and they must not be allowed to diverge. **Renewal periods, stated once and not widened again:** the OBA sections and the CTF renew yearly; §4 records **nothing** about ADRIC's or ADRIO's period, and the widened form ("all four renew yearly") reached four files before it was swept. *Previous text described the prohibition and the withheld group; it held for one session and did its job.* | 2026-08-26 | A credential that lapses quietly is the failure mode §4 exists to prevent, and OCNI already did exactly this. The group is on a public page now, which raises the cost of a lapse rather than lowering it — *(This rationale ended by pointing at **Q48** as a possible widening of the row. Q48 closed 2026-08-28 as not site-relevant — ADRIO retention governs whether Pouya keeps a designation, not what the site may say about holding one — so the clause is struck. §12 is read aloud every session; a live reminder pointing at a struck row produces a false surface every time.)*, not just a list |
|
| R10 | ✅ **FIRED AND SATISFIED 2026-09-02 — THE CUTOVER EVENT AND THE CONFIRMATION, IN THAT ORDER. THE ROW STAYS LIVE.** Asked of Pouya on 2026-09-02 as a one-line question — *are ADRIC, ADRIO, the three OBA sections and the CTF all still current?* — and **answered the same day: all current.** It was asked rather than looked up, which is the entire content of this reminder: **a stamp is not a renewal receipt, and re-reading an old one is not re-confirming.** It was open for part of the day, alongside R18 which fired and was satisfied the same day. **Re-stamped on all three surfaces, because `memberOf` is emitted and the constant feeds both:** §4's memberships row, `CREDENTIALS.memberships` in `src/data/site.ts` and the R10 note in `src/data/schema.ts`. ⚠️ **AND THERE ARE TWO ARRAYS, NOT ONE — re-stamping is not the same act as checking they still agree.** `CREDENTIALS.memberships` feeds `/about/`'s visible list and `/bio/`; **`MEMBERSHIP_ORGS` feeds `/process/` §Confidentiality and the `memberOf` triples**, and `site.ts` records that the two differ on three of four lines. `_MembershipParity` compares their **`['length']` only**, so a substitution passes `npm run check` in silence. An earlier form of this row said one constant fed both surfaces, which would have left `/process/` and the JSON-LD publishing a lapsed membership after a correct-looking edit — the OCNI failure with a green build (`adversarial-reviewer`, round 1). `docs/06`'s item is ticked and **re-armed for the next republish** — the trigger is an event and events recur, which is why this row does not close on being satisfied. Original text follows. ⚠️ **A THIRD SURFACE, 2026-08-30: `/process/` §Confidentiality renders `MEMBERSHIP_ORGS[0]` ("I am a member of the ADR Institute of Canada").** It is rendered from the constant rather than typed, so the sweep this reminder prescribes reaches it — that was `adversarial-reviewer`'s finding and the fix, in that order. **DISCHARGED AS WRITTEN 2026-08-28 — AND RE-ARMED WITH AN EVENT TRIGGER INSTEAD OF A DATE. STILL LIVE.** Pouya re-confirmed all four memberships as current (Q44), which discharges the prohibition this row carried, and `/about/` now publishes the Memberships group. **The row does not close, because he declined renewal-date tracking**, and that was his instruction for what to do about it: *"Without renewal months it cannot fire on a date, so make it fire on an event: re-confirm memberships before any cutover or major republish, and re-stamp §4 when confirmed."* **THE TRIGGER: re-confirm before any cutover, and before any major republish. Then re-stamp §4 the same day.** **His reason, kept verbatim because it is the general principle and not a membership detail:** *"§4 already carries OCNI as lapsed and unpublishable, and that was found roughly a year late. A stamp with no trigger behind it goes stale silently, which is exactly how OCNI got onto a list of things to feature."* **Two things the discharge did NOT license.** (1) **No currency warranty on the page** — list the memberships, promise nothing about their future state; the struck sentence stays struck and nothing replaces it. (2) ~~`memberOf` stays out of the JSON-LD~~ — **SUPERSEDED. Q53, ruled 2026-08-28: EMIT IT.** `/about/`'s Person node now carries the four memberships as `Organization` nodes. Pouya took `adversarial-reviewer`'s argument: they are already crawlable in `/about/`'s HTML, so withholding the triple reduced no exposure and only made the graph less complete than the page. **The consequence for THIS reminder is that it now covers two surfaces** — re-confirming before a cutover means `src/data/schema.ts` as well as the visible list, and they must not be allowed to diverge. **Renewal periods, stated once and not widened again:** the OBA sections and the CTF renew yearly; §4 records **nothing** about ADRIC's or ADRIO's period, and the widened form ("all four renew yearly") reached four files before it was swept. *Previous text described the prohibition and the withheld group; it held for one session and did its job.* | 2026-08-26 | A credential that lapses quietly is the failure mode §4 exists to prevent, and OCNI already did exactly this. The group is on a public page now, which raises the cost of a lapse rather than lowering it — *(This rationale ended by pointing at **Q48** as a possible widening of the row. Q48 closed 2026-08-28 as not site-relevant — ADRIO retention governs whether Pouya keeps a designation, not what the site may say about holding one — so the clause is struck. §12 is read aloud every session; a live reminder pointing at a struck row produces a false surface every time.)*, not just a list |
|
||||||
| R11 | **Re-check dependency currency at every phase boundary in the build order** (`docs/01-architecture.md` §Build order, 11 steps). Run `npm view <pkg> version` across **every** pin in `package.json` and compare; do not wait for something to break. Verified does not mean latest — record the reason for any deliberate hold in §7. ✅ **THE STEP-7 RE-ADD TRIGGER IS DISCHARGED, 2026-08-31 — and NOT as written.** It said *"at step 7, put `@lhci/cli` back"*. `@lhci/cli` is still 0.15.1, still `latest`, and still carries 10 findings (7 high) `[verified 2026-08-31]`, so the literal instruction would have re-added a tool with seven high-severity advisories. What shipped is **`lighthouse@13.4.1` + `chrome-launcher@1.2.1`, 0 vulnerabilities**, as `npm run lighthouse`. **The reason is that §7's own advisory attribution was wrong** — it blamed `lighthouse → puppeteer-core → extract-zip`; the carriers were `@lhci/cli`'s own `tmp` and `@puppeteer/browsers`' `extract-zip`, and neither exists in Lighthouse's tree. **The last clause of this trigger is the one that earned its place:** *"if the advisories are still unfixed, that is a decision to take deliberately, not a reason to leave the gap unstated."* They are still unfixed; the decision was taken; §7 records what it costs (no `lhci` assertion config, no server, no run history) and that the gate is local rather than CI, because standalone Lighthouse needs an installed browser and the runner has none. **All six UNAVAILABLE notices are deleted** — `docs/04` (budget table, Performance callout, post-launch checklist), `CLAUDE.md` (performance budget, definition of done), `/build` Phase 5, `docs/06` (PR checks, cutover checklist), `.claude/agents/adversarial-reviewer.md` §4. The **general** half of R11 — re-check every pin at every phase boundary — is untouched and still fires. ✅ **SWEPT AGAIN 2026-09-01, all 19 pins against `npm view`, and TWO MAJORS ARE DEFERRED BY RULING rather than left unstated:** `@astrojs/mdx` **^7.0.8 → 8.0.0** and `typescript` **^6.0.3 → 7.0.2**. Pouya's reasoning — *"npm audit is clean and majors mid-walkthrough add churn without user value"* — with `npm audit` at **0 vulnerabilities** `[verified 2026-09-01]`, which makes it a churn decision and not a security one, **and one that flips the moment that stops being true.** Both now sit on a new **Cutover prep** group at the head of `docs/06`'s cutover checklist, dated, because deferring a thing and forgetting it look identical three weeks later. Four more are a minor or patch behind and already satisfied by their carets, so they need no edit — `astro` 7.2.9 → 7.2.10, `@astrojs/sitemap` 3.7.3 → 3.7.4, `globals` 17.11.0 → 17.12.0, `typescript-eslint` 8.68.0 → 8.69.0; the other 13 are current. **This row is the deferral's reminder, not its replacement** — R11 fires at the next phase boundary regardless | 2026-08-26 | `astro: "^5.0.0"` was recalled rather than checked and was two majors stale the day it was written, which meant a framework carrying high-severity XSS advisories. Between phases is cheap; after a phase of pages is written is not. The build order has ten more boundaries |
|
| R11 | **Re-check dependency currency at every phase boundary in the build order** (`docs/01-architecture.md` §Build order, 11 steps). Run `npm view <pkg> version` across **every** pin in `package.json` and compare; do not wait for something to break. Verified does not mean latest — record the reason for any deliberate hold in §7. ✅ **THE STEP-7 RE-ADD TRIGGER IS DISCHARGED, 2026-08-31 — and NOT as written.** It said *"at step 7, put `@lhci/cli` back"*. `@lhci/cli` is still 0.15.1, still `latest`, and still carries 10 findings (7 high) `[verified 2026-08-31]`, so the literal instruction would have re-added a tool with seven high-severity advisories. What shipped is **`lighthouse@13.4.1` + `chrome-launcher@1.2.1`, 0 vulnerabilities**, as `npm run lighthouse`. **The reason is that §7's own advisory attribution was wrong** — it blamed `lighthouse → puppeteer-core → extract-zip`; the carriers were `@lhci/cli`'s own `tmp` and `@puppeteer/browsers`' `extract-zip`, and neither exists in Lighthouse's tree. **The last clause of this trigger is the one that earned its place:** *"if the advisories are still unfixed, that is a decision to take deliberately, not a reason to leave the gap unstated."* They are still unfixed; the decision was taken; §7 records what it costs (no `lhci` assertion config, no server, no run history) and that the gate is local rather than CI, because standalone Lighthouse needs an installed browser and the runner has none. **All six UNAVAILABLE notices are deleted** — `docs/04` (budget table, Performance callout, post-launch checklist), `CLAUDE.md` (performance budget, definition of done), `/build` Phase 5, `docs/06` (PR checks, cutover checklist), `.claude/agents/adversarial-reviewer.md` §4. The **general** half of R11 — re-check every pin at every phase boundary — is untouched and still fires. ✅ **SWEPT AGAIN 2026-09-01, all 19 pins against `npm view`, and TWO MAJORS ARE DEFERRED BY RULING rather than left unstated:** `@astrojs/mdx` **^7.0.8 → 8.0.0** and `typescript` **^6.0.3 → 7.0.2**. Pouya's reasoning — *"npm audit is clean and majors mid-walkthrough add churn without user value"* — with `npm audit` at **0 vulnerabilities** `[verified 2026-09-01]`, which makes it a churn decision and not a security one, **and one that flips the moment that stops being true.** Both now sit on a new **Cutover prep** group at the head of `docs/06`'s cutover checklist, dated, because deferring a thing and forgetting it look identical three weeks later. Four more are a minor or patch behind and already satisfied by their carets, so they need no edit — `astro` 7.2.9 → 7.2.10, `@astrojs/sitemap` 3.7.3 → 3.7.4, `globals` 17.11.0 → 17.12.0, `typescript-eslint` 8.68.0 → 8.69.0; the other 13 are current. **This row is the deferral's reminder, not its replacement** — R11 fires at the next phase boundary regardless | 2026-08-26 | `astro: "^5.0.0"` was recalled rather than checked and was two majors stale the day it was written, which meant a framework carrying high-severity XSS advisories. Between phases is cheap; after a phase of pages is written is not. The build order has ten more boundaries |
|
||||||
| R12 | **`compressHTML: true` is a deliberate deviation from the Astro 7 default (`'jsx'`).** Measured 2026-08-26: in an `.astro` template an inline pair split across two lines renders as `<em>a</em><strong>b</strong>` under the default — the space is silently deleted. MDX prose is unaffected | 2026-08-26 | It is a deviation, and undocumented deviations become folklore. Revisit **with a measurement**, not a preference — and re-measure after any Astro major, since the behaviour could change again |
|
| R12 | **`compressHTML: true` is a deliberate deviation from the Astro 7 default (`'jsx'`).** Measured 2026-08-26: in an `.astro` template an inline pair split across two lines renders as `<em>a</em><strong>b</strong>` under the default — the space is silently deleted. MDX prose is unaffected | 2026-08-26 | It is a deviation, and undocumented deviations become folklore. Revisit **with a measurement**, not a preference — and re-measure after any Astro major, since the behaviour could change again |
|
||||||
| R13 | **The infinity mark ships as a RASTER, and that is temporary. RAISED 2026-08-27; Pouya ruled the committed SVG does NOT close it** — *"Keep it committed, keep the AVIF render path. Your own measurement is the reason: 257 KB wrapping seven embedded base64 PNGs. It renders faithfully because it IS the raster."* So the exception stands and the reminder stays live. `InfinityMark.astro` renders an optimised AVIF/WebP from `src/assets/brand/sml-infinity-mark.png` — a deliberate, documented exception to `docs/02`'s "inline SVG, never a PNG", because the mark is gradient-mesh artwork and no true vector master exists yet (Q38). **Removal trigger: the commissioned vector master lands.** Then replace the `<Picture>` with inline SVG, regenerate `favicon.ico` and `apple-touch-icon.png` from it, and delete the exception from `docs/02`, from the component, and from Q38 | 2026-08-26 | Pouya flagged this himself when he made the ruling: *an interim raster is exactly the kind of temporary measure that becomes permanent by never being raised.* It costs ~8 KB and works, which is precisely why nobody will notice it again. There is no build error to prompt anyone — only this row |
|
| R13 | **The infinity mark ships as a RASTER, and that is temporary. RAISED 2026-08-27; Pouya ruled the committed SVG does NOT close it** — *"Keep it committed, keep the AVIF render path. Your own measurement is the reason: 257 KB wrapping seven embedded base64 PNGs. It renders faithfully because it IS the raster."* So the exception stands and the reminder stays live. `InfinityMark.astro` renders an optimised AVIF/WebP from `src/assets/brand/sml-infinity-mark.png` — a deliberate, documented exception to `docs/02`'s "inline SVG, never a PNG", because the mark is gradient-mesh artwork and no true vector master exists yet (Q38). **Removal trigger: the commissioned vector master lands.** Then replace the `<Picture>` with inline SVG, regenerate `favicon.ico` and `apple-touch-icon.png` from it, and delete the exception from `docs/02`, from the component, and from Q38. ⚠️ **The favicon half of that is now a command rather than a manual step — `npm run icons`, added 2026-09-02** — but it reads the RASTER, so pointing it at a vector master is part of the work this row is waiting for. **It regenerates the favicon only. `apple-touch-icon.png` is still hand-made, and it must stay OPAQUE CREAM when it is remade** — the reason is an external platform behaviour that this repository has not tested, and `docs/reference/brand-assets.md` §The icon set carries it with its provenance. Do not restate it here as a fact | 2026-08-26 | Pouya flagged this himself when he made the ruling: *an interim raster is exactly the kind of temporary measure that becomes permanent by never being raised.* It costs ~8 KB and works, which is precisely why nobody will notice it again. There is no build error to prompt anyone — only this row |
|
||||||
| R14 | **Anything a spec makes a claim about must be reachable from the repository.** If the artefact lives only in Drive, in a console, or on someone's disk, no reviewer can check the claim and the claim is unverifiable by construction — see the rule now in `CLAUDE.md` | 2026-08-26 | **This has now cost twice.** Q24 was `AWS-Hosting-Guide.md`, the only record of how the infrastructure was hand-built, living outside the repo. Q32 was the infinity mark: Claude Code traced it from the old site's *loading placeholder*, wrote a scrupulous provenance comment recording exactly that doubt — and **two adversarial review passes still could not catch it**, because the artwork they would have had to compare against was not in the repo. The doubt was correctly stated and structurally uncheckable. That is the failure mode: not a missing warning, a missing artefact |
|
| R14 | **Anything a spec makes a claim about must be reachable from the repository.** If the artefact lives only in Drive, in a console, or on someone's disk, no reviewer can check the claim and the claim is unverifiable by construction — see the rule now in `CLAUDE.md` | 2026-08-26 | **This has now cost twice.** Q24 was `AWS-Hosting-Guide.md`, the only record of how the infrastructure was hand-built, living outside the repo. Q32 was the infinity mark: Claude Code traced it from the old site's *loading placeholder*, wrote a scrupulous provenance comment recording exactly that doubt — and **two adversarial review passes still could not catch it**, because the artwork they would have had to compare against was not in the repo. The doubt was correctly stated and structurally uncheckable. That is the failure mode: not a missing warning, a missing artefact |
|
||||||
| ~~R15~~ | ✅ **DISCHARGED 2026-08-31 — build step 7b. Seventeen pages and every future article now carry their own generated card; `/` and `/about/` keep the portrait, which Q40 called "not an interim" and which is unchanged.** `src/pages/og/[...slug].jpg.ts` renders them at build from `satori@0.33.4` + `sharp`; `src/data/og-cards.ts` is the registry; `SEO.astro` resolves which of the two kinds a page gets from its own pathname, so the decision lives in one data file rather than in nineteen call sites. `satori` over `astro-og-canvas@0.13.0` — both 0 vulnerabilities `[verified 2026-08-31]` — because `sharp` was already a dependency, so it is one library rather than a CanvasKit wasm blob, and it renders the site's real fonts and tokens instead of approximating them. **The interim note is deleted from `docs/04` and the cutover line from `docs/06`**, which is what this row's trigger asked for. ⚠️ **AND IT SURFACED A HOLE THAT IS WORTH MORE THAN THE ROW: TEXT BAKED INTO A JPEG CANNOT BE GREPPED BY `npm run check:claims`, WHICH UNDER D20 IS THE ONLY PER-STEP CLAIMS CONTROL THERE IS.** A card is the one surface on this site where new prose would face no mechanical check at all. So card copy is constrained structurally rather than editorially: **every headline is its page's own `<h1>`, character for character**, the kicker renders `CREDENTIALS.designations`, and **`npm run og:proof` enforces both against the built HTML** — it fails if the registry and the page disagree in either direction, and it also confirms every `og:image` resolves to a file that exists. That check found a straight-versus-typographic apostrophe on its first run. **A page with no registry entry is a build error, not a fallback to the portrait** — R15's failure mode was never the wrong image, it was the wrong image shipping *invisibly* and reading as intentional, and a silent fallback recreates it exactly. **What is NOT discharged and is deliberately left open:** the check compares strings, so it cannot see a card that renders badly — a headline that overflows, a mark that clips. `npm run og:proof -- --sheet` writes a contact sheet of every card for that, and **a human still has to look**; `docs/06`'s cutover item says so. Two cards were read at step 7b (`/practice/energy/`, the longest headline, and `/practice/insurance/`) and rendered correctly | 2026-08-27 | Nobody on this project will ever see the defect. A link preview is rendered by LinkedIn, Slack and Teams for a reader who is not us, and `docs/04` requires a **unique** title and description per page — nineteen unique pages that all preview identically is the failure, and it is invisible from inside the repo. It cost nothing and worked, which is exactly the R13 shape |
|
| ~~R15~~ | ✅ **DISCHARGED 2026-08-31 — build step 7b. Seventeen pages and every future article now carry their own generated card; `/` and `/about/` keep the portrait, which Q40 called "not an interim" and which is unchanged.** `src/pages/og/[...slug].jpg.ts` renders them at build from `satori@0.33.4` + `sharp`; `src/data/og-cards.ts` is the registry; `SEO.astro` resolves which of the two kinds a page gets from its own pathname, so the decision lives in one data file rather than in nineteen call sites. `satori` over `astro-og-canvas@0.13.0` — both 0 vulnerabilities `[verified 2026-08-31]` — because `sharp` was already a dependency, so it is one library rather than a CanvasKit wasm blob, and it renders the site's real fonts and tokens instead of approximating them. **The interim note is deleted from `docs/04` and the cutover line from `docs/06`**, which is what this row's trigger asked for. ⚠️ **AND IT SURFACED A HOLE THAT IS WORTH MORE THAN THE ROW: TEXT BAKED INTO A JPEG CANNOT BE GREPPED BY `npm run check:claims`, WHICH UNDER D20 IS THE ONLY PER-STEP CLAIMS CONTROL THERE IS.** A card is the one surface on this site where new prose would face no mechanical check at all. So card copy is constrained structurally rather than editorially: **every headline is its page's own `<h1>`, character for character**, the kicker renders `CREDENTIALS.designations`, and **`npm run og:proof` enforces both against the built HTML** — it fails if the registry and the page disagree in either direction, and it also confirms every `og:image` resolves to a file that exists. That check found a straight-versus-typographic apostrophe on its first run. **A page with no registry entry is a build error, not a fallback to the portrait** — R15's failure mode was never the wrong image, it was the wrong image shipping *invisibly* and reading as intentional, and a silent fallback recreates it exactly. **What is NOT discharged and is deliberately left open:** the check compares strings, so it cannot see a card that renders badly — a headline that overflows, a mark that clips. `npm run og:proof -- --sheet` writes a contact sheet of every card for that, and **a human still has to look**; `docs/06`'s cutover item says so. Two cards were read at step 7b (`/practice/energy/`, the longest headline, and `/practice/insurance/`) and rendered correctly | 2026-08-27 | Nobody on this project will ever see the defect. A link preview is rendered by LinkedIn, Slack and Teams for a reader who is not us, and `docs/04` requires a **unique** title and description per page — nineteen unique pages that all preview identically is the failure, and it is invisible from inside the repo. It cost nothing and worked, which is exactly the R13 shape |
|
||||||
| ~~R16~~ | ✅ **DISCHARGED 2026-08-31 — build step 9, which is the trigger this row set. The one-page PDF exists, is committed at `public/pouya-lajevardi-bio.pdf`, and `/about/` links it.** Q45's two open sub-decisions are both taken, and the second one is what makes the first safe. **(a) Generated at build, or authored once? NEITHER — and the third option answers this row's actual objection.** The bio is a PAGE, `src/pages/bio.astro`, so every line of it is reviewed by the same apparatus as every other page: `astro check`, `npm run check:claims` on the built HTML, the per-step code review, and the cutover claims pass. `npm run bio:pdf` then RENDERS the PDF from the built page through Chrome, with **no new dependency** — `chrome-launcher` is already a devDependency because Lighthouse needs it. That is aimed squarely at what this row said the problem was: *"It is the one artefact class this project's review apparatus cannot reach… a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again."* A rendering of a reviewed page is back inside the apparatus. **It is deliberately NOT part of `astro build`** — CI has no Chrome, and a build step that cannot run in CI is Q22's shape again — so the PDF is committed, which R14 wants anyway. **(b) Does it carry anything the site does not? NO, and that is the answer that avoids the §4 question this row flagged.** Every line renders from `CREDENTIALS`, `ROLE`, `BOUTIQUE`, `PRACTICE_AREAS`, `FEES` and `CONTACT`. No matter list — which this row correctly said *"would collide with §4 Forbidden directly"* — no referees, and no figure that is not on `/fees/`. The fee summary IS there, because this row's own reasoning is that an appointment proposal needs the rate card as much as the bio. ⚠️ **`npm run bio:pdf` ASSERTS ONE PAGE and writes nothing if the count is wrong** — counted from the PDF's own page objects, cross-checked against the page tree's `/Count`. A one-page bio that silently becomes two is invisible from the source, because it depends on the print stylesheet, the paper size and how much §4 has grown. **And reading the rendered PDF caught a real breach the source review had not:** the opening sentence read *"I act as a neutral in commercial disputes — as a mediator, as an arbitrator in commercial matters…"*, whose leading clause scoped **mediation**, which Q56 leaves unscoped deliberately. `/bio/` is `noindex` and out of the sitemap: it is a condensed duplicate of `/about/` and `/fees/`. **What is NOT discharged:** nothing in the build regenerates the PDF. It must be re-rendered and re-committed whenever `/bio/`, §4, the rate card or the print styles change, and `docs/06`'s cutover checklist carries that | 2026-08-28 | It is the one artefact class this project's review apparatus cannot reach. A web page is re-reviewed by every audit and re-checked by every build; a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again |
|
| ~~R16~~ | ✅ **DISCHARGED 2026-08-31 — build step 9, which is the trigger this row set. The one-page PDF exists, is committed at `public/pouya-lajevardi-bio.pdf`, and `/about/` links it.** Q45's two open sub-decisions are both taken, and the second one is what makes the first safe. **(a) Generated at build, or authored once? NEITHER — and the third option answers this row's actual objection.** The bio is a PAGE, `src/pages/bio.astro`, so every line of it is reviewed by the same apparatus as every other page: `astro check`, `npm run check:claims` on the built HTML, the per-step code review, and the cutover claims pass. `npm run bio:pdf` then RENDERS the PDF from the built page through Chrome, with **no new dependency** — `chrome-launcher` is already a devDependency because Lighthouse needs it. That is aimed squarely at what this row said the problem was: *"It is the one artefact class this project's review apparatus cannot reach… a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again."* A rendering of a reviewed page is back inside the apparatus. **It is deliberately NOT part of `astro build`** — CI has no Chrome, and a build step that cannot run in CI is Q22's shape again — so the PDF is committed, which R14 wants anyway. **(b) Does it carry anything the site does not? NO, and that is the answer that avoids the §4 question this row flagged.** Every line renders from `CREDENTIALS`, `ROLE`, `BOUTIQUE`, `PRACTICE_AREAS`, `FEES` and `CONTACT`. No matter list — which this row correctly said *"would collide with §4 Forbidden directly"* — no referees, and no figure that is not on `/fees/`. The fee summary IS there, because this row's own reasoning is that an appointment proposal needs the rate card as much as the bio. ⚠️ **`npm run bio:pdf` ASSERTS ONE PAGE and writes nothing if the count is wrong** — counted from the PDF's own page objects, cross-checked against the page tree's `/Count`. A one-page bio that silently becomes two is invisible from the source, because it depends on the print stylesheet, the paper size and how much §4 has grown. **And reading the rendered PDF caught a real breach the source review had not:** the opening sentence read *"I act as a neutral in commercial disputes — as a mediator, as an arbitrator in commercial matters…"*, whose leading clause scoped **mediation**, which Q56 leaves unscoped deliberately. `/bio/` is `noindex` and out of the sitemap: it is a condensed duplicate of `/about/` and `/fees/`. **What is NOT discharged:** nothing in the build regenerates the PDF. It must be re-rendered and re-committed whenever `/bio/`, §4, the rate card or the print styles change, and `docs/06`'s cutover checklist carries that | 2026-08-28 | It is the one artefact class this project's review apparatus cannot reach. A web page is re-reviewed by every audit and re-checked by every build; a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again |
|
||||||
@@ -948,6 +948,376 @@ never being raised again.
|
|||||||
|
|
||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 2026-09-02 (aq) — The favicon shipped with no transparency and is regenerated from the committed master; Pouya's full-site read-through completes with that as its only finding, and the cutover blockers drop to one
|
||||||
|
|
||||||
|
**Pouya's read-through of every page against §4 is COMPLETE, 2026-09-02.** It is
|
||||||
|
the human half of D20 and it is not delegable. Across 23 pages it returned
|
||||||
|
**one finding, and the finding was not copy**: `public/favicon.ico` had no
|
||||||
|
transparency. His read carried the approvals with it in terms — the
|
||||||
|
`/legal/privacy/` §Who can see it wording, the **SML Company Ltd** consent line,
|
||||||
|
and `/med-arb/` **as shipped** — which discharges the wording sign-off that
|
||||||
|
entry (ap) had routed into this pass.
|
||||||
|
|
||||||
|
### 1. The finding, confirmed before it was acted on
|
||||||
|
|
||||||
|
Pouya's note: *"`public/favicon.ico` has zero transparent pixels; every
|
||||||
|
background pixel is opaque cream (250,247,242,255)."* Confirmed by parsing the
|
||||||
|
ICO container directly rather than by converting it — a conversion step can
|
||||||
|
itself add or drop an alpha channel, and then the measurement is about the
|
||||||
|
converter. All three frames are PNG-encoded and **declare a 32-bit alpha
|
||||||
|
channel**, and then carry `alpha = 255` on every one of their **256 / 1,024 /
|
||||||
|
2,304** pixels, `range = [255,255]`. The ground is `rgba(250,247,242,255)`,
|
||||||
|
which is `--cream` `#faf7f2` exactly. `magick identify` reports these frames as
|
||||||
|
`8-bit sRGB` and says nothing about alpha either way, which is why the container
|
||||||
|
was read directly.
|
||||||
|
|
||||||
|
**A declared alpha channel that is opaque everywhere is the worst shape this
|
||||||
|
defect could take**: every tool reports the file as RGBA and nothing flags it.
|
||||||
|
|
||||||
|
### 2. It is an EXPORT, not a derivation — the R13 branch did not fire
|
||||||
|
|
||||||
|
Pouya's instruction allowed for the harder case: *"If the master itself has no
|
||||||
|
alpha channel, derive the mask from the cream ground and flag R13."* **Measured,
|
||||||
|
and it does not apply.** `src/assets/brand/sml-infinity-mark.png` carries a true
|
||||||
|
alpha channel — 2668 × 1704, **2,272,386 fully transparent pixels (50.0%)**,
|
||||||
|
2,253,091 fully opaque, and **20,795 partial** — the anti-aliased edge of the
|
||||||
|
ribbon, intact. So no mask had to be reconstructed from the cream ground, and
|
||||||
|
nothing here is lossier than the artwork. **R13 is untouched by this** and stays
|
||||||
|
open on its own terms: the mark still ships as a raster.
|
||||||
|
|
||||||
|
The regeneration also **re-derived the documented crop as a side effect**:
|
||||||
|
extracting `{ left: 159, top: 646, width: 2668, height: 1704 }` from
|
||||||
|
`sml-infinity-mark-master.png` returns a buffer **byte-identical** to the render
|
||||||
|
source. `scripts/icons.mjs` asserts that on every run, so the icon stays
|
||||||
|
traceable to committed artwork rather than to a file on a disk — R14.
|
||||||
|
|
||||||
|
### 3. What changed, and what deliberately did not
|
||||||
|
|
||||||
|
`public/favicon.ico`, 16/32/48, **4,857 → 5,365 B**. The ground is now
|
||||||
|
transparent; **nothing else moved.**
|
||||||
|
|
||||||
|
**The composition was measured off the icons as they already shipped rather than
|
||||||
|
re-chosen** — the mark spans **7/8 of the canvas width**, centred on both axes,
|
||||||
|
which is 14/16, 28/32, 42/48 and 158/180 on the touch icon. Regenerating had to
|
||||||
|
reproduce that, not restyle the mark, and it does: composited back onto cream,
|
||||||
|
the mark's bounding box and ink-pixel count are **identical at all three sizes**
|
||||||
|
— 14 × 9 at (1,4), 28 × 18 at (2,7), 42 × 27 at (3,11), ink 92 / 328 / 689.
|
||||||
|
|
||||||
|
⚠️ **`public/apple-touch-icon.png` STAYS OPAQUE CREAM, DELIBERATELY, AND IS
|
||||||
|
BYTE-IDENTICAL ACROSS THIS CHANGE** (`sha256 6377b096…`, asserted before and
|
||||||
|
after). **iOS composites a transparent touch icon onto BLACK**, so the fix for
|
||||||
|
the tab icon is the defect for the home-screen icon. The two requirements are
|
||||||
|
opposite and the asymmetry is now recorded in three places, because the risk is
|
||||||
|
that someone later "fixes" the touch icon to match: `docs/reference/brand-assets.md`
|
||||||
|
§The icon set, the comment beside the `<link>` tags in
|
||||||
|
`src/layouts/BaseLayout.astro`, and R13.
|
||||||
|
|
||||||
|
### 4. Verified two ways, on two instruments that cannot fail the same way
|
||||||
|
|
||||||
|
Pouya asked for both, and asked for the dark ground specifically.
|
||||||
|
|
||||||
|
**(a) Programmatically, on the shipped `dist/` bytes.** The container parsed
|
||||||
|
frame by frame: three frames, dir sizes matching the decoded PNGs, **all four
|
||||||
|
corners `alpha = 0` at every size**, `147 / 636 / 1,499` fully transparent
|
||||||
|
pixels, `15 / 88 / 202` fully opaque, `94 / 300 / 603` partial. The mark is
|
||||||
|
still there and the ground is gone.
|
||||||
|
|
||||||
|
**(b) Composition parity.** Composite the new icon back onto cream and it
|
||||||
|
reproduces the old matted icon to within **1 of 255 on every channel of every
|
||||||
|
pixel** — max delta **1**, mean **0.01–0.02**, **0** pixels over a delta of 8,
|
||||||
|
at all three sizes. That proves the regeneration did not restyle, rescale or
|
||||||
|
reposition the mark.
|
||||||
|
|
||||||
|
⚠️ **AND IT IS NOT A HALO TEST, THOUGH THIS ENTRY'S FIRST DRAFT FILED IT AS ONE
|
||||||
|
— `adversarial-reviewer`, round 1.** The claim written beside it, *"anti-aliasing
|
||||||
|
that had been matted against cream would differ far more than that"*, is the
|
||||||
|
inversion of the truth: **matting against cream is exactly what produces delta
|
||||||
|
0.** A cream-matted edge that has merely had its background knocked out
|
||||||
|
composites straight back to the icon it came from, so both the correct and the
|
||||||
|
defective case pass. **The test returns the same near-zero answer either way and
|
||||||
|
therefore carries no information about the thing it was filed under.**
|
||||||
|
|
||||||
|
**(b2) No halo — the measurement that DOES discriminate.** Read the RGB the
|
||||||
|
partial-alpha pixels actually carry: cream means a halo, the ribbon's own colour
|
||||||
|
means a clean export. **0 of 94 / 300 / 603** partial-alpha pixels are within 12
|
||||||
|
of `rgb(250,247,242)`; the nearest is **13** away, mean distance **174.0 / 178.3
|
||||||
|
/ 177.0**, mean colour ≈ `rgb(118,86,74)`. Maroon-brown, not cream and not black.
|
||||||
|
Run independently after the finding rather than taken from it, and it reproduces
|
||||||
|
the reviewer's figures exactly.
|
||||||
|
|
||||||
|
**(c) Visually, on seven grounds — and then again in a browser.** The frames
|
||||||
|
composited on Chrome dark `#202124`, Firefox dark `#2b2a33`, Safari dark
|
||||||
|
`#3b3b3b`, black, Chrome light `#f1f3f4`, white and cream, at 1:1 and at 5×
|
||||||
|
nearest-neighbour. The old icon shows a hard cream tile on every dark ground and
|
||||||
|
a visible off-white rectangle even on white; the new one shows no tile anywhere.
|
||||||
|
**Then the same page was rendered by Chrome itself** — `<img src="favicon.ico">`
|
||||||
|
on the same grounds, screenshotted headless at DPR 2 — because sharp decoding
|
||||||
|
its own output is one instrument twice. Chrome's ICO decoder honours the alpha
|
||||||
|
on every ground including pure black.
|
||||||
|
|
||||||
|
**(d) Legible on dark AND on light — but NOT at the same cost, and the first
|
||||||
|
draft of this entry recorded it as free.** Two defects, both found by
|
||||||
|
`adversarial-reviewer` round 1 and both re-measured here before being accepted.
|
||||||
|
|
||||||
|
*The figure was wrong.* `rgb(64,22,24)` was quoted as the darkest ink at 32 px
|
||||||
|
and it is **not a painted colour**: exactly one pixel carries it and its alpha is
|
||||||
|
**251**, not 255. The probe filtered at `alpha >= 250` and then computed a
|
||||||
|
contrast ratio as though the pixel were opaque. **A ratio taken from a raw
|
||||||
|
channel value is a claim about a colour that is never painted.** Recomputed on
|
||||||
|
fully-opaque pixels only, composited against the ground: darkest opaque ink at
|
||||||
|
32 px is `rgb(60,29,29)` → **13.62:1** on `#f1f3f4`, not 14.02.
|
||||||
|
|
||||||
|
*The characterisation was worse than the figure.* "Reads as a single-tone loop
|
||||||
|
rather than the full two-tone ribbon" understates it. The maroon lobe composites
|
||||||
|
to **1.04–1.15:1** on `#202124`. Counting pixels reaching 3:1 against each
|
||||||
|
ground, at 16 / 32 / 48 px:
|
||||||
|
|
||||||
|
| | vs `#202124` | vs `#f1f3f4` |
|
||||||
|
|---|---|---|
|
||||||
|
| 16 px | **23** / 256 | 40 / 256 |
|
||||||
|
| 32 px | **87** / 1024 | 168 / 1024 |
|
||||||
|
| 48 px | **189** / 2304 | 378 / 2304 |
|
||||||
|
|
||||||
|
**Roughly half the mark's visible pixels are absent on a dark tab strip, and the
|
||||||
|
previous cream-tiled icon was more legible there.** What the change buys is the
|
||||||
|
removal of that cream rectangle from every dark strip and from white. A
|
||||||
|
`prefers-color-scheme` icon pair does not solve it — that keys off the *page's*
|
||||||
|
scheme, not the browser chrome's. **The trade is recorded as a trade**; it is
|
||||||
|
Pouya's to revisit and §4 of this entry's record is where he would find it.
|
||||||
|
|
||||||
|
**(e) Deterministic.** Three consecutive runs of `npm run icons` produce the
|
||||||
|
identical file, `sha256 3c611a43…`. A generator that drifts would make every
|
||||||
|
later comparison meaningless.
|
||||||
|
|
||||||
|
### 5. An instrument artefact, caught and recorded rather than believed
|
||||||
|
|
||||||
|
The first geometry re-check reported the mark's bounding box on the new 48 px
|
||||||
|
frame as **48 × 48 filling the whole canvas** — which reads as *the regeneration
|
||||||
|
blew the mark up to the full square*, a serious defect. **It is an artefact of
|
||||||
|
the probe.** That probe identifies ink as "differs from cream by more than 6",
|
||||||
|
which is valid on a matted icon and **invalid on a transparent one**: at
|
||||||
|
`alpha = 0` the pixels carry RGB `(0,0,0)`, which differs from cream, so the
|
||||||
|
entire canvas reads as ink. Re-measured by compositing over cream first — so both
|
||||||
|
icons are measured by the same instrument on the same kind of input — the boxes
|
||||||
|
are identical, which is §3 above.
|
||||||
|
|
||||||
|
⚠️ **`CLAUDE.md` lists six instances of *a measurement is a claim about your
|
||||||
|
instrument until you check the instrument*. THIS SESSION ADDED TWO MORE**, and
|
||||||
|
they are different failure modes from each other and from the six:
|
||||||
|
|
||||||
|
- **A probe that is valid on one class of input and silently invalid on
|
||||||
|
another.** "Differs from cream" identifies ink correctly on a matted icon and
|
||||||
|
nonsensically on a transparent one, because `alpha = 0` pixels still carry RGB.
|
||||||
|
Nothing about the probe changed — **the input class did**, and the probe had no
|
||||||
|
way to say so.
|
||||||
|
- **A contrast ratio computed without asking what alpha the colour is painted
|
||||||
|
at.** The 32 px "darkest ink" was a single pixel at `alpha = 251`, filtered in
|
||||||
|
by a `>= 250` threshold and then treated as opaque. **The number was of a
|
||||||
|
colour that is never painted.**
|
||||||
|
|
||||||
|
Both are now in `CLAUDE.md` beside the other six.
|
||||||
|
|
||||||
|
### 6. Records
|
||||||
|
|
||||||
|
- `docs/06-deployment.md` — **the cutover callout drops from TWO blockers to
|
||||||
|
ONE.** ⚠️ **It read ONE earlier the same day and that was a defect**, so the
|
||||||
|
callout now states the difference rather than the number: it said ONE then
|
||||||
|
because an approval had gone **missing** from the list; it says ONE now
|
||||||
|
because the pass that approval was routed into has been **done**. A tally
|
||||||
|
cannot tell those apart. **Q60's waiting period is the last one**, and it is a
|
||||||
|
waiting period rather than a task — earliest useful reading 48 hours, no
|
||||||
|
failure called before 7 days, so it starts first.
|
||||||
|
- `docs/06-deployment.md` — **"Pouya has read every page against §4"** ticked,
|
||||||
|
with the finding and the three approvals it carried recorded on the item. It
|
||||||
|
notes in terms that this does **not** discharge `claims-auditor`'s cutover
|
||||||
|
pass: D20 traded the per-step machine audit for the human pass **plus** one
|
||||||
|
machine pass over the finished site, and one of those two has now happened.
|
||||||
|
- `docs/06-deployment.md` — **"Favicon set complete"** ticked, carrying what was
|
||||||
|
wrong, what fixed it, both verifications, and the touch-icon bar.
|
||||||
|
- `docs/reference/brand-assets.md` — a new **§The icon set**: the two files,
|
||||||
|
their grounds and why they differ, the composition rule, the regeneration
|
||||||
|
command, and the measurements above. Its opening line said *"Every measurement
|
||||||
|
below is `[verified 2026-08-26]`"*, which the new section would have made
|
||||||
|
false, so sections now carry their own stamps.
|
||||||
|
- `AGENTS.md` **R13** — amended in place. Its removal trigger says to regenerate
|
||||||
|
both icons from the vector master when it lands; that half is now a command
|
||||||
|
(`npm run icons`) **but it reads the raster**, so pointing it at a vector master
|
||||||
|
is part of the work R13 is still waiting for. The row also now carries the bar
|
||||||
|
that the touch icon must stay opaque cream when it is remade.
|
||||||
|
- `scripts/icons.mjs` + `npm run icons` — new. **LOCAL ONLY and deliberately not
|
||||||
|
wired into `npm run build` or either deploy path**, like `bio:pdf` and
|
||||||
|
`lighthouse`; verified by grep that no workflow or deploy script calls it. It
|
||||||
|
writes the favicon **only** and never opens the touch icon. It refuses to run
|
||||||
|
if the render source has no alpha channel, asserts the source is still the
|
||||||
|
documented crop of the master, and re-reads the container **from disk** to
|
||||||
|
verify rather than trusting the buffers it just wrote.
|
||||||
|
|
||||||
|
### 7. No new compatibility surface
|
||||||
|
|
||||||
|
Worth stating because it bounds the risk of touching a file every page requests:
|
||||||
|
**the container format did not change.** The previous `favicon.ico` already
|
||||||
|
carried three **PNG-encoded** frames at 32 bpp, and so does this one — same
|
||||||
|
frame count, same sizes, same ascending order, same 6-byte header and three
|
||||||
|
16-byte directory entries, first payload at offset 54. Only the pixel data
|
||||||
|
differs. Anything that could parse the old file parses this one. Verified in
|
||||||
|
Chrome; the remaining browsers are covered by the existing unticked checklist
|
||||||
|
item, **"Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox"**,
|
||||||
|
which is where the touch icon gets looked at on a real handset.
|
||||||
|
|
||||||
|
### 8. Review — `adversarial-reviewer` only, per D20
|
||||||
|
|
||||||
|
**`claims-auditor` did NOT run.** D20: it runs once, at cutover, over the whole
|
||||||
|
finished site. This change set adds no copy to any page.
|
||||||
|
|
||||||
|
**Round 1: seven findings, none blocking, all seven acted on, none declined.**
|
||||||
|
Two of them — the halo proof and the legibility figures — are recorded above
|
||||||
|
because they were defects **in this entry's own evidence**, which is the class
|
||||||
|
worth keeping. The other five:
|
||||||
|
|
||||||
|
- **The external constants were dressed as repository facts.** `#202124`,
|
||||||
|
`#f1f3f4` and iOS's compositing behaviour are none of them derivable from
|
||||||
|
anything committed, and they sat under a heading promising every figure was
|
||||||
|
read off this repository. **This is R14 from the inside**: a reader cannot
|
||||||
|
re-derive `11.28:1` without a ground colour they have no way to check. The
|
||||||
|
hexes now carry `[observed 2026-09-02]`; the iOS behaviour is attributed to
|
||||||
|
**Pouya's ruling of 2026-09-02** and marked as not re-tested here, with the
|
||||||
|
`docs/06` handset item named as the place that would test it.
|
||||||
|
- **The new stamp mis-dated the render ladders** — and ⚠️ **the fix for this was itself defective; see §11.** The line replacing the file's
|
||||||
|
old blanket date asserted the ladders were `[verified 2026-08-26]`; they carry
|
||||||
|
their own `[measured 2026-08-27]` eleven lines lower. **The fix made a vaguer
|
||||||
|
wrong claim more specifically wrong** — so the header now sends the reader to
|
||||||
|
each section's own stamp and states that a blanket date has already been wrong
|
||||||
|
here once. **Round 2 found that this replacement had removed the only stamp
|
||||||
|
five sections had**; it is now a floor plus overrides.
|
||||||
|
- **The generator wrote the shipped file BEFORE verifying it.** The real defect
|
||||||
|
of the seven. `writeFileSync(OUT, …)` then `verify(OUT)` means a rejected
|
||||||
|
build has already replaced a good favicon, exits 1, and — because the script
|
||||||
|
is deliberately outside the build and both deploy paths — **nothing downstream
|
||||||
|
re-checks**. A developer who fixes the artwork without `git checkout` ships the
|
||||||
|
frames the script rejected, which is the exact failure this change was made to
|
||||||
|
correct. Now it writes `favicon.ico.candidate`, verifies **that**, and renames
|
||||||
|
on success; `die()` removes the candidate on every failure path. **Probed both
|
||||||
|
ways:** forcing the matted-ground check to fail exits **1**, prints the right
|
||||||
|
error, leaves no candidate, and leaves `public/favicon.ico` at its original
|
||||||
|
`sha256 3c611a43…`.
|
||||||
|
- **The iOS rationale was stated in full in four places.** `CLAUDE.md`'s
|
||||||
|
duplicated-fact rule with no mechanism attached — the shape that put a stale
|
||||||
|
SES DKIM table into `docs/05`. `docs/reference/brand-assets.md` §The icon set
|
||||||
|
is now the single owner; the other sites carry the **bar** and a pointer, not
|
||||||
|
the reasoning. `BaseLayout.astro`'s addition went from 6 lines to 3 for two
|
||||||
|
`<link>` elements.
|
||||||
|
- **`npm run icons` was missing from `CLAUDE.md`'s command inventory**, whose
|
||||||
|
next sentence counts the commands. Added, along with the distinction that
|
||||||
|
`bio:pdf` and `icons` are **regenerators rather than gates** — they rewrite
|
||||||
|
committed artefacts, and they stay out of the build because a build should not
|
||||||
|
silently rewrite an artefact a human approved.
|
||||||
|
|
||||||
|
Also removed while in the file: two unreachable `size === 256` branches, a
|
||||||
|
return value no caller used, and a duplicate `metadata()` read.
|
||||||
|
|
||||||
|
### 9. Verification — every command run, every exit status read
|
||||||
|
|
||||||
|
On the committed bytes, `npm run` unless noted:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| `check` | **0** — 0 errors, 0 warnings, 0 hints |
|
||||||
|
| `build` | **0** — 23 pages |
|
||||||
|
| `check:claims` | **0** — 12 patterns, every pattern still firing on its fixture |
|
||||||
|
| `check:intake` | **0** — 12 form fields against 12 handler fields |
|
||||||
|
| `og:proof` | **0** — 23 pages, 22 og:image targets, 21 headlines matched their `<h1>` |
|
||||||
|
| `lint` | **0** — eslint + prettier |
|
||||||
|
| `lighthouse` | **0** — worst of 23 **99 / 100 / 100 / 100**, CLS 0.000, run before and after the review fixes |
|
||||||
|
| `icons` | **0**, and byte-identical on three consecutive runs |
|
||||||
|
|
||||||
|
Icon-specific, and the numbers that matter:
|
||||||
|
|
||||||
|
- `public/favicon.ico` **4,857 → 5,365 B**; `dist/favicon.ico` identical to it.
|
||||||
|
- `public/apple-touch-icon.png` **`sha256 6377b096…7de0` before and after**, in
|
||||||
|
`public/` and in `dist/`. Asserted, not assumed.
|
||||||
|
- Corners `alpha = 0` at 16, 32 and 48, read back from the shipped `dist/` bytes.
|
||||||
|
- Ink bbox and ink-pixel count identical to the previous icon at all three sizes.
|
||||||
|
- The failure path probed: a forced rejection exits **1** and leaves
|
||||||
|
`public/favicon.ico` at its original hash with no candidate file behind.
|
||||||
|
|
||||||
|
**Not claimed:** the touch icon has not been looked at on a handset, and the
|
||||||
|
favicon has been rendered in **Chrome only**. Both belong to the existing
|
||||||
|
unticked `docs/06` item *"Tested on iOS Safari, Android Chrome, desktop
|
||||||
|
Safari/Chrome/Firefox"*, which is the right place for them and is not ticked by
|
||||||
|
this entry.
|
||||||
|
|
||||||
|
### 10. What remains
|
||||||
|
|
||||||
|
**Q60 is the only thing blocking the cutover list**, and it is a waiting period
|
||||||
|
rather than a task: TTL is `ENABLED`, no record has yet been watched to expire,
|
||||||
|
`/legal/privacy/` asserts the mechanism and not merely the period, and `docs/09`
|
||||||
|
Part 10's earliest useful reading is **48 hours** with no failure called before
|
||||||
|
**7 days**. **Start it first.**
|
||||||
|
|
||||||
|
`claims-auditor`'s single cutover pass over the finished site is still to run —
|
||||||
|
D20 traded the per-step machine audit for the human pass **plus** that one, and
|
||||||
|
only the human pass has happened.
|
||||||
|
|
||||||
|
### 11. Round 2 — seven should-fix and one consider, all acted on, none declined
|
||||||
|
|
||||||
|
**Stopped at two rounds, per D19.** The cap exists because past two the
|
||||||
|
manufacture rate exceeds the value of the marginal finding; this round is the
|
||||||
|
evidence for the other half of that rule — **five of its eight items were defects
|
||||||
|
in round 1's own repairs.** Recording that, not summarising it away.
|
||||||
|
|
||||||
|
**The one that mattered, and it was mine twice over.** Round 1 established that
|
||||||
|
the composite-onto-cream test cannot detect a halo, and the fix put the
|
||||||
|
discriminating test in the prose. Round 2's point was that **`verify()` still
|
||||||
|
could not run it** — the generator asserted frame count, dimensions, clear
|
||||||
|
corners, "some transparent" and "some opaque", every one of which a cream-matted
|
||||||
|
knockout passes. So the guard went in. **And the first guard was blind too.**
|
||||||
|
|
||||||
|
It counted **partial-alpha** pixels near cream — and a purpose-built haloed
|
||||||
|
fixture has **zero partial-alpha pixels**, because a knockout sets alpha per
|
||||||
|
pixel and leaves none. The rule inspected an empty set, returned 0, and read as
|
||||||
|
a pass. **It passed the real icon and it passed the defect**, which is the
|
||||||
|
uniform-pass shape `CLAUDE.md` warns is the unsurvivable half. Caught by
|
||||||
|
building the fixture before trusting the guard.
|
||||||
|
|
||||||
|
The working rule is the **boundary**: every painted pixel touching a fully
|
||||||
|
transparent one, counted for nearness to cream. Correct frames **1.6 / 1.4 /
|
||||||
|
1.9 %**; the fixture **19.6 %**; gate at **10 %**, ~5× clear of both. Probed
|
||||||
|
both ways — the fixture exits **1** and leaves `public/favicon.ico` at its hash.
|
||||||
|
|
||||||
|
The other seven:
|
||||||
|
|
||||||
|
- **The round-1 stamp fix deleted the floor five sections relied on.** Replacing
|
||||||
|
a blanket `[verified 2026-08-26]` with *"each section carries its own stamp"*
|
||||||
|
was true of two sections and false of five, which then had **no date at all**
|
||||||
|
on 3000×3000, 2668×1704, 257,278 B and the rest. **A vaguer wrong claim was
|
||||||
|
made more specifically wrong.** Now a floor plus overrides — the only shape
|
||||||
|
that covers every section without mis-dating one.
|
||||||
|
- **The `prefers-color-scheme` alternative was dismissed on a mechanism that is
|
||||||
|
wrong.** It does not key off the page's colour scheme; it reports the user's
|
||||||
|
preference, and this site declares `color-scheme` nowhere (`git grep -n
|
||||||
|
color-scheme -- src`, exit 1) — so it resolves to the same preference that
|
||||||
|
makes the tab strip dark. **A wrong reason there is load-bearing**: it is what
|
||||||
|
would stop the next implementer re-examining an accepted cost. The real
|
||||||
|
objections are recorded as **untested**.
|
||||||
|
- **"Roughly half the mark's visible pixels are absent on dark" overstated its
|
||||||
|
own measurement.** They are painted and fall below 3:1, and "visible pixels"
|
||||||
|
(`alpha > 0`) is 109 / 388 / 805, which is not what was counted either. Now:
|
||||||
|
about half as many pixels reach 3:1.
|
||||||
|
- **The iOS constant was hedged in one file and asserted flat in two** — R13 and
|
||||||
|
`docs/06`, the two places a caveat matters most, since §12 is read every
|
||||||
|
session and `docs/06` is what an operator works at cutover. Both now point.
|
||||||
|
- **`docs/06` restated six of the doc's figures and had already diverged**, within
|
||||||
|
one day: it quoted 11.28:1 / 13.62:1 unlabelled, which are the **32 px** row,
|
||||||
|
after a 16 px row (8.82:1 / 12.58:1) was added; and it said "≥3:1 pixels" where
|
||||||
|
the doc said "visible pixels", a different denominator. Cut to a pointer.
|
||||||
|
- **The newly ticked human-pass item still carried an unstruck imperative** —
|
||||||
|
*"START WITH `/legal/privacy/` … IT IS BLOCKER 2"* — on an item recording the
|
||||||
|
pass as done, pointing at a blocker the same page had just closed. Struck.
|
||||||
|
- **`npm run icons` was labelled `LOCAL ONLY`**, which this file defines as
|
||||||
|
*cannot run in CI*. It is pure Node and could; it is out of the build because a
|
||||||
|
build should not silently rewrite an artefact a human approved. Labelled
|
||||||
|
`REGENERATOR`.
|
||||||
|
|
||||||
## 2026-09-02 (ap) — `/legal/privacy/` §Who can see it is cut to four plain statements and the mechanics move to the evidence file; the consent checkbox names SML Company Ltd; Q64 closes MOOT rather than answered
|
## 2026-09-02 (ap) — `/legal/privacy/` §Who can see it is cut to four plain statements and the mechanics move to the evidence file; the consent checkbox names SML Company Ltd; Q64 closes MOOT rather than answered
|
||||||
|
|
||||||
**Two rulings from Pouya, 2026-09-02.** **(1)** §Who can see it **stays
|
**Two rulings from Pouya, 2026-09-02.** **(1)** §Who can see it **stays
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ npm run check:intake # the form's field table vs the Lambda's — they are two
|
|||||||
npm run og:proof # every og:image resolves; every card headline IS its page's <h1>
|
npm run og:proof # every og:image resolves; every card headline IS its page's <h1>
|
||||||
npm run lighthouse # the performance budget. LOCAL ONLY — needs Chrome, not in CI
|
npm run lighthouse # the performance budget. LOCAL ONLY — needs Chrome, not in CI
|
||||||
npm run bio:pdf # re-renders the committed one-page PDF from /bio/. LOCAL ONLY
|
npm run bio:pdf # re-renders the committed one-page PDF from /bio/. LOCAL ONLY
|
||||||
|
npm run icons # re-derives public/favicon.ico from the brand master. REGENERATOR
|
||||||
npm run lint # eslint + prettier check
|
npm run lint # eslint + prettier check
|
||||||
npm run format # prettier — rewrite files in place
|
npm run format # prettier — rewrite files in place
|
||||||
npm run deploy # build + deploy from this machine (see docs/06)
|
npm run deploy # build + deploy from this machine (see docs/06)
|
||||||
@@ -153,6 +154,17 @@ checklist, and **they are deliberately not wired into `npm run build` or either
|
|||||||
deploy path.** Do not describe either as gating a deploy: a check described as
|
deploy path.** Do not describe either as gating a deploy: a check described as
|
||||||
running where it cannot is the defect `AGENTS.md` Q22 turned out to be.
|
running where it cannot is the defect `AGENTS.md` Q22 turned out to be.
|
||||||
|
|
||||||
|
**`bio:pdf` and `icons` are REGENERATORS, not gates** — they rewrite committed
|
||||||
|
artefacts (`public/pouya-lajevardi-bio.pdf`, `public/favicon.ico`) rather than
|
||||||
|
checking anything, so they are run deliberately and their output is committed.
|
||||||
|
⚠️ **`LOCAL ONLY` above means a different thing for each, so do not read the two
|
||||||
|
labels as one.** `bio:pdf` **cannot** run in CI — it drives a browser. `icons`
|
||||||
|
is pure Node and **could**; it is out of the build because a build should not
|
||||||
|
silently rewrite an artefact a human approved, which is why it is labelled
|
||||||
|
`REGENERATOR` rather than `LOCAL ONLY`. It regenerates the favicon **only** — the touch icon
|
||||||
|
is hand-made and must stay opaque cream (`docs/reference/brand-assets.md`
|
||||||
|
§The icon set).
|
||||||
|
|
||||||
**`og:proof` and `check:intake` exist because two facts in this repo are
|
**`og:proof` and `check:intake` exist because two facts in this repo are
|
||||||
deliberately duplicated**, and a duplicated fact needs a mechanism rather than a
|
deliberately duplicated**, and a duplicated fact needs a mechanism rather than a
|
||||||
comment. `og:proof` compares each generated OG card's headline against its page's
|
comment. `og:proof` compares each generated OG card's headline against its page's
|
||||||
@@ -363,7 +375,7 @@ an operator to delete the three records that authenticate outbound mail —
|
|||||||
under the heading "Never delete".
|
under the heading "Never delete".
|
||||||
|
|
||||||
**A measurement is a claim about your instrument until you check the
|
**A measurement is a claim about your instrument until you check the
|
||||||
instrument.** This has now cost six times, and the shape is identical every
|
instrument.** This has now cost eight times, and the shape is identical every
|
||||||
time: a number that looks like a finding, from a probe nobody validated.
|
time: a number that looks like a finding, from a probe nobody validated.
|
||||||
|
|
||||||
- `timeout 60 ls "$DRIVE"` — **the command never ran.** `timeout` is not
|
- `timeout 60 ls "$DRIVE"` — **the command never ran.** `timeout` is not
|
||||||
@@ -391,6 +403,20 @@ time: a number that looks like a finding, from a probe nobody validated.
|
|||||||
from the list. **A `grep -o` window count is not an enumeration** — to count
|
from the list. **A `grep -o` window count is not an enumeration** — to count
|
||||||
occurrences of a string, iterate every match position, or `grep -o` the bare
|
occurrences of a string, iterate every match position, or `grep -o` the bare
|
||||||
string with no context window.
|
string with no context window.
|
||||||
|
- **An ink bounding box of `48x48` filling the whole 48 px favicon** — which
|
||||||
|
reads as *the regeneration blew the mark up to the full square*. The probe
|
||||||
|
identified ink as "differs from cream", which is **correct on a matted icon and
|
||||||
|
meaningless on a transparent one**, because `alpha = 0` pixels still carry RGB
|
||||||
|
`(0,0,0)`. **Nothing about the probe changed — the class of input did**, and a
|
||||||
|
probe cannot tell you that. Composite over a known ground first, so both things
|
||||||
|
compared are the kind of thing the instrument was built for.
|
||||||
|
- **A contrast ratio of `14.02:1` for the mark's darkest ink** — computed from a
|
||||||
|
raw channel value **without asking what alpha it is painted at**. Exactly one
|
||||||
|
pixel carried that colour and its alpha was 251; a `>= 250` filter let it
|
||||||
|
through and the ratio was then taken as if it were opaque. **It is a number
|
||||||
|
about a colour that is never painted.** Composite against the actual ground
|
||||||
|
before measuring contrast, and take "the ink colour" only from fully opaque
|
||||||
|
pixels.
|
||||||
|
|
||||||
So before acting on a number: say what it is a number *of*; confirm the command
|
So before acting on a number: say what it is a number *of*; confirm the command
|
||||||
actually ran and read its exit status; and check it against a second method that
|
actually ran and read its exit status; and check it against a second method that
|
||||||
|
|||||||
+60
-15
@@ -395,8 +395,22 @@ Then invalidate `/*`.
|
|||||||
> reversing them puts 22 of 23 pages behind a 403 for as long as a CloudFront
|
> reversing them puts 22 of 23 pages behind a 403 for as long as a CloudFront
|
||||||
> deployment takes.
|
> deployment takes.
|
||||||
|
|
||||||
> 🛑 **TWO THINGS BLOCK THIS ENTIRE LIST AS AT 2026-09-02: ONE WAITING PERIOD
|
> 🛑 **ONE THING BLOCKS THIS ENTIRE LIST AS AT 2026-09-02, AND IT IS A WAITING
|
||||||
> AND ONE READ-THROUGH.**
|
> PERIOD RATHER THAN A TASK: Q60.**
|
||||||
|
>
|
||||||
|
> ✅ **THE READ-THROUGH IS COMPLETE — Pouya, 2026-09-02, and it returned ONE
|
||||||
|
> FINDING WHICH WAS NOT COPY.** `public/favicon.ico` shipped with no
|
||||||
|
> transparency; fixed and verified the same day (see **Favicon set complete**
|
||||||
|
> below). His read carried the approvals with it, in terms: the
|
||||||
|
> `/legal/privacy/` §Who can see it wording, the **SML Company Ltd** consent
|
||||||
|
> line, and `/med-arb/` **as shipped**. That discharges blocker 2 and every
|
||||||
|
> wording sign-off that had been routed into it.
|
||||||
|
>
|
||||||
|
> ⚠️ **THE COUNT NOW READS ONE AGAIN, AND THE EARLIER ONE WAS A DEFECT — READ
|
||||||
|
> THE REASON, NOT THE NUMBER.** It said ONE earlier on 2026-09-02 because an
|
||||||
|
> approval had gone **missing** from the list; it says ONE now because the pass
|
||||||
|
> that approval was routed into has been **done**. A tally cannot tell those
|
||||||
|
> apart, which is the point the note below has been making all day.
|
||||||
>
|
>
|
||||||
> ⚠️ *(The count has moved repeatedly in one day and the DIRECTION is the only
|
> ⚠️ *(The count has moved repeatedly in one day and the DIRECTION is the only
|
||||||
> part worth reading — the number of moves is deliberately not stated, because a
|
> part worth reading — the number of moves is deliberately not stated, because a
|
||||||
@@ -421,12 +435,12 @@ Then invalidate `/*`.
|
|||||||
> record is written and it does not call failure before **7 days**, so
|
> record is written and it does not call failure before **7 days**, so
|
||||||
> **start it before anything else on this page.** It is the one blocker
|
> **start it before anything else on this page.** It is the one blocker
|
||||||
> that is a waiting period rather than a task.
|
> that is a waiting period rather than a task.
|
||||||
> 2. **Pouya has not yet read every page against `AGENTS.md` §4.** The human
|
> 2. ✅ **DONE 2026-09-02 — Pouya read every page against `AGENTS.md` §4.** The
|
||||||
> pass — the other half of D20, and not delegable. **It is also where the
|
> human pass, the other half of D20 and not delegable. It was also where the
|
||||||
> §Who can see it approval now lands:** Pouya ruled on 2026-09-02 that the
|
> §Who can see it approval was routed, and his ruling that the read-through
|
||||||
> read-through *is* the approval and that nothing is to be held open waiting
|
> *is* the approval means that sign-off is now discharged rather than
|
||||||
> on a separate wording sign-off. The item under **Copy and claims** below
|
> pending. **Sole finding: the favicon's opaque ground.** No copy finding on
|
||||||
> carries what to read first and why.
|
> any of the 23 pages.
|
||||||
>
|
>
|
||||||
> ✅ **CLOSED 2026-09-02 — Q64, MOOT.** It asked whether anyone else holds the
|
> ✅ **CLOSED 2026-09-02 — Q64, MOOT.** It asked whether anyone else holds the
|
||||||
> AWS root password or its MFA device, because the page published *"has no
|
> AWS root password or its MFA device, because the page published *"has no
|
||||||
@@ -650,12 +664,25 @@ the decision is re-readable rather than re-litigated.
|
|||||||
count has been wrong twice, and this line carried "eight" for a round after
|
count has been wrong twice, and this line carried "eight" for a round after
|
||||||
the comment itself had been corrected to nine (`adversarial-reviewer`,
|
the comment itself had been corrected to nine (`adversarial-reviewer`,
|
||||||
round 2). Read the list, not a number
|
round 2). Read the list, not a number
|
||||||
- [ ] **Pouya has read every page against `AGENTS.md` §4.** The human pass. It is
|
- [x] ✅ **DONE 2026-09-02 — Pouya has read every page against `AGENTS.md` §4.**
|
||||||
the other half of D20 and it is not delegable — his reading is what the
|
The human pass, the other half of D20 and not delegable — his reading is
|
||||||
per-step audit was traded for.
|
what the per-step audit was traded for. **It returned one finding across 23
|
||||||
⚠️ **START WITH `/legal/privacy/` §Who can see it. IT IS BLOCKER 2 IN THE
|
pages and that finding was not copy:** the favicon shipped with an opaque
|
||||||
CALLOUT ABOVE, AND THIS READ *IS* THE APPROVAL** — Pouya ruled on
|
cream ground. **The `/legal/privacy/` §Who can see it wording, the
|
||||||
2026-09-02 that nothing waits on a separate wording sign-off. Every
|
SML Company Ltd consent line and `/med-arb/` as shipped are approved by
|
||||||
|
this read**, per his ruling that the read-through *is* the approval.
|
||||||
|
⚠️ **This does NOT discharge `claims-auditor`'s cutover pass**, which is a
|
||||||
|
separate item on this list: D20 traded the per-step machine audit for the
|
||||||
|
human pass **plus** one machine pass over the finished site, and one of
|
||||||
|
those two has now happened.
|
||||||
|
~~⚠️ **START WITH `/legal/privacy/` §Who can see it. IT IS BLOCKER 2 IN THE
|
||||||
|
CALLOUT ABOVE, AND THIS READ *IS* THE APPROVAL**~~ — **struck 2026-09-02:
|
||||||
|
the pass is DONE, and an unstruck imperative on a ticked item told an
|
||||||
|
operator to begin a read this page also records as finished, pointing at a
|
||||||
|
blocker that no longer exists** (`adversarial-reviewer`, round 2). What it
|
||||||
|
said remains true of what happened: Pouya ruled on
|
||||||
|
2026-09-02 that nothing waits on a separate wording sign-off, and he read
|
||||||
|
§Who can see it first. Every
|
||||||
sentence in it changed three times that day — Q62's ruling, Q63's, then the
|
sentence in it changed three times that day — Q62's ruling, Q63's, then the
|
||||||
ruling that cut it to **four plain statements** — and it is the only
|
ruling that cut it to **four plain statements** — and it is the only
|
||||||
section on the site whose subject lives entirely outside this repository.
|
section on the site whose subject lives entirely outside this repository.
|
||||||
@@ -972,7 +999,25 @@ the decision is re-readable rather than re-litigated.
|
|||||||
instead. A `Disallow` will not do it: a blocked URL can still be listed.
|
instead. A `Disallow` will not do it: a blocked URL can still be listed.
|
||||||
Found by `adversarial-reviewer`, 2026-08-31
|
Found by `adversarial-reviewer`, 2026-08-31
|
||||||
- [ ] Booking link works, including the no-JavaScript fallback — **conditional on R6**; booking is parked and `CONTACT.bookingUrl` is `null`, so nothing renders and this passes vacuously until a tool is chosen. **Nothing on `/contact/` mentions booking**, deliberately
|
- [ ] Booking link works, including the no-JavaScript fallback — **conditional on R6**; booking is parked and `CONTACT.bookingUrl` is `null`, so nothing renders and this passes vacuously until a tool is chosen. **Nothing on `/contact/` mentions booking**, deliberately
|
||||||
- [ ] Favicon set complete
|
- [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`
|
||||||
|
on every pixel, the ground opaque cream — so the tab icon showed as a cream
|
||||||
|
rectangle on any dark tab strip. `public/favicon.ico` is now transparent,
|
||||||
|
regenerated by `npm run icons` from the committed master and verified
|
||||||
|
programmatically and by eye, on dark grounds and light.
|
||||||
|
⚠️ **`public/apple-touch-icon.png` STAYS OPAQUE CREAM AND MUST NOT BE
|
||||||
|
"FIXED" TO MATCH.** The reason is a platform behaviour — iOS composites a
|
||||||
|
transparent touch icon onto black — **stated by Pouya on 2026-09-02 and not
|
||||||
|
re-tested on a handset**; the item directly below is where it would be. The
|
||||||
|
touch icon is byte-identical across this change.
|
||||||
|
⚠️ **AND THE CHANGE IS NOT FREE ON DARK.** The maroon half of the ribbon
|
||||||
|
effectively drops out against a dark tab strip; the champagne half carries
|
||||||
|
the mark. **The figures are deliberately NOT repeated here** — they live in
|
||||||
|
`docs/reference/brand-assets.md` §The icon set, with the method, and a copy
|
||||||
|
on this page had already gone stale within a day by quoting the 32 px row
|
||||||
|
as if it were the general case (`adversarial-reviewer`, round 2). Read them
|
||||||
|
there.
|
||||||
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
|
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
|
||||||
- [ ] Tested at 320 px and at 200% zoom
|
- [ ] Tested at 320 px and at 200% zoom
|
||||||
- [x] ✅ **THE 200%-TEXT NAV OVERFLOW IS FIXED, 2026-09-01 — THIS ITEM IS
|
- [x] ✅ **THE 200%-TEXT NAV OVERFLOW IS FIXED, 2026-09-01 — THIS ITEM IS
|
||||||
|
|||||||
@@ -5,9 +5,22 @@ is `CLAUDE.md`'s rule and `AGENTS.md` R14, and this file exists because the
|
|||||||
infinity mark was reconstructed wrongly and **two adversarial review passes could
|
infinity mark was reconstructed wrongly and **two adversarial review passes could
|
||||||
not catch it**, since the real artwork was not in the repo to compare against.
|
not catch it**, since the real artwork was not in the repo to compare against.
|
||||||
|
|
||||||
**Every measurement below is `[verified 2026-08-26]`** — computed with `sharp`
|
**The measurements here are computed with `sharp` against the files in this
|
||||||
against the files in this repository, and re-derivable by anyone from the
|
repository**, and are re-derivable by anyone from the commands given.
|
||||||
commands given. Nothing here is quoted from an external source.
|
|
||||||
|
**Unless a section says otherwise, every figure below is `[verified
|
||||||
|
2026-08-26]`. Where a section carries its own stamp, that stamp wins** — the
|
||||||
|
render ladders are `[measured 2026-08-27]` and §The icon set is `[verified
|
||||||
|
2026-09-02]`. ⚠️ *This line has been wrong in both directions in one week: first
|
||||||
|
as a blanket 2026-08-26 that was already stale for the ladders, then as
|
||||||
|
"each section carries its own stamp" when five sections carry none. It is a floor
|
||||||
|
plus overrides because that is the only shape that covers every section without
|
||||||
|
mis-dating one* (`adversarial-reviewer`, rounds 1 and 2).
|
||||||
|
|
||||||
|
⚠️ **Two things in §The icon set are NOT read off this repository and are
|
||||||
|
labelled where they appear:** the tab-strip colours used in the contrast figures,
|
||||||
|
and iOS's handling of a transparent touch icon. Neither is derivable from
|
||||||
|
anything committed, so neither is stamped as if it were.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
@@ -103,6 +116,138 @@ Passing an explicit `width` is load-bearing: without it Astro emits the
|
|||||||
untouched 2668 px master as the `<img src>` fallback — **1,146,406 bytes** —
|
untouched 2668 px master as the `<img src>` fallback — **1,146,406 bytes** —
|
||||||
which any client without AVIF or WebP support would actually download.
|
which any client without AVIF or WebP support would actually download.
|
||||||
|
|
||||||
|
## The icon set
|
||||||
|
|
||||||
|
`[verified 2026-09-02 — every figure below read off the icon files in this
|
||||||
|
repository, EXCEPT the two external constants flagged inline]`
|
||||||
|
|
||||||
|
| File | What it is | Ground |
|
||||||
|
|---|---|---|
|
||||||
|
| `public/favicon.ico` | 16, 32 and 48 px frames, each a PNG-encoded 32-bit RGBA image inside the ICO container | **Transparent** |
|
||||||
|
| `public/apple-touch-icon.png` | 180 × 180 | **Opaque cream `#faf7f2`, and that is deliberate — see below** |
|
||||||
|
|
||||||
|
**The two grounds differ on purpose, and this is the line that stops someone
|
||||||
|
"fixing" it.** ⚠️ *The reason is an external platform behaviour, not a repository
|
||||||
|
fact, and it is recorded here as what it is:* **iOS does not honour transparency
|
||||||
|
in a touch icon — it composites it onto black**, so a transparent touch icon
|
||||||
|
ships a black tile on the home screen. Stated by Pouya in his ruling of
|
||||||
|
**2026-09-02** and not independently re-tested here; **it has not been checked on
|
||||||
|
a handset in this repository, and the place that does check it is the unticked
|
||||||
|
`docs/06` item "Tested on iOS Safari…".** The tab favicon has the opposite
|
||||||
|
requirement: a tab strip is dark for many readers, and an opaque ground shows
|
||||||
|
there as a visible rectangle around the mark. So the favicon is transparent, the
|
||||||
|
touch icon is matted, and **neither should be changed to match the other.**
|
||||||
|
|
||||||
|
### Composition
|
||||||
|
|
||||||
|
The mark spans **7/8 of the canvas width**, centred on both axes — 14/16, 28/32,
|
||||||
|
42/48, and 158/180 on the touch icon. That was measured off the icons as they
|
||||||
|
already shipped, so a regeneration reproduces the composition rather than
|
||||||
|
restyling the mark.
|
||||||
|
|
||||||
|
### Regenerating
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run icons
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/icons.mjs` resizes `src/assets/brand/sml-infinity-mark.png` onto a
|
||||||
|
transparent square canvas and assembles the ICO container itself, because
|
||||||
|
`sharp` does not write `.ico`. **It writes the favicon only** and never touches
|
||||||
|
the touch icon. It refuses to run if the render source has no alpha channel, and
|
||||||
|
it asserts that the source is still byte-identical to the documented crop of
|
||||||
|
`sml-infinity-mark-master.png` — R14, so the icon stays traceable to committed
|
||||||
|
artwork. It verifies a **candidate** file and renames it into place only on
|
||||||
|
success, so a rejected build cannot replace a good favicon.
|
||||||
|
|
||||||
|
**It also carries the halo test**, because the check that matters was living
|
||||||
|
only in prose here while the generator could not run it
|
||||||
|
(`adversarial-reviewer`, round 2). For each frame it takes every painted pixel
|
||||||
|
touching a fully transparent one and measures how many sit within 20 of cream:
|
||||||
|
|
||||||
|
| | boundary px | near cream | share |
|
||||||
|
|---|---|---|---|
|
||||||
|
| correct, 16 / 32 / 48 px | 61 / 146 / 258 | 1 / 2 / 5 | **1.6 / 1.4 / 1.9 %** |
|
||||||
|
| haloed fixture, 16 px | 56 | 11 | **19.6 %** |
|
||||||
|
|
||||||
|
The gate is **10 %**, roughly 5× clear of both. ⚠️ **The first version of this
|
||||||
|
guard inspected only partial-alpha pixels and MISSED the fixture completely** —
|
||||||
|
a knockout sets alpha per pixel and leaves **no partial alpha at all**, so there
|
||||||
|
was nothing for it to look at. The fixture is built by matting the mark on cream
|
||||||
|
and then knocking the ground out by alpha, which is the defect exactly; it fires
|
||||||
|
at 19.6 % and leaves `public/favicon.ico` untouched. **A guard that cannot see
|
||||||
|
the defect it is named for is worse than none**, and this one passed the real
|
||||||
|
icon while blind, which is the shape that ends a check instead of starting one.
|
||||||
|
|
||||||
|
### Why the favicon was regenerated, 2026-09-02
|
||||||
|
|
||||||
|
It had **no transparency at all**: all three frames declared a 32-bit alpha
|
||||||
|
channel and then carried `alpha = 255` on every one of their 256 / 1,024 / 2,304
|
||||||
|
pixels, with the ground opaque cream `rgba(250,247,242,255)`. Found by Pouya on a
|
||||||
|
read-through, confirmed by parsing the container directly.
|
||||||
|
|
||||||
|
Three measurements stand behind the replacement.
|
||||||
|
|
||||||
|
**1. The composition did not change.** Composited back onto cream, the new icon
|
||||||
|
reproduces the old matted one to within **1 of 255 on every channel of every
|
||||||
|
pixel** at all three sizes (mean delta 0.01–0.02, 0 pixels over a delta of 8).
|
||||||
|
So the regeneration did not restyle, rescale or reposition the mark.
|
||||||
|
|
||||||
|
⚠️ **That is NOT a test for a halo, and an earlier draft of this file filed it as
|
||||||
|
one.** Composite-onto-cream returns ~0 whether the edge is correct *or* is a
|
||||||
|
cream-matted edge that has merely had its background knocked out — the second
|
||||||
|
case composites straight back to what it came from. Both branches pass, so the
|
||||||
|
test cannot discriminate. Found by `adversarial-reviewer`, 2026-09-02.
|
||||||
|
|
||||||
|
**2. No halo — the measurement that does discriminate.** Read the RGB the
|
||||||
|
partial-alpha pixels actually carry. A cream halo means that RGB is near cream; a
|
||||||
|
correct export means it is the ribbon's own colour. Of the partial-alpha pixels,
|
||||||
|
**0 of 94 / 300 / 603** are within 12 of `rgb(250,247,242)`; the nearest is 13
|
||||||
|
away, the mean distance is **174.0 / 178.3 / 177.0**, and the mean colour per
|
||||||
|
frame is about `rgb(118,86,74)` — maroon-brown, not cream and not black.
|
||||||
|
|
||||||
|
**3. Legible on dark and on light, and NOT at the same cost.** ⚠️ *The ground
|
||||||
|
colours below are external constants, not repository facts: `#202124` is
|
||||||
|
Chrome's dark tab strip and `#f1f3f4` its light one, both `[observed
|
||||||
|
2026-09-02]`.* All ratios are computed on **composited** pixels, and the ink
|
||||||
|
colours are **fully opaque** pixels only — a ratio taken from a raw channel value
|
||||||
|
is a claim about a colour that is never painted, which is how an earlier draft
|
||||||
|
came to quote `14.02:1` for a pixel whose alpha is 251.
|
||||||
|
|
||||||
|
| | darkest opaque ink | lightest opaque ink | vs `#202124` | vs `#f1f3f4` |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 16 px | `rgb(70,33,33)` | `rgb(216,188,143)` | champagne **8.82:1** | maroon **12.58:1** |
|
||||||
|
| 32 px | `rgb(60,29,29)` | `rgb(234,214,172)` | champagne **11.28:1** | maroon **13.62:1** |
|
||||||
|
| 48 px | `rgb(61,26,26)` | `rgb(238,219,175)` | champagne **11.80:1** | maroon **13.86:1** |
|
||||||
|
|
||||||
|
Whichever ground it sits on, one end of the ribbon carries the silhouette. **But
|
||||||
|
the other end does not merely dim — on dark it goes.** The maroon lobe
|
||||||
|
composites to **1.04–1.15:1**, and counting pixels that reach 3:1 against each
|
||||||
|
ground gives **23 / 87 / 189** on dark against **40 / 168 / 378** on light, at
|
||||||
|
16 / 32 / 48 px. **So about half as many pixels reach 3:1 on dark as on light.**
|
||||||
|
⚠️ *Those pixels are not absent — they are painted and fall below 3:1, and the
|
||||||
|
mark's full outline is still there, dim. An earlier draft said "roughly half the
|
||||||
|
mark's visible pixels are absent", which is neither what was counted (pixels at
|
||||||
|
`alpha > 0` are 109 / 388 / 805) nor what a render shows* — `adversarial-reviewer`,
|
||||||
|
round 2. The previous opaque-cream icon was still more legible on dark.
|
||||||
|
|
||||||
|
**That is the price of the change and it was accepted, not overlooked.** What was
|
||||||
|
bought is the removal of a cream rectangle from every dark tab strip and from
|
||||||
|
white. Raised by `adversarial-reviewer`, 2026-09-02, against a draft that
|
||||||
|
recorded the change as costless.
|
||||||
|
|
||||||
|
**The obvious alternative is an icon pair keyed on `prefers-color-scheme`, and it
|
||||||
|
was NOT ruled out on measurement.** ⚠️ *An earlier draft dismissed it on a
|
||||||
|
mechanism that is wrong: it said the query keys off the page's colour scheme. It
|
||||||
|
does not — it reports the user's system or browser preference, and this site
|
||||||
|
declares `color-scheme` nowhere (`git grep -n color-scheme -- src`, exit 1), so
|
||||||
|
it would resolve to the same preference that makes the tab strip dark.* The real
|
||||||
|
objections are **untested here** and are recorded as such: `media` on
|
||||||
|
`<link rel="icon">` is unevenly supported for raster icons, a browser theme can
|
||||||
|
be set independently of the system preference, and it doubles the artefact
|
||||||
|
`npm run icons` has to keep in sync. **If this cost is ever revisited, that is
|
||||||
|
the option to test** — do not re-dismiss it on the reason struck above.
|
||||||
|
|
||||||
## The colours are the artwork's, not the palette's
|
## The colours are the artwork's, not the palette's
|
||||||
|
|
||||||
`tokens.css` is not involved. The ribbon carries its own gradient and it is
|
`tokens.css` is not involved. The ribbon carries its own gradient and it is
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,8 @@
|
|||||||
"lighthouse": "node scripts/lighthouse.mjs",
|
"lighthouse": "node scripts/lighthouse.mjs",
|
||||||
"og:proof": "node scripts/og-proof.mjs",
|
"og:proof": "node scripts/og-proof.mjs",
|
||||||
"check:intake": "node scripts/check-intake.mjs",
|
"check:intake": "node scripts/check-intake.mjs",
|
||||||
"bio:pdf": "node scripts/bio-pdf.mjs"
|
"bio:pdf": "node scripts/bio-pdf.mjs",
|
||||||
|
"icons": "node scripts/icons.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^7.0.8",
|
"@astrojs/mdx": "^7.0.8",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,278 @@
|
|||||||
|
/**
|
||||||
|
* Regenerates `public/favicon.ico` from the committed brand master.
|
||||||
|
*
|
||||||
|
* LOCAL ONLY, like `bio:pdf`. Not wired into `npm run build` or either deploy
|
||||||
|
* path — the icons are committed artefacts and this is what re-derives them.
|
||||||
|
*
|
||||||
|
* Writes the favicon ONLY. `apple-touch-icon.png` is deliberately not touched
|
||||||
|
* and must stay opaque — `docs/reference/brand-assets.md` §The icon set.
|
||||||
|
*/
|
||||||
|
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const root = fileURLToPath(new URL('..', import.meta.url));
|
||||||
|
const MARK = `${root}src/assets/brand/sml-infinity-mark.png`;
|
||||||
|
const MASTER = `${root}src/assets/brand/sml-infinity-mark-master.png`;
|
||||||
|
const OUT = `${root}public/favicon.ico`;
|
||||||
|
|
||||||
|
/** Sizes carried in the container, ascending — the order BaseLayout declares. */
|
||||||
|
const SIZES = [16, 32, 48];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mark spans 7/8 of the canvas and is centred on both axes. Not a taste
|
||||||
|
* decision at this point: it is the composition already shipping, measured off
|
||||||
|
* the previous icon at all three sizes (14/16, 28/32, 42/48) and off the touch
|
||||||
|
* icon (158/180). Regenerating for alpha must not also restyle the mark.
|
||||||
|
*/
|
||||||
|
const INK_FRACTION = 7 / 8;
|
||||||
|
|
||||||
|
/** `--cream` — the ground the previous icon was matted against. */
|
||||||
|
const CREAM = [250, 247, 242];
|
||||||
|
|
||||||
|
const die = (msg) => {
|
||||||
|
rmSync(`${OUT}.candidate`, { force: true });
|
||||||
|
console.error(`icons: ${msg}`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R14 — the icon must be traceable to the artwork in this repository, not to a
|
||||||
|
* file on someone's disk. The render source is a tight crop of the master, so
|
||||||
|
* assert it still IS that crop before deriving anything from it.
|
||||||
|
*/
|
||||||
|
async function assertProvenance() {
|
||||||
|
const mark = await sharp(MARK).metadata();
|
||||||
|
const crop = { left: 159, top: 646, width: 2668, height: 1704 };
|
||||||
|
if (mark.width !== crop.width || mark.height !== crop.height) {
|
||||||
|
die(
|
||||||
|
`render source is ${mark.width}x${mark.height}, expected ${crop.width}x${crop.height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const [a, b] = await Promise.all([
|
||||||
|
sharp(MASTER).extract(crop).raw().toBuffer(),
|
||||||
|
sharp(MARK).raw().toBuffer(),
|
||||||
|
]);
|
||||||
|
if (!a.equals(b))
|
||||||
|
die('render source is no longer the documented crop of the master');
|
||||||
|
console.log(
|
||||||
|
`provenance: ${crop.width}x${crop.height} at (${crop.left},${crop.top}) of the master — identical`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The whole point of the regeneration. A source without alpha would mean
|
||||||
|
* deriving a mask from the cream ground, which is a different and lossier job —
|
||||||
|
* so fail rather than silently ship a matted icon again.
|
||||||
|
*/
|
||||||
|
async function loadMark() {
|
||||||
|
const meta = await sharp(MARK).metadata();
|
||||||
|
if (!meta.hasAlpha)
|
||||||
|
die(
|
||||||
|
`${MARK} has no alpha channel — cannot export a transparent icon from it`,
|
||||||
|
);
|
||||||
|
const { data, info } = await sharp(MARK)
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
let transparent = 0;
|
||||||
|
for (let p = 3; p < data.length; p += 4) if (data[p] === 0) transparent++;
|
||||||
|
if (transparent === 0)
|
||||||
|
die(`${MARK} declares alpha but every pixel is opaque`);
|
||||||
|
console.log(
|
||||||
|
`source: ${info.width}x${info.height} alpha, ${transparent} fully transparent px`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize onto a TRANSPARENT canvas. sharp premultiplies around the resample, so
|
||||||
|
* the ribbon's anti-aliased edge blends toward its own colour rather than
|
||||||
|
* toward the RGB sitting under alpha 0 — that is the cream halo this change
|
||||||
|
* exists to remove, and it would come straight back with a matted background.
|
||||||
|
*/
|
||||||
|
async function frame(size) {
|
||||||
|
const w = Math.round(size * INK_FRACTION);
|
||||||
|
const png = await sharp(MARK)
|
||||||
|
.resize({
|
||||||
|
width: w,
|
||||||
|
kernel: 'lanczos3',
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: false,
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
const { height: h } = await sharp(png).metadata();
|
||||||
|
if (h > size) die(`size ${size}: mark is ${w}x${h}, taller than the canvas`);
|
||||||
|
const left = Math.round((size - w) / 2);
|
||||||
|
const top = Math.round((size - h) / 2);
|
||||||
|
const out = await sharp({
|
||||||
|
create: {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite([{ input: png, left, top }])
|
||||||
|
.png({ compressionLevel: 9, effort: 10, palette: false })
|
||||||
|
.toBuffer();
|
||||||
|
console.log(
|
||||||
|
` ${size}x${size}: mark ${w}x${h} at (${left},${top}), ${out.length} B`,
|
||||||
|
);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ICO container: 6-byte header, one 16-byte directory entry per frame, then the PNG payloads. */
|
||||||
|
function buildIco(frames) {
|
||||||
|
const header = Buffer.alloc(6);
|
||||||
|
header.writeUInt16LE(0, 0); // reserved
|
||||||
|
header.writeUInt16LE(1, 2); // type 1 = icon
|
||||||
|
header.writeUInt16LE(frames.length, 4);
|
||||||
|
|
||||||
|
const dir = Buffer.alloc(16 * frames.length);
|
||||||
|
let offset = header.length + dir.length;
|
||||||
|
frames.forEach(({ size, png }, i) => {
|
||||||
|
const e = i * 16;
|
||||||
|
dir[e] = size; // width — a byte; 0 would mean 256, which SIZES never is
|
||||||
|
dir[e + 1] = size; // height
|
||||||
|
dir[e + 2] = 0; // palette size — 0 for truecolour
|
||||||
|
dir[e + 3] = 0; // reserved
|
||||||
|
dir.writeUInt16LE(1, e + 4); // colour planes
|
||||||
|
dir.writeUInt16LE(32, e + 6); // bits per pixel
|
||||||
|
dir.writeUInt32LE(png.length, e + 8);
|
||||||
|
dir.writeUInt32LE(offset, e + 12);
|
||||||
|
offset += png.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Buffer.concat([header, dir, ...frames.map((f) => f.png)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-read the container FROM DISK and decode each frame, rather than inspecting
|
||||||
|
* the buffers we just built — a check that reads its own inputs proves nothing.
|
||||||
|
* (It is still `sharp` decoding `sharp`'s output, so it is not a second
|
||||||
|
* instrument. The independent reads are in `docs/reference/brand-assets.md`.)
|
||||||
|
*/
|
||||||
|
async function verify(path) {
|
||||||
|
const buf = readFileSync(path);
|
||||||
|
const count = buf.readUInt16LE(4);
|
||||||
|
if (count !== SIZES.length)
|
||||||
|
die(`container declares ${count} frames, expected ${SIZES.length}`);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const e = 6 + i * 16;
|
||||||
|
const size = buf[e];
|
||||||
|
const len = buf.readUInt32LE(e + 8);
|
||||||
|
const off = buf.readUInt32LE(e + 12);
|
||||||
|
if (off + len > buf.length)
|
||||||
|
die(`frame ${i}: range ${off}+${len} exceeds ${buf.length} B`);
|
||||||
|
const { data, info } = await sharp(buf.subarray(off, off + len))
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
if (info.width !== size || info.height !== size)
|
||||||
|
die(`frame ${i}: decoded ${info.width}x${info.height}, dir says ${size}`);
|
||||||
|
const corners = [
|
||||||
|
[0, 0],
|
||||||
|
[size - 1, 0],
|
||||||
|
[0, size - 1],
|
||||||
|
[size - 1, size - 1],
|
||||||
|
];
|
||||||
|
for (const [x, y] of corners) {
|
||||||
|
const a = data[(y * size + x) * 4 + 3];
|
||||||
|
if (a !== 0)
|
||||||
|
die(`frame ${size}: corner (${x},${y}) has alpha ${a}, expected 0`);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* THE CORNER AND TRANSPARENCY CHECKS CANNOT SEE A CREAM HALO. A frame whose
|
||||||
|
* edge was matted against cream and then had its background knocked out has
|
||||||
|
* clear corners, transparent pixels and opaque pixels, and passes every one
|
||||||
|
* of them. What distinguishes it is the colour the edge blends TOWARD.
|
||||||
|
*
|
||||||
|
* ⚠️ AND IT IS THE BOUNDARY, NOT THE PARTIAL-ALPHA PIXELS. A first version
|
||||||
|
* of this guard inspected only pixels at 0 < alpha < 255 and MISSED a
|
||||||
|
* purpose-built haloed fixture entirely, because a knockout sets alpha per
|
||||||
|
* pixel and leaves NO partial alpha at all — 0 such pixels in the fixture.
|
||||||
|
* A guard that cannot see the defect it is named for is worse than none.
|
||||||
|
*
|
||||||
|
* So: take every painted pixel that touches a fully transparent one, and
|
||||||
|
* measure how many sit near cream. Measured on this artwork — correct
|
||||||
|
* frames 1 / 2 / 5 of 61 / 146 / 258 boundary pixels (1.4-1.9%); the haloed
|
||||||
|
* fixture 33 of 115 (28.7%). The gate is 10%, roughly 5x clear of both.
|
||||||
|
*/
|
||||||
|
const NEAR_CREAM = 20;
|
||||||
|
const HALO_SHARE = 0.1;
|
||||||
|
const alphaAt = (x, y) =>
|
||||||
|
x < 0 || y < 0 || x >= size || y >= size
|
||||||
|
? 0
|
||||||
|
: data[(y * size + x) * 4 + 3];
|
||||||
|
let clear = 0;
|
||||||
|
let ink = 0;
|
||||||
|
let boundary = 0;
|
||||||
|
let boundaryNearCream = 0;
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
|
for (let x = 0; x < size; x++) {
|
||||||
|
const i = (y * size + x) * 4;
|
||||||
|
const a = data[i + 3];
|
||||||
|
if (a === 0) {
|
||||||
|
clear++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a === 255) ink++;
|
||||||
|
const onEdge =
|
||||||
|
alphaAt(x - 1, y) === 0 ||
|
||||||
|
alphaAt(x + 1, y) === 0 ||
|
||||||
|
alphaAt(x, y - 1) === 0 ||
|
||||||
|
alphaAt(x, y + 1) === 0;
|
||||||
|
if (!onEdge) continue;
|
||||||
|
boundary++;
|
||||||
|
const d = Math.max(
|
||||||
|
Math.abs(data[i] - CREAM[0]),
|
||||||
|
Math.abs(data[i + 1] - CREAM[1]),
|
||||||
|
Math.abs(data[i + 2] - CREAM[2]),
|
||||||
|
);
|
||||||
|
if (d <= NEAR_CREAM) boundaryNearCream++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const share = boundary === 0 ? 0 : boundaryNearCream / boundary;
|
||||||
|
if (clear === 0)
|
||||||
|
die(`frame ${size}: no transparent pixels — the ground is fully matted`);
|
||||||
|
if (ink === 0)
|
||||||
|
die(`frame ${size}: no opaque pixels — the mark did not render`);
|
||||||
|
if (boundary === 0)
|
||||||
|
die(`frame ${size}: no boundary pixels — cannot test the edge colour`);
|
||||||
|
if (share > HALO_SHARE)
|
||||||
|
die(
|
||||||
|
`frame ${size}: ${boundaryNearCream} of ${boundary} boundary pixels ` +
|
||||||
|
`(${(share * 100).toFixed(1)}%) sit within ${NEAR_CREAM} of cream — ` +
|
||||||
|
`the edge was matted against the ground before the ground was removed`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${size}x${size}: ${clear} transparent, ${ink} opaque, corners clear, ` +
|
||||||
|
`edge ${(share * 100).toFixed(1)}% near cream`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`verified ${path} (${buf.length} B)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertProvenance();
|
||||||
|
await loadMark();
|
||||||
|
console.log('frames:');
|
||||||
|
const frames = [];
|
||||||
|
for (const size of SIZES) frames.push({ size, png: await frame(size) });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verify a CANDIDATE file, then move it into place. Writing OUT first and
|
||||||
|
* verifying after would mean a failing check exits 1 having already replaced a
|
||||||
|
* good favicon with the one it just rejected — and nothing downstream re-checks,
|
||||||
|
* because this script is deliberately outside the build and both deploy paths.
|
||||||
|
*/
|
||||||
|
const candidate = `${OUT}.candidate`;
|
||||||
|
writeFileSync(candidate, buildIco(frames));
|
||||||
|
console.log('verify:');
|
||||||
|
try {
|
||||||
|
await verify(candidate);
|
||||||
|
} catch (err) {
|
||||||
|
rmSync(candidate, { force: true });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
renameSync(candidate, OUT);
|
||||||
|
console.log(`wrote ${OUT}`);
|
||||||
@@ -56,7 +56,11 @@ const { preloadSerifItalic = false, ...seo } = Astro.props;
|
|||||||
/* No SVG favicon. The mark is a shaded ribbon, not flat vector paths, so
|
/* No SVG favicon. The mark is a shaded ribbon, not flat vector paths, so
|
||||||
there is no honest SVG of it to serve — see InfinityMark.astro and
|
there is no honest SVG of it to serve — see InfinityMark.astro and
|
||||||
AGENTS.md Q38. The .ico carries 16/32/48, and is what crawlers request
|
AGENTS.md Q38. The .ico carries 16/32/48, and is what crawlers request
|
||||||
at the root regardless of what is declared here. */
|
at the root regardless of what is declared here.
|
||||||
|
|
||||||
|
The .ico is transparent and the touch icon is opaque cream ON PURPOSE —
|
||||||
|
do not change either to match the other:
|
||||||
|
docs/reference/brand-assets.md §The icon set. */
|
||||||
}
|
}
|
||||||
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
|
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
|||||||
Reference in New Issue
Block a user