feat: upgrade to Astro 7; harden the content schema; wire a11y linting
Amends D1 to pin the major explicitly (v7.x) rather than inherit it. The ^5.0.0 pin was recalled rather than checked and was two majors stale the day it was written, which meant shipping a framework carrying high-severity XSS advisories. CLAUDE.md now requires every version pin to be verified against the registry, and R11 requires re-checking at each build-order boundary. npm audit now reports 0 vulnerabilities, down from 16. Every Astro advisory is cleared; the residual 10 all traced to @lhci/cli, which is removed — it was the sole source of 7 high-severity findings, 0.15.1 is latest so there was no clean upgrade, and it cannot run without pages or a lighthouserc. Re-added at build step 7 with a freshly verified pin. Content collections migrated to the Content Layer API: src/content.config.ts, loader: glob(), z from astro/zod. Two review passes found seven defects in the fix itself, all now closed: - z.coerce.date() read an unquoted 20260801 as epoch milliseconds and yielded 1970-01-01 silently; the first replacement then accepted 2026-13-45 as an Invalid Date and rolled 2026-02-30 over to 2026-03-02. Dates are now anchored, date-only, parsed as UTC and round-tripped. - The title bound applied the SEO spec's 50-60 to the headline rather than the rendered <title>, which guaranteed 68-78 on every article and rejected all five planned launch headlines. Articles are now the documented exception: the headline is the <title>, no suffix. - An article could ship an image with no alt text, or whitespace-only alt. - Two schema comments asserted controls nothing enforced; both are now real refinements, each tested with a failing and a passing case. - PRACTICE_SLUGS and PRACTICE_AREAS could drift silently; a compile-time check now catches both directions. - eslint.config.js imported globals and @eslint/js undeclared, resolving by hoisting accident. - scripts/deploy-local.sh claimed parity with CI while skipping npm run check and two credential guards — on the only path this site can ship today. Accessibility linting is on (36 jsx-a11y rules) before step 1 writes the layout. An earlier claim in §7 that none was possible was wrong twice, and is corrected in AGENTS.md entry (t) along with the reasoning. Opens Q30 and Q31 for two unregistered claims in src/data/site.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
This commit is contained in:
co-authored by
Claude Opus 5
parent
0d8b63380a
commit
7514a49803
@@ -88,7 +88,7 @@ without an explicit instruction and a Change Log entry. `[verified 2026-08-25]`
|
||||
|
||||
| # | Decision | Chosen | Rejected alternatives |
|
||||
|---|---|---|---|
|
||||
| D1 | Framework | **Astro**, static output | Next.js; patching the single-file build; hand-written HTML |
|
||||
| D1 | Framework | **Astro, static output, v7.x.** *Amended 2026-08-26:* the major is now pinned deliberately rather than inherited. The original `^5.0.0` pin was **recalled, not checked, and was two majors stale the day it was written**. Upgrading costs three config files now and compounds with every page; the v5 advisories were **high-severity XSS in the framework rendering a public site**, and "we don't use those authoring patterns" is a claim about all future code, not just today's. Starting two majors behind means being three behind at launch | Next.js; patching the single-file build; hand-written HTML; staying on v5 |
|
||||
| D2 | Content scope | **Full re-architecture** — new IA, new pages, all copy rewritten | Copy rewrite on existing structure; audit only; technical layer only |
|
||||
| D3 | Hosting & CI/CD | **Git repo + Gitea Actions → existing S3 + CloudFront.** *Amended 2026-08-26:* self-hosted **Gitea**, repo `adr-sml`. Gitea Actions uses GitHub Actions syntax, so the workflow ports nearly as is — but Gitea is not an AWS OIDC provider, so deploys authenticate with a **scoped IAM user**, rotated quarterly | GitHub Actions + OIDC; Terraform/CDK IaC; Amplify; manual deploys |
|
||||
| D4 | Languages | **English only** | Full EN/FA bilingual; EN + one Farsi page; EN with FA scaffolding |
|
||||
@@ -188,7 +188,7 @@ cannot vouch for.
|
||||
| Testimonials, endorsements, quotes from counterparties | None exist. The prior site's testimonial was fabricated |
|
||||
| "Since 2009", "sixteen years", London / New York offices, Co. № 07452218 | Artefacts of the placeholder template. All false |
|
||||
| The name "S. M. Lawrence" | Fictitious founder from the template |
|
||||
| Guarantees of outcome, or superlatives ("best", "leading", "top-rated") | LSO marketing rules |
|
||||
| Guarantees of outcome, or superlatives ("best", "leading", "top-rated") | Unverifiable as written, and they read as insecure to the audience this site is for. Marketing rules for regulated professions also treat them as suspect. *(Reason restated 2026-08-26: this row previously read "LSO marketing rules", which asserts the regulatory status the row above marks NOT ESTABLISHED. It was the last surviving instance, flagged in entries (o) and (q) and answered in Q26 as already fixed — it was not. **Restated by Claude Code; the rule itself is unchanged and the restatement needs Pouya's ratification.**)* |
|
||||
| The word **"lawyer"** used of Pouya; "called to the bar"; "licensed"; "my law practice"; "my litigation practice"; any post-nominal implying a licence | D13. The site asserts the JD and nothing further |
|
||||
| Any phrasing that *implies* entitlement to practise law without saying so — "acts for clients", "represents parties", "my clients", "legal advice" | Same rule. Implication is the risk, not just the word. Describe the role factually instead: *Director of Firm Operations at a Toronto litigation and ADR boutique*, and the matter types worked on |
|
||||
| Q.Arb described as held, imminent, or "nearly complete" | It commenced August 2026 |
|
||||
@@ -235,11 +235,13 @@ the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — dec
|
||||
|
||||
| Thing | Value |
|
||||
|---|---|
|
||||
| Framework | Astro, `output: 'static'` `[verified 2026-08-25 — D1]` |
|
||||
| Node | 22 LTS, pinned in `.nvmrc`; `package.json` `engines.node` `>=22` `[verified 2026-08-26 — .nvmrc, package.json]` |
|
||||
| Framework | **Astro 7.2.7**, `output: 'static'` `[verified 2026-08-26 — npm view astro version, D1 as amended]`. `@astrojs/mdx` 7.0.8, `@astrojs/sitemap` 3.7.3, `sharp` 0.35.4 |
|
||||
| Lint toolchain | ESLint **10.9.1**, `@eslint/js` 10.0.1, `globals` 17.11.0, `eslint-plugin-astro` **3.1.0**, `eslint-plugin-jsx-a11y` 6.10.2, `typescript-eslint` 8.68.0, `typescript` 5.9.3 — **every pin at the registry's `latest` except `typescript`** `[verified 2026-08-26 — npm view, all 14 pins]`. `@eslint/js` and `globals` are declared explicitly; before 2026-08-26 `eslint.config.js` imported them and they resolved only by npm hoisting accident. **Accessibility linting is on** — `flat/jsx-a11y-recommended`, 36 rules `[verified 2026-08-26 — 7 rules fired on a deliberately inaccessible .astro file]`. `eslint-plugin-jsx-a11y@6.10.2` declares a stale `eslint ^3..^9` peer range; a one-line `overrides` entry in `package.json` resolves it. ⚠️ **`eslint-plugin-astro@3.1.0` declares `node ^22.22.3 \|\| ^24.16.0 \|\| >=26.3.0`**, which excludes Pouya's Node 25.6.0 — `npm install` prints EBADENGINE there. Dev-time only, and `nvm use` (Node 22 LTS, per `.nvmrc`) clears it. `typescript` is held at 5.x: `typescript-eslint` peers `<6.1.0` and `@astrojs/check` peers `^5 \|\| ^6`, so TypeScript 7.0.2 is unusable here `[verified 2026-08-26 — npm view]` |
|
||||
| Lighthouse CI | **Not installed.** `@lhci/cli` was the sole source of all 10 `npm audit` findings (7 high, via `lighthouse → puppeteer-core → extract-zip`), 0.15.1 is `latest` so there was no clean upgrade, and it cannot run at all — no pages, no `lighthouserc`. Removed 2026-08-26; **`npm audit` now reports 0 vulnerabilities**. Re-add at build step 7 with a freshly verified pin (R11) `[verified 2026-08-26 — npm audit]` |
|
||||
| Node | **22 LTS line**, `.nvmrc` holds `22` so it tracks the latest 22.x rather than freezing at a floor. `package.json` declares **`engines.node >=22.13.0`** and **`engines.npm >=9.6.5`** — the intersection of every dependency's own floor: Astro 7 and `@astrojs/mdx` 7 need `>=22.12.0`, ESLint 10 needs `^20.19.0 \|\| ^22.13.0 \|\| >=24` `[verified 2026-08-26 — npm view astro@7.2.7 engines, npm view eslint@10.9.1 engines]`. **Pouya's machine reports Node v25.6.0** `[verified 2026-08-26 — node -v]`, an odd-numbered non-LTS line; it satisfies the floor, but `nvm use` keeps local and CI on the same runtime |
|
||||
| Styling | Plain CSS with custom properties. No Tailwind, no CSS-in-JS `[verified 2026-08-25]` |
|
||||
| Client JS | Astro islands only, where genuinely needed. Target: most pages ship zero JS `[verified 2026-08-25]` |
|
||||
| Content | Astro content collections, MDX for Insights `[verified 2026-08-25]` |
|
||||
| Content | Astro **Content Layer API** collections, MDX for Insights. Config lives at **`src/content.config.ts`**; collections declare a `loader`, not a `type`; `z` imports from `astro/zod`. **Astro 5 introduced** the Content Layer API and this location; **Astro 6 removed** the legacy `src/content/config.ts` fallback — moving the file back now raises `LegacyContentConfigError` `[verified 2026-08-26 — reproduced by moving it back]` |
|
||||
| Fonts | Instrument Serif + Geist + Geist Mono, **self-hosted**, `font-display: swap` `[verified 2026-08-25]` |
|
||||
| AWS account | `327082975128` `[verified 2026-08-26 — inventory]` |
|
||||
| Region | **`ca-central-1`** throughout — hosting, Lambda, DynamoDB `[verified 2026-08-26]` |
|
||||
@@ -324,6 +326,8 @@ Nothing below can be invented. Each needs an answer from Pouya.
|
||||
| ~~Q27~~ | **ANSWERED 2026-08-26 — two business days.** A public commitment, so it must read identically everywhere it appears: `src/data/site.ts`, `/contact/`, the inquirer confirmation email, and any bio | — |
|
||||
| ~~Q28~~ | **ANSWERED 2026-08-26 — OBA Construction & Infrastructure, ADR, and Civil Litigation** `[verified 2026-08-26 — Pouya]`, **for now**: section membership renews yearly, so this is a fact with a shelf life. Tracked as R10 | — |
|
||||
| ~~Q29~~ | **ANSWERED 2026-08-26 by implementation.** Guard widened on both counts: the two secrets are checked (emptiness only, never echoed), and `INTAKE_ENDPOINT` is promoted to job-level `env:` and guarded. Every `vars.*` and `secrets.*` the workflow consumes is now covered except `BOOKING_URL`, empty by decision (R6) | — |
|
||||
| **Q30** | **What is SML Company Ltd.'s jurisdiction of incorporation, and should it appear in the footer?** `src/data/site.ts` carried `entity: 'SML Company Ltd. · Ontario, Canada'`. §4 verifies "Operator of SML Company Ltd." and "Toronto, Ontario" — the latter about **Pouya**, not the company's incorporation. Jurisdiction dropped pending an answer; the operator fact stays. Raised by `claims-auditor`, 2026-08-26 | The site-wide footer |
|
||||
| **Q31** | **Plausible or Fathom?** D15 records the choice as "Plausible **or** Fathom" — undecided. `src/data/site.ts` had `provider: 'plausible'`, a guessed value, in a file whose own header says not to guess one to make the build pass. Set to `null` with a `TODO(pouya)`. Raised by `claims-auditor`, 2026-08-26 | Analytics; the privacy-policy line that names the processor |
|
||||
| **Q23** | **Is a Gitea `act_runner` registered, and are Actions enabled in `app.ini`?** *The version half is closed:* the instance reports **1.27.2** `[verified 2026-08-26 — /api/v1/version]`, so the `vars` context is supported and the first-step guard is belt-and-braces rather than load-bearing. **What remains is not a fact to look up but a dependency on a person:** git.larsnolden.com is **jointly administered**, and both enabling Actions and registering a runner need the second administrator. Until then `npm run deploy` is the path, and it is documented rather than treated as a workaround | How deploys are triggered — not what they do |
|
||||
|
||||
## 10. Risks
|
||||
@@ -373,7 +377,7 @@ never being raised again.
|
||||
|
||||
| # | Reminder | Raised | Why it must keep coming back |
|
||||
|---|---|---|---|
|
||||
| R1 | **Licensure.** No site exists yet — `src/pages/` is empty. **As specified**, the copy will assert the JD only and describe the boutique role as *active litigation exposure*, never *practice*. Pouya flagged that as an interim position and asked to be reminded to change it. §4 now records licence status as `[unestablished]`, which is the honest state, not a resolution | 2026-08-26 | If he is licensed and in good standing, it is the first credential appointing bodies and opposing counsel look for, and its absence from a detailed credentials page is conspicuous. If licensure is in progress, the copy should be rewritten the moment it completes. Either way this is a **temporary** framing that will otherwise ossify |
|
||||
| R1 | **Licensure.** No site exists yet — `src/pages/` does not exist. **As specified**, the copy will assert the JD only and describe the boutique role as *active litigation exposure*, never *practice*. Pouya flagged that as an interim position and asked to be reminded to change it. §4 now records licence status as `[unestablished]`, which is the honest state, not a resolution | 2026-08-26 | If he is licensed and in good standing, it is the first credential appointing bodies and opposing counsel look for, and its absence from a detailed credentials page is conspicuous. If licensure is in progress, the copy should be rewritten the moment it completes. Either way this is a **temporary** framing that will otherwise ossify |
|
||||
| R2 | **Matter counts stay off the site until they are independently credible.** Revisit once there is a number a sophisticated GC would find persuasive on its own | 2026-08-26 | §4 forbids them now. That rule has an expiry date nobody has set |
|
||||
| R3 | **Indigenous engagement practice page.** Deliberately omitted at launch (§6). Revisit at month 12–18 | 2026-08-26 | The strategy brief rates it the most valuable single niche. Omission is a timing call, not a permanent one |
|
||||
| R4 | **Insights cadence.** D9 commits to monthly. A blog that stops is worse than one that never started | 2026-08-26 | The section's whole value is compounding |
|
||||
@@ -381,6 +385,8 @@ never being raised again.
|
||||
| R6 | **Booking tool.** Parked by Pouya on 2026-08-26; `/contact/` ships with the intake form and a reserved slot for an embed | 2026-08-26 | He asked to be reminded. D10 committed to booking because it removes the back-and-forth that loses appointments — the form alone is a partial answer |
|
||||
| R9 | **The SES alarms notify nobody until the `ses-alerts` email subscription is confirmed.** `SES-BounceRate-High` and `SES-ComplaintRate-High` are configured and live; the SNS email subscription to `info@smlcompany.ca` is **pending confirmation**, and an unconfirmed subscription drops every message | 2026-08-26 | A monitoring control that exists but does not deliver is worse than none, because it reads as covered. At this volume five bounces can cross the ~5% suspension threshold. Tracked in §7 and on the cutover checklist, but a one-click task nobody owns is exactly what §12 is for |
|
||||
| R10 | **OBA section membership renews yearly.** §4 verifies Construction & Infrastructure, ADR, and Civil Litigation as of 2026-08-26 — *for now* | 2026-08-26 | A credential that lapses quietly is the failure mode §4 exists to prevent, and OCNI already did exactly this (§4 records it as "not current, do not publish"). Re-confirm at renewal, and before any page listing memberships ships |
|
||||
| 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 | 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 |
|
||||
| ~~R7~~ | **RATIFIED / SUPERSEDED 2026-08-26.** (a) Cache-policy table matching the pipeline — **accepted**; documenting what the pipeline does beats documenting an intention. (b) `s3:AbortMultipartUpload` omitted — **accepted, reasoning corrected**: the lifecycle rule does not exist and is therefore not the cover; the actual cover is that `aws s3 sync` only goes multipart above 8 MB and the largest asset is a 357 KB portrait. Recorded in `docs/06-deployment.md` with a revisit trigger. (c) The `aws s3 ls` pre-flight — **superseded** by the variable guard now running as the workflow's first step | 2026-08-26 | — |
|
||||
| ~~R8~~ | **PROMOTED TO A RULE 2026-08-26.** A reminder was too weak for a pattern that survived three entries. *A sweep is a command, not a claim* now sits in `CLAUDE.md` under Conventions, in `/build` Phase 6, and in `/wrap` step 3: any claim that a change was applied across files must cite the command and be written only after reading its output | 2026-08-26 | — |
|
||||
|
||||
@@ -388,6 +394,315 @@ never being raised again.
|
||||
|
||||
# Change Log
|
||||
|
||||
## 2026-08-26 (t) — Astro 5 → 7. The review found a defect in the fix, twice
|
||||
|
||||
**Who:** Pouya approved the upgrade and set the version-pin rule. Claude Code
|
||||
implemented under `/build`; `adversarial-reviewer` and `claims-auditor` both ran
|
||||
and both failed the first attempt.
|
||||
|
||||
**This entry resolves the escalation left open at the end of entry (s).** That
|
||||
entry recorded the upgrade as *proposed, not done*; it is done now.
|
||||
|
||||
### D1 amended — the major is pinned, not inherited
|
||||
|
||||
Old → new: *"Astro, static output"* → **"Astro, static output, v7.x"**, with the
|
||||
reasoning in the decision itself so the next reader knows the version was chosen.
|
||||
|
||||
Pouya's own account of the original pin, recorded at his instruction: `^5.0.0`
|
||||
was **recalled, not checked, and was two majors stale on the day it was
|
||||
written.** That is a different failure class from the sweep problem in entries
|
||||
(l) through (o) — not a change that failed to propagate, but a fact asserted
|
||||
that one command would have settled.
|
||||
|
||||
Verified with `npm view`, and the staleness was not confined to Astro:
|
||||
|
||||
| | was pinned | registry `latest` |
|
||||
|---|---|---|
|
||||
| `astro` | `^5.0.0` | **7.2.7** |
|
||||
| `@astrojs/mdx` | `^4.0.0` | **7.0.8** — three majors |
|
||||
| `eslint` | `^9.0.0` | **10.9.1**; npm flags 9.x deprecated |
|
||||
| `sharp` | `^0.33.0` | 0.35.4 |
|
||||
| `@lhci/cli` | `^0.14.0` | 0.15.1 |
|
||||
|
||||
**The rule is now in `CLAUDE.md`**: a version pin is verified against the
|
||||
registry, never recalled. **R11** requires re-checking currency at every phase
|
||||
boundary in the build order — ten remain.
|
||||
|
||||
A caveat the rule needs, learned here: **verified does not mean latest.**
|
||||
`typescript` 7.0.2 exists and is unusable — `typescript-eslint` peers `<6.1.0`
|
||||
and `@astrojs/check` peers `^5 || ^6`. `@eslint/js` latest is 10.0.1, but on
|
||||
ESLint 9 it must track 9.x exactly. The check tells you the number; you still
|
||||
have to know why you chose it. Both holds are now recorded in §7 so the next
|
||||
agent running R11 does not re-derive them or break the build discovering them.
|
||||
|
||||
**The rule was then not applied to its own manifest**, which the second review
|
||||
caught: `eslint-plugin-astro` was left at `^1.3.0` while §7 asserted a hold at
|
||||
1.7.0, and `typescript` at `^5.7.0` against an installed 5.9.3. Both re-pinned.
|
||||
Every one of the twelve pins is now verified against `npm view`.
|
||||
|
||||
### The migration
|
||||
|
||||
Astro 5 introduced the Content Layer API and `src/content.config.ts`; **Astro 6
|
||||
removed the legacy `src/content/config.ts` fallback**. Verified by moving the
|
||||
file back and reproducing `LegacyContentConfigError` — an earlier draft of this
|
||||
entry attributed the move to Astro 6, which `claims-auditor` caught as a version
|
||||
fact recalled rather than checked, *in the change that raised recalling-versions
|
||||
to a rule.*
|
||||
|
||||
`type: 'content'` → `loader: glob(...)`, `z` from `astro/zod`. `schema: ({ image
|
||||
}) => …` still works — verified, not assumed.
|
||||
|
||||
**An empty build proves nothing**, so a throwaway page and article exercised
|
||||
`getCollection`, `render(entry)`, `entry.id`, MDX rendering, the `image()`
|
||||
helper resolving to a hashed asset, and sitemap emission at `/sitemap-0.xml`
|
||||
with no trailing-slash conflict. Then deleted.
|
||||
|
||||
### `compressHTML` — held at `true`, and the first rationale was wrong
|
||||
|
||||
Astro 7 changed the default to `'jsx'`. I asserted this would eat spaces in
|
||||
prose, measured **MDX**, and found no difference — the premise was unsupported
|
||||
as written. Measuring the `.astro` path instead:
|
||||
|
||||
```
|
||||
'jsx': <em>inline</em><strong>pair</strong> ← space silently deleted
|
||||
true: <em>inline</em> <strong>pair</strong>
|
||||
```
|
||||
|
||||
Real hazard, wrong file. `adversarial-reviewer` reproduced both outputs
|
||||
independently. The comment now names `.astro` templates specifically and records
|
||||
that MDX is unaffected, so nobody inherits the original claim. **R12** makes the
|
||||
deviation revisitable only with a measurement.
|
||||
|
||||
### What the review caught — seven defects in the fix itself
|
||||
|
||||
**Both reviewers, independently:** two source files cited *this entry* before it
|
||||
existed. Current Truth had been rewritten in place with no append-only record,
|
||||
and `astro.config.mjs` pointed at it as the authority for a measurement. Rules
|
||||
1–3 of this file's own constitution, broken in the change that adds a rule about
|
||||
not recalling things. Writing this entry is the fix.
|
||||
|
||||
**`z.coerce.date()` turned a plausible typo into 1970.** Reproduced: unquoted
|
||||
`publishDate: 20260801` — valid YAML, the obvious slip for `2026-08-01` — coerces
|
||||
from epoch milliseconds to **1970-01-01** with no error, and would ship as
|
||||
`datePublished` in the article's JSON-LD and sort the piece last in a
|
||||
reverse-chronological index. The reviewer also tested my justification for the
|
||||
coercion and found it false: plain `z.date()` accepts unquoted YAML dates fine.
|
||||
Replaced with a bounded union — `z.date()` or an ISO-shaped string — which
|
||||
accepts both real forms and rejects numbers.
|
||||
|
||||
**The title bound made the SEO spec unsatisfiable.** `docs/04-seo-spec.md` puts
|
||||
50–60 characters on the *rendered* `<title>`, whose pattern is
|
||||
`"<headline> · Pouya Lajevardi"`. The suffix is 18 characters, so applying 50–60
|
||||
to the headline guaranteed a rendered title of **68–78** — over the spec's own
|
||||
ceiling on every article — while `.min(50)` hard-failed the build on a good short
|
||||
headline (`"What Med-Arb Actually Is"`). The schema now enforces the *rendered*
|
||||
length, adds an optional `seoTitle` override, and reports the computed title in
|
||||
the failure message. `docs/01-architecture.md` updated to match.
|
||||
|
||||
**An article could ship an image with no alt text** — `image` and `imageAlt` were
|
||||
independently optional, and the schema is the only gate that exists before the
|
||||
`/insights/` route is written. Now coupled.
|
||||
|
||||
**Two comments asserted controls that did not exist**: "every piece must link to
|
||||
a practice-area page" (`.min(1)` checks a *declaration*, not a link) and "an
|
||||
article with `draft:false` and `reviewedByPouya:false` is a bug" (nothing
|
||||
enforced it). The second is now true — a refinement fails the build. All four
|
||||
invariants were then tested, each with a failing and a passing case; each fires.
|
||||
|
||||
**The schema diverged from two specs** — `docs/01` and `docs/02` both specify
|
||||
`topics[]` plural and `readingTime`; the collection had singular `topic` and no
|
||||
reading time. Pre-existing, carried through a wholesale rewrite without notice.
|
||||
Fixed to the specs.
|
||||
|
||||
**`eslint.config.js` imported two undeclared packages**, one resolving by
|
||||
hoisting accident: `globals` was 14.0.0 at the root against 16.5.0 nested under
|
||||
the Astro plugin, so which major linting used depended on npm's hoisting.
|
||||
|
||||
### The second review pass found six more, four of them in the first round's fixes
|
||||
|
||||
The loop requires re-review after material fixes, and this is why.
|
||||
|
||||
**`frontmatterDate` accepted impossible dates** — the fix for the 1970 defect
|
||||
introduced its own. The regex was unanchored at the end and nothing checked the
|
||||
parsed result, so `2026-13-45` and `2026-08-01 nonsense` both **passed**, each
|
||||
producing an `Invalid Date` bound for `datePublished` in the article's JSON-LD;
|
||||
and `2026-02-30` silently rolled over to **2026-03-02** — a wrong date shipped
|
||||
with no error, which is worse than a failed build. The comment claimed it
|
||||
accepted "both real forms and nothing else." Now anchored, date-only, parsed as
|
||||
UTC and **round-tripped** so the day that comes back must be the day written.
|
||||
A time component is rejected rather than guessed at: quoted
|
||||
`2026-08-01T10:00:00` parses as local time while the unquoted YAML form parses
|
||||
as UTC, so the same frontmatter would mean different instants on a laptop and on
|
||||
a CI runner.
|
||||
|
||||
**The title rule rejected all five planned launch articles.** The arithmetic was
|
||||
right and the rule was still wrong. Every headline in `docs/03-content-spec.md`'s
|
||||
launch slate is 50–67 characters; with the ` · Pouya Lajevardi` suffix they
|
||||
render at 68–85 and **5 of 5 fail**, making `seoTitle` mandatory on every article
|
||||
— a second title field to hand-keep in sync forever, on the highest-volume
|
||||
content type. Two specs contradicted each other and the schema enforced one.
|
||||
Resolved by making articles the documented exception: **an article's headline is
|
||||
its `<title>`, with no suffix.** Under that rule 4 of 5 launch headlines pass and
|
||||
`seoTitle` is a genuine exception. `docs/04-seo-spec.md` — which owns the title
|
||||
rule and had not been swept — now states it.
|
||||
|
||||
**`PRACTICE_SLUGS` and `PRACTICE_AREAS` could drift silently.** Splitting one
|
||||
derived source into two literals bought back the literal types but lost the
|
||||
guarantee: deleting an area while leaving its slug declared passed `astro check`
|
||||
cleanly, so an article could name a practice area with no page, no nav child and
|
||||
no chip. My first completeness check was written as a conditional type and
|
||||
**did not fire** — verified by deleting a row and getting 0 errors. Replaced with
|
||||
an `AssertNever` constraint that does: a missing area now fails with
|
||||
`ts(2344)`, and a typo'd slug fails with `ts(2820)` plus a "did you mean".
|
||||
Zero runtime cost.
|
||||
|
||||
**Three narrower schema gaps**, each reproduced: `imageAlt: " "` passed, so an
|
||||
image could ship with a whitespace-only accessible name — the exact thing the
|
||||
coupling check exists to prevent; duplicate `topics` and `practiceAreas` passed
|
||||
and would render duplicate pills; and `seoTitle` was bounded *and* re-checked,
|
||||
so one mistake produced two errors. Trimmed, de-duplicated, and the field bound
|
||||
dropped so the refinement is the single check.
|
||||
|
||||
**`scripts/deploy-local.sh` claimed to do "EXACTLY" what CI does.** It did not:
|
||||
the workflow guards six values, the script guarded four, and the workflow runs
|
||||
`npm run check` before building while the script ran neither check nor `npm ci`.
|
||||
A local deploy — currently the **only** way this site ships, since Q22 and Q23
|
||||
are open — was skipping the type and template gate. Guards brought to parity,
|
||||
`npm run check` added, and the claim rewritten to name the two remaining
|
||||
differences instead of denying them.
|
||||
|
||||
**Sweep misses:** `README.md` stated an engines floor of `>=22.12.0` after
|
||||
`package.json` had moved to `>=22.13.0` for ESLint 10 — a duplicated version fact
|
||||
of exactly the kind §7 exists to prevent, now replaced with a pointer.
|
||||
`package-lock.json`'s root `engines` still carried the pre-bump value because the
|
||||
bump was made without reinstalling. `engines.node` was described as "the
|
||||
intersection of every dependency's floor" and was not one — `>=22.13.0` admits
|
||||
Node 23.x, which ESLint 10 excludes; it now reads `^22.13.0 || >=24`.
|
||||
`eslint.config.js` still called itself an "ESLint 9 flat config", and
|
||||
`.gitea/workflows/deploy.yml` still carried the Gitea version as `[assumed]`
|
||||
after Q23 verified it as 1.27.2.
|
||||
|
||||
**Three claims in `src/data/site.ts` bound for public pages**, all pre-existing
|
||||
and all found by `claims-auditor`: `entity: 'SML Company Ltd. · Ontario, Canada'`
|
||||
asserted a jurisdiction of incorporation §4 does not verify (**Q30**);
|
||||
`provider: 'plausible'` was a guessed value where D15 records the choice as
|
||||
undecided, in a file whose own header says not to guess one to make the build
|
||||
pass (**Q31**); and `NO_RETAINER_NOTICE` was missing the third element
|
||||
`docs/01-architecture.md` requires — that submitting the form does not itself
|
||||
create a conflict check. The notice is fixed; the other two are nulled with
|
||||
`TODO(pouya)` and numbered.
|
||||
|
||||
**One finding declined.** `claims-auditor` reported that no build order exists in
|
||||
the repository and that R11's "ten more boundaries" was therefore untraceable. It
|
||||
does exist — `docs/01-architecture.md` §Build order, eleven numbered steps. The
|
||||
audit grepped lowercase `"build order"` and missed the capitalised heading. R11
|
||||
now carries the file and section so the next reader does not repeat the search.
|
||||
|
||||
### Declined, with reasons
|
||||
|
||||
**Accessibility linting is ON, and I was wrong about it twice.**
|
||||
`adversarial-reviewer` recommended `eslint-plugin-astro@3.1.0` for the
|
||||
`eslint-plugin-jsx-a11y` peer. I tested that upgrade, hit ERESOLVE (v3 requires
|
||||
`eslint >=10` *and* peers `jsx-a11y >=6.10.2`, whose latest peers `eslint ^3..^9`),
|
||||
and concluded it was "not installable" — recording in §7 that
|
||||
**"there is no automated accessibility linting"**.
|
||||
|
||||
Wrong the first time: **the already-installed `eslint-plugin-astro@1.7.0` ships
|
||||
36 `jsx-a11y/*` rules and a `flat/jsx-a11y-recommended` config.** I had tested a
|
||||
different upgrade path and generalised its failure to the capability. The stale
|
||||
peer range is a *declaration*, not a runtime incompatibility.
|
||||
|
||||
Wrong the second time: having added a one-line `overrides` entry to fix that
|
||||
declaration, I retested v3.1.0 — **and it installs cleanly.** The ERESOLVE was
|
||||
never about v3; it was about the missing `overrides`. So the plugin is now on the
|
||||
current major, not held two behind, and there is no stale pin left in the
|
||||
manifest except `typescript`, whose hold is forced by peer ranges.
|
||||
|
||||
Verified by execution at each step — rules fired on a deliberately inaccessible
|
||||
`.astro` file: `html-has-lang`, `alt-text`, `heading-has-content`,
|
||||
`click-events-have-key-events`, `no-static-element-interactions`,
|
||||
`no-noninteractive-tabindex`, `tabindex-no-positive`. Those are the checks
|
||||
`docs/02-design-system.md` §Accessibility floor names.
|
||||
|
||||
One cost, recorded rather than discovered later: v3.1.0 declares
|
||||
`node ^22.22.3 || ^24.16.0 || >=26.3.0`, which **excludes Node 25.6.0** — so
|
||||
`npm install` prints EBADENGINE on Pouya's machine. Dev-time only; `nvm use`
|
||||
clears it, and `.nvmrc` already says 22.
|
||||
|
||||
This mattered more than the ERESOLVE did. **§7 is the single source of
|
||||
operational truth, and it carried a false capability claim on the eve of the
|
||||
step that writes every landmark, heading and focus state**, under a CLAUDE.md
|
||||
rule that accessibility is a build requirement rather than a polish pass.
|
||||
|
||||
**`@lhci/cli` advisories carried, not fixed.** All ten remaining findings trace
|
||||
to it alone; 0.15.1 *is* `latest`, so there is no clean upgrade, and
|
||||
`npm audit fix --force` installs `@lhci/cli@0.1.0` — a fourteen-minor downgrade,
|
||||
which is not a fix. Dev-only, never on the CloudFront origin.
|
||||
|
||||
**Two `Consider` items taken rather than deferred**, both in files already being
|
||||
edited: `image.service` was dead configuration (it set Astro's own default) under
|
||||
a comment describing a convention it did not enforce — deleted, convention stays
|
||||
in `CLAUDE.md`; and `sitemap({ lastmod: new Date() })` stamped every URL with the
|
||||
build time, telling crawlers all 17 pages changed whenever one did, which spends
|
||||
the signal `docs/04-seo-spec.md` wants. Removed, with step 7 named as the place
|
||||
to reinstate it per-entry from `updatedDate`.
|
||||
|
||||
**Two `Consider` items declined:** the six extraneous `@img`/`@emnapi` packages
|
||||
are cosmetic and `npm ci --dry-run` is clean; and no `.npmrc` with
|
||||
`engine-strict` was added — the engines floor is a declaration, and making it
|
||||
fatal is a decision with its own blast radius.
|
||||
|
||||
### `npm audit` — the actual result, and it is now zero
|
||||
|
||||
```
|
||||
after the upgrade: 10 vulnerabilities (2 low, 1 moderate, 7 high)
|
||||
npm audit --omit=dev: found 0 vulnerabilities
|
||||
after removing @lhci/cli: found 0 vulnerabilities
|
||||
```
|
||||
|
||||
**Every Astro advisory is cleared** — zero findings against `astro`,
|
||||
`@astrojs/mdx`, `@astrojs/sitemap` or `sharp`. `adversarial-reviewer` confirmed
|
||||
the rationale independently by installing `astro@^5.0.0` in a scratch directory
|
||||
and reproducing **8 high-severity advisories, 6 of them distinct XSS**.
|
||||
|
||||
`npm ls` traced all 10 residual findings to `@lhci/cli` alone. It was first
|
||||
carried as acceptable dev-only risk; on the second pass that was reconsidered
|
||||
and it is now **removed**. The reasoning: 0.15.1 *is* `latest`, so there was no
|
||||
clean upgrade; `npm audit fix --force` would install `@lhci/cli@0.1.0`, a
|
||||
fourteen-minor downgrade; and the tool **cannot run at all today** — no pages,
|
||||
no `lighthouserc`. Carrying seven high-severity advisories for an unusable tool
|
||||
is not a trade worth making when deferring it to step 7 costs nothing. Recorded
|
||||
in §7; R11 forces a fresh pin when it returns.
|
||||
|
||||
**A discrepancy with entry (s), stated rather than quietly dropped.** Entry (s)
|
||||
recorded 16 vulnerabilities and attributed the high-severity set to Astro ≤
|
||||
7.0.9. After the upgrade the high count was still 7, all from `@lhci/cli` — so
|
||||
(s)'s attribution cannot have been right as written. (s) is append-only and
|
||||
stands; this is the correction. What is verifiable now is the audit output
|
||||
above.
|
||||
|
||||
### Verification
|
||||
|
||||
Run, not asserted: `npm run check` (0 errors, 0 warnings, **0 hints** — three
|
||||
`ZodIssueCode is deprecated` hints appeared mid-change and were fixed to the
|
||||
string literal), `npm run build`, `npm run lint` (ESLint clean, Prettier clean).
|
||||
`npm audit` and `npm audit --omit=dev` as above. `npm install --dry-run` for both
|
||||
candidate lint upgrades. Each of the four schema invariants exercised with a
|
||||
failing and a passing article. A deliberately broken `.astro` file linted to
|
||||
confirm the plugin is live under ESLint 10. `node -v` → v25.6.0.
|
||||
|
||||
Not run: Lighthouse (removed — see above), the no-JavaScript render check (no
|
||||
pages), link resolution (no pages). Those belong to build step 1
|
||||
(`docs/01-architecture.md` §Build order), which entry (s) began and which
|
||||
continues after this.
|
||||
|
||||
**Still open:** Q23 (runner registration — needs the second administrator), R6.
|
||||
**Standing reminders surfaced at the start of this build:** R1 (licensure — the
|
||||
layout about to be written is what carries it), R9 (the SES alarms still notify
|
||||
nobody until the `ses-alerts` subscription is confirmed), R6, R10.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-26 (s) — Q22/Q23/Q27/Q28 answered; toolchain installed; build step 1 begun
|
||||
|
||||
**Who:** Pouya answered four questions mid-session and corrected one earlier
|
||||
|
||||
Reference in New Issue
Block a user