Compare commits
146 Commits
pages
..
33ce544a1e
| Author | SHA1 | Date | |
|---|---|---|---|
| 33ce544a1e | |||
| f53159f0b4 | |||
| 0c02780dd9 | |||
| aa28158534 | |||
| 429b4e2e87 | |||
| c9796a3e7f | |||
| 9486188983 | |||
| 2560437bfa | |||
| bf46363ea9 | |||
| 18862e8e13 | |||
| 6974800d2e | |||
| bd3805f883 | |||
| 8f82304758 | |||
| a97c2a4034 | |||
| a1eb1e79ea | |||
| c2b35d5355 | |||
| 8c7ca2aded | |||
| c7d06a120d | |||
| aefbd206f9 | |||
| 0bca43cbfd | |||
| 47fff7d289 | |||
| 2b621843a2 | |||
| d15c301fdb | |||
| 9ca3f48def | |||
| 12bc1dc067 | |||
| 71c74fb7f1 | |||
| edd03f9716 | |||
| 6bfae2033e | |||
| af1133f019 | |||
| c2a046d6f7 | |||
| 64a7f2e4c5 | |||
| 83c956c2b6 | |||
| 208ec9447b | |||
| 054393f7af | |||
| c022a93302 | |||
| ea5178c3da | |||
| aa49218fc8 | |||
| 2b49106b7f | |||
| 38b92bc66c | |||
| b46d8008fd | |||
| db3ffdf6b0 | |||
| cac1115035 | |||
| a5d9630dd8 | |||
| 715f4f80b1 | |||
| f7985bfe2a | |||
| 677c511979 | |||
| 98ab6db3de | |||
| 6d669d760a | |||
| 0b4f2dd403 | |||
| be2cf2d1c5 | |||
| 64b506aa32 | |||
| dc01460ee3 | |||
| 595006bcba | |||
| 24d0af4840 | |||
| 4b758c765d | |||
| 7f11b6e88e | |||
| fc063f606a | |||
| 421c84ff92 | |||
| d62c6bf958 | |||
| 99ccc8e249 | |||
| 966dfcb926 | |||
| 99d42f8fbc | |||
| 44f7fb8a01 | |||
| ea5f9356e7 | |||
| e8f6b8f488 | |||
| 7a1211ac5f | |||
| c6e6657086 | |||
| 9975757445 | |||
| 6ec1e31ec5 | |||
| 59bbff0ad9 | |||
| c19e77bcac | |||
| aa2653340a | |||
| 73d1b62989 | |||
| 47c50d43dd | |||
| 66c7c0b014 | |||
| 8388dd63fe | |||
| 9621ba44bf | |||
| d234f40134 | |||
| 0812a02219 | |||
| 3fdfbe3ae9 | |||
| 4305de9eb7 | |||
| 7d86c28c90 | |||
| 2a46cdc67d | |||
| 134bd37ec4 | |||
| 71e4775573 | |||
| 6119abfa32 | |||
| 2bd96d1f8c | |||
| d58e08b89e | |||
| 107e429fb9 | |||
| 34f6961264 | |||
| c00964132c | |||
| 1247c48ce4 | |||
| 4352745612 | |||
| 6bb01602b3 | |||
| f8c3394f3d | |||
| ba56f7a0c9 | |||
| fe25e053a9 | |||
| fa57cc156a | |||
| 723abeafb5 | |||
| da790de20d | |||
| 950dd3229c | |||
| d7820edb04 | |||
| ba460f5b8b | |||
| 9c013b056b | |||
| 29a5ca0035 | |||
| c159f4cdb8 | |||
| 23060cca74 | |||
| 233cc5d6e6 | |||
| 914a813b8f | |||
| c6b87b74ae | |||
| 82601e104e | |||
| 73ceae2aa8 | |||
| 3caa276573 | |||
| 61f6afd66c | |||
| e0361a3bda | |||
| 3b54b45427 | |||
| 0a60601272 | |||
| d96d61fa49 | |||
| b484302afd | |||
| db4ae19c0a | |||
| febb8914b4 | |||
| 4acdd1e571 | |||
| f241c5581a | |||
| 3cc5dd7652 | |||
| 48c31dc1b3 | |||
| 63da0a4727 | |||
| dcef196f25 | |||
| 38bac60c88 | |||
| a6107e4998 | |||
| 7896b634a8 | |||
| ed7f0f461f | |||
| 71021e60cf | |||
| 2e79aacecc | |||
| db86edbc21 | |||
| c9ec9e3393 | |||
| b554f87d51 | |||
| a45aa84318 | |||
| 8123965bf2 | |||
| eef1d9a86b | |||
| db2e969fc3 | |||
| 971b5ec7bc | |||
| f5631acb2c | |||
| 33df09f541 | |||
| 88eecb1c71 | |||
| 1cc9469d2d | |||
| aae4d42229 |
@@ -0,0 +1,66 @@
|
|||||||
|
# Orchestrator
|
||||||
|
|
||||||
|
Map of `.agents/`, and how to pick who does what.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
.agents/
|
||||||
|
ORCHESTRATOR.md you are here
|
||||||
|
context/ how things are — read before deciding
|
||||||
|
rules/ how things must be — binding
|
||||||
|
checklists/ gates to run before you claim done
|
||||||
|
skills/ procedures an agent loads on demand
|
||||||
|
agents/ specialist agent definitions
|
||||||
|
templates/ component and page starting points
|
||||||
|
scripts/ deterministic helpers (prefer these over prose)
|
||||||
|
```
|
||||||
|
|
||||||
|
`context/` describes reality, `rules/` constrain change. When they disagree,
|
||||||
|
reality won — update the rule and say so.
|
||||||
|
|
||||||
|
## Specialists
|
||||||
|
|
||||||
|
Each agent has one responsibility, one set of rules, and its own worktree. Full
|
||||||
|
definitions in [`agents/`](agents/).
|
||||||
|
|
||||||
|
| Agent | Owns | Loads skills |
|
||||||
|
| ---------------------------------------------------------- | --------------------------------------------------- | -------------------------------------- |
|
||||||
|
| [`astro-architect`](agents/astro-architect.md) | project scaffold, config, routing, layouts | `astro-page` |
|
||||||
|
| [`design-system-keeper`](agents/design-system-keeper.md) | tokens, the single palette, type scale | `design-tokens` |
|
||||||
|
| [`component-builder`](agents/component-builder.md) | one component per task, from templates | `astro-component`, `design-tokens` |
|
||||||
|
| [`page-migrator`](agents/page-migrator.md) | one page per task, HTML → `.astro` | `astro-page`, `content-migration` |
|
||||||
|
| [`motion-designer`](agents/motion-designer.md) | transitions, islands with animation | `motion` |
|
||||||
|
| [`content-i18n-migrator`](agents/content-i18n-migrator.md) | strings out of `app.js` into content collections | `content-migration` |
|
||||||
|
| [`verification-engineer`](agents/verification-engineer.md) | keeping `verify.mjs` meaningful across the refactor | `verify-contract`, `visual-regression` |
|
||||||
|
| [`reviewer`](agents/reviewer.md) | merge gate; reads diffs, never writes features | all |
|
||||||
|
|
||||||
|
## Working agreement
|
||||||
|
|
||||||
|
1. **One task, one worktree, one agent.** See
|
||||||
|
[`rules/git-worktrees.md`](rules/git-worktrees.md).
|
||||||
|
2. **Read `context/` first.** Especially `design-system.md` and
|
||||||
|
`verification.md`. Most wrong answers here come from assuming the CSS is
|
||||||
|
already coherent.
|
||||||
|
3. **Templates over invention.** `templates/components/` and `templates/pages/`
|
||||||
|
exist so ten parallel agents produce one house style, not ten.
|
||||||
|
4. **The gate is `pnpm run verify` plus the relevant checklist.** Green tests
|
||||||
|
with deleted assertions is a failed task.
|
||||||
|
5. **Report what you did not do.** Partial work with an honest boundary is
|
||||||
|
useful; silent narrowing is not.
|
||||||
|
|
||||||
|
## Task flow
|
||||||
|
|
||||||
|
```
|
||||||
|
plans/astro-refactor/task-NN.md ← the brief
|
||||||
|
↓
|
||||||
|
git worktree add ../af-task-NN ← isolation
|
||||||
|
↓
|
||||||
|
agent loads .agents/agents/<role>.md + its skills
|
||||||
|
↓
|
||||||
|
checklists/before-*.md ← self-gate
|
||||||
|
↓
|
||||||
|
pnpm run verify ← hard gate
|
||||||
|
↓
|
||||||
|
reviewer agent on the diff ← merge gate
|
||||||
|
```
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: astro-architect
|
||||||
|
description:
|
||||||
|
Owns the Astro scaffold — config, routing, layouts, build pipeline, and the
|
||||||
|
publishing decision. Use for task 01 and any later change to astro.config.mjs,
|
||||||
|
package.json, or the deploy path. Do not use for component or page work.
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You own the foundation. Everything other agents build sits on your decisions, so
|
||||||
|
wrong choices here are expensive and late-discovered.
|
||||||
|
|
||||||
|
**Read first**: `.agents/context/architecture.md`,
|
||||||
|
`.agents/context/publishing.md`, `.agents/rules/astro.md`. **Load skill**:
|
||||||
|
`astro-page`.
|
||||||
|
|
||||||
|
## You own
|
||||||
|
|
||||||
|
`astro.config.mjs`, `package.json`, `tsconfig.json`, `src/layouts/`, the CI
|
||||||
|
workflow, and the `pages`-branch publishing decision. You are the **only**
|
||||||
|
writer of these. Other agents report problems with them; they do not edit.
|
||||||
|
|
||||||
|
## Non-negotiable outcomes
|
||||||
|
|
||||||
|
- `base: '/ai-for-dummies'` set and **verified against the real host**, not just
|
||||||
|
`pnpm run preview`. Base-path bugs are the most likely production-only
|
||||||
|
failure.
|
||||||
|
- Every existing URL resolves identically, trailing slash included.
|
||||||
|
- Zero JS by default. Astro ships none unless a component asks.
|
||||||
|
- `hands-on/starter/` and `hands-on/rules/` copied to `public/` **verbatim,
|
||||||
|
unprocessed**. They are lab fixtures; the exercise is that they are plain
|
||||||
|
files.
|
||||||
|
- No UI framework, no CSS framework, no runtime dependencies.
|
||||||
|
|
||||||
|
## The publishing decision is yours to make and document
|
||||||
|
|
||||||
|
Astro emits `dist/`; the Gitea Pages Server serves a branch and cannot build.
|
||||||
|
Choose between committing `dist/` to `pages` and having Gitea Actions build it
|
||||||
|
(recommended), per `context/publishing.md`. Then **rewrite
|
||||||
|
`docs/operations-guide.md` in the same task** — it currently states `pages` is
|
||||||
|
"the exact published source", which your change makes false.
|
||||||
|
|
||||||
|
Known trap: this Gitea's act-runner registration lives in an `emptyDir`, so a
|
||||||
|
pod restart silently kills CI. Put that in the runbook.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
`pnpm run build` succeeds, one migrated page serves correctly from the real host
|
||||||
|
under `/ai-for-dummies/`, `pnpm run verify` and `node scripts/audit-ui.mjs` are
|
||||||
|
green, and the operations guide matches reality.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: component-builder
|
||||||
|
description:
|
||||||
|
Builds one Astro component per task from the project templates. Use for the
|
||||||
|
component-extraction tasks (05-09). Do not use for page migration, token
|
||||||
|
changes, or verify.mjs.
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You build **one component per task**, from a template, in your own worktree.
|
||||||
|
Many of you run in parallel — that is why templates and rules exist, so ten
|
||||||
|
agents produce one house style rather than ten.
|
||||||
|
|
||||||
|
**Read first**: `.agents/rules/componentization.md`, `.agents/rules/astro.md`,
|
||||||
|
`.agents/rules/theming.md`. **Load skills**: `astro-component`, `design-tokens`.
|
||||||
|
|
||||||
|
## Scope discipline
|
||||||
|
|
||||||
|
Your task names one component. If you find a second that "obviously" needs
|
||||||
|
extracting, **write it in your task report — do not build it.** Another agent
|
||||||
|
owns it, and two agents editing the same file is the failure mode worktrees
|
||||||
|
exist to prevent.
|
||||||
|
|
||||||
|
You may not edit `tokens.css`, `verify.mjs`, `astro.config.mjs`, or
|
||||||
|
`src/content/config.ts`. If you need a change there, report it.
|
||||||
|
|
||||||
|
## Rules that bite
|
||||||
|
|
||||||
|
- No raw hex, no px font sizes, no ad-hoc breakpoints. Tokens only.
|
||||||
|
- **Never reshape CSS to slip past `check-tokens.mjs`** — e.g. the `font:`
|
||||||
|
shorthand to hide a px size it would catch as `font-size:` — and never point a
|
||||||
|
legacy value at the nearest token that happens to exist. Both are silent
|
||||||
|
redesigns. Keep the true value and mark it
|
||||||
|
`/* token-gap: <reason>; owner design-system-keeper */`, which waives the
|
||||||
|
finding and queues it. You may not add tokens. See `.agents/rules/gates.md`.
|
||||||
|
- No `client:*` unless genuinely interactive, with written justification.
|
||||||
|
- Every ARIA attribute from the markup you replace survives. `verify.mjs`
|
||||||
|
asserts several by name.
|
||||||
|
- Keep the `gap:1px` over a coloured parent trick where the original used it.
|
||||||
|
- Under ~120 lines of markup. More means two components.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
`.agents/checklists/before-component.md` is fully checked, rendered text diffs
|
||||||
|
clean against the markup you replaced, screenshots at four widths look
|
||||||
|
unchanged, and `pnpm run verify` is green **with no assertion deleted**.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: content-i18n-migrator
|
||||||
|
description:
|
||||||
|
Moves bilingual copy out of app.js and catalog.js into typed Astro content
|
||||||
|
collections without altering a string. Use for tasks 03-04 and any later
|
||||||
|
content relocation. Do not use for markup or styling.
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You own `src/content/` and `src/content/config.ts`, and you are their only
|
||||||
|
writer. Your job is a lossless move, not an edit.
|
||||||
|
|
||||||
|
**Read first**: `.agents/rules/content-i18n.md`. **Load skill**:
|
||||||
|
`content-migration`.
|
||||||
|
|
||||||
|
## What you are moving
|
||||||
|
|
||||||
|
~50 `{ en, pt }` keys from `app.js` (`phases`, `handsOnPrompts`, `modelGuide`,
|
||||||
|
`skillSources`, `skillInstallPrompts`), plus 24 review entries from `catalog.js`
|
||||||
|
and `submitted-catalog.js`.
|
||||||
|
|
||||||
|
These are hand-written translations with deliberate tone. **Copy them
|
||||||
|
mechanically. Never retype.** Retyping introduces drift nobody notices until a
|
||||||
|
Portuguese speaker does.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
Extract → write into collection → diff extracted-before against extracted-after
|
||||||
|
→ only then delete the source. If the diff is not empty, you changed content.
|
||||||
|
Fix it before continuing.
|
||||||
|
|
||||||
|
Both locales required in the schema. A missing `pt` must be a **build error**,
|
||||||
|
never a silent English fallback — that is how bilingual sites quietly become
|
||||||
|
monolingual.
|
||||||
|
|
||||||
|
## The trap that will catch you
|
||||||
|
|
||||||
|
`skill-reviews/improved/**/SKILL.md` is generated from `catalog.js` by
|
||||||
|
`scripts/build-skill-review.mjs` and the output is **committed**. Move
|
||||||
|
`catalog.js` and the generator keeps running against nothing — silently. Either
|
||||||
|
re-point it or replace it, and update `package.json`, `README.md`,
|
||||||
|
`docs/operations-guide.md`, and the review desk footer, all of which reference
|
||||||
|
it.
|
||||||
|
|
||||||
|
Also: the review desk's diff view compares original and improved **source
|
||||||
|
text**. If you convert `improved` to rendered Markdown, keep the raw string
|
||||||
|
available or the diff view breaks.
|
||||||
|
|
||||||
|
## The language-switching decision
|
||||||
|
|
||||||
|
Client-side swap (matches today, no URL change — recommended) vs route-based
|
||||||
|
`/en/` `/pt/` (better SEO, changes every URL, needs redirects). Surface it, get
|
||||||
|
a decision, record it. Either way `<html lang>` tracks the active language.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
The string diff is empty, both locales validate, the generator still produces
|
||||||
|
identical output, and `pnpm run verify` is green.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: design-system-keeper
|
||||||
|
description:
|
||||||
|
Owns src/styles/tokens.css — the palette, type scale, and breakpoints. Use for
|
||||||
|
task 02 and any later token change or check-tokens failure. Do not use for
|
||||||
|
building components.
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You own the token layer and are its only writer. Your job is to make "maintain
|
||||||
|
the same styles" true and verifiable.
|
||||||
|
|
||||||
|
**Read first**: `.agents/context/design-system.md` — it documents three drifting
|
||||||
|
palettes and a broken `@font-face`. **Load skills**: `design-tokens`,
|
||||||
|
`visual-regression`.
|
||||||
|
|
||||||
|
## The two decisions you must surface, not silently make
|
||||||
|
|
||||||
|
1. **Three palettes → one.** `--ink` exists as `#172f42`, `#122534`, `#173044`;
|
||||||
|
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
||||||
|
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
|
||||||
|
visibly different — screenshot both and get a human decision.
|
||||||
|
2. ~~**The fonts have never rendered.**~~ **Settled 2026-09-05 — do not
|
||||||
|
reopen.** The malformed `@font-face` was escalated and the human chose the
|
||||||
|
real fonts. Manrope and DM Mono are self-hosted in `public/fonts/`, wired
|
||||||
|
through `public/fonts/fonts.css`, which `BaseLayout.astro` links and the
|
||||||
|
legacy root `styles.css` `@import`s. **Do not delete these faces and do not
|
||||||
|
replace the stacks with `Arial`/`ui-monospace`** — that instruction is
|
||||||
|
obsolete. You may add `--font-sans` / `--font-mono` tokens pointing at them.
|
||||||
|
|
||||||
|
## You own
|
||||||
|
|
||||||
|
`src/styles/tokens.css`, `src/styles/base.css`, and
|
||||||
|
`.agents/scripts/check-tokens.mjs`.
|
||||||
|
|
||||||
|
Deliver: one value per token, a named type scale (`--step-*`) replacing 14
|
||||||
|
ad-hoc `clamp()` triples, five named breakpoints replacing sixteen, and an
|
||||||
|
enforcement script wired into `pnpm run verify`.
|
||||||
|
|
||||||
|
## Preserve the house style
|
||||||
|
|
||||||
|
Flat colour blocks, `1px` hairlines, near-zero radius, tight negative tracking
|
||||||
|
on display type, `Georgia, serif` emphasis spans, and the `gap:1px` over a
|
||||||
|
coloured parent trick used for grid separators. That last one is deliberate —
|
||||||
|
never convert it to `border`.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
Every token has exactly one value, `check-tokens.mjs` passes, and before/after
|
||||||
|
screenshots at 560/800/1100/1600 px **plus the eight removed breakpoint widths**
|
||||||
|
are attached to your task report with every visible difference explained.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: motion-designer
|
||||||
|
description:
|
||||||
|
Adds and audits animation — transitions, state changes, optional view
|
||||||
|
transitions. Use for task 17 and any change involving movement. Do not use for
|
||||||
|
static layout or styling work.
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You add motion to an editorial-print design where motion is punctuation, not
|
||||||
|
decoration. Your most valuable output is often **deciding not to animate**.
|
||||||
|
|
||||||
|
**Read first**: `.agents/rules/animation.md`. **Load skill**: `motion`.
|
||||||
|
|
||||||
|
## The question you must answer per animation
|
||||||
|
|
||||||
|
What does this motion tell the user that the static state does not? Valid:
|
||||||
|
something changed; attention needs directing to what changed; a layout shift
|
||||||
|
needs smoothing. "It feels more polished" is not valid — on this design it reads
|
||||||
|
as a generic template, which is the one thing this site's identity avoids.
|
||||||
|
|
||||||
|
Shipping a component static is a legitimate, common, correct outcome.
|
||||||
|
|
||||||
|
## Hard constraints
|
||||||
|
|
||||||
|
- `transform` and `opacity` only. Animating layout properties fails the 200ms
|
||||||
|
INP budget.
|
||||||
|
- 150–250ms UI feedback, ≤400ms page transition. `cubic-bezier(.2,0,0,1)` in,
|
||||||
|
`ease-out` out. No bounce or elastic — wrong register.
|
||||||
|
- One thing moves at a time. No staggered card cascades.
|
||||||
|
- `prefers-reduced-motion` honoured **and tested** via DevTools emulation. The
|
||||||
|
end state must still be correct: reduced, not broken.
|
||||||
|
- No animation library. This site's thesis is having no runtime dependencies.
|
||||||
|
|
||||||
|
## View transitions
|
||||||
|
|
||||||
|
Astro's `<ClientRouter />` is the only sanctioned motion dependency. Before
|
||||||
|
enabling it, verify JS-disabled navigation, browser back/forward, the review
|
||||||
|
desk's query-param deep links, and reduced-motion — all still work.
|
||||||
|
|
||||||
|
## Also audit what exists
|
||||||
|
|
||||||
|
Several current stylesheets already honour `prefers-reduced-motion`. Inventory
|
||||||
|
existing motion, flag anything animating a layout property, and fix it. That is
|
||||||
|
often higher value than anything you add.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
Every animation has a written purpose, animates only compositor properties,
|
||||||
|
respects reduced motion under test, and screenshots of start and end states are
|
||||||
|
attached.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: page-migrator
|
||||||
|
description:
|
||||||
|
Migrates one hand-written HTML page to an Astro route with identical URL,
|
||||||
|
content, and JS budget. Use for the page-migration tasks (10-16). Do not use
|
||||||
|
for component extraction or config changes.
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You migrate **one page per task**. The bar is that a visitor cannot tell.
|
||||||
|
|
||||||
|
**Read first**: `.agents/context/architecture.md`,
|
||||||
|
`.agents/context/verification.md`. **Load skills**: `astro-page`,
|
||||||
|
`content-migration`, `visual-regression`.
|
||||||
|
|
||||||
|
## Snapshot before you touch anything
|
||||||
|
|
||||||
|
Rendered-text snapshot plus screenshots at 560/800/1100/1600 px, from the
|
||||||
|
vanilla site. Without a before, you cannot prove an after. This is not optional
|
||||||
|
and it is the step agents skip.
|
||||||
|
|
||||||
|
## Hard constraints
|
||||||
|
|
||||||
|
- **URL identical**, trailing slash included. Internal links go through
|
||||||
|
`import.meta.env.BASE_URL`, never a hand-written absolute path.
|
||||||
|
- **Query params survive** — the review desk uses `?author=`, `?skill=`,
|
||||||
|
`?view=`, `?file=`, `?compare=`, `?render=` and they are shared externally.
|
||||||
|
- **JS budget does not grow.** A page that shipped zero JS still ships zero.
|
||||||
|
Seven of ten pages are in that category.
|
||||||
|
- Every `data-*` hook, ARIA attribute, `<title>`, and `<meta>` preserved.
|
||||||
|
- Reuse existing components before creating new ones.
|
||||||
|
- Do **not** port `responsive.css` wholesale. Take what the page needs; prove
|
||||||
|
the rest is dead and delete it.
|
||||||
|
|
||||||
|
## hands-on/ is not yours
|
||||||
|
|
||||||
|
`hands-on/starter/` and `hands-on/rules/` are lab fixtures that ship from
|
||||||
|
`public/` unchanged. Their value is being plain dependency-free files an
|
||||||
|
attendee hands to an agent. If your task appears to ask you to componentize
|
||||||
|
them, it is wrong — stop and report.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
`.agents/checklists/before-page.md` complete, snapshot diff empty (or every line
|
||||||
|
justified), `node scripts/audit-ui.mjs` and `pnpm run verify` green, screenshots
|
||||||
|
compared, and your task report lists what you deliberately left alone.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: reviewer
|
||||||
|
description:
|
||||||
|
Merge gate. Reviews a task branch diff against its brief and the project
|
||||||
|
rules. Use before merging any refactor task. Never writes features or fixes
|
||||||
|
findings itself.
|
||||||
|
tools: Read, Grep, Glob, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
You are the merge gate. You read diffs and report. **You do not write features
|
||||||
|
and you do not fix what you find** — you name it precisely enough that the
|
||||||
|
owning agent can.
|
||||||
|
|
||||||
|
**Read**: the task file, then every rule in `.agents/rules/` relevant to the
|
||||||
|
diff.
|
||||||
|
|
||||||
|
## Order of checks — highest-value first
|
||||||
|
|
||||||
|
1. **Coverage did not fall.** `grep -c 'throw new Error' scripts/verify.mjs`
|
||||||
|
against `origin/main`. A drop by anyone other than `verification-engineer`,
|
||||||
|
with a stated reason, is an automatic reject. This is the single most likely
|
||||||
|
way content silently disappears.
|
||||||
|
2. **Scope.** `git diff --stat origin/main` matches the brief. Contended files
|
||||||
|
(`tokens.css`, `verify.mjs`, `astro.config.mjs`, `src/content/config.ts`)
|
||||||
|
edited only by their owner.
|
||||||
|
3. **Content preserved.** Snapshot diff attached and empty, or every line
|
||||||
|
justified. No attached evidence means not reviewed — send it back.
|
||||||
|
4. **URLs and query params unchanged.** Trailing slashes. `BASE_URL` used
|
||||||
|
instead of hand-written absolute paths.
|
||||||
|
5. **JS budget.** A previously-zero-JS page still ships zero. Every `client:*`
|
||||||
|
has a written justification.
|
||||||
|
6. **Tokens.** No raw hex, px font sizes, or ad-hoc breakpoints outside
|
||||||
|
`tokens.css`.
|
||||||
|
7. **Accessibility.** ARIA attributes from the original survived. Native
|
||||||
|
elements. Focus ring intact.
|
||||||
|
8. **Motion.** Compositor properties only; `prefers-reduced-motion` honoured.
|
||||||
|
9. **Hygiene.** No `.serena/`, `__pycache__/`, `dist/`, or scratch files staged.
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
```
|
||||||
|
path:line: <severity>: <problem>. <fix>.
|
||||||
|
```
|
||||||
|
|
||||||
|
Severities: `blocker` (content loss, coverage drop, URL change, scope
|
||||||
|
violation), `major` (rule violation, missing evidence), `minor` (style, naming).
|
||||||
|
|
||||||
|
No praise, no summary of what the diff does — the author knows. Findings only.
|
||||||
|
If there are none, say so in one line.
|
||||||
|
|
||||||
|
## What you do not do
|
||||||
|
|
||||||
|
Do not suggest improvements outside the task's scope. Scope creep at review time
|
||||||
|
is how a bounded task becomes an unbounded one. Note it as a follow-up instead.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: verification-engineer
|
||||||
|
description:
|
||||||
|
Keeps scripts/verify.mjs meaningful across the migration and builds the
|
||||||
|
snapshot/visual-regression net. Use for tasks 18-19 and whenever a verify
|
||||||
|
assertion needs re-pointing. The only role permitted to reduce coverage.
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You own `scripts/verify.mjs`, `scripts/audit-ui.mjs`, `.agents/snapshots/`, and
|
||||||
|
the visual-regression tooling. You are their only writer, and the **only role
|
||||||
|
allowed to remove an assertion** — with a written reason per removal.
|
||||||
|
|
||||||
|
**Read first**: `.agents/context/verification.md`. **Load skills**:
|
||||||
|
`verify-contract`, `visual-regression`.
|
||||||
|
|
||||||
|
## Why the role exists
|
||||||
|
|
||||||
|
42 assertions pin this site's real content. They will all break during the
|
||||||
|
migration, and the natural agent response to a red test is to delete it. That
|
||||||
|
turns a content-loss bug into a green build. You are the check on that.
|
||||||
|
|
||||||
|
`grep -c 'throw new Error' scripts/verify.mjs` must not decrease across the
|
||||||
|
migration.
|
||||||
|
|
||||||
|
## Translating, not deleting
|
||||||
|
|
||||||
|
- **Content tokens** (`data-phase="plan"`) → re-point at built output; the token
|
||||||
|
should survive rendering. If it does not, a component dropped content.
|
||||||
|
- **Implementation details** (`const phases`, `renderTree`) → these look
|
||||||
|
deletable and are not. They pin a feature. Replace with an output-level
|
||||||
|
assertion of the same feature.
|
||||||
|
- **Asset versions** (`app.js?v=…`) → assert the built HTML references a hashed
|
||||||
|
asset.
|
||||||
|
|
||||||
|
## Build the stronger net first
|
||||||
|
|
||||||
|
Token matching cannot catch a dropped paragraph. Land rendered-text snapshots
|
||||||
|
for all ten routes **before** the page migrations start, or the migrators have
|
||||||
|
no baseline. This is early, blocking work.
|
||||||
|
|
||||||
|
## Fix the audit gap
|
||||||
|
|
||||||
|
`audit-ui.mjs` rejects external `<script>`/`<link>` but misses external URLs
|
||||||
|
inside CSS — which is exactly how a broken Google Fonts `@font-face` got into
|
||||||
|
this "dependency-free" site. Add `@import`, `src: url(https:…)`, and
|
||||||
|
`url(https:…)` detection.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
Coverage has not fallen, every removal has a reason, snapshots exist for all ten
|
||||||
|
routes, `check-tokens.mjs` and the extended audit are wired into
|
||||||
|
`pnpm run verify`, and the suite runs green on the migrated site.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Checklist: before you call a component done
|
||||||
|
|
||||||
|
- [ ] It appears (or will appear) in **three** places, or has a name a person
|
||||||
|
says out loud
|
||||||
|
- [ ] Lives in the right folder: `primitives/`, `blocks/`, or `islands/`
|
||||||
|
- [ ] Typed `interface Props`; every field intentional; no `any`
|
||||||
|
- [ ] **No raw hex, px font sizes, or ad-hoc breakpoints** — tokens only
|
||||||
|
- [ ] Ships zero JS, or has a `client:*` directive with a written justification
|
||||||
|
- [ ] Markup under ~120 lines
|
||||||
|
- [ ] Native elements: `<button>` for actions, `<a>` for navigation
|
||||||
|
- [ ] Keyboard: tab to it, operate with Enter/Space, focus ring visible
|
||||||
|
- [ ] Interactive state exposed via ARIA (`aria-pressed`, `aria-current`), not
|
||||||
|
just colour
|
||||||
|
- [ ] Any motion respects `prefers-reduced-motion` and animates only
|
||||||
|
`transform`/`opacity`
|
||||||
|
- [ ] Renders correctly at 560 / 800 / 1100 / 1600 px
|
||||||
|
- [ ] Both `en` and `pt` strings present; no hard-coded copy
|
||||||
|
- [ ] `pnpm run verify` green with no assertion deleted
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Checklist: before you merge a task branch
|
||||||
|
|
||||||
|
- [ ] `origin/main` **merged in** (never rebased), conflicts resolved in the
|
||||||
|
worktree
|
||||||
|
- [ ] `pnpm run verify` and `node scripts/audit-ui.mjs` both green
|
||||||
|
- [ ] **Assertion count in `verify.mjs` did not fall**
|
||||||
|
(`grep -c 'throw new Error' scripts/verify.mjs`)
|
||||||
|
- [ ] Only files in your task's scope changed — `git diff --stat origin/main`
|
||||||
|
matches the brief
|
||||||
|
- [ ] No contended file edited unless you own it (see `rules/git-worktrees.md`)
|
||||||
|
- [ ] No `.serena/`, `__pycache__/`, `dist/`, or scratch files staged
|
||||||
|
- [ ] Commit message: what, why, and what you deliberately did not do
|
||||||
|
- [ ] Task report written: verified behaviours, known gaps, follow-ups
|
||||||
|
- [ ] `reviewer` agent has read the diff
|
||||||
|
- [ ] Worktree and branch cleaned up after merge
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Checklist: before you call a page migrated
|
||||||
|
|
||||||
|
- [ ] URL identical to the old one, **trailing slash included**
|
||||||
|
- [ ] Any query params the old page honoured still work (`?author=`, `?skill=`,
|
||||||
|
`?view=`, `?file=`, `?compare=`, `?render=`)
|
||||||
|
- [ ] Rendered text content diffs clean against the pre-migration snapshot — or
|
||||||
|
every difference is listed and justified
|
||||||
|
- [ ] Same number of JS bytes or fewer; a previously-zero-JS page still ships
|
||||||
|
zero
|
||||||
|
- [ ] `<title>`, `<meta name="description">`, and `<meta name="viewport">`
|
||||||
|
preserved
|
||||||
|
- [ ] One `<h1>`; heading levels do not skip
|
||||||
|
- [ ] Every ARIA attribute from the original survived (`verify.mjs` asserts
|
||||||
|
several)
|
||||||
|
- [ ] No external `<script>`, `<link>`, `@import`, or `url()` —
|
||||||
|
`node scripts/audit-ui.mjs` green
|
||||||
|
- [ ] Internal links go through `BASE_URL`, not hand-written absolute paths
|
||||||
|
- [ ] Screenshots at 560 / 800 / 1100 / 1600 px compared against the old page
|
||||||
|
- [ ] `<html lang>` correct and still switching with the language toggle
|
||||||
|
- [ ] `pnpm run verify` green with no assertion deleted
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Context: architecture, current and target
|
||||||
|
|
||||||
|
## Current (no build step)
|
||||||
|
|
||||||
|
Ten hand-written HTML pages, each linking its own CSS and one ES module:
|
||||||
|
|
||||||
|
| Route | Page | Script | Stylesheets |
|
||||||
|
| -------------------- | -------------------------- | ---------------------- | --------------------------------------------- |
|
||||||
|
| `/` | `index.html` | — | `chapters.css`, `landing.css` |
|
||||||
|
| `/full-guide/` | `full-guide/index.html` | `app.js` (50 KB) | `styles.css`, `responsive.css`, `audit.css` |
|
||||||
|
| `/summary/` | `summary/index.html` | — | `chapters.css` |
|
||||||
|
| `/models/` | `models/index.html` | — | `chapters.css` |
|
||||||
|
| `/agents/` | `agents/index.html` | — | `chapters.css` |
|
||||||
|
| `/skills/` | `skills/index.html` | `skills/app.js` | `skills/styles.css` |
|
||||||
|
| `/rules/` | `rules/index.html` | `rules/app.js` | `rules/styles.css` |
|
||||||
|
| `/skills-review/` | `skills-review/index.html` | `skills-review/app.js` | `skills-review/styles.css`, `change-lens.css` |
|
||||||
|
| `/hands-on/starter/` | lab fixture | own | own |
|
||||||
|
| `/hands-on/rules/` | lab fixture | own | own |
|
||||||
|
|
||||||
|
Weight is concentrated: `app.js` 50 KB, `responsive.css` 30 KB,
|
||||||
|
`skills-review/catalog.js` 27 KB, `skills-review/submitted-catalog.js` 18 KB.
|
||||||
|
|
||||||
|
### What each big file actually is
|
||||||
|
|
||||||
|
- **`app.js`** — not really application code. It is a **bilingual content
|
||||||
|
database** (`phases`, `handsOnPrompts`, `modelGuide`, `skillSources`,
|
||||||
|
`skillInstallPrompts`, each keyed `{en, pt}`) plus ~12 small `render*`
|
||||||
|
functions that swap `innerHTML` on tab clicks. ~50 `en:` keys. The content
|
||||||
|
should become data; only the tab behaviour is interactive.
|
||||||
|
- **`responsive.css`** — a 30 KB append-only layer of overrides bolted on top of
|
||||||
|
`styles.css`. Expect large parts to be dead once layout moves into components.
|
||||||
|
Do not port it verbatim.
|
||||||
|
- **`skills-review/catalog.js`** — the real data model of the review desk: one
|
||||||
|
entry per submitted skill with `id`, `author`, `title`, `status`, `focus`,
|
||||||
|
`wins[]`, `improve[]`, `extras`, `improved` (full markdown). 24 entries across
|
||||||
|
`catalog.js` + `submitted-catalog.js`. This is already a content collection in
|
||||||
|
all but name.
|
||||||
|
- **`skills-review/files.js` / `submitted-files.js`** — generated file
|
||||||
|
manifests.
|
||||||
|
- **`vote.js`** — the vote widget island; talks to `vote-service/`.
|
||||||
|
|
||||||
|
## Target (Astro)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
content/ catalog entries, chapter copy, EN/PT strings (typed collections)
|
||||||
|
layouts/ BaseLayout, ChapterLayout, GuideLayout
|
||||||
|
components/ .astro by default; islands only where marked
|
||||||
|
styles/ tokens.css, base.css, then per-component styles
|
||||||
|
pages/ routes mirroring today's URLs exactly
|
||||||
|
public/
|
||||||
|
hands-on/ lab fixtures copied verbatim, never processed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Non-negotiables for the target
|
||||||
|
|
||||||
|
- **URLs do not change.** `/full-guide/`, `/skills-review/`,
|
||||||
|
`/hands-on/starter/` and the rest must resolve exactly as they do now,
|
||||||
|
trailing slash included. Existing links (including `docs/`, SilverBullet, and
|
||||||
|
shared URLs with `?author=…&skill=…&view=…` query params) must keep working.
|
||||||
|
- **Zero JS by default.** Seven of the ten pages ship no JavaScript today. They
|
||||||
|
must still ship none. Islands are opt-in, per component, and justified.
|
||||||
|
- **`hands-on/` stays vanilla.** It goes in `public/` untouched. It is a lab
|
||||||
|
fixture, not a component.
|
||||||
|
- **No external runtime requests.** `audit-ui.mjs` enforces this and it is part
|
||||||
|
of the site's thesis. Self-host anything you add.
|
||||||
|
- **The review desk's query-param deep links keep working** — `?author=`,
|
||||||
|
`?skill=`, `?view=`, `?file=`, `?compare=`, `?render=`. They are documented in
|
||||||
|
the page footer and shared externally.
|
||||||
|
|
||||||
|
## Companion service
|
||||||
|
|
||||||
|
`vote-service/` is a Go API on its own Kubernetes deploy cycle, reached by the
|
||||||
|
review desk over `window.SKILLS_REVIEW_VOTE_API`. The refactor does not touch
|
||||||
|
it. Keep the global, or replace it with a build-time `PUBLIC_VOTE_API` env var —
|
||||||
|
but if you do, update `vote-service/README.md` in the same change.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Context: full-guide language switching
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The Astro full guide keeps the current client-side language switch on its
|
||||||
|
existing `/full-guide/` URL. It server-renders both locale variants and the
|
||||||
|
language-toggle island shows the selected variant after `client:idle` hydration.
|
||||||
|
|
||||||
|
This deliberately preserves the current no-URL-change contract, including links
|
||||||
|
shared without a locale segment, and avoids a route/redirect and publishing
|
||||||
|
change. The cost is duplicated localized HTML and both locales in the response.
|
||||||
|
That is acceptable for this small guide and avoids sending duplicated string
|
||||||
|
data through every interactive island.
|
||||||
|
|
||||||
|
## Markup and island contract for task 15d
|
||||||
|
|
||||||
|
- Render each static localized fragment twice. Put `data-language-content="en"`
|
||||||
|
or `data-language-content="pt"` on its outer element. English is visible in
|
||||||
|
server HTML; the toggle uses the native `hidden` attribute for the inactive
|
||||||
|
locale.
|
||||||
|
- Add `<LanguageToggle />` to the guide top bar. Astro's `client:idle` directive
|
||||||
|
is only valid for framework components; this `.astro` island defers its
|
||||||
|
browser setup with `requestIdleCallback` (and a timeout fallback) instead. Do
|
||||||
|
not hydrate the page or use `client:load`; the control is deliberately
|
||||||
|
idle-priority.
|
||||||
|
- The island owns the `ai-for-dummies-language` localStorage key. Every read and
|
||||||
|
write remains inside `try`/`catch`, because previews may disable storage.
|
||||||
|
- On each selection the island sets `<html lang>` to `en` or `pt-BR`, updates
|
||||||
|
its `[data-lang]` buttons' `.active` class and `aria-pressed` state, updates
|
||||||
|
`[data-language-content]`, then dispatches `ai-for-dummies:languagechange` on
|
||||||
|
`window`. The event detail is `{ language: 'en' | 'pt' }`.
|
||||||
|
- The guide selector island (15a) must read `document.documentElement.lang` when
|
||||||
|
it hydrates and listen for that event. On receipt it must re-render the
|
||||||
|
currently active phase and all active selector panels from their collection
|
||||||
|
data. This preserves today’s `applyLanguage` behaviour without coupling the
|
||||||
|
toggle to page selectors.
|
||||||
|
|
||||||
|
This is a page-local contract: the existing `/rules/` toggle continues using its
|
||||||
|
own `rules-language` key and must not be changed as part of full-guide
|
||||||
|
migration.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Context: the design system (as it actually is)
|
||||||
|
|
||||||
|
Read this before touching CSS. Everything here was extracted from the current
|
||||||
|
files, not assumed.
|
||||||
|
|
||||||
|
## There is no single palette. There are three.
|
||||||
|
|
||||||
|
The same semantic names carry different values depending on which stylesheet
|
||||||
|
loaded them:
|
||||||
|
|
||||||
|
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
|
||||||
|
| ---------- | -------------------------------- | ------------------------------------------ | ----------------------- |
|
||||||
|
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
|
||||||
|
| `--ink` | `#172f42` | `#122534` | `#173044` |
|
||||||
|
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
|
||||||
|
| `--line` | `#d8dee2` | `#d0d5d2` | `#d5dde1` |
|
||||||
|
| `--blue` | `#527f9f` | `#215675` | `#5683a1` |
|
||||||
|
| `--gold` | `#efc76b` | `#ebbf58` | `#efc86d` |
|
||||||
|
| `--accent` | `#7c78a8` | — | — |
|
||||||
|
| `--deep` | `#102536` | — | — |
|
||||||
|
| `--red` | — | `#a7483f` (chapters only) | — |
|
||||||
|
| `--violet` | — | `#6b668f` (review desk only) | — |
|
||||||
|
|
||||||
|
Most deltas are a few units per channel — drift, not intent. `--blue` is the
|
||||||
|
exception: `#527f9f` vs `#215675` is a visible difference and may be deliberate.
|
||||||
|
|
||||||
|
**Decision required before any component work** (task 02). Options:
|
||||||
|
|
||||||
|
- **Canonicalize** to one palette. Recommended. The sub-perceptual deltas
|
||||||
|
collapse; only `--blue` needs a human's eye on a before/after screenshot.
|
||||||
|
- **Keep three named surfaces** (`--surface-guide`, `--surface-chapter`,
|
||||||
|
`--surface-lab`) if the drift turns out to be intentional per section.
|
||||||
|
|
||||||
|
Do not "just pick one" silently in the middle of another task. This is its own
|
||||||
|
reviewed change with visual diffs attached.
|
||||||
|
|
||||||
|
## The typography — fixed 2026-09-05
|
||||||
|
|
||||||
|
`styles.css` line 1 used to read:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@font-face {
|
||||||
|
font-family: Manrope;
|
||||||
|
src: url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`src:` in an `@font-face` must point at a font binary. That URL returns a CSS
|
||||||
|
stylesheet, so no browser could load a face from it. For the whole life of the
|
||||||
|
site, every `font-family:Manrope,Arial,sans-serif` rendered as **Arial** and
|
||||||
|
every `font:… 'DM Mono',monospace` rendered as the **generic monospace** face —
|
||||||
|
`'DM Mono'` was never declared as a family at all.
|
||||||
|
|
||||||
|
**This was escalated and the human chose the real fonts.** Manrope and DM Mono
|
||||||
|
are now self-hosted in `public/fonts/`, latin and latin-ext subsets only, under
|
||||||
|
the SIL Open Font License. One `fonts.css` serves both trees: Astro links it
|
||||||
|
from `BaseLayout.astro`, the legacy root `styles.css` `@import`s it. Self-hosted
|
||||||
|
rather than linked from Google because `scripts/audit-ui.mjs` rejects any
|
||||||
|
external `<link>`/`<script>`, and because the site is presented in workshop
|
||||||
|
rooms with unreliable networks.
|
||||||
|
|
||||||
|
**This changed how every page renders**, deliberately. It is the one sanctioned
|
||||||
|
visual change in the migration. Screenshots taken before 2026-09-05 show Arial
|
||||||
|
and are no longer a valid baseline.
|
||||||
|
|
||||||
|
`Georgia, serif` for emphasis (`h1 em`, `.hero em`) is untouched and still real.
|
||||||
|
|
||||||
|
## Type scale
|
||||||
|
|
||||||
|
`Georgia, serif` is used deliberately for emphasis (`h1 em`, `.hero em`) and is
|
||||||
|
real — it is a system font, so it does render. Keep it.
|
||||||
|
|
||||||
|
Sizes are all `clamp()`, roughly:
|
||||||
|
|
||||||
|
| Role | Value |
|
||||||
|
| --------------- | --------------------------------------------------------- |
|
||||||
|
| Display / `h1` | `clamp(56px,9vw,126px)` |
|
||||||
|
| Section `h2` | `clamp(36px,5vw,65px)` |
|
||||||
|
| Sub-head | `clamp(24px,3vw,38px)` |
|
||||||
|
| Pull-quote | `clamp(22px,3vw,36px)` |
|
||||||
|
| Body | `15px/1.6` … `18px` |
|
||||||
|
| Eyebrow / label | `10–11px` monospace, `letter-spacing:.08–.1em`, uppercase |
|
||||||
|
|
||||||
|
There are 14+ distinct clamp triples doing near-identical jobs. Collapse to a
|
||||||
|
named scale (`--step-0` … `--step-6`) during tokenization; the visual result
|
||||||
|
should be unchanged within a pixel or two at common viewports.
|
||||||
|
|
||||||
|
## Breakpoints
|
||||||
|
|
||||||
|
Sixteen distinct max-widths are in use: 420, 520, 530, 560, 600, 620, 720, 800,
|
||||||
|
850, 880, 900, 1000, 1050, 1100 — plus `min-width:1600px` and
|
||||||
|
`min-width:2200px`.
|
||||||
|
|
||||||
|
Collapse to a named set (suggested: 560 / 800 / 1100 / 1600 / 2200) and prove
|
||||||
|
equivalence with screenshots at the _old_ breakpoint values, since that is where
|
||||||
|
regressions will hide.
|
||||||
|
|
||||||
|
`@media(prefers-reduced-motion:reduce)` is already respected in several
|
||||||
|
stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
|
||||||
|
|
||||||
|
## House style worth preserving
|
||||||
|
|
||||||
|
The visual identity is editorial-print: flat colour blocks, hairline `1px`
|
||||||
|
rules, uppercase monospace eyebrows with wide tracking, very tight negative
|
||||||
|
letter-spacing on display type (`-.06em` … `-.08em`), grid layouts with
|
||||||
|
`gap:1px` over a background colour to fake borders, and near-zero border-radius.
|
||||||
|
|
||||||
|
That last trick (`gap:1px` + parent background) is used everywhere. It is
|
||||||
|
intentional. Do not replace it with `border`.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Context: publishing, and what a build step changes
|
||||||
|
|
||||||
|
## How it works today
|
||||||
|
|
||||||
|
The Gitea Pages Server serves the **`pages` branch tree directly**. There is no
|
||||||
|
build. `main` and `pages` end up with byte-identical trees; `pages` exists only
|
||||||
|
because the Pages Server publishes a branch, not a directory.
|
||||||
|
|
||||||
|
Live at `https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/`.
|
||||||
|
|
||||||
|
Full procedure, including the fact that `merge --ff-only main` fails (the
|
||||||
|
histories diverged), is in
|
||||||
|
[`../../docs/operations-guide.md`](../../docs/operations-guide.md).
|
||||||
|
|
||||||
|
## What Astro changes
|
||||||
|
|
||||||
|
Astro emits `dist/`. The Pages Server cannot run a build, so **something has to
|
||||||
|
put built output on `pages`**. Pick one, deliberately, in task 01:
|
||||||
|
|
||||||
|
| Option | How | Cost |
|
||||||
|
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **A. Build locally, commit `dist/` to `pages`** | `pnpm run build`, copy `dist/*` into the `pages` worktree, commit | `pages` stops being "the exact source". Diffs become unreadable. Publishing depends on one workstation. Simple, no new infra. |
|
||||||
|
| **B. Gitea Actions builds and pushes `pages`** | workflow on `main` → `pnpm install --frozen-lockfile && pnpm run build` → force-push `dist/` to `pages` | `pages` becomes a machine-owned branch (force-push is fine _because_ nothing else writes it). Needs the act-runner to be healthy. **Recommended.** |
|
||||||
|
| **C. Serve from a container instead** | drop Pages Server for an nginx pod behind the existing ingress | most control, most infra, changes the URL story |
|
||||||
|
|
||||||
|
**Recommended: B**, with A as the documented manual fallback for when the runner
|
||||||
|
is down. Note the known failure mode: this Gitea's act-runner registration lives
|
||||||
|
in an `emptyDir`, so a pod restart silently kills CI until re-registered. The
|
||||||
|
runbook must say "if the site stopped updating, check the runner first".
|
||||||
|
|
||||||
|
Whichever you pick, `docs/operations-guide.md` must be rewritten in the same
|
||||||
|
task — it currently promises `pages` is "the exact published source", and that
|
||||||
|
stops being true under A and B.
|
||||||
|
|
||||||
|
## Base path
|
||||||
|
|
||||||
|
The site is served from a **subdirectory**: `/ai-for-dummies/`. Astro needs
|
||||||
|
`base: '/ai-for-dummies'` in `astro.config.mjs`, and every internal link must go
|
||||||
|
through `import.meta.env.BASE_URL` or Astro's `<a href={...}>` helpers rather
|
||||||
|
than a hand-written absolute `/models/`.
|
||||||
|
|
||||||
|
This is the single most likely source of "works locally, 404s in production" in
|
||||||
|
this migration. Verify it on the real host, not just `pnpm run preview`.
|
||||||
|
|
||||||
|
## Verification before you call it published
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -o /dev/null -w '%{http_code}\n' \
|
||||||
|
"https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/?v=$(git rev-parse --short HEAD)"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `?v=` cache-buster matters: the Pages Server caches, and a stale 200 looks
|
||||||
|
exactly like a successful deploy. Check one nested route
|
||||||
|
(`/ai-for-dummies/skills-review/`) and one static asset too — the base-path bug
|
||||||
|
shows up on assets first.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Context: the verification contract
|
||||||
|
|
||||||
|
`scripts/verify.mjs` is 13 KB, 42 `throw new Error` sites, 16 checkpoints. It
|
||||||
|
reads 26 source files and asserts that specific **string tokens** appear in them
|
||||||
|
— `data-phase="plan"`, `renderTree`, `.change-lens`,
|
||||||
|
`styles.css?v=20260904-vote-widget`, and so on.
|
||||||
|
|
||||||
|
## Why this matters more than it looks
|
||||||
|
|
||||||
|
These assertions are the only thing standing between this site and silent
|
||||||
|
content loss during a large refactor. They are also **all going to break**,
|
||||||
|
because they assert against files that will stop existing.
|
||||||
|
|
||||||
|
The failure mode to guard against: an agent runs `pnpm run verify`, sees red,
|
||||||
|
and "fixes" it by deleting the assertion. The suite goes green and the site
|
||||||
|
loses a section. **Deleting an assertion is a change that requires review, the
|
||||||
|
same as deleting a feature.**
|
||||||
|
|
||||||
|
## How the contract must evolve
|
||||||
|
|
||||||
|
Three kinds of assertion, three different fates:
|
||||||
|
|
||||||
|
| Kind | Example | Fate |
|
||||||
|
| ------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Content presence** | `'data-phase="plan"'` in `full-guide/index.html` | Re-point at built output (`dist/`) — the token should survive rendering. If it does not, the component dropped content. |
|
||||||
|
| **Implementation detail** | `'const phases'`, `'renderTree'` in `app.js` | Obsolete. Replace with an assertion about _behaviour or output_, never delete outright. |
|
||||||
|
| **Cache-busting version** | `'app.js?v=20260904-vote-widget'` | Obsolete — Astro hashes assets. Replace with "the built HTML references a hashed asset". |
|
||||||
|
|
||||||
|
**Rule: the assertion count must not fall.** Every removed token is replaced by
|
||||||
|
one that pins the same user-visible fact against the new architecture. The
|
||||||
|
verification engineer owns this and is the only role allowed to reduce coverage,
|
||||||
|
with a written reason per removal.
|
||||||
|
|
||||||
|
## The stronger check to add
|
||||||
|
|
||||||
|
Token-matching is brittle. During the migration, add a **rendered-output diff**:
|
||||||
|
snapshot the current site's DOM text content per route, then assert the Astro
|
||||||
|
build produces the same text. That catches dropped paragraphs the way token
|
||||||
|
matching cannot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# before migrating a page, from the vanilla site:
|
||||||
|
node .agents/scripts/snapshot-route.mjs /models/ > .agents/snapshots/models.txt
|
||||||
|
# after: same script against dist/, diff must be empty (or reviewed)
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`../skills/verify-contract/SKILL.md`](../skills/verify-contract/SKILL.md).
|
||||||
|
|
||||||
|
## Also in the suite
|
||||||
|
|
||||||
|
`scripts/audit-ui.mjs` asserts every page has a viewport meta and **no external
|
||||||
|
`<script>`/`<link>`**. Keep it and extend it: it currently misses external URLs
|
||||||
|
inside CSS (`@font-face src`, `@import`, `url()`), which is exactly how the
|
||||||
|
broken Google Fonts request in `styles.css:1` got in.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Rule: accessibility
|
||||||
|
|
||||||
|
## The gate is a keyboard, not a scanner
|
||||||
|
|
||||||
|
Automated tooling catches roughly **57%** of accessibility defects, and the
|
||||||
|
misses cluster exactly where usability is decided: focus visibility, focus
|
||||||
|
obscured by sticky elements, target size, and drag alternatives. Run axe, then
|
||||||
|
do the manual sweep anyway.
|
||||||
|
|
||||||
|
**Manual sweep, every interactive component:**
|
||||||
|
|
||||||
|
1. Tab through it. Every control reachable, in a sensible order.
|
||||||
|
2. Focus ring visible at every stop — this site uses
|
||||||
|
`outline: 3px solid #a7483f; outline-offset: 2px`. Keep it.
|
||||||
|
3. Operate it with Enter and Space. Escape closes anything that opened.
|
||||||
|
4. Nothing is reachable only by hover or only by pointer.
|
||||||
|
5. Zoom to 200%. Nothing clipped, nothing overlapping.
|
||||||
|
|
||||||
|
## Semantics
|
||||||
|
|
||||||
|
- Native elements first. `<button>` for actions, `<a>` for navigation. A `<div>`
|
||||||
|
with a click handler is a defect, not a style choice.
|
||||||
|
- ARIA only when HTML cannot express it. The current code does this well —
|
||||||
|
`role="tablist"`, `aria-pressed`, `aria-current="page"`, `aria-label` on
|
||||||
|
regions. Preserve every one during migration; they are asserted in
|
||||||
|
`verify.mjs`.
|
||||||
|
- One `<h1>` per page. Heading levels never skip.
|
||||||
|
- Every image needs `alt`. Decorative images get `alt=""`.
|
||||||
|
|
||||||
|
## State
|
||||||
|
|
||||||
|
Interactive state must be exposed, not just painted:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- wrong: only colour says it is selected -->
|
||||||
|
<button class="active">Improved draft</button>
|
||||||
|
<!-- right -->
|
||||||
|
<button class="active" aria-pressed="true">Improved draft</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
The vote widget and preview switcher already do this. Match them.
|
||||||
|
|
||||||
|
## Contrast
|
||||||
|
|
||||||
|
Body text ≥ 4.5:1, large text ≥ 3:1, UI boundaries ≥ 3:1. Check any new
|
||||||
|
combination against the token palette — `--muted` on `--paper` is the pair most
|
||||||
|
likely to fail; verify before shipping.
|
||||||
|
|
||||||
|
## Bilingual content
|
||||||
|
|
||||||
|
`<html lang>` must change with the language toggle, not just the text. Screen
|
||||||
|
readers pick pronunciation from it. This already works today — do not regress
|
||||||
|
it.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Rule: animation
|
||||||
|
|
||||||
|
This is an editorial-print design. Motion is punctuation, not decoration.
|
||||||
|
|
||||||
|
## Budget
|
||||||
|
|
||||||
|
- **Purpose or nothing.** Motion may signal a state change, direct attention to
|
||||||
|
what just changed, or smooth a layout shift. Nothing else.
|
||||||
|
- Duration: **150–250ms** for UI feedback, up to 400ms for a page transition.
|
||||||
|
Longer reads as sluggish; shorter reads as a glitch.
|
||||||
|
- Easing: `cubic-bezier(.2,0,0,1)` for entrances, `ease-out` for exits. Never
|
||||||
|
`linear` for anything a person watches. Never bounce/elastic — wrong register
|
||||||
|
for this design.
|
||||||
|
- One thing moves at a time. Staggered cascades of cards are a template default;
|
||||||
|
this site has a point of view and does not do them.
|
||||||
|
|
||||||
|
## `prefers-reduced-motion` is mandatory
|
||||||
|
|
||||||
|
Several current stylesheets already honour it. Every new animation must:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reduced motion means _reduced_, not _broken_: the end state must still be
|
||||||
|
correct and the interface still usable. Test it — in DevTools, Rendering →
|
||||||
|
Emulate `prefers-reduced-motion`.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Animate **`transform` and `opacity` only.** They composite on the GPU.
|
||||||
|
Animating `width`, `height`, `top`, `left`, or `margin` forces layout on every
|
||||||
|
frame and will show up as a failed INP.
|
||||||
|
- `will-change` only on an element about to animate, removed after. Leaving it
|
||||||
|
on permanently costs memory and can _hurt_ performance.
|
||||||
|
- Prefer CSS transitions. Reach for the Web Animations API only for sequencing
|
||||||
|
that CSS cannot express. Do not add an animation library — it is a runtime
|
||||||
|
dependency on a site whose thesis is having none.
|
||||||
|
- INP budget is **200ms**. An animation that delays interaction response fails.
|
||||||
|
|
||||||
|
## Astro view transitions
|
||||||
|
|
||||||
|
If page transitions are wanted, use Astro's `<ClientRouter />`. It is the only
|
||||||
|
sanctioned motion dependency, and it must:
|
||||||
|
|
||||||
|
- degrade cleanly with JS disabled (it does — full navigation)
|
||||||
|
- respect `prefers-reduced-motion`
|
||||||
|
- not break the review desk's query-param deep links or browser back/forward
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
- Never animate anything that conveys information on its own. Motion is
|
||||||
|
redundant reinforcement.
|
||||||
|
- Nothing flashes more than three times per second.
|
||||||
|
- Focus must stay visible throughout a transition, and focus order must not
|
||||||
|
change because of one.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Rule: Astro
|
||||||
|
|
||||||
|
Binding for every `.astro` file.
|
||||||
|
|
||||||
|
## Zero JS is the default
|
||||||
|
|
||||||
|
A component ships no JavaScript unless it has a `client:*` directive. Seven of
|
||||||
|
this site's ten pages ship no JS today and must continue to.
|
||||||
|
|
||||||
|
- Never add `client:load` without justifying it in the PR description.
|
||||||
|
- Prefer, in order: no JS → `client:visible` → `client:idle` → `client:load`.
|
||||||
|
- An island is a **leaf**, not a wrapper. Hydrate the tab panel, not the page.
|
||||||
|
|
||||||
|
## Islands in this project
|
||||||
|
|
||||||
|
Only these need interactivity. Anything else claiming island status is wrong:
|
||||||
|
|
||||||
|
| Island | Why | Directive |
|
||||||
|
| --------------------------------- | ---------------------------------- | ---------------- |
|
||||||
|
| Guide phase/tab switchers | click-driven panel swap | `client:visible` |
|
||||||
|
| Review desk catalog + file viewer | search, filter, fetch source files | `client:load` |
|
||||||
|
| Vote widget | talks to `vote-service/` | `client:visible` |
|
||||||
|
| Language toggle | swaps EN/PT across the page | `client:idle` |
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// 1. imports
|
||||||
|
// 2. Props interface
|
||||||
|
// 3. destructure Astro.props
|
||||||
|
// 4. derived values — no side effects, no fetch in components
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- markup -->
|
||||||
|
<style>
|
||||||
|
/* component-scoped */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Typed props always: `interface Props { … }`, then `const { … } = Astro.props`.
|
||||||
|
- Data loading belongs in `src/content/` collections or the page frontmatter,
|
||||||
|
not inside a component.
|
||||||
|
- No barrel files (`index.ts` re-export hubs). They cost tree-shaking and invite
|
||||||
|
cycles.
|
||||||
|
|
||||||
|
## Content collections
|
||||||
|
|
||||||
|
All copy lives in `src/content/`, typed with a Zod schema in
|
||||||
|
`src/content/config.ts`. The review desk's `catalog.js` maps onto a collection
|
||||||
|
almost one-to-one — do that rather than importing a 27 KB JS file.
|
||||||
|
|
||||||
|
## Styles
|
||||||
|
|
||||||
|
- Component styles go in the component's `<style>` block. Astro scopes them.
|
||||||
|
- Only tokens and true resets live in global CSS.
|
||||||
|
- Do not port `responsive.css` verbatim. It is an override layer whose reason
|
||||||
|
for existing disappears once layout is componentized. Port what a component
|
||||||
|
needs, prove the rest is dead, delete it.
|
||||||
|
|
||||||
|
## URLs and the base path
|
||||||
|
|
||||||
|
The site is served from `/ai-for-dummies/`. Set `base` in `astro.config.mjs` and
|
||||||
|
never hand-write an absolute internal path. Use `import.meta.env.BASE_URL`.
|
||||||
|
|
||||||
|
Existing routes are load-bearing and must not change, including trailing slashes
|
||||||
|
and the review desk's query params.
|
||||||
|
|
||||||
|
## Never
|
||||||
|
|
||||||
|
- No UI framework (React/Vue/Svelte) unless a task brief explicitly calls for
|
||||||
|
it. Astro components plus a little vanilla JS cover everything here.
|
||||||
|
- No CSS framework. This site has a hand-built visual identity — see
|
||||||
|
[`theming.md`](theming.md).
|
||||||
|
- No external runtime requests. Self-host. `audit-ui.mjs` enforces it.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Rule: code style
|
||||||
|
|
||||||
|
## Match what is there
|
||||||
|
|
||||||
|
This codebase has a real voice: dense one-liner CSS, terse ES modules, comments
|
||||||
|
that explain _why_ and never _what_. Do not reformat it into someone else's
|
||||||
|
house style as a side effect of a task.
|
||||||
|
|
||||||
|
The one exception is CSS minification-by-hand — `styles.css` is single-line and
|
||||||
|
unreadable. Component `<style>` blocks should be normally formatted. That is an
|
||||||
|
improvement, not a style disagreement.
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
Write the comment that stops the next person from making a mistake. The existing
|
||||||
|
codebase does this well:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// The cluster's nginx ingress runs with `use-forwarded-headers` off, so it
|
||||||
|
// *overwrites* X-Forwarded-For with its own downstream peer — the VPS's
|
||||||
|
// tailnet address — which would collapse every visitor into a single voter.
|
||||||
|
```
|
||||||
|
|
||||||
|
That comment earns its place. `// set the colour` does not.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- Components `PascalCase.astro`; everything else `kebab-case`.
|
||||||
|
- Booleans read as assertions: `isOpen`, `hasVoted`, not `open`, `voted`.
|
||||||
|
- No abbreviations that are not already in the codebase's vocabulary.
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
Astro brings TS. Use it: typed props, typed collections, `strict` on. Never
|
||||||
|
`any` — if the type is genuinely unknown, `unknown` plus a narrow.
|
||||||
|
|
||||||
|
## Dead code
|
||||||
|
|
||||||
|
Delete it. Do not comment it out, do not leave it behind a flag. Git remembers.
|
||||||
|
|
||||||
|
This matters here specifically: `responsive.css` is 30 KB of accumulated
|
||||||
|
overrides, and the temptation during migration will be to port it wholesale
|
||||||
|
"just in case". Prove each rule is needed or drop it.
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
Present tense, lowercase, `type: subject`, matching the existing log (`feat:`,
|
||||||
|
`fix:`, `docs:`). The body explains why, and states what you did not do.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Rule: componentization
|
||||||
|
|
||||||
|
## When to make a component
|
||||||
|
|
||||||
|
Extract when the same markup appears **three times**, or when a block has a name
|
||||||
|
a person would use out loud ("the eyebrow", "the route card", "the phase
|
||||||
|
panel").
|
||||||
|
|
||||||
|
Do not extract on the second occurrence. Two similar blocks often diverge; the
|
||||||
|
premature abstraction costs more than the duplication.
|
||||||
|
|
||||||
|
## Sizes
|
||||||
|
|
||||||
|
- A component that exceeds ~120 lines of markup is doing two jobs. Split it.
|
||||||
|
- A page that is a bare list of components with no markup of its own has been
|
||||||
|
over-split. Pages are allowed to contain layout.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
```
|
||||||
|
src/components/
|
||||||
|
primitives/ Eyebrow, Rule, Callout, CodeBlock — no domain knowledge
|
||||||
|
blocks/ RouteCard, PhasePanel, HandoffTable, SkillPackage — composed, page-agnostic
|
||||||
|
islands/ interactive only; each one justified per rules/astro.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- Primitives never import blocks.
|
||||||
|
- Blocks never import page-specific data; they take props.
|
||||||
|
- Islands are leaves. An island must not wrap static children that could have
|
||||||
|
been server-rendered.
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
- Typed `interface Props`, every field. No `any`, no untyped rest spread.
|
||||||
|
- Required by default. Optional props need a default and a reason.
|
||||||
|
- Pass data, not markup. If you find yourself passing an HTML string, you want a
|
||||||
|
`<slot>`.
|
||||||
|
|
||||||
|
## Named exports, no barrels
|
||||||
|
|
||||||
|
Import the file you need. Barrel `index.ts` files break tree-shaking and create
|
||||||
|
import cycles; bulletproof-react advises against them and so do we.
|
||||||
|
|
||||||
|
## The catalog is data, not components
|
||||||
|
|
||||||
|
The 24 review-desk entries are content, not 24 components. One
|
||||||
|
`SkillReviewCard.astro` iterating a typed collection. If you are writing the
|
||||||
|
25th near-identical component, stop and model the data.
|
||||||
|
|
||||||
|
## Do not componentize
|
||||||
|
|
||||||
|
`hands-on/starter/` and `hands-on/rules/` are lab fixtures. Their whole value is
|
||||||
|
being flat, dependency-free files an attendee hands to an agent. They ship from
|
||||||
|
`public/` unchanged.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Rule: content and i18n
|
||||||
|
|
||||||
|
## Every user-visible string is content
|
||||||
|
|
||||||
|
No hard-coded copy in components. Strings live in `src/content/`, typed with
|
||||||
|
Zod, and reach components as props or collection entries.
|
||||||
|
|
||||||
|
## The existing shape
|
||||||
|
|
||||||
|
`app.js` already stores content as `{ en: '…', pt: '…' }` objects across
|
||||||
|
`phases`, `handsOnPrompts`, `modelGuide`, `skillSources`, and
|
||||||
|
`skillInstallPrompts` — about 50 `en:` keys. That shape is fine and should be
|
||||||
|
carried across, not redesigned:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/content/config.ts
|
||||||
|
const localized = z.object({ en: z.string(), pt: z.string() });
|
||||||
|
```
|
||||||
|
|
||||||
|
Both locales are **required**. A missing `pt` must be a build error, not a
|
||||||
|
silent English fallback — that is how bilingual sites quietly become
|
||||||
|
monolingual.
|
||||||
|
|
||||||
|
## Migrating strings
|
||||||
|
|
||||||
|
Copy, do not retype. These are hand-written translations with specific tone
|
||||||
|
(`'Transforme ambiguidade em trabalho'`). Retyping introduces typos and drift.
|
||||||
|
Move the literal, then diff the extracted content against the original file to
|
||||||
|
prove nothing changed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json
|
||||||
|
node .agents/scripts/extract-strings.mjs src/content/guide/ > /tmp/after.json
|
||||||
|
diff /tmp/before.json /tmp/after.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Language switching
|
||||||
|
|
||||||
|
Today the toggle swaps text client-side and updates `<html lang>`. Two options
|
||||||
|
for Astro, decide in task 03:
|
||||||
|
|
||||||
|
- **Keep client-side swap.** Both languages ship in the payload. Zero routing
|
||||||
|
change, matches today exactly, no URL work. Recommended — the content is small
|
||||||
|
and the current behaviour is already what people link to.
|
||||||
|
- **Route-based (`/en/`, `/pt/`).** Better SEO, more correct, but it changes
|
||||||
|
every existing URL. Only do this with an explicit decision plus redirects.
|
||||||
|
|
||||||
|
Whichever you pick, `<html lang>` must track the active language.
|
||||||
|
|
||||||
|
## Rendered content
|
||||||
|
|
||||||
|
Markdown in `catalog.js` entries (the `improved` field) should become real
|
||||||
|
Markdown files in the collection, rendered at build time rather than by a
|
||||||
|
hand-rolled client-side renderer. That deletes code and improves fidelity.
|
||||||
|
|
||||||
|
Careful: `skill-reviews/improved/**/SKILL.md` is **generated** from those
|
||||||
|
entries by `scripts/build-skill-review.mjs`, and the generated files are
|
||||||
|
committed. Keep that generator working, or replace it and update every reference
|
||||||
|
to it.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Rule: quality gates
|
||||||
|
|
||||||
|
Three tiers. Each is scoped so that **many agents committing in parallel
|
||||||
|
worktrees stay fast** — the whole point is that a gate you are tempted to skip
|
||||||
|
is not a gate.
|
||||||
|
|
||||||
|
| Tier | Hook | Scope | Budget | Runs |
|
||||||
|
| ---- | ------------------ | ------------------------------------- | ------- | -------------------- |
|
||||||
|
| 1 | `pre-commit` | **staged files only** (lint-staged) | < 3s | every commit |
|
||||||
|
| 2 | `pre-push` | whole project: build + verify + audit | < 90s | every push |
|
||||||
|
| 3 | CI (Gitea Actions) | tier 2 + lint, on a clean install | minutes | every push to `main` |
|
||||||
|
|
||||||
|
Tier 1 must stay under a few seconds. If it creeps, move the check to tier 2. An
|
||||||
|
agent that waits 40s per commit will start passing `--no-verify`, and then you
|
||||||
|
have no gate at all.
|
||||||
|
|
||||||
|
## Tier 1 — pre-commit (lint-staged)
|
||||||
|
|
||||||
|
Formats and lints **only what you staged**. Scoped by construction, so ten
|
||||||
|
parallel worktrees do ten small jobs, not ten full-project sweeps.
|
||||||
|
|
||||||
|
- Prettier + ESLint on `*.{js,mjs,ts,astro}`
|
||||||
|
- Prettier + Stylelint on `*.css`
|
||||||
|
- `check-tokens.mjs` on changed `.astro`/`.css` — catches raw hex before it
|
||||||
|
lands
|
||||||
|
- Prettier on `*.{json,md}`
|
||||||
|
|
||||||
|
## Tier 2 — pre-push
|
||||||
|
|
||||||
|
The real gate:
|
||||||
|
|
||||||
|
```
|
||||||
|
astro check types
|
||||||
|
astro build it compiles
|
||||||
|
verify.mjs 42 content assertions — count must not fall
|
||||||
|
audit-ui.mjs no external runtime dependencies
|
||||||
|
check-tokens.mjs full sweep
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tier 3 — CI
|
||||||
|
|
||||||
|
Tier 2 plus `pnpm run lint`, run against a clean
|
||||||
|
`pnpm install --frozen-lockfile` — which is the part a local worktree cannot
|
||||||
|
prove.
|
||||||
|
|
||||||
|
Screenshot comparison against `.agents/snapshots/` is **not** wired in yet.
|
||||||
|
`visual-regression.mjs` can only capture baselines, not diff them, so running it
|
||||||
|
in CI would overwrite the baselines and pass unconditionally. It also needs
|
||||||
|
Playwright, which is not a project dependency. Add the step back when the script
|
||||||
|
grows a compare mode.
|
||||||
|
|
||||||
|
## Bypassing
|
||||||
|
|
||||||
|
`--no-verify` is allowed exactly once: a work-in-progress commit **on your own
|
||||||
|
task branch that you will amend or squash away**. It is never allowed on a
|
||||||
|
commit you intend to merge, and the pre-push gate has no bypass.
|
||||||
|
|
||||||
|
If a gate is wrong, fix the gate in its own commit. Do not route around it.
|
||||||
|
|
||||||
|
## Never restructure code to slip past a checker
|
||||||
|
|
||||||
|
A checker is a proxy for a rule. Passing the proxy while breaking the rule is
|
||||||
|
worse than failing, because failure is visible and this is not.
|
||||||
|
|
||||||
|
`check-tokens.mjs` matches `font-size: Npx`. Writing the same value as the
|
||||||
|
`font:` shorthand passes it. Task 07 did exactly that, in **two** components,
|
||||||
|
with a comment saying so. Both hardcoded values survived into a "green" branch.
|
||||||
|
|
||||||
|
### Do not substitute a near-miss token either
|
||||||
|
|
||||||
|
The second way to break this is subtler, and both tasks 10 and 11 did it: keep
|
||||||
|
the gate happy by pointing a legacy value at the closest token that already
|
||||||
|
exists. `#e5eeeb` became `var(--paper)`. Diff-**added** green became
|
||||||
|
`var(--accent)` — purple. `12px` and `14px` both became `var(--step-1)`, 15px.
|
||||||
|
|
||||||
|
That is a silent redesign, and it is _worse_ than leaving the raw value in,
|
||||||
|
because a raw hex is at least honest about being unresolved.
|
||||||
|
|
||||||
|
### What to do instead: mark the gap
|
||||||
|
|
||||||
|
`tokens.css` has one owner (`design-system-keeper`) so that "add a token" is a
|
||||||
|
decision, not a side effect. You may not add one. You **can** keep the true
|
||||||
|
value and stay green — mark it:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* token-gap: no --step-* covers 12px; owner design-system-keeper */
|
||||||
|
font-size: 12px;
|
||||||
|
```
|
||||||
|
|
||||||
|
The marker waives that one finding. It needs a real reason after the colon; a
|
||||||
|
bare `token-gap:` is rejected. Every marked value is listed on each run, so the
|
||||||
|
debt stays visible rather than disappearing.
|
||||||
|
|
||||||
|
Write it in your task report as well: selector, legacy value, owning file.
|
||||||
|
Marking a gap is not resolving it — it keeps the site truthful until whoever
|
||||||
|
owns the token layer decides.
|
||||||
|
|
||||||
|
## Parallelism
|
||||||
|
|
||||||
|
- Hooks are **per-worktree**. Git's `index.lock` is per-worktree, so parallel
|
||||||
|
commits do not contend.
|
||||||
|
- The heavy tier-2 gate takes a **lock** (`.git/af-gate.lock`, shared across
|
||||||
|
worktrees) so ten agents pushing at once do not run ten concurrent builds and
|
||||||
|
thrash the machine. Waiters queue; they do not fail.
|
||||||
|
- `pnpm install --frozen-lockfile` in a fresh worktree is cheap: pnpm hardlinks
|
||||||
|
from the shared content-addressable store, so a second worktree costs seconds
|
||||||
|
and almost no disk instead of another 225 MB. No `--prefer-offline` needed.
|
||||||
|
|
||||||
|
## The silent-failure mode you must know about
|
||||||
|
|
||||||
|
Husky sets `core.hooksPath` to `.husky/_`, and **`.husky/_` is generated by
|
||||||
|
`pnpm install`, not committed**. A fresh `git worktree add` therefore has hooks
|
||||||
|
configured but the directory missing — so **hooks silently do not run**. Every
|
||||||
|
commit passes. Nothing is checked.
|
||||||
|
|
||||||
|
`.agents/scripts/worktree.sh start` runs the install and then verifies. Check
|
||||||
|
manually any time you did not use it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.agents/scripts/verify-hooks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this first in any worktree you did not create with the script.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Rule: git worktrees
|
||||||
|
|
||||||
|
This project teaches worktrees. It should use them properly.
|
||||||
|
|
||||||
|
## One task, one worktree, one agent
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# from the main checkout
|
||||||
|
git worktree add ../af-task-07 -b refactor/task-07-route-cards
|
||||||
|
cd ../af-task-07
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Naming: directory `../af-task-NN`, branch `refactor/task-NN-<slug>`. Both
|
||||||
|
derived from the task file so the mapping is never ambiguous.
|
||||||
|
|
||||||
|
## Why isolation matters here
|
||||||
|
|
||||||
|
The migration runs many agents in parallel over the same small set of files
|
||||||
|
(`tokens.css`, `verify.mjs`, `astro.config.mjs` are contended). Worktrees give
|
||||||
|
each agent its own working directory over one object store — cheap, and no agent
|
||||||
|
can see another's half-finished state.
|
||||||
|
|
||||||
|
The failure mode without them: two agents both "fix" `verify.mjs`, and the
|
||||||
|
second overwrites the first's assertions.
|
||||||
|
|
||||||
|
## Contended files
|
||||||
|
|
||||||
|
These are touched by many tasks. Whoever owns them per the plan is the **only**
|
||||||
|
writer; everyone else opens an issue in their task report instead of editing:
|
||||||
|
|
||||||
|
| File | Owner |
|
||||||
|
| ---------------------------------- | ----------------------- |
|
||||||
|
| `src/styles/tokens.css` | `design-system-keeper` |
|
||||||
|
| `scripts/verify.mjs` | `verification-engineer` |
|
||||||
|
| `astro.config.mjs`, `package.json` | `astro-architect` |
|
||||||
|
| `src/content/config.ts` | `content-i18n-migrator` |
|
||||||
|
|
||||||
|
## Never rebase a task branch
|
||||||
|
|
||||||
|
Not onto `main`, not onto another task branch, not "just to tidy up". A rebase
|
||||||
|
rewrites every commit with a new SHA, so a branch that others branched from — or
|
||||||
|
that has already been merged — turns into a duplicate history that no longer
|
||||||
|
shares an ancestor with the original. Task 03 did this and flattened task 01's
|
||||||
|
merge into four look-alike commits; it is the same divergent-history trap that
|
||||||
|
broke the `pages` branch.
|
||||||
|
|
||||||
|
To pick up new work from `main`, **merge it in**: `git merge origin/main`. The
|
||||||
|
extra merge commit is the price of a history that stays true, and it is cheap.
|
||||||
|
|
||||||
|
## Before you start
|
||||||
|
|
||||||
|
1. `git fetch origin` — then branch from the ref your task file names as its
|
||||||
|
base. Do not assume that base is `main`.
|
||||||
|
2. Read your task file end to end before writing anything.
|
||||||
|
3. Confirm your task's dependencies are merged. Task files list them.
|
||||||
|
|
||||||
|
## Before you finish
|
||||||
|
|
||||||
|
1. `pnpm run verify` green — without deleting assertions.
|
||||||
|
2. The relevant checklist in [`../checklists/`](../checklists/) complete.
|
||||||
|
3. `git fetch origin && git merge origin/main`, resolve conflicts in your
|
||||||
|
worktree. **Merge — never rebase.**
|
||||||
|
4. Task report: what changed, what you verified, **what you did not do**.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd - # back to the main checkout
|
||||||
|
git worktree remove ../af-task-07
|
||||||
|
git branch -d refactor/task-07-route-cards
|
||||||
|
```
|
||||||
|
|
||||||
|
Stale worktrees hold locks and confuse the next agent. `git worktree list`
|
||||||
|
should be short.
|
||||||
|
|
||||||
|
## Never
|
||||||
|
|
||||||
|
- Never work directly on `main`.
|
||||||
|
- Never force-push a shared branch. The `pages` branch is the sole exception,
|
||||||
|
and only if CI owns it (see
|
||||||
|
[`../context/publishing.md`](../context/publishing.md)).
|
||||||
|
- Never `git add -A` from the repository root. This repo has untracked local
|
||||||
|
scratch (`.serena/`, `scripts/inspect.py`) that must not be swept into a
|
||||||
|
commit.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Rule: theming
|
||||||
|
|
||||||
|
Binding for every colour, font size, spacing value, and breakpoint.
|
||||||
|
|
||||||
|
**Read [`../context/design-system.md`](../context/design-system.md) first.** The
|
||||||
|
current CSS has three drifting palettes and a broken `@font-face`. This rule
|
||||||
|
describes the target; that file describes what you are migrating from.
|
||||||
|
|
||||||
|
## One token layer
|
||||||
|
|
||||||
|
Every value comes from `src/styles/tokens.css`. If a component needs a value
|
||||||
|
that is not a token, either it is a genuine one-off (justify it in a comment) or
|
||||||
|
the token layer is missing something (add it there, not inline).
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* forbidden */
|
||||||
|
color: #172f42;
|
||||||
|
color: rgba(23, 47, 66, 0.6);
|
||||||
|
|
||||||
|
/* required */
|
||||||
|
color: var(--ink);
|
||||||
|
color: var(--muted);
|
||||||
|
```
|
||||||
|
|
||||||
|
No raw hex outside `tokens.css`. `.agents/scripts/check-tokens.mjs` enforces it;
|
||||||
|
wire it into `pnpm run verify`.
|
||||||
|
|
||||||
|
## Semantic names, not literal ones
|
||||||
|
|
||||||
|
`--ink`, `--paper`, `--muted`, `--line`, `--accent`, `--gold`, `--blue`,
|
||||||
|
`--deep` are the existing vocabulary. Keep it — it is already semantic and the
|
||||||
|
team reads it fluently. Do not rename to `--color-neutral-900`.
|
||||||
|
|
||||||
|
If a genuine second surface is needed, extend semantically (`--surface-lab`,
|
||||||
|
`--ink-inverse`), never numerically.
|
||||||
|
|
||||||
|
## Type scale
|
||||||
|
|
||||||
|
Replace the 14 ad-hoc `clamp()` triples with named steps:
|
||||||
|
|
||||||
|
```css
|
||||||
|
--step-display: clamp(56px, 9vw, 126px); /* h1 */
|
||||||
|
--step-6: clamp(36px, 5vw, 65px); /* section h2 */
|
||||||
|
--step-5: clamp(24px, 3vw, 38px); /* sub-head */
|
||||||
|
--step-4: clamp(22px, 3vw, 36px); /* pull-quote */
|
||||||
|
--step-1: 15px; /* body */
|
||||||
|
--step-0: 11px; /* eyebrow / label */
|
||||||
|
```
|
||||||
|
|
||||||
|
The eyebrow treatment (`10–11px` monospace, `letter-spacing:.08–.1em`,
|
||||||
|
uppercase) is a signature of this design. Make it one class, not fifteen
|
||||||
|
repetitions.
|
||||||
|
|
||||||
|
## Breakpoints
|
||||||
|
|
||||||
|
Five named widths replace the current sixteen:
|
||||||
|
|
||||||
|
```css
|
||||||
|
--bp-sm: 560px;
|
||||||
|
--bp-md: 800px;
|
||||||
|
--bp-lg: 1100px;
|
||||||
|
--bp-xl: 1600px;
|
||||||
|
--bp-2xl: 2200px;
|
||||||
|
```
|
||||||
|
|
||||||
|
When collapsing a component's old breakpoint onto a named one, screenshot at the
|
||||||
|
**old** value. That is where the regression will be.
|
||||||
|
|
||||||
|
## Preserve the house style
|
||||||
|
|
||||||
|
- Flat colour blocks, hairline `1px` rules, near-zero border-radius.
|
||||||
|
- Tight negative tracking on display type (`-.06em` … `-.08em`).
|
||||||
|
- Grid separators built as `gap:1px` over a coloured parent background. This is
|
||||||
|
deliberate. Do not "fix" it into `border`.
|
||||||
|
- `Georgia, serif` for emphasis spans (`h1 em`). It renders today; keep it.
|
||||||
|
|
||||||
|
## Fonts
|
||||||
|
|
||||||
|
Do not add a webfont without an explicit decision recorded in the task. The
|
||||||
|
intended Manrope/DM Mono has never rendered; introducing it is a visual
|
||||||
|
redesign, not a refactor. Default: match what renders today.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Fails when a raw colour, px font-size, or ad-hoc breakpoint appears outside
|
||||||
|
// the token layer. A rule nobody checks is a suggestion — wire this into
|
||||||
|
// `pnpm run verify`.
|
||||||
|
//
|
||||||
|
// ESCAPE HATCH — `token-gap:`. Some legacy values have no token yet, and only
|
||||||
|
// `design-system-keeper` may add one. Without an escape, an agent told both
|
||||||
|
// "keep the site identical" and "get the gate green" has to break one of them,
|
||||||
|
// and tasks 10 and 11 both broke the first: `#e5eeeb` became `var(--paper)`,
|
||||||
|
// diff-added green became `var(--accent)` purple. Substituting a near-miss
|
||||||
|
// token is a silent redesign; it is worse than a raw value, because the raw
|
||||||
|
// value is at least honest about what it is.
|
||||||
|
//
|
||||||
|
// So: mark the line, keep the true value, stay green.
|
||||||
|
//
|
||||||
|
// /* token-gap: no --step-* covers 12px; owner design-system-keeper */
|
||||||
|
// font-size: 12px;
|
||||||
|
//
|
||||||
|
// Marked values are counted and listed on every run — they are a visible debt
|
||||||
|
// queue, not a way to make the finding disappear. The marker needs a reason;
|
||||||
|
// a bare `token-gap:` does not count.
|
||||||
|
//
|
||||||
|
// Usage: node .agents/scripts/check-tokens.mjs [srcDir]
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||||
|
import { join, extname } from 'node:path';
|
||||||
|
|
||||||
|
// lint-staged appends staged file paths; a bare run sweeps `src`.
|
||||||
|
const ARGS = process.argv.slice(2);
|
||||||
|
const TOKEN_FILES = ['tokens.css', 'base.css'];
|
||||||
|
const ALLOWED_BREAKPOINTS = ['560px', '800px', '1100px', '1600px', '2200px'];
|
||||||
|
|
||||||
|
const walk = (dir) =>
|
||||||
|
readdirSync(dir).flatMap((name) => {
|
||||||
|
const path = join(dir, name);
|
||||||
|
return statSync(path).isDirectory() ? walk(path) : [path];
|
||||||
|
});
|
||||||
|
|
||||||
|
const targets = ARGS.length
|
||||||
|
? ARGS.flatMap((arg) => (statSync(arg).isDirectory() ? walk(arg) : [arg]))
|
||||||
|
: walk('src');
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
const gaps = [];
|
||||||
|
|
||||||
|
// A finding is waived when its own line, or the line above it, carries a
|
||||||
|
// `token-gap:` marker with a reason after the colon.
|
||||||
|
const MARKER = /token-gap:([^\n]*)/;
|
||||||
|
// The reason is what is left after the marker once the comment terminator and
|
||||||
|
// punctuation are stripped. `/* token-gap: */` is not a reason.
|
||||||
|
const reason = (line) => {
|
||||||
|
const found = MARKER.exec(line ?? '');
|
||||||
|
if (!found) return null;
|
||||||
|
const text = found[1]
|
||||||
|
.replace(/\*\/\s*$/, '')
|
||||||
|
.replace(/[\s*/]+$/, '')
|
||||||
|
.trim();
|
||||||
|
return /[a-z0-9]/i.test(text) ? [null, text] : null;
|
||||||
|
};
|
||||||
|
const waiver = (lines, index) =>
|
||||||
|
reason(lines[index]) || (index > 0 ? reason(lines[index - 1]) : null);
|
||||||
|
|
||||||
|
for (const path of targets) {
|
||||||
|
if (!['.astro', '.css'].includes(extname(path))) continue;
|
||||||
|
if (TOKEN_FILES.some((allowed) => path.endsWith(allowed))) continue;
|
||||||
|
|
||||||
|
const lines = readFileSync(path, 'utf8').split('\n');
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const at = `${path}:${index + 1}`;
|
||||||
|
const waived = waiver(lines, index);
|
||||||
|
const record = (finding) => {
|
||||||
|
if (waived) gaps.push(`${at}: ${finding.slice(at.length + 2)} [${waived[1]}]`);
|
||||||
|
else findings.push(finding);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Raw hex — the drifted-palette failure mode this whole layer exists to stop.
|
||||||
|
const hex = line.match(/#[0-9a-fA-F]{3,8}\b/g);
|
||||||
|
if (hex) record(`${at}: raw hex ${hex.join(', ')} — use a token from tokens.css`);
|
||||||
|
|
||||||
|
// rgb()/hsl() literals are the same problem wearing a different hat.
|
||||||
|
if (/\b(rgba?|hsla?)\(\s*\d/.test(line)) record(`${at}: raw colour function — use a token`);
|
||||||
|
|
||||||
|
// Hard-coded font sizes bypass the type scale.
|
||||||
|
const fontSize = line.match(/font-size:\s*\d+(\.\d+)?px/);
|
||||||
|
if (fontSize) record(`${at}: hard-coded ${fontSize[0]} — use var(--step-*)`);
|
||||||
|
|
||||||
|
// Ad-hoc breakpoints are how sixteen of them accumulated last time.
|
||||||
|
const media = line.match(/@media[^{]*?\(\s*(?:max|min)-width:\s*(\d+px)/);
|
||||||
|
if (media && !ALLOWED_BREAKPOINTS.includes(media[1]))
|
||||||
|
record(
|
||||||
|
`${at}: breakpoint ${media[1]} is not a named one (${ALLOWED_BREAKPOINTS.join(', ')})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gaps.length) {
|
||||||
|
console.log(`token check: ${gaps.length} marked token-gap(s) awaiting design-system-keeper:\n`);
|
||||||
|
gaps.forEach((gap) => console.log(` ${gap}`));
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findings.length) {
|
||||||
|
console.error(`token check failed — ${findings.length} finding(s):\n`);
|
||||||
|
findings.forEach((finding) => console.error(` ${finding}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { basename } from 'node:path';
|
||||||
|
|
||||||
|
if (existsSync('dist')) {
|
||||||
|
const builtCss = walk('dist').filter((p) => p.endsWith('.css'));
|
||||||
|
const tokensBuilt = builtCss.find((p) => /[\\/]tokens\.[^\\/]+\.css$/.test(p));
|
||||||
|
|
||||||
|
if (!tokensBuilt) {
|
||||||
|
console.error('token check failed — tokens.css was not built into dist/');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokensContent = readFileSync(tokensBuilt, 'utf8');
|
||||||
|
if (!tokensContent.includes('#527f9f')) {
|
||||||
|
console.error(
|
||||||
|
'token check failed — built tokens.css does not contain the canonical --blue value #527f9f',
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlFiles = walk('dist').filter(
|
||||||
|
(p) =>
|
||||||
|
p.endsWith('.html') &&
|
||||||
|
!p.includes('/hands-on/') &&
|
||||||
|
!p.includes('\\hands-on\\') &&
|
||||||
|
!p.includes('/submitted-skills/') &&
|
||||||
|
!p.includes('\\submitted-skills\\'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenChunkName = basename(tokensBuilt);
|
||||||
|
for (const html of htmlFiles) {
|
||||||
|
const content = readFileSync(html, 'utf8');
|
||||||
|
if (!content.includes(tokenChunkName)) {
|
||||||
|
console.error(
|
||||||
|
`token check failed — ${html} does not load the token layer (${tokenChunkName})`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('token check passed');
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Mechanically extract every { en, pt } string pair from a file or directory,
|
||||||
|
// sorted and normalised, so a content migration can be proven lossless:
|
||||||
|
//
|
||||||
|
// node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json
|
||||||
|
// node .agents/scripts/extract-strings.mjs src/content/ > /tmp/after.json
|
||||||
|
// diff /tmp/before.json /tmp/after.json
|
||||||
|
//
|
||||||
|
// A non-empty diff means you altered content. These are hand-written
|
||||||
|
// translations with deliberate tone — copy them, never retype them.
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const target = process.argv[2];
|
||||||
|
if (!target) {
|
||||||
|
console.error('usage: extract-strings.mjs <file|dir>');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const walk = (dir) =>
|
||||||
|
readdirSync(dir).flatMap((name) => {
|
||||||
|
const path = join(dir, name);
|
||||||
|
return statSync(path).isDirectory() ? walk(path) : [path];
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = statSync(target).isDirectory() ? walk(target) : [target];
|
||||||
|
|
||||||
|
// Matches `en: '…'` / "en": "…" and the pt counterpart, single or double quoted,
|
||||||
|
// tolerating escaped quotes inside.
|
||||||
|
const PAIR = /["']?\b(en|pt)\b["']?\s*:\s*(['"])((?:\\.|(?!\2)[\s\S])*)\2/g;
|
||||||
|
|
||||||
|
const strings = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const source = readFileSync(file, 'utf8');
|
||||||
|
for (const match of source.matchAll(PAIR)) {
|
||||||
|
strings.push({ lang: match[1], value: match[3] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort so file ordering and structure changes do not show up as content changes.
|
||||||
|
strings.sort((a, b) => (a.lang + a.value).localeCompare(b.lang + b.value));
|
||||||
|
|
||||||
|
console.log(JSON.stringify(strings, null, 2));
|
||||||
|
console.error(`extracted ${strings.length} strings from ${files.length} file(s)`);
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tier 2 gate: types, build, content contracts, dependency audit, tokens.
|
||||||
|
# Invoked by .husky/pre-push, and safe to run by hand at any time.
|
||||||
|
#
|
||||||
|
# Parallel-safe: takes a lock in the SHARED git dir, so ten agents pushing from
|
||||||
|
# ten worktrees queue instead of running ten concurrent Astro builds and
|
||||||
|
# thrashing the machine. Waiters block; they do not fail.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root=$(git rev-parse --show-toplevel)
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
# --git-common-dir resolves to the ONE shared .git across all worktrees, which
|
||||||
|
# is exactly the scope we want the lock to cover.
|
||||||
|
common=$(git rev-parse --git-common-dir)
|
||||||
|
lock="$common/af-gate.lock"
|
||||||
|
|
||||||
|
exec 9>"$lock"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
echo "gate: another worktree is running the gate — waiting for it…"
|
||||||
|
flock 9
|
||||||
|
fi
|
||||||
|
|
||||||
|
started=$(date +%s)
|
||||||
|
step() { printf '\n\033[1m▸ %s\033[0m\n' "$1"; }
|
||||||
|
|
||||||
|
# Fail loudly rather than passing vacuously when the toolchain is not installed.
|
||||||
|
if [ ! -d node_modules ]; then
|
||||||
|
echo "gate: node_modules missing — run 'pnpm install --frozen-lockfile' first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This project is pnpm-only. An agent that runs `npm install` out of habit gets
|
||||||
|
# a second, divergent dependency tree and a lockfile nobody reads — the same
|
||||||
|
# class of failure that cost two sessions during phase 0. Catch it here.
|
||||||
|
if [ -f package-lock.json ] || [ -f yarn.lock ] || [ -f bun.lock ] || [ -f bun.lockb ]; then
|
||||||
|
echo "gate: a non-pnpm lockfile is present. This project uses pnpm only." >&2
|
||||||
|
echo " Delete it, then run 'pnpm install --frozen-lockfile'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "types"
|
||||||
|
pnpm exec astro check
|
||||||
|
|
||||||
|
step "build"
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
step "content contracts"
|
||||||
|
pnpm run verify
|
||||||
|
|
||||||
|
# The assertion count is the thing agents are most tempted to "fix" downward.
|
||||||
|
# Compare against origin/main and refuse a silent reduction.
|
||||||
|
step "assertion coverage"
|
||||||
|
current=$(grep -c 'throw new Error' scripts/verify.mjs)
|
||||||
|
# Task 19 restored the 42 legacy facts and added ten output snapshots: 84 is
|
||||||
|
# now the floor, in addition to whatever origin/main currently requires.
|
||||||
|
baseline=$(git show origin/main:scripts/verify.mjs 2>/dev/null | grep -c 'throw new Error' || echo 0)
|
||||||
|
if [ "$baseline" -lt 84 ]; then baseline=84; fi
|
||||||
|
if [ "$current" -lt "$baseline" ]; then
|
||||||
|
echo "gate: verify.mjs coverage fell from $baseline to $current assertions." >&2
|
||||||
|
echo " Only verification-engineer may reduce it, with a reason per removal." >&2
|
||||||
|
echo " See .agents/context/verification.md" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " $current assertions (baseline $baseline)"
|
||||||
|
|
||||||
|
step "runtime dependency audit"
|
||||||
|
node scripts/audit-ui.mjs
|
||||||
|
|
||||||
|
step "design tokens"
|
||||||
|
node .agents/scripts/check-tokens.mjs
|
||||||
|
|
||||||
|
printf '\n\033[32mgate passed\033[0m in %ss\n' "$(( $(date +%s) - started ))"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# `.agents/` is the vendor-neutral home for this project's rules, skills, and
|
||||||
|
# agent definitions — MiniMax, Gemini, and Codex all read plain files from it.
|
||||||
|
#
|
||||||
|
# Claude Code, however, discovers subagents and skills at fixed paths. This
|
||||||
|
# symlinks them so there is exactly ONE copy of every definition and no drift.
|
||||||
|
#
|
||||||
|
# .agents/scripts/install-claude-agents.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
mkdir -p .claude
|
||||||
|
|
||||||
|
for target in agents skills; do
|
||||||
|
link=".claude/$target"
|
||||||
|
if [ -e "$link" ] && [ ! -L "$link" ]; then
|
||||||
|
echo "refusing: $link exists and is not a symlink — move it aside first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ln -sfn "../.agents/$target" "$link"
|
||||||
|
echo "linked $link -> .agents/$target"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Claude Code will now load:"
|
||||||
|
ls -1 .agents/agents/*.md | sed 's|.*/| agent: |;s|\.md$||'
|
||||||
|
ls -1d .agents/skills/*/ | sed 's|.*/skills/| skill: |;s|/$||'
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Launch one refactor task in its own worktree, on the CLI its brief routes it to.
|
||||||
|
#
|
||||||
|
# .agents/scripts/launch.sh 01 scaffold
|
||||||
|
# .agents/scripts/launch.sh 02 tokens --base refactor/task-01-scaffold
|
||||||
|
# .agents/scripts/launch.sh 07 primitives --cli mm --fg
|
||||||
|
#
|
||||||
|
# Routing comes from plans/astro-refactor/MODEL-ROUTING.md. Override with --cli.
|
||||||
|
# All four CLIs are launched with their permission prompts disabled: these run
|
||||||
|
# unattended inside a worktree, and a blocked edit or bash call just hangs.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
number=${1:?usage: launch.sh <task-number> <slug> [--base ref] [--cli codex|agy|mm|oc] [--fg]}
|
||||||
|
slug=${2:?slug, e.g. scaffold}
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
base="main"
|
||||||
|
cli=""
|
||||||
|
foreground=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--base) base=$2; shift 2 ;;
|
||||||
|
--cli) cli=$2; shift 2 ;;
|
||||||
|
--fg) foreground=1; shift ;;
|
||||||
|
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Model routing. Codex takes the long iterate-until-green loops, Gemini the two
|
||||||
|
# tasks that need whole-corpus context plus visual judgement, MiniMax the rest.
|
||||||
|
if [ -z "$cli" ]; then
|
||||||
|
case "$number" in
|
||||||
|
01|03|15|16|19) cli=codex ;;
|
||||||
|
02|18) cli=agy ;;
|
||||||
|
*) cli=mm ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
dir="../af-task-${number}"
|
||||||
|
branch="refactor/task-${number}-${slug}"
|
||||||
|
plan="plans/astro-refactor/task-${number}-${slug}.md"
|
||||||
|
log="$(pwd)/.agents/logs/task-${number}-${slug}.log"
|
||||||
|
|
||||||
|
[ -f "$plan" ] || { echo "no plan at $plan — check the number and slug" >&2; exit 1; }
|
||||||
|
mkdir -p .agents/logs
|
||||||
|
|
||||||
|
# The brief names its own agent; pull it out so the prompt can point at the file.
|
||||||
|
# [a-z0-9-] not [a-z-]: content-i18n-migrator has digits in it.
|
||||||
|
agent=$(sed -n 's/.*\*\*Agent\*\*: `\([a-z0-9-]*\)`.*/\1/p' "$plan" | head -1)
|
||||||
|
[ -n "$agent" ] || { echo "could not read the agent name out of $plan" >&2; exit 1; }
|
||||||
|
[ -f ".agents/agents/${agent}.md" ] || { echo "no such agent: .agents/agents/${agent}.md" >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
echo "worktree $dir already exists — reusing it"
|
||||||
|
else
|
||||||
|
git worktree add "$dir" -b "$branch" "$base"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pnpm install works only once task 01 has produced a lockfile. The pre-existing root
|
||||||
|
# package.json carries two scripts and no dependencies, so before task 01 there
|
||||||
|
# is no toolchain to install and no hooks to verify.
|
||||||
|
if [ -f "$dir/pnpm-lock.yaml" ]; then
|
||||||
|
( cd "$dir" && pnpm install --frozen-lockfile && .agents/scripts/verify-hooks.sh )
|
||||||
|
fi
|
||||||
|
|
||||||
|
prompt=$(cat <<PROMPT
|
||||||
|
You are the \`${agent}\` specialist on the ai-for-dummies Astro refactor.
|
||||||
|
You are working alone in the git worktree at $(cd "$dir" && pwd), on branch ${branch}.
|
||||||
|
|
||||||
|
Read these, in this order, before you write anything:
|
||||||
|
|
||||||
|
1. AGENTS.md — project entry point and the "never touch" list
|
||||||
|
2. .agents/agents/${agent}.md — your role, what you own, what you must not do
|
||||||
|
3. ${plan} — your task brief: scope, steps, done-when, do-not
|
||||||
|
4. every file the brief and your agent definition tell you to read
|
||||||
|
(.agents/context/*, .agents/rules/*, .agents/skills/*, .agents/checklists/*)
|
||||||
|
|
||||||
|
Then do the task. Rules that override your own judgement:
|
||||||
|
|
||||||
|
- Stay inside the scope in the brief. Do not do work belonging to another task.
|
||||||
|
- Honour every "Do not" line in the brief and in your agent definition.
|
||||||
|
- The site must look and read exactly as it does today. This is a refactor,
|
||||||
|
not a redesign. If you find something that looks like a bug in the current
|
||||||
|
design, write it down in the final report — do not fix it.
|
||||||
|
- Never delete or weaken an assertion in scripts/verify.mjs to make a suite
|
||||||
|
green. Only the verification-engineer may reduce assertion count, with a
|
||||||
|
written reason per removal.
|
||||||
|
- Commit as you go with conventional-commit messages. The commit-msg and
|
||||||
|
pre-commit hooks are live; if a hook rejects you, fix the cause, never
|
||||||
|
bypass with --no-verify.
|
||||||
|
- Work through the done-when checklist at the end and actually run each check.
|
||||||
|
|
||||||
|
Finish by printing: what you changed, which done-when boxes are genuinely
|
||||||
|
ticked, which are not and why, and anything the next task needs to know.
|
||||||
|
PROMPT
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "task : ${number} ${slug}"
|
||||||
|
echo "agent : ${agent}"
|
||||||
|
echo "cli : ${cli}"
|
||||||
|
echo "worktree: ${dir} (branch ${branch}, from ${base})"
|
||||||
|
echo "log : ${log}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
run() {
|
||||||
|
case "$cli" in
|
||||||
|
codex)
|
||||||
|
codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check \
|
||||||
|
-C "$dir" "$prompt"
|
||||||
|
;;
|
||||||
|
agy)
|
||||||
|
( cd "$dir" && agy --dangerously-skip-permissions \
|
||||||
|
--model gemini-3.1-pro-high --print-timeout 4h -p "$prompt" )
|
||||||
|
;;
|
||||||
|
mm)
|
||||||
|
( cd "$dir" && mm --dangerously-skip-permissions \
|
||||||
|
--model opus -p "$prompt" )
|
||||||
|
;;
|
||||||
|
oc)
|
||||||
|
# Claude Code against a local-Ollama-backed model, through the headroom
|
||||||
|
# hub. Unproven on this repo — give it the task whose failure is cheapest.
|
||||||
|
( cd "$dir" && OLLAMA_CLAUDE_MODEL="${OC_MODEL:-glm-5.3:cloud}" \
|
||||||
|
ollama-claude --dangerously-skip-permissions -p "$prompt" )
|
||||||
|
;;
|
||||||
|
*) echo "unknown cli: $cli" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$foreground" = 1 ]; then
|
||||||
|
run 2>&1 | tee "$log"
|
||||||
|
else
|
||||||
|
export cli dir prompt
|
||||||
|
nohup bash -c "$(declare -f run); run" >"$log" 2>&1 &
|
||||||
|
echo "launched in background, pid $!"
|
||||||
|
echo "follow: tail -f $log"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Compare the *rendered* text of a legacy page against its Astro replacement.
|
||||||
|
//
|
||||||
|
// node .agents/scripts/rendered-text-diff.mjs full-guide
|
||||||
|
// node .agents/scripts/rendered-text-diff.mjs full-guide --pt
|
||||||
|
//
|
||||||
|
// Why this exists: scripts/verify.mjs reads the legacy files, so a migrated
|
||||||
|
// page can drop half its content and still pass the gate. Task 15d shipped
|
||||||
|
// /full-guide/ missing 86 rendered spans -- the entire verification section,
|
||||||
|
// the hands-on exercise brief, and both "Clone from Gitea" links -- and every
|
||||||
|
// check was green.
|
||||||
|
//
|
||||||
|
// Static HTML comparison is useless here: the guide's tab panels are injected
|
||||||
|
// by an island at runtime, so half the legacy page's markup has no static
|
||||||
|
// counterpart. This walks the live DOM instead and skips anything the browser
|
||||||
|
// is not painting -- which also drops the hidden Portuguese half of each
|
||||||
|
// bilingual pair, so the two sides line up.
|
||||||
|
//
|
||||||
|
// Requires playwright (devDependency) and two static servers; it starts both.
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { cpSync, mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { createServer } from 'node:net';
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
// `index` is the landing page: it lives at the repository root, not in a
|
||||||
|
// directory of its own, so it needs a different path on the legacy side.
|
||||||
|
const route = process.argv[2];
|
||||||
|
if (!route) {
|
||||||
|
console.error('usage: rendered-text-diff.mjs <route> [--pt] e.g. full-guide, or index');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `--pt` clicks the language toggle on both pages first. English parity is
|
||||||
|
// only half the contract: a page can render every English string and still
|
||||||
|
// leave a restored block untranslated, because the Portuguese half is a
|
||||||
|
// separate set of nodes. Only /full-guide/ and /rules/ have a toggle.
|
||||||
|
const portuguese = process.argv.includes('--pt');
|
||||||
|
const legacyPath = route === 'index' ? 'index.html' : `${route}/index.html`;
|
||||||
|
const astroPath = route === 'index' ? '' : `${route}/`;
|
||||||
|
|
||||||
|
// The built site expects to be served under the configured base path.
|
||||||
|
const staging = mkdtempSync(join(tmpdir(), 'af-rtd-'));
|
||||||
|
cpSync('dist', join(staging, 'ai-for-dummies'), { recursive: true });
|
||||||
|
|
||||||
|
// Ask the kernel for a free port rather than pinning one. Back-to-back runs
|
||||||
|
// used to collide: the previous run's server was still holding the fixed port
|
||||||
|
// while its staging directory had already been deleted, so every page came
|
||||||
|
// back as a 404 and the diff reported the whole route missing.
|
||||||
|
const freePort = () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const probe = createServer();
|
||||||
|
probe.on('error', reject);
|
||||||
|
probe.listen(0, '127.0.0.1', () => {
|
||||||
|
const { port } = probe.address();
|
||||||
|
probe.close(() => resolve(port));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const legacyPort = await freePort();
|
||||||
|
const astroPort = await freePort();
|
||||||
|
|
||||||
|
const serve = (dir, port) =>
|
||||||
|
spawn('python3', ['-m', 'http.server', String(port), '-d', dir], { stdio: 'ignore' });
|
||||||
|
const servers = [serve('.', legacyPort), serve(staging, astroPort)];
|
||||||
|
const stop = () => {
|
||||||
|
servers.forEach((s) => s.kill());
|
||||||
|
rmSync(staging, { recursive: true, force: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Visible text nodes, in document order, whitespace collapsed.
|
||||||
|
const visibleText = () => {
|
||||||
|
const out = [];
|
||||||
|
const walk = (node) => {
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = child.textContent.replace(/\s+/g, ' ').trim();
|
||||||
|
if (text) out.push(text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (child.nodeType !== Node.ELEMENT_NODE) continue;
|
||||||
|
if (child.tagName === 'SCRIPT' || child.tagName === 'STYLE') continue;
|
||||||
|
const style = getComputedStyle(child);
|
||||||
|
if (child.hidden || style.display === 'none' || style.visibility === 'hidden') continue;
|
||||||
|
walk(child);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(document.body);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const grab = async (url) => {
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } });
|
||||||
|
const response = await page.goto(url, { waitUntil: 'load' });
|
||||||
|
// A 404 renders as four spans of python's error page and the diff then
|
||||||
|
// reports the entire route as missing, which reads exactly like a real
|
||||||
|
// regression. Fail loudly instead.
|
||||||
|
if (!response || !response.ok()) {
|
||||||
|
throw new Error(`${url} returned ${response ? response.status() : 'no response'}`);
|
||||||
|
}
|
||||||
|
// The islands hydrate and render their initial panel on load; without this
|
||||||
|
// every panel's copy reads as missing.
|
||||||
|
await page.waitForTimeout(1200);
|
||||||
|
if (portuguese) {
|
||||||
|
const toggle = await page.$('[data-lang="pt"]');
|
||||||
|
if (!toggle) throw new Error(`no language toggle on ${url}`);
|
||||||
|
await toggle.click();
|
||||||
|
await page.waitForTimeout(1200);
|
||||||
|
}
|
||||||
|
// Islands hydrate at their own pace, and the language toggle repaints in
|
||||||
|
// more than one frame. A single read after a fixed wait is flaky, so read
|
||||||
|
// until two consecutive reads agree.
|
||||||
|
let spans = await page.evaluate(visibleText);
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const next = await page.evaluate(visibleText);
|
||||||
|
if (next.length === spans.length && next.every((span, j) => span === spans[j])) {
|
||||||
|
spans = next;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
spans = next;
|
||||||
|
}
|
||||||
|
await page.close();
|
||||||
|
return spans;
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacy = await grab(`http://localhost:${legacyPort}/${legacyPath}`);
|
||||||
|
const astro = await grab(`http://localhost:${astroPort}/ai-for-dummies/${astroPath}`);
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Count occurrences, not membership. A set comparison reports zero when a
|
||||||
|
// string the legacy page paints four times is painted three times here --
|
||||||
|
// exactly the kind of near-miss that got past the earlier checks.
|
||||||
|
const tally = (spans) => {
|
||||||
|
const counts = new Map();
|
||||||
|
for (const span of spans) counts.set(span, (counts.get(span) || 0) + 1);
|
||||||
|
return counts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyCounts = tally(legacy);
|
||||||
|
const astroCounts = tally(astro);
|
||||||
|
|
||||||
|
const missing = [];
|
||||||
|
for (const [span, count] of legacyCounts) {
|
||||||
|
const short = count - (astroCounts.get(span) || 0);
|
||||||
|
for (let i = 0; i < short; i += 1) missing.push(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both directions. A string the Astro page paints and the legacy page does
|
||||||
|
// not is just as wrong: it means a translation was invented, or an English
|
||||||
|
// string was left standing where the legacy page swaps it.
|
||||||
|
const extra = [];
|
||||||
|
for (const [span, count] of astroCounts) {
|
||||||
|
const over = count - (legacyCounts.get(span) || 0);
|
||||||
|
for (let i = 0; i < over; i += 1) extra.push(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order counts too. Both pages can paint the same strings while a block
|
||||||
|
// sits in the wrong place -- the Portuguese eyebrow, or a reordered card
|
||||||
|
// deck -- and a count-only comparison calls that clean.
|
||||||
|
const firstOutOfOrder = legacy.findIndex((span, i) => astro[i] !== span);
|
||||||
|
|
||||||
|
const mode = portuguese ? 'pt' : 'en';
|
||||||
|
console.log(
|
||||||
|
`${mode} · legacy ${legacy.length} spans · astro ${astro.length} spans · missing ${missing.length} · extra ${extra.length}`,
|
||||||
|
);
|
||||||
|
for (const span of missing) console.log(` - ${span}`);
|
||||||
|
for (const span of extra) console.log(` + ${span}`);
|
||||||
|
if (firstOutOfOrder !== -1) {
|
||||||
|
console.log(` order diverges at span ${firstOutOfOrder}`);
|
||||||
|
console.log(` legacy: ${legacy[firstOutOfOrder]}`);
|
||||||
|
console.log(` astro: ${astro[firstOutOfOrder]}`);
|
||||||
|
}
|
||||||
|
process.exitCode = missing.length === 0 && extra.length === 0 && firstOutOfOrder === -1 ? 0 : 1;
|
||||||
|
} finally {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Rendered-text snapshot of one route. This is the migration's regression net:
|
||||||
|
// token matching in verify.mjs cannot catch a dropped paragraph, this can.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/
|
||||||
|
// node .agents/scripts/snapshot-route.mjs dist/models/index.html
|
||||||
|
//
|
||||||
|
// Take a snapshot from the vanilla site BEFORE migrating, then diff the built
|
||||||
|
// output against it. An empty diff is the proof that nothing was lost.
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const target = process.argv[2];
|
||||||
|
if (!target) {
|
||||||
|
console.error('usage: snapshot-route.mjs <url|path>');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = target.startsWith('http')
|
||||||
|
? await (await fetch(target)).text()
|
||||||
|
: readFileSync(target, 'utf8');
|
||||||
|
|
||||||
|
const text = html
|
||||||
|
// Drop anything that is not user-visible prose.
|
||||||
|
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||||
|
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||||||
|
.replace(/<!--[\s\S]*?-->/g, '')
|
||||||
|
.replace(/<[^>]+>/g, '\n')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/�?39;/g, "'")
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
console.log(text);
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Guards the silent-failure mode described in .agents/rules/gates.md.
|
||||||
|
#
|
||||||
|
# Husky points core.hooksPath at `.husky/_`, but that directory is GENERATED by
|
||||||
|
# `pnpm install` and is NOT committed. A fresh `git worktree add` therefore has
|
||||||
|
# hooks configured and the directory missing — so every hook silently does
|
||||||
|
# nothing and every commit passes unchecked.
|
||||||
|
#
|
||||||
|
# Run this in any worktree you did not create with worktree.sh.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
path=$(git config --get core.hooksPath || true)
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
echo "✗ core.hooksPath is unset — husky was never installed here"
|
||||||
|
fail=1
|
||||||
|
else
|
||||||
|
echo "✓ core.hooksPath = $path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "${path:-.husky/_}" ]; then
|
||||||
|
echo "✗ ${path:-.husky/_}/ does not exist — HOOKS ARE NOT RUNNING"
|
||||||
|
fail=1
|
||||||
|
else
|
||||||
|
echo "✓ ${path} exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for hook in pre-commit commit-msg pre-push; do
|
||||||
|
if [ -f ".husky/$hook" ]; then
|
||||||
|
echo "✓ .husky/$hook present"
|
||||||
|
else
|
||||||
|
echo "✗ .husky/$hook missing"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -d node_modules ]; then
|
||||||
|
echo "✗ node_modules missing — lint-staged and astro check cannot run"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "Fix: pnpm install --frozen-lockfile (its prepare script regenerates .husky/_)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "hooks are live in this worktree"
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Keep vanilla screenshots before a page changes; compare them against the
|
||||||
|
// migrated preview when a visual diff needs investigation.
|
||||||
|
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
'/',
|
||||||
|
'/full-guide/',
|
||||||
|
'/summary/',
|
||||||
|
'/models/',
|
||||||
|
'/agents/',
|
||||||
|
'/skills/',
|
||||||
|
'/rules/',
|
||||||
|
'/skills-review/',
|
||||||
|
'/hands-on/starter/',
|
||||||
|
'/hands-on/rules/',
|
||||||
|
];
|
||||||
|
const widths = [560, 800, 1100, 1600];
|
||||||
|
const hasReducedMotion = process.argv.includes('--reduced-motion');
|
||||||
|
const base = process.env.VISUAL_BASE_URL ?? 'http://localhost:4173';
|
||||||
|
const output =
|
||||||
|
process.env.VISUAL_OUTPUT_DIR ??
|
||||||
|
`.agents/snapshots/${hasReducedMotion ? 'before-reduced-motion' : 'before'}`;
|
||||||
|
|
||||||
|
mkdirSync(output, { recursive: true });
|
||||||
|
|
||||||
|
let chromium;
|
||||||
|
try {
|
||||||
|
({ chromium } = await import('playwright'));
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
'visual regression requires Playwright; install its project dependency and run pnpm exec playwright install chromium',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
...(process.env.VISUAL_BROWSER_PATH ? { executablePath: process.env.VISUAL_BROWSER_PATH } : {}),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
for (const route of routes) {
|
||||||
|
for (const width of widths) {
|
||||||
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
||||||
|
if (hasReducedMotion) await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||||
|
// The legacy site intentionally calls optional services that may be down;
|
||||||
|
// visual capture is about the rendered page, not their network lifetime.
|
||||||
|
await page.goto(new URL(route, base).toString(), { waitUntil: 'domcontentloaded' });
|
||||||
|
const name = route.replace(/^\/+|\/+$/g, '').replaceAll('/', '_') || 'index';
|
||||||
|
await page.screenshot({ path: `${output}/${name}-${width}.png`, fullPage: true });
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`captured ${routes.length * widths.length} screenshots in ${output}`);
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Spin up (or tear down) an isolated worktree for one refactor task.
|
||||||
|
#
|
||||||
|
# .agents/scripts/worktree.sh start 07 route-cards
|
||||||
|
# .agents/scripts/worktree.sh finish 07 route-cards
|
||||||
|
#
|
||||||
|
# One task, one worktree, one agent. See .agents/rules/git-worktrees.md
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
action=${1:?usage: worktree.sh <start|finish> <task-number> <slug>}
|
||||||
|
number=${2:?task number, e.g. 07}
|
||||||
|
slug=${3:?slug, e.g. route-cards}
|
||||||
|
|
||||||
|
dir="../af-task-${number}"
|
||||||
|
branch="refactor/task-${number}-${slug}"
|
||||||
|
plan="plans/astro-refactor/task-${number}-${slug}.md"
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
start)
|
||||||
|
[ -f "$plan" ] || echo "warning: no plan at $plan — check the task number" >&2
|
||||||
|
git fetch origin
|
||||||
|
git worktree add "$dir" -b "$branch" origin/main
|
||||||
|
|
||||||
|
# Not optional. `.husky/_` is generated by install and is NOT committed, so
|
||||||
|
# a fresh worktree has hooks configured but absent — every commit would pass
|
||||||
|
# unchecked. --prefer-offline keeps ten parallel spin-ups off the registry.
|
||||||
|
( cd "$dir" && pnpm install --frozen-lockfile && .agents/scripts/verify-hooks.sh )
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "worktree : $dir"
|
||||||
|
echo "branch : $branch"
|
||||||
|
echo "brief : $plan"
|
||||||
|
echo
|
||||||
|
echo "next: cd $dir && read the brief end to end before writing anything"
|
||||||
|
;;
|
||||||
|
finish)
|
||||||
|
# Never remove a worktree with uncommitted work in it.
|
||||||
|
if [ -n "$(git -C "$dir" status --porcelain)" ]; then
|
||||||
|
echo "refusing: $dir has uncommitted changes" >&2
|
||||||
|
git -C "$dir" status --short >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
git worktree remove "$dir"
|
||||||
|
echo "removed $dir — branch $branch kept; delete it after the merge lands"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown action: $action (expected start|finish)" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
name: astro-component
|
||||||
|
description:
|
||||||
|
Build one Astro component for the ai-for-dummies site from the project
|
||||||
|
templates. Use when creating a primitive, block, or island, or when splitting
|
||||||
|
existing markup into a component.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Building an Astro component
|
||||||
|
|
||||||
|
## Decide it should exist
|
||||||
|
|
||||||
|
Three occurrences, or a name a person says out loud. Two is not enough — see
|
||||||
|
[`../../rules/componentization.md`](../../rules/componentization.md).
|
||||||
|
|
||||||
|
Then place it:
|
||||||
|
|
||||||
|
- `primitives/` — no domain knowledge (Eyebrow, Rule, Callout, CodeBlock)
|
||||||
|
- `blocks/` — composed, page-agnostic, takes props (RouteCard, PhasePanel)
|
||||||
|
- `islands/` — interactive, justified, a leaf
|
||||||
|
|
||||||
|
## Start from a template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .agents/templates/components/static-block.astro src/components/blocks/RouteCard.astro
|
||||||
|
# interactive? use island.astro instead
|
||||||
|
```
|
||||||
|
|
||||||
|
Templates exist so ten parallel agents produce one house style. Do not start
|
||||||
|
from a blank file.
|
||||||
|
|
||||||
|
## Write it
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
number: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
const { number, title, summary, href } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<b>{number}</b>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<p>{summary}</p>
|
||||||
|
<a href={href}>Open chapter →</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 22px;
|
||||||
|
background: var(--paper);
|
||||||
|
}
|
||||||
|
b {
|
||||||
|
color: var(--accent);
|
||||||
|
font: var(--font-eyebrow);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules that bite most often here:
|
||||||
|
|
||||||
|
- **No raw hex.** Tokens only. `check-tokens.mjs` will fail you.
|
||||||
|
- **No `client:*`** unless it is genuinely interactive, and say why in the PR.
|
||||||
|
- Keep the `gap:1px` over a coloured parent trick where the original used it —
|
||||||
|
it is the house style, not a bug.
|
||||||
|
- Preserve every ARIA attribute from the markup you are replacing.
|
||||||
|
|
||||||
|
## Prove it
|
||||||
|
|
||||||
|
1. Render it at 560 / 800 / 1100 / 1600 px.
|
||||||
|
2. Tab to it. Focus ring visible (`outline: 3px solid #a7483f`).
|
||||||
|
3. Diff its rendered text against the markup it replaced.
|
||||||
|
4. Run
|
||||||
|
[`../../checklists/before-component.md`](../../checklists/before-component.md).
|
||||||
|
5. `pnpm run verify` — green, with no assertion deleted.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: astro-page
|
||||||
|
description:
|
||||||
|
Migrate one hand-written HTML page of the ai-for-dummies site to an Astro
|
||||||
|
route without changing its URL, content, or JS budget. Use for any page-level
|
||||||
|
migration task.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Migrating a page to Astro
|
||||||
|
|
||||||
|
## Snapshot first, migrate second
|
||||||
|
|
||||||
|
The snapshot is the only objective evidence that no content was lost.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run serve & # vanilla site on :4173
|
||||||
|
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ \
|
||||||
|
> .agents/snapshots/models.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Screenshot the same route at 560 / 800 / 1100 / 1600 px as well.
|
||||||
|
|
||||||
|
## Migrate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .agents/templates/pages/chapter.astro src/pages/models.astro
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in order:
|
||||||
|
|
||||||
|
1. Move markup into the layout + components. Reuse existing components before
|
||||||
|
creating new ones.
|
||||||
|
2. Move copy into `src/content/`. Both `en` and `pt`, copied literally — never
|
||||||
|
retyped.
|
||||||
|
3. Move page CSS into component `<style>` blocks. Do **not** port
|
||||||
|
`responsive.css` wholesale; take what this page needs and prove the rest
|
||||||
|
dead.
|
||||||
|
4. Keep every `data-*` hook. `verify.mjs` asserts many of them by name
|
||||||
|
(`data-phase`, `data-tree`, `data-route`, `data-model-provider`, …).
|
||||||
|
5. Keep every ARIA attribute and the `<title>` / `<meta name="description">`.
|
||||||
|
|
||||||
|
## The URL must not change
|
||||||
|
|
||||||
|
Served from `/ai-for-dummies/`, so `base` is set in `astro.config.mjs`. Never
|
||||||
|
hand-write an internal absolute path; use `import.meta.env.BASE_URL`.
|
||||||
|
|
||||||
|
Trailing slashes matter. `/models/` must not become `/models`.
|
||||||
|
|
||||||
|
If the page honours query params (the review desk uses `?author=`, `?skill=`,
|
||||||
|
`?view=`, `?file=`, `?compare=`, `?render=`), they must still work — they are
|
||||||
|
documented in the page footer and shared externally.
|
||||||
|
|
||||||
|
## Prove it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run build
|
||||||
|
node .agents/scripts/snapshot-route.mjs dist/models/index.html > /tmp/after.txt
|
||||||
|
diff .agents/snapshots/models.txt /tmp/after.txt # empty, or justify every line
|
||||||
|
node scripts/audit-ui.mjs
|
||||||
|
pnpm run verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Then screenshots at the same four widths, and
|
||||||
|
[`../../checklists/before-page.md`](../../checklists/before-page.md).
|
||||||
|
|
||||||
|
## JS budget
|
||||||
|
|
||||||
|
A page that shipped zero JS must still ship zero. Check the build output. If
|
||||||
|
your migration added a `client:load` to a static page, you did it wrong.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
name: content-migration
|
||||||
|
description:
|
||||||
|
Move bilingual copy out of app.js and catalog.js into typed Astro content
|
||||||
|
collections without losing or altering a single string. Use for any task that
|
||||||
|
relocates user-visible text.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Content migration
|
||||||
|
|
||||||
|
## What you are moving
|
||||||
|
|
||||||
|
- `app.js` — ~50 `{ en, pt }` keys across `phases`, `handsOnPrompts`,
|
||||||
|
`modelGuide`, `skillSources`, `skillInstallPrompts`
|
||||||
|
- `skills-review/catalog.js` + `submitted-catalog.js` — 24 entries with `id`,
|
||||||
|
`author`, `title`, `status`, `focus`, `wins[]`, `improve[]`, `extras`,
|
||||||
|
`improved` (full markdown)
|
||||||
|
|
||||||
|
These are hand-written translations with deliberate tone. **Copy them. Never
|
||||||
|
retype them.** Retyping introduces drift you will not notice.
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/content/config.ts
|
||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const localized = z.object({ en: z.string(), pt: z.string() });
|
||||||
|
|
||||||
|
const guide = defineCollection({
|
||||||
|
type: 'data',
|
||||||
|
schema: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
model: localized,
|
||||||
|
title: localized,
|
||||||
|
copy: localized,
|
||||||
|
code: localized,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Both locales **required**. A missing `pt` must be a build error — silent English
|
||||||
|
fallback is how a bilingual site quietly becomes monolingual.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
1. Extract the literals mechanically (script, not by hand).
|
||||||
|
2. Write them into the collection.
|
||||||
|
3. Diff extracted-before against extracted-after. Must be empty.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json
|
||||||
|
node .agents/scripts/extract-strings.mjs src/content/ > /tmp/after.json
|
||||||
|
diff /tmp/before.json /tmp/after.json
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Only then delete the source literals.
|
||||||
|
|
||||||
|
## The generator trap
|
||||||
|
|
||||||
|
`skill-reviews/improved/**/SKILL.md` is **generated** from `catalog.js`
|
||||||
|
`improved` fields by `scripts/build-skill-review.mjs`, and the output is
|
||||||
|
committed to the repo. If you move `catalog.js`, that generator breaks silently
|
||||||
|
— it will still run, just against nothing.
|
||||||
|
|
||||||
|
Either keep the generator pointed at the new collection, or replace it and
|
||||||
|
update `package.json`, `README.md`, `docs/operations-guide.md`, and the review
|
||||||
|
desk footer, all of which reference it.
|
||||||
|
|
||||||
|
## Markdown
|
||||||
|
|
||||||
|
The `improved` fields are full Markdown rendered client-side today. Move them to
|
||||||
|
real `.md` files in the collection and let Astro render at build time. That
|
||||||
|
deletes the hand-rolled renderer and improves fidelity — but re-check the review
|
||||||
|
desk's diff view, which compares original and improved source text and needs the
|
||||||
|
raw string, not just rendered HTML.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: design-tokens
|
||||||
|
description:
|
||||||
|
Extract, name, and enforce the design token layer for the ai-for-dummies site.
|
||||||
|
Use when touching any colour, font size, spacing value, or breakpoint, when
|
||||||
|
consolidating the three drifted palettes, or when a check-tokens failure needs
|
||||||
|
resolving.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Design tokens
|
||||||
|
|
||||||
|
## Before anything
|
||||||
|
|
||||||
|
Read [`../../context/design-system.md`](../../context/design-system.md). This
|
||||||
|
site has **three drifting palettes** and a **broken `@font-face`**. Both are
|
||||||
|
traps. If you have not read that file, you will "preserve the styles" by copying
|
||||||
|
a bug.
|
||||||
|
|
||||||
|
## Extracting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 - <<'PY'
|
||||||
|
import re
|
||||||
|
files=['styles.css','chapters.css','landing.css','rules/styles.css','skills/styles.css',
|
||||||
|
'skills-review/styles.css','hands-on/starter/styles.css','hands-on/rules/styles.css']
|
||||||
|
seen={}
|
||||||
|
for f in files:
|
||||||
|
for m in re.finditer(r'--([a-z-]+):\s*([^;}]+)', open(f).read()):
|
||||||
|
seen.setdefault(m.group(1),{}).setdefault(m.group(2).strip(),[]).append(f)
|
||||||
|
for k,v in sorted(seen.items()):
|
||||||
|
print(f"--{k}:")
|
||||||
|
for val,fs in v.items(): print(f" {val:12} <- {', '.join(fs)}")
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-run this after any consolidation. Every token should print exactly one value.
|
||||||
|
|
||||||
|
## Consolidating a drifted token
|
||||||
|
|
||||||
|
1. List every value and where it is used (above).
|
||||||
|
2. Compute the perceptual delta. Sub-perceptual (a few units per channel) →
|
||||||
|
canonicalize freely. Visible (`--blue`: `#527f9f` vs `#215675`) → screenshot
|
||||||
|
both, get a human decision, record it in the task file.
|
||||||
|
3. Pick the canonical value. Prefer the one used on the most-visited surface.
|
||||||
|
4. Replace, then screenshot every affected page at 560/800/1100/1600 px.
|
||||||
|
5. Attach before/after images to the task report. "It looked fine to me" is not
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
Semantic, matching the existing vocabulary — `--ink`, `--paper`, `--muted`,
|
||||||
|
`--line`, `--accent`, `--gold`, `--blue`, `--deep`. Never numeric scales. New
|
||||||
|
surfaces extend semantically: `--surface-lab`, `--ink-inverse`.
|
||||||
|
|
||||||
|
## Type scale and breakpoints
|
||||||
|
|
||||||
|
Collapse the 14 ad-hoc `clamp()` triples to named steps and the 16 breakpoints
|
||||||
|
to five, per [`../../rules/theming.md`](../../rules/theming.md). When collapsing
|
||||||
|
a breakpoint, **screenshot at the old value** — that is where the regression is.
|
||||||
|
|
||||||
|
## Enforcing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .agents/scripts/check-tokens.mjs # fails on raw hex outside tokens.css
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire it into `pnpm run verify`. A rule nobody checks is a suggestion.
|
||||||
|
|
||||||
|
## The font decision
|
||||||
|
|
||||||
|
Do not self-host Manrope/DM Mono as part of a refactor task. The intended fonts
|
||||||
|
have never rendered, so adopting them is a visual redesign. Default to matching
|
||||||
|
what renders today (Arial / generic monospace) and delete the dead `@font-face`.
|
||||||
|
If a human wants the real fonts, that is its own task with its own screenshots.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: motion
|
||||||
|
description:
|
||||||
|
Add or review animation on the ai-for-dummies site — transitions, state
|
||||||
|
changes, view transitions. Use when any element moves, fades, or transforms,
|
||||||
|
or when auditing existing motion for performance and reduced-motion support.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Motion
|
||||||
|
|
||||||
|
This is an editorial-print design. Motion is punctuation. Read
|
||||||
|
[`../../rules/animation.md`](../../rules/animation.md) — it is binding; this is
|
||||||
|
the procedure.
|
||||||
|
|
||||||
|
## Decide it should move
|
||||||
|
|
||||||
|
Answer out loud: **what does this motion tell the user that the static state
|
||||||
|
does not?** Valid answers: something changed, attention needs directing to what
|
||||||
|
changed, a layout shift needs smoothing. "It feels more polished" is not an
|
||||||
|
answer — on this design it reads as generic.
|
||||||
|
|
||||||
|
If there is no answer, ship it static. That is a legitimate, common outcome.
|
||||||
|
|
||||||
|
## Build it
|
||||||
|
|
||||||
|
```css
|
||||||
|
.panel {
|
||||||
|
transition:
|
||||||
|
opacity 180ms cubic-bezier(0.2, 0, 0, 1),
|
||||||
|
transform 180ms cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
}
|
||||||
|
.panel[data-state='entering'] {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`transform` and `opacity` only.** Animating `width`/`height`/`top`/`left`
|
||||||
|
forces layout every frame and shows up as a failed INP (budget: 200ms).
|
||||||
|
- 150–250ms for UI feedback; up to 400ms for a page transition.
|
||||||
|
- One thing moves at a time. No staggered card cascades.
|
||||||
|
- `will-change` only immediately before animating, removed after.
|
||||||
|
- No animation library. This site's thesis is having no runtime dependencies.
|
||||||
|
|
||||||
|
## Reduced motion is not optional
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **test it**: DevTools → Rendering → Emulate
|
||||||
|
`prefers-reduced-motion: reduce`. The end state must still be correct and the UI
|
||||||
|
still usable. Reduced, not broken.
|
||||||
|
|
||||||
|
## Page transitions
|
||||||
|
|
||||||
|
Astro's `<ClientRouter />` is the only sanctioned motion dependency. Before
|
||||||
|
enabling it, verify:
|
||||||
|
|
||||||
|
- JS disabled → full navigation still works
|
||||||
|
- browser back/forward still works
|
||||||
|
- the review desk's query-param deep links survive
|
||||||
|
- `prefers-reduced-motion` is honoured
|
||||||
|
|
||||||
|
## Audit an existing animation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "transition\|animation\|@keyframes\|transform" src/ --include="*.astro" --include="*.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
For each hit ask: does it animate a layout property? does it respect reduced
|
||||||
|
motion? is there a stated purpose? Three questions, three possible fixes.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: verify-contract
|
||||||
|
description:
|
||||||
|
Evolve scripts/verify.mjs across the Astro migration without losing coverage.
|
||||||
|
Use whenever a verify assertion fails because of a refactor, or when adding
|
||||||
|
checks for new architecture.
|
||||||
|
---
|
||||||
|
|
||||||
|
# The verification contract
|
||||||
|
|
||||||
|
Read [`../../context/verification.md`](../../context/verification.md) first.
|
||||||
|
|
||||||
|
`verify.mjs` has 42 assertions pinning real content and interactions. They are
|
||||||
|
the only thing preventing silent content loss during this migration, and they
|
||||||
|
will all break, because they assert against files that stop existing.
|
||||||
|
|
||||||
|
## The rule
|
||||||
|
|
||||||
|
**A failing assertion is a question, not a bug to delete.**
|
||||||
|
|
||||||
|
```
|
||||||
|
assertion fails
|
||||||
|
↓
|
||||||
|
does the user-visible fact it pins still exist?
|
||||||
|
├── yes → re-point the assertion at the new location
|
||||||
|
└── no → you deleted content. Put it back, or get sign-off.
|
||||||
|
```
|
||||||
|
|
||||||
|
`grep -c 'throw new Error' scripts/verify.mjs` must not decrease. If it must,
|
||||||
|
the `verification-engineer` writes a one-line reason per removal in the task
|
||||||
|
report. Nobody else may reduce coverage.
|
||||||
|
|
||||||
|
## Translating assertions
|
||||||
|
|
||||||
|
| Kind | Old | New |
|
||||||
|
| --------------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
|
||||||
|
| Content presence | `html.includes('data-phase="plan"')` | same token, read from `dist/full-guide/index.html` |
|
||||||
|
| Implementation detail | `js.includes('renderTree')` | assert the rendered output has the tree UI, not that a function is named that |
|
||||||
|
| Asset version | `'app.js?v=20260904-vote-widget'` | assert the built HTML references a hashed asset |
|
||||||
|
|
||||||
|
Implementation-detail assertions are the dangerous ones: they _look_ deletable.
|
||||||
|
They are pinning a feature. Replace with an output-level assertion of the same
|
||||||
|
feature; never drop.
|
||||||
|
|
||||||
|
## Add the stronger check
|
||||||
|
|
||||||
|
Token matching cannot catch a dropped paragraph. Add rendered-text snapshots:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# before migrating
|
||||||
|
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ > .agents/snapshots/models.txt
|
||||||
|
# after
|
||||||
|
node .agents/scripts/snapshot-route.mjs dist/models/index.html | diff .agents/snapshots/models.txt -
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit the snapshots. They are the migration's regression net.
|
||||||
|
|
||||||
|
## Extend audit-ui.mjs
|
||||||
|
|
||||||
|
It rejects external `<script>`/`<link>` but **misses external URLs inside CSS**
|
||||||
|
— which is exactly how the broken Google Fonts `@font-face` in `styles.css:1`
|
||||||
|
got into a "dependency-free" site. Add:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (/@import|src:\s*url\(['"]?https?:|url\(['"]?https?:/i.test(css))
|
||||||
|
throw new Error(`${file} has an external CSS dependency`);
|
||||||
|
```
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
name: visual-regression
|
||||||
|
description:
|
||||||
|
Prove a refactor did not change how the site looks. Use before and after any
|
||||||
|
page migration, token consolidation, or breakpoint change on ai-for-dummies.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Visual regression
|
||||||
|
|
||||||
|
"Maintain the same styles" is a testable claim. Test it.
|
||||||
|
|
||||||
|
## Capture
|
||||||
|
|
||||||
|
The repo already has a Playwright pattern (`scripts/inspect.py`). Extend it
|
||||||
|
rather than inventing one.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
ROUTES = ['/', '/full-guide/', '/summary/', '/models/', '/agents/',
|
||||||
|
'/skills/', '/rules/', '/skills-review/',
|
||||||
|
'/hands-on/starter/', '/hands-on/rules/']
|
||||||
|
WIDTHS = [560, 800, 1100, 1600]
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
for route in ROUTES:
|
||||||
|
for w in WIDTHS:
|
||||||
|
page = browser.new_page(viewport={'width': w, 'height': 900})
|
||||||
|
page.goto(f'{BASE}{route}', wait_until='networkidle')
|
||||||
|
page.screenshot(path=f'{OUT}/{route.strip("/").replace("/","_") or "index"}-{w}.png',
|
||||||
|
full_page=True)
|
||||||
|
page.close()
|
||||||
|
browser.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
Run once against the vanilla site (`pnpm run serve`), once against
|
||||||
|
`pnpm run preview`. Keep both sets.
|
||||||
|
|
||||||
|
## Compare
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for f in before/*.png; do
|
||||||
|
compare -metric AE "$f" "after/$(basename $f)" null: 2>&1 # ImageMagick
|
||||||
|
echo " <- $(basename $f)"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Pixel-exact is not the bar — antialiasing differs. Judge by eye where the metric
|
||||||
|
is non-trivial, and attach the pair to the task report.
|
||||||
|
|
||||||
|
## The three widths that catch the most
|
||||||
|
|
||||||
|
- **560px** — where the 16 ad-hoc breakpoints collapse to `--bp-sm`. Highest
|
||||||
|
risk in the whole migration.
|
||||||
|
- **800px** — the most common existing breakpoint; layout flips here.
|
||||||
|
- **1600px** — `min-width` rules that only fire on large screens are the ones
|
||||||
|
nobody notices are broken.
|
||||||
|
|
||||||
|
Also screenshot at the **old** breakpoint values you removed (520, 530, 600,
|
||||||
|
620, 720, 850, 880, 900), not just the new ones. Regressions hide exactly there.
|
||||||
|
|
||||||
|
## What a real difference looks like
|
||||||
|
|
||||||
|
Expect and accept: sub-pixel text shifts, antialiasing.
|
||||||
|
|
||||||
|
Investigate: anything that moves by more than ~2px, any colour change (that is a
|
||||||
|
token bug), any element that appears or disappears (that is content loss — stop
|
||||||
|
and check the snapshot diff).
|
||||||
|
|
||||||
|
## Reduced motion
|
||||||
|
|
||||||
|
Capture one pass with `prefers_reduced_motion='reduce'`. Animations must land in
|
||||||
|
their correct end state, not vanish.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
AI For Dummies — Agents and trees
|
||||||
|
← ROUTE MAP
|
||||||
|
02 / AGENTS & TREES
|
||||||
|
field guide ↗
|
||||||
|
Subagent workflow
|
||||||
|
One branch
|
||||||
|
per
|
||||||
|
hand.
|
||||||
|
Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent.
|
||||||
|
The tree
|
||||||
|
Split at
|
||||||
|
the
|
||||||
|
seam.
|
||||||
|
MAIN / ORCHESTRATOR
|
||||||
|
├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence
|
||||||
|
FRAME
|
||||||
|
Orchestrator
|
||||||
|
Owns scope, task graph, boundaries, and integration.
|
||||||
|
HAND OFF
|
||||||
|
Worker
|
||||||
|
Owns one coherent slice and one worktree.
|
||||||
|
PROVE
|
||||||
|
Verifier
|
||||||
|
Re-runs gates and reports remaining gaps.
|
||||||
|
Handoff
|
||||||
|
Context that
|
||||||
|
can
|
||||||
|
travel.
|
||||||
|
01
|
||||||
|
Brief
|
||||||
|
Goal, owned files, dependencies, non-goals, acceptance.
|
||||||
|
02
|
||||||
|
Isolation
|
||||||
|
One branch and worktree per independent change.
|
||||||
|
03
|
||||||
|
Evidence
|
||||||
|
Commands, result, changed files, screenshots, gaps.
|
||||||
|
Previous: models →
|
||||||
|
Rules case study →
|
||||||
|
Try the rules lab →
|
||||||
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |