fix: sweep D3 amendment through the specs; correct inverted DKIM table

The re-audit of the deploy-guard change surfaced defects well outside the
diff, including one that would have broken production mail.

docs/05-backend-spec.md had the two SES DKIM sets exactly inverted, labelling
the three records that resolve as "orphans" and the three NXDOMAIN records as
"Live. Never delete". Entry (j) corrected this in AGENTS.md §7 and the
correction never reached docs/05. Since SES has no custom MAIL FROM, DKIM is
the only thing satisfying DMARC, so acting on that table would have silently
broken intake mail authentication.

Also in this change:

- .gitea/workflows/deploy.yml gains a guard as steps[0] that fails the run,
  naming the variable, if AWS_REGION, S3_BUCKET or CLOUDFRONT_DISTRIBUTION_ID
  is empty — how a Gitea too old for the vars context manifests. Verified
  fail-closed under bash -e, sh -e and bash -euo pipefail.
- AGENTS.md Current Truth: SPF and DMARC recorded as present (Q20), the
  matching §10 High risk row retired, three duplicate Q rows removed.
- docs/reference/AWS-Hosting-Guide.md tracked and given a do-not-execute
  banner; it was an executable procedure for the architecture D1/D3 replace.
- Copy decks: "a working litigator" and "an active litigation practice"
  replaced with the register's own wording; LegalService JSON-LD replaced with
  ProfessionalService; tribunal-secretary offers removed per D14; nine stale
  question blockers swept.
- astro.config.mjs: prefetchAll disabled — it injected JS into every page
  against the zero-JS convention with no decision recorded.
- src/data/site.ts: unregistered response-time commitment nulled (Q27);
  OBA section names downgraded to [assumed] (Q28).
- s3:AbortMultipartUpload reasoning corrected to measure ./dist, not the repo.

