Q19 is closed — SES production access granted in ca-central-1, confirmed in writing. Nothing now blocks /contact/. The structural change is the important one. Specs in docs/ carried their own copies of resource IDs, regions, DNS records and service state. AGENTS.md §7 is now the single source of truth for operational facts and docs/ cite it rather than restating it, with the rule recorded in CLAUDE.md under Conventions. The reason is the previous commit's DKIM inversion, generalised: the same fact lived in §7 and docs/05, a correction reached one of them, and the stale copy told an operator to delete the records that authenticate outbound mail. A duplicated fact is one that will eventually be wrong in one place, and the copy that goes stale is the one nobody re-reads. Verified by grep over docs/*.md — no operational identifier remains. Also in this change: - §7 records the SES monitoring: SNS topic ses-alerts, alarms SES-BounceRate-High (>= 0.03) and SES-ComplaintRate-High (>= 0.001), and the deliberate choice of email feedback forwarding over an SNS feedback topic at this volume. The ses-alerts email subscription is stamped PENDING CONFIRMATION — the alarms currently notify nobody, now tracked as R9 and on the cutover checklist. - docs/05 records why those alarms are a real control: SES suspends above roughly a 5% bounce rate, and under 100 messages a month five bounces crosses it. - Q29: the deploy guard now covers AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (emptiness only, never echoed) and INTAKE_ENDPOINT, promoted to job-level env. An empty intake endpoint ships a live form posting to nothing, which is worse than a failed build. Executed under sh -e across four input states; fails closed, leaks nothing. - docs/06: account ID removed from the backup-bucket callout, pointing at §10 instead, as README already does. - astro.config.mjs: prefetch removed entirely. Any setting ships Astro's prefetch script to every page against the zero-JS convention. Recorded as a decision; revisit against real Lighthouse numbers. AGENTS.md entry (r) records the full reasoning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
adr.smlcompany.ca
The dispute resolution practice of Pouya Lajevardi — Toronto.
A static site built with Astro, built for deployment to Amazon S3 behind CloudFront by Gitea Actions — see Deployment; the pipeline is not yet proven.
Quick start
nvm use # Node 22
npm install
npm run dev # http://localhost:4321
Scripts
| Command | Does |
|---|---|
npm run dev |
Development server with hot reload |
npm run build |
Static build to ./dist |
npm run preview |
Serve the built site locally |
npm run check |
astro check — type and template errors |
npm run lint |
ESLint + Prettier check — not yet wired, no ESLint config exists |
npm run format |
Prettier — rewrite files in place |
npm run lighthouse |
Lighthouse CI — not yet wired, no lighthouserc exists |
Before you contribute
Read AGENTS.md first, and maintain it as you work — it is the living
record of what this project is, what was decided, and why. Then read
CLAUDE.md for the working rules, and the specs in docs/.
The single hardest rule: no factual claim about Pouya, his credentials, his
experience, or his practice ships unless it appears in the verified register in
AGENTS.md §4. This is a public marketing surface, and the site it replaces
contained fabricated credentials.
How work is done here
Pouya decides; Claude Code implements and then adversarially reviews its own
work. Run /build <task> for any substantive change — it plans, implements,
runs two independent review agents on the diff (the claims audit wherever copy
changed), resolves the findings, verifies the build, and records the session in
AGENTS.md. /review runs the review pass alone; /wrap closes a session.
Full protocol and prompt guidance: docs/08-execution-protocol.md.
Deployment
.gitea/workflows/deploy.yml is the deploy pipeline — Gitea Actions, not
GitHub Actions. On a push to main it builds, syncs to S3, and invalidates
CloudFront.
It has never run green. There is no package-lock.json, so npm ci exits
at step one; src/pages/ is empty, so there is nothing to build; and whether an
act_runner is registered is recorded nowhere. The workflow is written; the
pipeline is unproven.
The workflow's first step guards against the other way this fails quietly:
it aborts the run, naming the variable, if AWS_REGION, S3_BUCKET, or
CLOUDFRONT_DISTRIBUTION_ID is empty — which is how a Gitea too old for the
vars context manifests.
The GitHub Actions equivalent, which uses OIDC role assumption, is kept as
docs/reference/github-actions-oidc.yml.example in case the project ever moves
to a forge that supports it. It sits outside .github/workflows/ on purpose:
Gitea falls back to that directory when .gitea/workflows is absent, so a
workflow file left there with a push trigger would be only conditionally
inert. As an .example under docs/ it cannot be picked up at all.
The pipeline is designed around a long-lived AWS credential. Gitea is not an
AWS OIDC provider, so there is no role to assume: deploys are to authenticate as
a scoped IAM user, adr-sml-deploy, with its access key held in the
repository's Gitea Actions secrets. Nothing in AGENTS.md records that user as
created or that key as issued — docs/06-deployment.md is a procedure to
perform, not a record of one performed. Two things are meant to bound the risk,
and neither is confirmed in place:
- The policy must stay narrow. Four actions:
s3:ListBucketon one bucket,s3:PutObjectands3:DeleteObjecton that bucket's contents, andcloudfront:CreateInvalidationon one distribution. NoAction: "*", noResource: "*", nothing outside that one bucket and that one distribution. The AWS account is shared with unrelated projects, including a bucket whose name indicates another business's production database backups — that narrowness is what keeps a compromised runner away from it, and it is load-bearing rather than hygiene. SeeAGENTS.md§10. If a deploy step needs a permission the policy lacks, question the step; do not widen the policy. - The key must be rotated quarterly, and nobody owns that yet. Create a second access key, update the Gitea secrets, confirm a deploy succeeds, then delete the old one — rotation that leaves the old key active is not rotation. OIDC would have removed the obligation entirely; it is unavailable, so this is a standing calendar task still waiting on an owner.
Full procedure, IAM policy, runner setup, and cutover checklist:
docs/06-deployment.md.