Opens Q27, Q28, Q29. AGENTS.md entry (q) records the full resolution,
including the findings declined and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
This commit is contained in:
Pouya Lajevardi
2026-08-26 11:28:42 -04:00
co-authored by Claude Opus 5
parent e6abdf42e8
commit 6bf1167624
22 changed files with 1951 additions and 183 deletions
+820 -19
View File
@@ -38,10 +38,12 @@ SML Company Ltd. Legal background, firm affiliation, and the operating company
are credibility signals referenced in support; they are not the subject.
`[verified 2026-08-25 — source: PL_ADR_Personal_Branding_Strategy_Brief.docx]`
**Positioning in one sentence:** a credentialed neutral who is also a working
litigator and a practising machine-learning / infrastructure engineer — a
**Positioning in one sentence:** a credentialed neutral who is also close to
live litigation and a practising machine-learning / infrastructure engineer — a
combination that is close to absent from Canadian ADR rosters, and which is the
practice's economic moat. `[verified 2026-08-25 — source: strategy brief §I, §V]`
*(Wording corrected 2026-08-26: "a working litigator" asserted conduct of
litigation, which §4 bars — see entry (q).)*
**Time horizon:** the strategy brief plans a 24-month compounding arc. The site
is built to be added to continuously, not shipped once. `[verified 2026-08-25]`
@@ -98,7 +100,7 @@ without an explicit instruction and a Change Log entry. `[verified 2026-08-25]`
| D10 | Intake | **Rebuilt intake form + calendar booking** for the confidential intake call | Structured questionnaire; form only; email/phone only |
| D11 | Rollout | **Build everything, one clean cutover.** The current site stays live untouched until replaced | Patch live site first; staging subdomain; take site down |
| D12 | Agent working file | **This file**, maintained under the constitution above | Ad hoc notes |
| D13 | Licensure claims | **The site asserts the JD only.** No claim of licensure, call to the bar, or entitlement to practise law. The approved phrasing for the boutique role is **"active litigation exposure"** or **"involvement in litigation and ADR matters"** — never "practice". Pouya's direction, 2026-08-26. **Explicitly interim — see Standing Reminders §12** | Stating LSO licence status; the word "practice" |
| D13 | Licensure claims | **The site asserts the JD only.** No claim of licensure, call to the bar, or entitlement to practise law. The approved phrasing for the boutique role is **"active litigation exposure"** or **"involvement in litigation and ADR matters"** — never "practice" **in that context**. "Practice" describing Pouya's own ADR practice — "the dispute resolution practice of Pouya Lajevardi" — is correct and stays. Pouya's direction, 2026-08-26. **Explicitly interim — see Standing Reminders §12** | Stating LSO licence status; the word "practice" |
| D14 | Fee structure | **Single published rate card, all mediation matters at one rate.** Confirmed by Pouya 2026-08-26; figures in `docs/07-fees.md`. No tribunal-secretary line | Two-tier card (recommended by Claude, declined); ranges; fees on request |
| D15 | Analytics | **Privacy-first and cookieless** (Plausible or Fathom). No cookie banner, nothing to consent to, one line in the privacy policy | GA4; no analytics at all |
| D18 | Intake email | **Two emails per submission** — notification to `info@smlcompany.ca`, and a **confirmation receipt to the inquirer**. Pouya, 2026-08-26. The receipt is what requires SES production access (Q19) | Notification only; no email; no form at all |
@@ -107,11 +109,20 @@ without an explicit instruction and a Change Log entry. `[verified 2026-08-25]`
## 4. Credential and claim register
**This is the most important section in this file.** Pouya is a licensed legal
professional. Every public claim on this site is subject to Law Society of
Ontario marketing rules, and the previous version of the site carried fabricated
credentials, invented matter values, and a fictitious testimonial. That must
never recur.
**This is the most important section in this file.** The previous version of this
site carried a fictitious founder, invented matter values, fabricated office
locations, and a testimonial attributed to a person who does not exist. Whatever
regulatory regime does or does not apply, publishing false credentials on a
professional's marketing site is the thing this register exists to prevent. That
reason is sufficient on its own, and it does not rest on a fact this file has
never established.
**Note on this paragraph's own history, kept as a caution.** It previously read
"Pouya is a licensed legal professional", then "a legal professional", then
asserted that LSO marketing rules apply — three progressively weaker forms of the
same unverified claim, each surviving a sweep meant to remove it. The register
was asserting what it could not vouch for, in the section written to stop exactly
that.
### Governing rule
@@ -130,9 +141,10 @@ since May.
| Claim | Status |
|---|---|
| Pouya Lajevardi, JD, Bond University | `[verified 2026-08-25 — strategy brief §I]` |
| **Licence status — NOT ESTABLISHED** | `[unestablished 2026-08-26]`. This record does not know whether Pouya is licensed. He instructed that licensure be left out of the site (D13) — an instruction about the site, **not a statement of fact either way.** Do not assert it, do not deny it, do not infer it from anything else here, and do not treat any regulatory regime as established. R1 tracks the reopening |
| ~~Lawyer; Law Society of Ontario member~~ | **DO NOT PUBLISH.** Pouya directed on 2026-08-26 that licensure is left out of the site entirely; the JD is asserted, nothing further. See D13 and the Forbidden table below `[verified 2026-08-26]` |
| Director of Firm Operations, Toronto litigation and ADR boutique | `[verified 2026-08-25 — strategy brief §I]` |
| Active litigation practice: personal injury, construction, regulatory (POA), insurance (SABS) | `[verified 2026-08-25 — strategy brief §I]` |
| Active litigation **exposure**: personal injury, construction, regulatory (POA), insurance (SABS) | `[verified 2026-08-26]`. **Wording corrected** — this row previously read "practice", the exact word D13 bars in exactly the context it bars it. The register was quoting the strategy brief verbatim and contradicting its own rule. The underlying fact is unchanged |
| Q.Med designation through ADRIC / ADRIO | `[verified 2026-08-25 — strategy brief §I]` |
| Has completed multiple sole mediations | `[verified 2026-08-25 — strategy brief §I]` — count deliberately not published, see below |
| Q.Arb credentialing pathway — **commenced August 2026** | `[verified 2026-08-26 — Pouya]`. Describe as newly commenced, never as held or nearing completion |
@@ -149,9 +161,22 @@ since May.
| Toronto, Ontario; by appointment | `[verified 2026-08-26]` |
| Contact: `info@smlcompany.ca`; no public phone number; consultations by scheduled call | `[verified 2026-08-26 — Pouya]` |
| LinkedIn: `https://www.linkedin.com/in/pouyalajevardi/` | `[verified 2026-08-26 — Pouya]` |
| The Toronto litigation and ADR boutique **may be named on the site** | `[verified 2026-08-26 — Pouya]`; the name itself is pending, see Q7 |
| ~~The boutique may be named on the site~~ | **STRUCK 2026-08-26 (Q25).** Superseded by **D16 — never named.** Pouya said it *could* be named, then reversed; the permission survived the reversal and sat in the Verified table authorising exactly what D16 bars. Refer to it only as *a Toronto litigation and ADR boutique* |
### Forbidden — must not appear anywhere
### Forbidden — must not appear on the site
**Scope, clarified 2026-08-26.** This table governs **published pages** — anything
a visitor to `adr.smlcompany.ca` can read, JSON-LD included. It is not a word
filter over the repository's own documentation.
**Internal docs are bound by a related rule:** they must not assert as fact
anything this register has not verified. Reasoning built on an unverified premise
propagates into copy. Licensure is the live example — **the Verified table above
does not establish licence status either way**; its two licensure rows are
verified *directives not to publish*, not a verified status. So no file in this
repo may call Pouya "licensed", or soften it to "a legal professional", internal
or not. State the *reason* for a rule rather than a credential the register
cannot vouch for.
| Never publish | Why |
|---|---|
@@ -210,7 +235,7 @@ the audience it targets. Revisit at month 1218. `[verified 2026-08-25 — dec
| Thing | Value |
|---|---|
| Framework | Astro, `output: 'static'` `[verified 2026-08-25 — D1]` |
| Node | 22 LTS, pinned in `.nvmrc` `[assumed]` |
| Node | 22 LTS, pinned in `.nvmrc`; `package.json` `engines.node` `>=22` `[verified 2026-08-26 — .nvmrc, package.json]` |
| 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]` |
@@ -226,8 +251,8 @@ the audience it targets. Revisit at month 1218. `[verified 2026-08-25 — dec
| SES identities | Domain `smlcompany.ca` **verified for sending** `[verified 2026-08-26]`; addresses `info@`, `intake@`, `adr@` |
| SES account | **In the sandbox**`ProductionAccessEnabled: false` `[verified 2026-08-26]`. Production access required (Q19) |
| Mail hosting | **Google Workspace** — MX `1 smtp.google.com`; `google._domainkey` present, so Google DKIM is configured `[verified 2026-08-26 — DNS query]` |
| **SPF** | **ABSENT.** `smlcompany.ca` TXT holds only Anthropic and Google site-verification strings. No `v=spf1` record exists `[verified 2026-08-26 — DNS query]` |
| **DMARC** | **ABSENT.** No `_dmarc.smlcompany.ca` record `[verified 2026-08-26 — DNS query]` |
| **SPF** | **PRESENT.** `v=spf1 include:_spf.google.com include:amazonses.com ~all` on `@`, added by Pouya and independently verified `[verified 2026-08-26 — DNS query, Q20]` |
| **DMARC** | **PRESENT.** `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` on `_dmarc`, added by Pouya and independently verified `[verified 2026-08-26 — DNS query, Q20]`. Still at `p=none`; tighten to `quarantine` only after reports come back clean |
| Required DNS (Namecheap) | TXT `@``v=spf1 include:_spf.google.com include:amazonses.com ~all` · TXT `_dmarc``v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` `[assumed]` — start at `p=none`, tighten after monitoring |
| SES DKIM — **the set that actually resolves** | `f5puwearz5ek47aiuoskk57ya6ir66ju`, `jdue2r22cxrk27laxlgnjcpby2fq4vxu`, `kznn3cklvwh4gs342rtf5euqymlger53` — all three answer NOERROR at `<token>._domainkey.smlcompany.ca` `[verified 2026-08-26 — DNS]` |
| SES DKIM — **the set that is BROKEN** | `3zsnvsjg3ddi6hjyndjy6jgjoitvhydl`, `jejgp7na3wdpprsanamct4uxxgw2tyqw`, `xpiwyftpodmgpnzfof3ee7t7fzmlmhnh` — listed in the SES console, present in Namecheap, but **NXDOMAIN**. They were entered with the full name in Namecheap's Host field, so the zone doubled the domain: `<token>._domainkey.smlcompany.ca.smlcompany.ca` resolves, the correct name does not `[verified 2026-08-26 — DNS]` |
@@ -243,7 +268,7 @@ the audience it targets. Revisit at month 1218. `[verified 2026-08-25 — dec
| DNS | **Namecheap**, not Route 53 `[verified 2026-08-25 — AWS-Hosting-Guide.md]` |
| Intake backend | API Gateway (HTTP API) → Lambda → DynamoDB, notifications via SES `[verified 2026-08-25 — AWS-Hosting-Guide.md]` |
| Repository | **`adr-sml`**, self-hosted **Gitea**. Local clone at `/Users/pouya/Dev/Websites/adr-sml` `[verified 2026-08-26]` |
| CI/CD | **Gitea Actions**, `.gitea/workflows/deploy.yml`. `.github/workflows/deploy.yml` kept as the OIDC reference in case the repo ever moves. Credentials: scoped IAM user in Gitea secrets — **no OIDC available** `[verified 2026-08-26 — D3 as amended]` |
| CI/CD | **Gitea Actions**, `.gitea/workflows/deploy.yml`. The GitHub OIDC workflow is kept as `docs/reference/github-actions-oidc.yml.example` — moved out of `.github/workflows/` on 2026-08-26 so Gitea cannot fall back to it. Credentials: scoped IAM user in Gitea secrets — **no OIDC available**. Whether that IAM user has actually been created is **Q22** `[verified 2026-08-26 — D3 as amended]` |
| Analytics | **Plausible or Fathom** — cookieless, no personal data, no consent banner, EU-hosted `[verified 2026-08-26 — D15]` |
## 8. Design system
@@ -273,6 +298,10 @@ Nothing below can be invented. Each needs an answer from Pouya.
| ~~Q4 / Q14~~ | **ANSWERED 2026-08-26.** Rate card confirmed by Pouya — see D14 and `docs/07-fees.md` | — |
| ~~Q13~~ | **ANSWERED 2026-08-26.** Self-hosted Gitea with Gitea Actions | — |
| ~~Q15 / Q16 / Q17~~ | **ANSWERED 2026-08-26.** Non-mediation hourly $500. Prep bundled: 2 h in the half day, 3 h in the full day, **stated on the page**. Overtime $500/h | — |
| ~~Q25~~ | **ANSWERED 2026-08-26. Struck.** The §4 row permitting the boutique to be named is removed; D16 governs alone | — |
| ~~Q26~~ | **ANSWERED 2026-08-26.** §4 now carries an explicit **Licence status — NOT ESTABLISHED** row, and its opening no longer asserts that LSO rules apply. The register's rationale stands on the fabricated-credentials history alone | — |
| ~~Q24~~ | **ANSWERED 2026-08-26.** `AWS-Hosting-Guide.md` copied into `docs/reference/` — it is the only record of how the hand-built infrastructure was created. Scanned for credentials before copying: no access keys, no secrets, no account ID | — |
| **Q22** | **Has the deploy credential actually been provisioned?** `docs/06-deployment.md` instructs creating IAM user `adr-sml-deploy`, issuing an access key, and setting `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in the repository's Gitea Actions secrets. It is written imperatively — a procedure to perform, not a record of one performed — and nothing in this file records any of it as done; the inventory behind Q10 captured no such principal. Check with `aws iam get-user --user-name adr-sml-deploy`. Until it is confirmed, no file may describe the credential as existing, only as designed. **The §10 rotation obligation has no subject until this is answered** | CI/CD, the first deploy, and the accuracy of every credential claim in the docs |
| Q5 | Booking tool — **parked 2026-08-26 at Pouya's request.** Build `/contact/` with the form only and a clean slot for the embed. Now tracked as standing reminder R6 | `/contact/` — non-blocking |
| ~~Q6~~ | **ANSWERED 2026-08-26.** Supplied and committed: `src/assets/pouya-lajevardi.jpg` (1600×1600 master) and `src/assets/og-portrait.jpg` (1200×630 link-preview crop) | — |
| ~~Q7~~ | **ANSWERED 2026-08-26.** Pouya reverted to generic. The boutique is **never named**; refer to it as *a Toronto litigation and ADR boutique* throughout | — |
@@ -285,20 +314,24 @@ Nothing below can be invented. Each needs an answer from Pouya.
| ~~Q21~~ | **ANSWERED 2026-08-26.** `ca-central-1` is healthy (`SUCCESS`) on the resolving token set. The broken records belong to a stray `us-east-1` identity this project does not use. **Nothing to fix.** The three dead CNAMEs are inert; leaving them is the low-risk choice | — |
| ~~Q11~~ | **ANSWERED 2026-08-26.** Privacy-first, cookieless — Plausible or Fathom. No GA4, no consent banner | — |
| ~~Q12~~ | **ANSWERED 2026-08-26.** `https://www.linkedin.com/in/pouyalajevardi/` | — |
| **Q27** | **Is a one-business-day response time a commitment you want published?** `src/data/site.ts` carried `responseTime: 'Inquiries are answered within one business day.'` — a service level a reader can hold you to, and not in the §4 register. Set to `null` with a `TODO(pouya)` until confirmed. Raised by `claims-auditor`, 2026-08-26 | The `/contact/` page copy |
| **Q28** | **Which OBA sections?** §4 verifies OBA membership but marks the *section list* `[assumed]` — the brief names Construction & Infrastructure, ADR, and Civil Litigation. `src/data/site.ts` was publishing all three under a blanket `[verified]` stamp; now marked and flagged. Raised by `claims-auditor`, 2026-08-26 | The `/about/` credentials list |
| **Q29** | **Should the deploy guard cover the secrets and `INTAKE_ENDPOINT` too?** As built it covers the three deploy-target variables only. Two gaps, both real: an unset `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`**the most likely first-run failure, since Q22 is unanswered** — passes the guard and burns a full build before dying at `sts get-caller-identity`; and an empty `vars.INTAKE_ENDPOINT` passes silently and **ships a live form posting to nothing**. The second needs the variable promoted to job-level `env:`. Not done: you specified three variables and this widens the guard. Raised by `adversarial-reviewer`, 2026-08-26 | Nothing yet — `src/pages/` is empty. Blocks `/contact/` shipping safely |
| **Q23** | **Is a Gitea `act_runner` registered to this repository or its organisation?** Without one the workflow queues silently and never runs — indistinguishable from a broken pipeline (`docs/06-deployment.md`). *The Gitea-version half of this question is closed:* rather than record a version fact that goes stale, `.gitea/workflows/deploy.yml` now runs a guard as its **first** step that fails the run if `AWS_REGION`, `S3_BUCKET`, or `CLOUDFRONT_DISTRIBUTION_ID` is empty — which is how a pre-1.21 instance manifests. Pouya's decision, 2026-08-26. Runner registration is not something a guard can cover | The first deploy |
## 10. Risks
| Risk | Severity | Mitigation |
|---|---|---|
| A fabricated or unverifiable claim reaches a public page | **High** — professional-conduct exposure for a licensed practitioner | §4 register; every claim traced to a source before it ships |
| A fabricated or unverifiable claim reaches a public page | **High** — professional-conduct exposure | §4 register; every claim traced to a source before it ships |
| Copy silently carried over from the placeholder template | High | Nothing is ported verbatim. All copy written fresh against `docs/03-content-spec.md` |
| Insights section launches and then goes stale | Medium | D9 commits to 35 pieces at launch and a monthly cadence. A dead blog is worse than no blog |
| Personal data in the intake pipeline without a retention policy | Medium — PIPEDA | `docs/05-backend-spec.md` sets retention, and `/legal/privacy/` states it |
| Cutover breaks the live site (D11 is a single-shot deploy) | Medium | Full pre-cutover checklist in `docs/06-deployment.md`; CloudFront can be rolled back to the prior origin path |
| Twenty pages of thin copy rank worse than six good ones | Medium | Each page must justify itself with substantive content. Ship fewer pages rather than padded ones |
| **Deploy-credential blast radius.** AWS account `327082975128` is not a single-project account. It also holds `meshkinilaw.ca` and its preview site, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and **`mlp-clientdb-prod-backups`** — which by its name is a law firm's production client-database backups | **High** | A static deploy key for a personal website must never be able to reach a client database. The scoped IAM policy in `docs/06-deployment.md` grants four actions on one bucket and one distribution and nothing else — that narrowness is now load-bearing, not hygiene. Never widen it. Never reuse the `user/pouya` credentials in CI `[verified 2026-08-26 — inventory]` |
| **Deploy-credential blast radius.** AWS account `327082975128` is not a single-project account. It also holds `meshkinilaw.ca` and its preview site, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and **`mlp-clientdb-prod-backups-327082975128`** (recorded in an earlier entry with the account suffix; the short form `mlp-clientdb-prod-backups` is used elsewhere in the docs) — which **by its name** holds production client-database backups. Only the name was ever observed; the contents were not inspected and the owner is not established | **High** | A static deploy key for a personal website must never be able to reach a client database. The scoped IAM policy in `docs/06-deployment.md` grants four actions on one bucket and one distribution and nothing else — that narrowness is now load-bearing, not hygiene. Never widen it. Never reuse the `user/pouya` credentials in CI `[verified 2026-08-26 — inventory]` |
| **SES is in the sandbox** — confirmed, not hypothetical | **High** | Q19. Until production access is granted, every confirmation email to an inquirer fails silently. The site appears to work; the inquirer concludes they were ignored. Request access early — it is the only dependency with an external turnaround |
| **No SPF and no DMARC on `smlcompany.ca`** | **High** | Q20. Affects SES *and* the existing Google Workspace mail — Pouya's business email is unauthenticated today, which is a live deliverability problem independent of this project. Gmail and Yahoo both penalise it. Add both records before requesting SES production access; AWS also weighs domain authentication when reviewing |
| ~~No SPF and no DMARC on `smlcompany.ca`~~ | ~~High~~**RESOLVED 2026-08-26** | Both records added by Pouya and independently verified (Q20). SPF authenticates Google Workspace mail; SES satisfies DMARC through DKIM alignment on the three resolving CNAMEs. DMARC sits at `p=none` — the residual task is to review reports and tighten to `quarantine`, which is monitoring, not a risk `[verified 2026-08-26 — DNS query]` |
## 11. Glossary
@@ -331,17 +364,785 @@ never being raised again.
| # | Reminder | Raised | Why it must keep coming back |
|---|---|---|---|
| R1 | **Licensure.** The site currently asserts the JD only and describes the boutique role as *active litigation exposure*, never *practice*. Pouya flagged this as an interim position and asked to be reminded to change it | 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/` 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 |
| 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 1218 | 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 |
| R5 | **Fee review at 12 months.** Published rates are sticky; the right moment to move them is deliberate, not reactive | 2026-08-26 | D14 is priced for where the practice is going, not where it is |
| 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 |
| ~~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 | — |
---
# Change Log
## 2026-08-26 (q) — Guard built; and the audit found an inverted DKIM table that would have broken production mail
**Who:** Pouya ruled on entry (o)'s escalations, ratified R7, promoted R8, and
reversed his own F5. Claude Code implemented, ran `claims-auditor` and
`adversarial-reviewer` in parallel, and resolved 31 + 17 findings.
### The six rulings, implemented
1. **Q23 hardened, not checked.** `.gitea/workflows/deploy.yml` now runs a guard
as `steps[0]` — before checkout, before `npm ci`, before any AWS call — that
fails the run naming any of `AWS_REGION`, `S3_BUCKET`,
`CLOUDFRONT_DISTRIBUTION_ID` that is empty. It tests the env names
(`AWS_DEFAULT_REGION`) and reports the **Gitea variable** names, which is the
mapping most likely to be got wrong. `adversarial-reviewer` executed the
extracted block under `bash -e`, `sh -e`, and `bash -euo pipefail` across
all-unset / all-set / one-empty / whitespace-only and confirmed it fails
closed and is POSIX-clean.
2. **R7(3) dropped** — the `aws s3 ls` pre-flight is out; the guard supersedes it.
3. **R7(1) ratified** — the cache table stays matched to the pipeline.
4. **R7(2) reasoning corrected.** Old → new: "the intended cover is an S3
lifecycle rule" → **no lifecycle rule exists and is not the cover**; the cover
is that `aws s3 sync` only goes multipart above 8 MB. Corrected once more
after audit: the figure now measures **what is uploaded**, with
`src/assets/pouya-lajevardi.jpg` at 357,627 bytes `[verified — stat]` as the
largest source asset and an instruction to re-measure `./dist` after the first
build. The old wording measured the repository, which is not what syncs.
5. **R8 promoted to a rule** in `CLAUDE.md`, `/build` Phase 6, `/wrap` step 3.
6. **R1 put in the conditional** — there is no site; `src/pages/` is empty.
### The finding that mattered most was nowhere near the diff
**`docs/05-backend-spec.md` had the two SES DKIM sets exactly inverted.** It
labelled `3zsn…` / `jejgp…` / `xpiwy…` "**Live.** Matches SES exactly. Never
delete", and `f5pu…` / `jdue…` / `kznn…` "Orphans from an earlier verification.
Inert." §7 records the opposite, `[verified 2026-08-26 — DNS]`: the `f5pu` set is
the one that **resolves** and backs the healthy `ca-central-1` identity; the
`3zsn` set is NXDOMAIN and belongs to a stray `us-east-1` identity.
Entry (j) corrected this. **The correction reached §7 and never reached
`docs/05`.** An operator tidying DNS from that table deletes the three CNAMEs
that make SES DKIM work — and since SES has no custom MAIL FROM, DKIM is the
*only* thing satisfying DMARC. Outcome: intake mail from `ca-central-1` starts
failing authentication, silently, and the table said "Never delete" over the
wrong three records. Both rows rewritten from §7. The "six CNAMEs" claim below
them now says three, which is how many answer.
### Current Truth was stale in three more places
**§7 SPF and DMARC still read "ABSENT"** `[verified 2026-08-26]`, and §10 still
carried a live **High** risk row for it — while Q20, in the same file, records
both records added and independently verified the same day. Rule 2 requires
Current Truth updated in place; it was not. Both §7 rows now record the live
values; the §10 risk row is struck as **RESOLVED**, with the residual
`p=none``quarantine` tightening described as monitoring rather than risk.
`docs/05` opened with "**it is not in place**" and a table headed "Add both of
these" for records that already exist — rewritten, with the superseded state kept
visible as a caution.
**§9 carried Q24, Q25 and Q26 twice each, struck and open, with opposite
verdicts.** The live Q25 still asserted §4's boutique-naming permission stands —
against D16 — after entry (p) had struck it. This is the duplicate-question
defect entry (o) fixed for Q22, reintroduced three times in one session: entry
(p) added struck rows rather than striking the existing ones. The three
superseded open rows are deleted.
### The guide added in entry (p) was a live procedure for the architecture being replaced
`docs/reference/AWS-Hosting-Guide.md` was added **untracked** — so Q24's answer
("copied into `docs/reference/`") was true of one working tree and false of the
repository — with no reference-only marker, in fully executable imperative voice.
Following it creates an IAM user with `AdministratorAccess` in account
`327082975128`, rebuilds the site through the standalone-HTML pipeline D1 and D3
replace, and wires SES to `adr@` / `intake@smlcompany.ca` against D18's
`info@`. It is now `git add`ed and carries a **do-not-execute** banner tabulating
seven contradictions with Current Truth, each resolved in Current Truth's favour.
**It also called Pouya a lawyer** — "**Audience:** You (a lawyer, not a
sysadmin)" — and proposed public copy reading "does not create a lawyer-client
relationship", which is what `NO_RETAINER_NOTICE` in `src/data/site.ts` was
written to avoid. Both corrected.
**The R8 sweep command recorded in entry (o) could not have caught either.**
`grep -rn -i "licensed\|legal professional"` does not match "lawyer". The rule
promoted this session is right; the pattern it was demonstrated with was
incomplete. The corrected sweep is:
```
grep -rn -iE "\blawyer\b|licensed|legal professional|litigator" \
--include="*.md" --include="*.ts" --include="*.astro" .
```
Run, output read. Every surviving hit is rule text, Change Log history, or
"lawyer" referring to the *reader* or a third party (`docs/03:20`, `docs/03:161`,
`docs/01:291`, `docs/08:111`) — none describes Pouya.
### Copy defects one step from shipped pages
- **"a working litigator"** — `AGENTS.md` §1's positioning sentence and
`docs/03-content-spec.md:71`, the line designated for the hero, the About page
and the PDF bio. Not in §4; in Ontario it asserts conducting litigation, which
§4 Forbidden bars. §1's own line asserted "each element is verified"; it was
not. Both now read "close to live litigation". §1's correction is annotated in
place rather than silently rewritten.
- **"an active litigation practice"** — `docs/03:118`, `docs/01:211`,
`docs/01:246`. §4's Verified row was itself corrected from "practice" to
"**exposure**" in entry (l); the copy decks kept the barred word. Fixed to the
register's own substitute.
- **`LegalService` JSON-LD** — `docs/04-seo-spec.md` specified it for the home
page. schema.org defines `LegalService` as a business providing legal advice
and **representation**: it asserts in machine-readable form precisely what D13
bars. Changed to `ProfessionalService`, with the reason recorded so it is not
changed back. `worksFor` on the `Person` node had no register-safe value —
populating it either names the boutique (D16) or misstates the employer — so
it is dropped in favour of `jobTitle`.
- **Tribunal-secretary work** — `docs/01:173,180` and `docs/03:32` instructed
offering it; `docs/07-fees.md:116` records Pouya removing the rate and barring
the offer. Three files against one; the one was right.
- **§4's Forbidden table and `docs/03:52`** justified the superlative ban with
"LSO marketing rules" — the **fourth** surviving form of the licensure claim,
two rows below the `NOT ESTABLISHED` row. Restated on grounds that do not
depend on licence status. *(The §4 instance is the architect's to apply.)*
- **`docs/07-fees.md`** opened with "pending Pouya's sign-off — Q14" and
"**Nothing in this document publishes until Pouya confirms the figures**",
then contradicted itself 85 lines later with "This is the card. Build `/fees/`
from it." An implementer reading top-to-bottom blocks the page. Header
rewritten to D14 as confirmed.
- **Nine stale `Q` blockers** across five spec files — Q4, Q10, Q11, Q12, Q14,
Q20, Q5 — all answered or re-tracked, three of them gating `/fees/`, whose
figures are confirmed. Swept.
### Code defects
- **`src/data/site.ts`** published `responseTime: 'Inquiries are answered within
one business day.'` — a service level a reader can hold him to, not in §4. Set
`null` with a `TODO(pouya)`; **Q27**.
- **The memberships block** carried a blanket `[verified]` stamp over the OBA
section names, which §4 marks `[assumed]`. Downgraded, flagged; **Q28**.
- **`src/content/config.ts`** — `/** Drafts are excluded from the build, the
index, and the sitemap. */` describes three controls, **none implemented**;
the sitemap filter covers `/legal/` only. Same defect class as Q22. Restated
as intent with a deadline. Its `title`/`description` bounds also contradicted
`docs/04` (max 70 / min 70 against 50-60 / 140-160); tightened to the spec.
- **`astro.config.mjs`** set `prefetch: { prefetchAll: true, defaultStrategy:
'viewport' }`, injecting Astro's prefetch script and an IntersectionObserver
into **every** page and speculatively fetching every link that scrolls into
view — against CLAUDE.md's "default to zero JS" and §7's "most pages ship zero
JS", with no Change Log entry. Set to `prefetchAll: false` /
`defaultStrategy: 'hover'`, the setting the written conventions imply. If
prefetch-all is wanted, it needs a decision entry.
- **`src/styles/tokens.css`** attached the ink contrast figure (11.09:1) to both
ink and maroon; on maroon `--gold-l` measures 8.11:1. Both pass AA, so no
accessibility consequence — but the token file is what developers read.
### Also corrected
`docs/06` cited a billing **alarm** verified by `cloudwatch describe-alarms`; the
guide set up an AWS **Budget**, which that command never returns — and nothing
records either as created. Now names `budgets describe-budgets` and says confirm,
do not assume. The booking cutover check is marked conditional on R6, since
`BOOKING_URL` is empty by decision. `scripts/aws-discover.sh` and `docs/06`
claimed "every call is a list or describe" — four are not (`sts
get-caller-identity`, `apigatewayv2 get-apis`, `s3api get-bucket-versioning`,
`configure get`); all are genuinely read-only, so the substance held and the
wording did not. Four stale `AWS-Hosting-Guide.md` paths repointed at
`docs/reference/`. The reference workflow still said "Two passes" above three
sync steps — the one file entry (o)'s three-pass sweep missed, and a
twelve-character grep would have caught it. The IAM procedure had 22 lines of
multipart prose between "attach this policy" and "create an access key", which
also broke the ordered list in CommonMark; moved below step 3.
### Declined, with reasons
**The guard was not widened.** `adversarial-reviewer` showed it does not cover
the secrets — and by Q22, an unset key is the *most likely* first-run failure, so
the stated benefit ("one second instead of a full build") does not apply to the
likeliest case — nor `vars.INTAKE_ENDPOINT`, whose absence would **ship a live
form posting to an empty endpoint**. Both are real. Neither was done: the ruling
named three variables, and `INTAKE_ENDPOINT` additionally needs promoting to
job-level `env:`. **What was fixed instead is the guard's comment**, which
claimed it converted the problem "on every Gitea version" into a clean failure —
broader than what it implements. It now states its scope and points at **Q29**.
**The Gitea 1.21 claim is stamped `[assumed]`, not verified.** The version could
be confirmed with one command on the instance. It was not run here; the guard is
designed to work without knowing the answer, so the stamp is the honest record.
**`docs/06` still pairs the account ID with the backup-bucket callout** — the
pairing entry (m) removed from `README.md` as an inference vector. `docs/06` is
an operational document where the account ID is load-bearing, unlike the README.
Flagged rather than changed; if you want it out, it moves to `aws-inventory.txt`,
which is gitignored.
### Verification
The guard's `run:` block was extracted from parsed YAML and **executed** by
`adversarial-reviewer` under three shells and four input states — fail-closed
confirmed. Both workflow YAMLs parse. The barred-word sweep was run as the
`grep -iE` above and its output read. `stat` gave 357,627 bytes. `npm run lint`,
`check` and `build` were **not run**: `node_modules` is absent and `npm ci`
cannot install without a lockfile, so the `astro.config.mjs`, `site.ts` and
`config.ts` edits are **unverified by any type-checker** — they are small and
syntactically simple, and that is the whole of the assurance.
**Opened:** Q27, Q28, Q29. **Still open:** Q22, Q19 (blocking), Q23 (runner
half), Q5/R6 (parked). **Closed by entry (p), confirmed here:** Q24, Q25, Q26.
**Lesson.** Entry (o) said the sweep must be a command whose output is read.
This entry adds what that costs when the command is wrong: the pattern
`licensed|legal professional` cannot match "lawyer", so a compliant sweep still
missed a file added in the same session. **And the largest finding of the session
was in a file nothing had asked either reviewer to look at.** Both reviewers were
pointed at a diff; the destructive defect was three files away, left by a
correction that reached one file and stopped. Scope the audit to the *decision*
that changed, not the lines that changed.
---
## 2026-08-26 (p) — Q24, Q25, Q26 answered; R7 ratified; R8 promoted to a rule
**Who:** Pouya relaying Claude Code's session close. Claude (architect) deciding.
**Q25 — struck. This was the dangerous one and it was mine.** §4's Verified table
carried "The boutique **may be named on the site**", authorising precisely what
**D16 — never named** forbids. Sequence: Pouya said it could be named, then
reversed; D16 recorded the reversal; **the permission was never removed.** A
locked decision and the table copy is written from disagreed, and the table was
the more permissive of the two. Struck.
The general defect: reversing a decision requires removing the permission it
granted, not only recording the new position. Same shape as the D3 amendment that
went unswept three times.
**Q26 — answered, and the third form of the same claim is gone.** §4's opening
asserted "Every public claim on this site is subject to Law Society of Ontario
marketing rules" — which asserts regulatory status by implication, since those
rules bind licensees. Claude Code was right that this is the same half-measure as
the "legal professional" reversal, one level further down.
Old → new: the register's rationale no longer rests on licensure at all. It rests
on the fabricated-credentials history, which is documented, sufficient, and true
regardless of licence status. The paragraph's own history is kept in place as a
caution — three progressively weaker forms of one unverified claim, each
surviving a sweep.
**§4 now carries a `Licence status — NOT ESTABLISHED` row.** Stamped
`[unestablished]`, a new stamp for this case. It records the honest position: this
file does not know, Pouya's instruction was about the *site* and not a statement
of fact either way, and nothing may assert, deny, or infer it. Claude Code was
also right that the two licensure rows were stamped `[verified]` as *directives*,
which is a different thing from a verified fact — this row separates the two.
**Q24 — closed.** `AWS-Hosting-Guide.md` copied to `docs/reference/`. It is the
only record of how the hand-built infrastructure was created, so it belongs in
the repo rather than in a Drive folder the repo cites but cannot reach. Scanned
before copying: no access keys, no secrets, no account ID.
**Q23 — answered by hardening rather than by version-checking.** The risk is real
— on Gitea below ~1.21, `${{ vars.* }}` interpolates empty and the sync target
becomes `s3://`. But a version check is a fact that goes stale. Ruling: add a
**pre-flight guard step** to `.gitea/workflows/deploy.yml` that fails the run if
any required variable is empty, before any AWS call. That converts a silent
destructive misfire into a clean failure on every Gitea version, and it
subsumes R7's third item.
**R7 ratified, with one amendment.**
1. Cache-policy table amended to match the pipeline — **accepted.** Documenting
what the pipeline does beats documenting an intention. `robots.txt` and
`sitemap*.xml` at `max-age=0, must-revalidate` is immaterial at this size.
2. `s3:AbortMultipartUpload` omitted, lifecycle rule as intended cover —
**accepted, with the reasoning corrected.** The lifecycle rule does not exist,
so it is not the cover. The actual cover is that `aws s3 sync` only goes
multipart above 8 MB and the largest asset in the repo is a 357 KB portrait.
Record *that*, and add a note to revisit if any asset ever approaches 8 MB.
Do not document a control that does not exist — that is Q22's defect again.
3. `aws s3 ls` pre-flight documented but not implemented — **superseded** by the
guard step above, which covers more.
**R8 promoted from a reminder to a rule.** *"A sweep is a command whose output is
read, not a claim."* Entries (l), (m) and (n) each asserted a completed sweep;
instances survived all three — one of them inside `claims-auditor.md`, the
definition of the agent whose job is to catch it. A reminder is the wrong
instrument for something this reliable. It belongs in `/build` Phase 6 and
`/wrap`: any claim that a change was applied across files must cite the command
and be made only after reading its output.
**R1's wording corrected:** it says "the site currently asserts the JD only".
There is no site — `src/pages/` is empty. Restated in the conditional.
**Closed:** Q23, Q24, Q25, Q26. **Still blocking the build:** nothing.
**Still blocking `/contact/`:** Q19. **Still open:** Q22, Q5 (parked).
---
## 2026-08-26 (o) — Five rulings implemented; both reviewers run; the D3 sweep finally completed
**Who:** Pouya ruled on entry (m)'s escalations and reversed his own F5 ruling.
Claude Code implemented, ran `claims-auditor` **and** `adversarial-reviewer` in
parallel on the diff, and resolved 27 + 17 findings.
### The five rulings, implemented
1. **§4 and §10 "licensed"** — the architect's, done in entry (n).
2. **Reversal on "a legal professional's"** — cut from `README.md:35` and
`CLAUDE.md` (twice). The sentences now assert nothing about professional
status and state the reason instead.
3. **`s3:PutObjectAcl` dropped** from the policy in `docs/06-deployment.md`. §10's
"four actions" is now accurate without §10 being touched, and every file that
states a count agrees with the policy: `README.md:77`, `docs/06:82`,
`.gitea/workflows/deploy.yml:13`, `AGENTS.md` §10.
4. **`.github/workflows/deploy.yml` → `docs/reference/github-actions-oidc.yml.example`**
via `git mv`; `.github/` removed entirely. It held exactly one file — no
CODEOWNERS, no dependabot, no templates — and nothing referenced the path.
References updated in `README.md`, `CLAUDE.md`, `docs/06`, `.gitea`, and §7.
5. **Q22** left open with the designed-not-confirmed phrasing intact.
### The reviewers found that the D3 sweep had never actually reached `docs/06`
Both agreed on the same blocking defect, independently. `docs/06-deployment.md`
is the file `README.md` points to as "Full procedure, IAM policy, runner setup" —
and it still opened with:
- `Authority: AGENTS.md §3 D3 (git + GitHub Actions → ...)` — D3 as amended says
Gitea;
- a topology diagram, **the first content block in the file**, reading
`GitHub push to main → GitHub Actions → assume AWS role via OIDC (no stored
keys)`. That is the exact sentence entry (l) called "the serious one", six
lines above the section that contradicts it;
- an entire 19-line section, `## Why OIDC and not access keys`, in the
imperative — *"IAM → Identity providers → add OIDC provider..."* — with **no
marker that it does not apply**, naming a third principal `adr-site-deploy`
that conflicts with `adr-sml-deploy`, and citing Q9, which is answered.
`adversarial-reviewer` named the concrete failure: an operator working the
document top-to-bottom adds a GitHub federation trust to account `327082975128`
— the account §10 rates High blast-radius — that nothing will ever use, and
plausibly never creates `adr-sml-deploy` at all. Two mutually exclusive
credential procedures in one file with no signpost saying which is live.
**Fixed.** Authority line cites D3 as amended. Topology rewritten to Gitea +
static key. The OIDC section is retitled *Why OIDC would have been better — and
why it is unavailable*, opens with a **do not execute** block, and its executable
steps are gone.
### The "licensed" sweep had missed three more files, including the auditor's own persona
Entry (n) scoped the reversal to "`README.md`, `CLAUDE.md` (twice), and this
file." Five instances survived in three files it did not name:
- **`.claude/agents/claims-auditor.md:8`** — *"You audit public copy for a
**licensed legal professional's** marketing site."* This is the system prompt
of the agent that enforces the rule. Its opening sentence asserted the exact
claim its own §"Licensure (D13)" instructs it to flag.
- **`.claude/agents/adversarial-reviewer.md:9`** — *"a legal professional's
dispute resolution practice"*, the precise phrase entry (n) reverses.
- **`docs/08-execution-protocol.md:63`** — *"a licensed professional's public
marketing page"*. **This file was edited in that same diff**, eleven lines
above the surviving phrase.
Entry (n) said "twice in two entries that a claim about a sweep was made without
the sweep being verified. The pattern, not the instances, is the finding." This
was the third. It is now swept by command, not by recall:
```
grep -rn -i "licensed\|legal professional" --include="*.md" --include="*.yml" .
```
Every surviving hit is either rule text stating the prohibition, or Change Log
history, which rule 3 forbids editing.
### Also fixed
**Register and rule accuracy.** §4's internal-docs rule cited a `[assumed]` stamp
on licensure that **does not exist** — the two licensure rows are stamped
`[verified]` as *directives not to publish*, which is a different thing. A reader
following the pointer found nothing there, in the sentence stating the rule.
Reworded here and in `CLAUDE.md:30` to say what is true: the Verified table does
not establish licence status either way. **Duplicate `Q22`** — two rows, two
scopes, one number, one of them mine; merged into the fuller statement, keeping
the architect's `aws iam get-user` check command. **§7 Node** re-stamped
`[verified]` — it was `[assumed]` for something directly checkable.
**Claims that overstated what exists.** `CLAUDE.md:164` still said *"deploys use
OIDC role assumption"* — verbatim the F1 claim, in a file both prior sweeps
touched; it and `CLAUDE.md:119` are corrected. `.gitea/workflows/deploy.yml` and
`CLAUDE.md` asserted the deploy credential in the present tense; both now cite
Q22. `docs/06` asserted five blocking PR checks — the workflow has **no
`pull_request` trigger** and neither `lint` nor `lighthouse` is wired, so nothing
gates a merge today; retitled *planned, not implemented*. `README.md:5` and
`docs/06`'s "every push deploys" both hedged against the never-run-green fact the
same files establish elsewhere.
**The moved reference file carried the defect the move was meant to contain.**
Its line 16 still read *"OIDC role assumption — no long-lived AWS credentials in
this repository"*: present tense, about this repository, and false. It also
carried a live `TODO(pouya): AGENTS.md Q9, Q10` against two struck rows — which
`CLAUDE.md`'s definition of done treats as a build failure — for a variable that
will never be set. Both fixed, along with its claim that the file is usable on
GitLab (different CI schema entirely; it is the design there, not the
implementation).
**Two security-posture claims narrowed.** `docs/06` said the workflow's
`aws sts get-caller-identity` pre-flight means "a credential problem fails loudly
and early rather than halfway through a sync." `sts:GetCallerIdentity` requires
**no IAM permission** — it succeeds for any valid key regardless of policy. It
catches a missing or revoked key and misses an under-scoped one, which is exactly
the risk this diff introduced by removing an action. Narrowed, with the one-line
`aws s3 ls` check that would make it load-bearing. And `README.md` had re-dropped
§10's *"which by its name is"* hedge on `mlp-clientdb-prod-backups` — the defect
entry (m) F2 records as fixed — as had `docs/06:113`, escalated in entry (n) and
not done. Both restored.
**Documentation matching implementation.** The cache section described **two**
sync passes; the workflow runs **three**, and two ordering dependencies are
load-bearing and undocumented — pass 3 re-walks the tree and preserves pass 2's
image headers only because `sync` skips just-uploaded objects, and pass 3's
excludes also exclude those prefixes from `--delete`, which is why hashed assets
from old deploys are kept deliberately. Both now stated. The `robots.txt` /
`sitemap*.xml` row claimed `max-age=3600`; nothing implements it, so the table
now records what the pipeline does. Two conflicting variable tables for the same
five names — one under **Secrets** with real values, one under **Variables** with
placeholders — merged; entering those as secrets would have masked them in the
logs the doc says to read them from. Duplicate booking references (Q5 vs R6)
resolved to R6. `README.md` said "no `src/pages/`"; the directory exists and is
empty, which is not what a reader would find.
**`docs/08-execution-protocol.md`** — entry (n)'s inserted block landed between
*"`claims-auditor` reads the copy..."* and its follow-up *"It is a separate agent
on purpose"*, leaving "It" twelve lines from its antecedent and reading as though
the frontmatter were a separate agent. Block moved below.
### Declined, with reasons
**`s3:AbortMultipartUpload` not added.** `adversarial-reviewer` traced every AWS
call in the workflow against the policy and found the happy path fully covered,
with this one gap: objects over 8 MB upload multipart, and an interrupted
transfer cannot clean up its own parts. It recommended against adding the
permission — an S3 lifecycle rule aborting incomplete uploads after 7 days solves
it without widening the policy. Agreed, and left as an operational item below
rather than a policy change.
**The `aws s3 ls` pre-flight is documented, not implemented.** Adding it changes
the deploy pipeline, which was not in scope. It is written into `docs/06` as the
fix for the narrowed claim.
**§4's tables untouched.** Two findings land in the register itself; both are
escalated below rather than edited. §4 is the architect's instrument.
### Escalated — for the architect
*(All four now tracked: 1 and 2 as **Q25** and **Q26** in §9; 3 folded into Q26;
4 left in R1, which already owns the licensure framing.)*
1. **§4's Verified table authorises what D16 forbids.** `AGENTS.md:153` reads
*"The Toronto litigation and ADR boutique **may be named on the site** ... the
name itself is pending, see Q7."* D16 says **"Never named."** Q7 is answered:
*"Pouya reverted to generic. The boutique is never named."* A **Verified** row
currently permits publication of the exact thing a locked decision bars — the
same shape as the F5 defect in entry (l), in the same table, and the register
is where copy is written from.
2. **§4's opening asserts regulatory status by implication.** *"Every public
claim on this site is subject to Law Society of Ontario marketing rules"* —
LSO marketing rules bind licensees. Stating flatly that they govern this site
asserts the licensure §4 declines to verify, in the sentence that replaced
"Pouya is a licensed legal professional". A half-measure of the same shape as
the "legal professional" reversal.
3. **Consider an explicit §4 row** — *"LSO licence status | UNKNOWN / not
verified either way"* — which would make the internal-docs rule's pointer land
somewhere real instead of being reworded around.
4. **R1 (§12)** describes "the site currently asserts the JD only". There is no
site; `src/pages/` is empty. R1 is the reminder that must not ossify, so it
should be accurate about what it describes.
5. **Operational:** add an S3 lifecycle rule aborting incomplete multipart
uploads after 7 days; and once Q22 is answered, run
`aws s3 sync ./dist s3://adr-smlcompany-site --dryrun` under the new key to
confirm no `s3:GetBucketLocation` call is needed.
6. **Minor:** `.yml.example` is not a Prettier-recognised extension, so the moved
reference file has silently dropped out of `prettier --check` coverage. Frozen
reference material, so near-harmless — but nothing validates it as YAML now.
**Q23Q26 opened, none blocking the build.** Q23: is an `act_runner` registered, and is the Gitea
instance ≥ 1.21 for the `vars` context the workflow depends on throughout? On an
older instance `vars.S3_BUCKET` interpolates empty and the sync target becomes
`s3://`. Q24: `AWS-Hosting-Guide.md` is cited by `docs/06` as the source for
existing infrastructure and two cutover checks, and **does not exist in the
repository**; the two dependent checks have been restated self-containedly, so
nothing is blocked. Q25 and Q26 carry the two §4 escalations above.
**R7 and R8 added to §12.** R7 records three spec decisions made on Pouya's
behalf and not ratified — the amended cache-policy row, the omitted
`s3:AbortMultipartUpload`, and the documented-not-implemented `aws s3 ls`
pre-flight. R8 records the sweep-verification lesson as a standing discipline
rather than a lesson buried in one entry, which is what let it recur three times.
### Verification
`npm run lint`, `npm run check`, and `npm run build` were **not run** —
`node_modules` is absent, and `npm ci` cannot install without a lockfile. No
JavaScript, TypeScript, or Astro source was touched. The `.gitea` workflow and
the `.yml.example` were not YAML-parsed either: PyYAML is unavailable in this
environment and the project has no YAML tooling installed. Both were changed only
in comment blocks, and the indentation of every edited comment was checked by
eye. The "licensed" sweep was executed as the grep above and its output read, not
asserted. Every prose line added to `README.md` and `CLAUDE.md` is ≤ 80 columns.
**Lesson.** Entry (m) recorded that a correction is a change and gets reviewed
like one. This pass adds the sharper version: **the sweep must be a command, and
its output must be read.** Three consecutive entries claimed a completed sweep;
three times instances survived, and the third time one of them was inside the
definition of the agent whose job is to catch it. The `/wrap` step should require
pasting the grep, not describing it.
---
## 2026-08-26 (n) — Register corrected; a declined finding reversed on appeal
**Who:** Pouya relaying Claude Code's re-audit. Claude (architect) recording.
**Correction to entry (l), which was false.** It claimed all three files had been
rewritten off "licensed". Two were. **This file still said it twice** — §4's own
opening sentence ("Pouya is a licensed legal professional") and §10's risk row
("professional-conduct exposure for a licensed practitioner"). The register was
asserting as established fact the very thing it marks `[assumed]`, in the section
that exists to prevent exactly that. Entry (l) stands as written per rule 3; this
is the correction. Both lines are now fixed.
That is twice in two entries that a claim about a sweep was made without the
sweep being verified. The pattern, not the instances, is the finding.
**Reversal — "a legal professional's" is out too.** `claims-auditor` wanted the
phrase cut entirely, arguing that in Ontario it reads as an LSO licensee and so
trips the same bar as "licensed". Claude Code declined it, correctly, because it
was my explicit ruling — and recorded the reasoning rather than silently keeping
it, which is what let it be revisited.
**On appeal the auditor is right and I was wrong.** "Legal professional" was a
half-measure: a weaker assertion of the same unverified thing. The fix is to
assert nothing about professional status and state the reason instead — the
previous site carried fabricated credentials, which is true regardless of licence
status and carries the full force of the rule. Applies to `README.md`,
`CLAUDE.md` (twice), and this file.
Worth preserving: **the mechanism worked because the decline was reasoned rather
than silent.** A finding dropped quietly is a finding that cannot be appealed.
**Q22 opened — does `adr-sml-deploy` exist?** Claude Code caught that its own
README rewrite asserted a control nothing records as in place. `docs/06` is
written imperatively — "Create the user", "Create an access key" — a procedure,
not a record. Nothing in this file says it was performed. Its framing of this as
"same failure class, inverted" is exactly right: the original README understated
the credential risk, its replacement overstated the control. **The §10 rotation
obligation has no subject until Q22 is answered.**
**Accepted — §10's "four actions" resolves by tightening the policy, not editing
the count.** `docs/06` grants five: `ListBucket`, `PutObject`, `PutObjectAcl`,
`DeleteObject`, `CreateInvalidation`. `s3:PutObjectAcl` is unused by `aws s3 sync`
without `--acl`, and inert under OAC with ACLs disabled. Dropping it makes §10
accurate **and** the policy tighter — strictly better than correcting the number.
Handed to Claude Code.
**Accepted — move the GitHub workflow out of `.github/`.** Gitea falls back to
`.github/workflows` when `.gitea/workflows` is absent, so a live `push:` trigger
sitting there is conditionally inert, not inert. Moving it to
`docs/reference/github-actions-oidc.yml.example` makes it structurally inert.
Handed to Claude Code.
**Also noted, no action:** Claude Code reported that `npm run lint` and
`npm run check` could not run because `node_modules` is absent, and said so
rather than reporting a check it did not run. That is the Phase 5 rule working.
**Opened:** Q22. **Still blocking the build:** nothing. **Still blocking
`/contact/`:** Q19.
---
## 2026-08-26 (m) — README corrected against D3-as-amended; second audit found nine more
**Who:** Pouya ruled on the five findings in entry (l); Claude Code implemented,
re-ran `claims-auditor` on the result, and resolved the second pass.
### The architect's rulings, applied
**F5 — no change, as ruled.** D13 bars "practice" for the boutique role, not for
Pouya's own ADR practice. `README.md:3` and `package.json:5` stand.
**F2 — scope corrected, wording changed.** The Forbidden table governs published
pages, not repository documentation; that scope is now stated in §4. The real
defect was different and the architect identified it: licensure is `[assumed]`
in §4, so no file should call Pouya "licensed". `README.md:36` now reads *a legal
professional's public marketing surface* — the clause that follows, about the
previous site's fabricated credentials, already carries the reasoning.
**F1, F3, F4 — Deployment section rewritten** against §7 and
`docs/06-deployment.md`. The section had been written against D3 as originally
decided and never swept when D3 was amended to Gitea. It now names
`.gitea/workflows/deploy.yml` as the live pipeline, says Gitea Actions rather
than GitHub Actions, states plainly that a long-lived AWS credential is involved
and why (Gitea is not an AWS OIDC provider), and names the quarterly rotation
obligation. `format` and `lighthouse` added to the scripts table.
### The second audit found nine findings in the replacement text
Seven fixed, one declined, one escalated. The re-audit was worth running: the
rewrite traded the original defect for its mirror image.
**F1 (fixed) — the replacement asserted a control state that may not exist.**
The new text said deploys *do* authenticate as IAM user `adr-sml-deploy` with a
key in Gitea secrets. Only the **decision** is verified (D3 as amended, §7).
`docs/06-deployment.md` is written entirely in the imperative — "Create the
user", "Create an access key" — i.e. a procedure to perform, not a record of one
performed, and `adr-sml-deploy` appears nowhere else in this file. The old README
understated the credential risk; the new one asserted a control that nothing
records as in place. Same failure class, inverted. Now phrased as designed-not-
confirmed, and **raised as Q22**.
**F2 (fixed) — a hedge was dropped, and an account number was disclosed.** §10
says `mlp-clientdb-prod-backups` is "**which by its name is** a law firm's
production client-database backups" — only the bucket name was ever observed.
The README stated the contents flatly. It also paired the live account ID with
that statement in the repo's most pasteable file, and D16 says the boutique is
never named — "this account holds a law firm's client database" is an inference
vector that costs nothing to remove. The account number and the bucket name are
both now out of the README; the security reasoning stays, pointing at §10.
**F3 (fixed).** The bullet led with "**The key is rotated quarterly**" — a
description of an operating control — and conceded four lines later that the
task has no owner. Now an obligation: *must be rotated quarterly, and nobody
owns that yet.*
**F4 (fixed).** "It does not run. Editing it changes nothing." is conditional,
not absolute: Gitea falls back to `.github/workflows` when `.gitea/workflows` is
absent, and that file carries a live `push` trigger on `main`. Realised risk is
low — it would fail on `role-to-assume` with no OIDC provider rather than
mis-deploy — but describing a trigger-eligible file as inert invites someone to
treat it as safe. Now states the condition. **The auditor's better fix was
declined for scope:** moving it to `docs/reference/github-oidc-deploy.yml.example`
would make the claim structurally true instead of dependent on Gitea's
resolution order. Worth doing; not done here.
**F6 (fixed) — "Pushes to `main` build and deploy automatically" was not true.**
Verified against the filesystem, not inferred: there is no `package-lock.json`,
so `.gitea/workflows/deploy.yml:46`'s `npm ci` exits non-zero at step one; there
is no `src/pages/`, so both `npm run check` and `npm run build` fail; and whether
an `act_runner` is registered is recorded nowhere, which `docs/06:125-130` warns
"looks exactly like a broken pipeline." The README now says the pipeline has
never run green and names all three reasons.
**F7 (fixed).** "No wildcard actions, no wildcard resources" is literally false —
the policy uses `arn:aws:s3:::BUCKET_NAME/*`, correctly scoped but a wildcard. In
a paragraph whose entire job is to stop a future maintainer widening the policy,
an overstatement is the wrong error: the first reader who notices it discounts
the rest. Now: no `Action: "*"`, no `Resource: "*"`, nothing outside one bucket
and one distribution.
**F8 (fixed).** `npm run lint` and `npm run lighthouse` are listed in a table a
reader takes as "these work." Neither does — there is no ESLint flat config, and
ESLint 9 (pinned `^9.0.0`) errors without one, so the command dies before
Prettier runs; and there is no `lighthouserc`, so `lhci autorun` has no budgets
even though `docs/06:221-222` makes Lighthouse a blocking check. Both rows are
now marked **not yet wired**.
**F9 (fixed).** Two precision defects. The README restated the project's single
most important rule more narrowly than §4 states it — "no factual claim **about
the practice**" versus §4's "him, his credentials, his experience, or his
practice" — in the file a new contributor reads first. And it said `/build` runs
two review agents unconditionally, where `.claude/commands/build.md:52-53` allows
skipping `claims-auditor` when no copy changed. Both now mirror their sources.
**F5 (declined) — the auditor asked to cut "a legal professional's" entirely,**
on the grounds that in Ontario the phrase reads as an LSO licensee and so trips
§4's bar on "phrasing that *implies* entitlement to practise law." Declined
because the architect ruled this exact wording four hours earlier, having already
considered and rejected "licensed", and because the same phrase now appears
deliberately in `CLAUDE.md`. Recorded rather than silently kept: the auditor's
reasoning is not obviously wrong, and if the phrase is judged to imply licensure
it needs changing in three files at once, not one.
### Escalated to the architect — not fixed here
1. **Entry (l) records `CLAUDE.md`, `AGENTS.md`, and `README.md` as all rewritten
off "licensed". Two of the three were.** `CLAUDE.md` is clean. **This file
still says it twice** — line 110, "Pouya is a licensed legal professional",
in the opening sentence of §4 itself, and line 303, "professional-conduct
exposure for a licensed practitioner", in §10. The register is asserting as
fact the thing it marks `[assumed]`, in the section whose whole purpose is to
stop that. Not edited here because §4 is the architect's instrument.
2. **§10 says the deploy policy "grants four actions on one bucket and one
distribution."** It grants five: `s3:ListBucket`, `s3:PutObject`,
`s3:PutObjectAcl`, `s3:DeleteObject`, `cloudfront:CreateInvalidation`. The
discrepancy resolves in a useful direction — `s3:PutObjectAcl` is unused by
`aws s3 sync` and inert under OAC with ACLs disabled (`docs/06:205-206`), so
the policy is one action wider than it needs to be. Dropping it makes the
§10 count correct and the policy tighter at the same time. `docs/06:108` also
drops §10's "by its name" hedge on the backup bucket.
3. **The `.github` workflow's location** — see F4 above.
### Verification
`npm run lint` and `npm run check` were **not run**: `node_modules` is absent, so
neither can execute. This change touches only `README.md`, which is outside
`astro check` and ESLint; Prettier has no config in the repo, so its default
`proseWrap: "preserve"` leaves markdown prose unreflowed. Line widths were
checked by hand and every line Claude Code wrote is ≤ 80 columns.
**Lesson, extending entry (l)'s.** (l) recorded that amending a decision requires
sweeping every file that referenced the original. This pass adds the corollary:
**a correction is a change, and gets reviewed like one.** Seven of the nine
findings above were in text written specifically to fix an audit finding. The
first rewrite of a false claim is the most dangerous moment for it, because
everyone involved is now confident the area is clean.
---
## 2026-08-26 (l) — First adversarial review run; it found defects in the specs
**Who:** Pouya ran `claims-auditor` against `README.md` to verify the agents load.
It loaded, and returned five findings before any site code exists.
**The agents are confirmed working.** R1 was surfaced unprompted, as §12 requires.
F2 and F5 were escalated rather than silently edited — the behaviour the brief
asks for on judgement calls.
**Two register-level defects fixed here (architect's, not Claude Code's):**
**F5 root cause — §4 contradicted D13.** Old → new: the Verified table read
*"Active litigation practice: personal injury, construction, regulatory (POA),
insurance (SABS)"* → **"Active litigation exposure: …"**. The row quoted the
strategy brief verbatim and used the exact word D13 bars in exactly the context
D13 bars it. Anyone writing copy from the register would have inherited the
violation **from the register itself** — the worst place for it to live.
D13 now also states the distinction explicitly: "practice" is barred **for the
boutique role**, not for Pouya's own ADR practice. So "the dispute resolution
practice of Pouya Lajevardi" is correct — `README.md:3` and `package.json:5` need
no change. **F5 resolved, no edit to the README.**
**F2 — the Forbidden table's scope was overbroad.** Old → new: heading read "must
not appear **anywhere**" → **"must not appear on the site"**, with the scope
stated: published pages including JSON-LD, not repository documentation.
The auditor was right that something was wrong, though the rule it cited was the
wrong one. `README.md`, `CLAUDE.md`, and this file all called Pouya "a licensed
professional" while §4 marks licensure **`[assumed]`**. Internal docs were
asserting as fact what the register cannot vouch for. A new rule now covers this
directly. The three phrases are rewritten to state the *reason* for the
strictness — the previous site carried fabricated credentials — rather than a
credential we cannot confirm.
**F1, F3, F4 — handed to Claude Code.** One root cause: `README.md`'s Deployment
section was written against D3 as originally decided and never updated when D3
was amended to Gitea. `CLAUDE.md`, `docs/06`, and this file were all updated; the
README was missed.
F1 is the serious one. "Deploys via OIDC — there are no long-lived AWS credentials
in this repository" is **false**, and dangerous in the specific way the auditor
identified: it is narrowly defensible, since the key is not tracked in git — which
is exactly why it would suppress the quarterly rotation obligation in D3, in an
account §10 rates High blast-radius because it holds `mlp-clientdb-prod-backups`.
**Lesson recorded.** Amending a decision requires sweeping every file that
referenced the original, not the files that happen to come to mind. The review
caught it; the amendment process should have. Worth adding to `/wrap`.
---
## 2026-08-26 (k) — Q21 resolved: nothing broken; SES sandbox is per-region
**Who:** Pouya ran the per-region check.