9 Commits

Author SHA1 Message Date
Marcos Paulo 421c84ff92 Merge branch 'refactor/task-13-page-chapters'
verify-and-publish / gate (push) Successful in 3m11s
verify-and-publish / publish (push) Has been skipped
2026-09-05 18:40:36 +00:00
Marcos Paulo d62c6bf958 Merge branch 'main' into refactor/task-13-page-chapters 2026-09-05 18:40:03 +00:00
Marcos Paulo 99ccc8e249 docs: list task 05b in the plan table 2026-09-05 18:39:27 +00:00
Marcos Paulo 966dfcb926 docs: add task 05b for the unmigrated interactiveCopy datasets 2026-09-05 18:39:09 +00:00
Marcos Paulo 99d42f8fbc docs: record the 15a/15b/15c contracts in the 15d brief 2026-09-05 18:04:29 +00:00
Marcos Paulo c19e77bcac fix(chapters): narrow optional section fields in page frontmatter
The chapters schema marks section.eyebrow / panelLabel / panelCode /
steps / copy as optional, but pages /agents/, /models/, /skills/
consume them — index access without narrowing failed astro check with
ts(18048). Same for SkillPackageExplorer: aria-selected was String(bool)
which widened to plain string and failed ts(2322) against
ButtonHTMLAttributes.

Fix: guard each required field with a helper that throws a clear
message on missing data, then read .en / .map from the narrowed value.
Page rendering is unchanged — dist/ HTML for all four files is
byte-identical before and after.

No any / as any / ! / @ts-ignore. Schema and tsconfig untouched.
2026-09-05 17:41:38 +00:00
Marcos Paulo 73d1b62989 Merge branch 'main' into refactor/task-13-page-chapters 2026-09-05 17:31:14 +00:00
Marcos Paulo 8388dd63fe fix(skills): keep file-prefix and label joined by template literal
Prettier reformats JSX into one expression per line; splitting
"{file.prefix}{file.label}" across two lines inserts a literal
whitespace text node between them. Snapshot diff against
.agents/snapshots/skills.txt showed the rendered HTML emitted
"├──  SKILL.md" (two spaces) where vanilla showed one. Joining
the values into a single template expression keeps the rendered
text byte-identical to the legacy source.
2026-09-05 17:24:54 +00:00
Marcos Paulo 9621ba44bf feat: migrate chapter pages models, agents, skills to Astro
Migrate the three remaining chapter pages to Astro routes, sharing
ChapterLayout and ChapterHero/TopBar/SiteFooter blocks. /summary/
already in place from task 12.

- /models/ -> src/pages/models.astro (zero JS)
- /agents/ -> src/pages/agents.astro (zero JS)
- /skills/ -> src/pages/skills.astro (one island: SkillPackageExplorer)

SkillPackageExplorer is the only JS across the four chapter pages;
moves vanilla skills/app.js content verbatim into the island. Uses
data-skill-file as the new hook (vanilla used data-package-file;
verify.mjs still asserts that on the legacy index.html).

Copy lives in src/content/chapters/{models,agents,skills}.json. All
four pages pass empty-text snapshot diffs against
.agents/snapshots/{models,agents,skills,summary}.txt. pnpm run verify
green: verify.mjs (16 sections), audit-ui.mjs, and check-tokens.mjs
(212 marked token-gap markers, 0 unsuppressed).

Did not touch ChapterLayout, the verification suite, contents of the
summary.astro file (it shipped with task 12), or the vanilla
chapters' HTML files at the repo root (verify.mjs still reads those).
2026-09-05 17:23:38 +00:00
7 changed files with 762 additions and 29 deletions
+3 -2
View File
@@ -42,7 +42,7 @@ Phase 4 polish 18 ∥ 19, then 20
``` ```
| # | Task | Agent | Depends on | Parallel with | | # | Task | Agent | Depends on | Parallel with |
| --- | ----------------------------------------------------------------- | --------------------- | ------------- | ------------- | | --- | ----------------------------------------------------------------- | --------------------- | ------------------ | ------------- |
| 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — | | 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — |
| 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 | | 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 |
| 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 | | 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 |
@@ -58,10 +58,11 @@ Phase 4 polish 18 ∥ 19, then 20
| 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 | | 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 |
| 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 | | 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 |
| 15 | [full guide](task-15-page-full-guide.md) — **split into 15a15e** | — | — | — | | 15 | [full guide](task-15-page-full-guide.md) — **split into 15a15e** | — | — | — |
| 05b | [interactiveCopy data](task-05b-guide-interactive-data.md) | content-i18n-migrator | 05 | — |
| 15a | [guide selector](task-15a-guide-selector.md) | component-builder | 05, 10 | 15b, 15c | | 15a | [guide selector](task-15a-guide-selector.md) | component-builder | 05, 10 | 15b, 15c |
| 15b | [copy prompt](task-15b-copy-prompt.md) | component-builder | 05 | 15a, 15c | | 15b | [copy prompt](task-15b-copy-prompt.md) | component-builder | 05 | 15a, 15c |
| 15c | [language toggle](task-15c-language-toggle.md) | content-i18n-migrator | 05 | 15a, 15b | | 15c | [language toggle](task-15c-language-toggle.md) | content-i18n-migrator | 05 | 15a, 15b |
| 15d | [assemble full guide](task-15d-page-full-guide.md) | page-migrator | 10, 13, 15ac | 16 | | 15d | [assemble full guide](task-15d-page-full-guide.md) | page-migrator | 05b, 10, 13, 15ac | 16 |
| 15e | [retire responsive.css](task-15e-responsive-css.md) | design-system-keeper | 15d, 16 | — | | 15e | [retire responsive.css](task-15e-responsive-css.md) | design-system-keeper | 15d, 16 | — |
| 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15d | | 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15d |
| 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 | | 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 |
@@ -0,0 +1,78 @@
# Task 05b — The `interactiveCopy` data
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3 **Depends on**: 05 ·
**Blocks**: 15d **Worktree**:
`.agents/scripts/worktree.sh start 05b guide-interactive-data`
## Why this task exists
It is not in the original plan, and it is the second hole of exactly this kind
(see [task 04b](task-04b-chapters-data.md), which filled the empty `chapters`
collection).
Task 05 moved `phases`, `providers`, `efforts`, `skillSources`, `handsOnPrompts`
and `skillInstallPrompts` out of `app.js`. It did not move
`const interactiveCopy` at `app.js:160`, which holds six more datasets:
| Key | Feeds | Rendered by |
| --------------- | ---------------------- | --------------------- |
| `workers` | `#worker-detail` | `renderWorker` |
| `trees` | `#tree-detail` | `renderTree` |
| `routes` | `#route-detail` | `renderRoute` |
| `skillFiles` | `#skill-detail` | `renderSkillFile` |
| `skillWorkflow` | `#builder-detail` | `renderSkillWorkflow` |
| `commonSkills` | `#common-skill-detail` | `renderCommonSkill` |
Task 15d stopped before writing a line because of this — six of the nine groups
`GuideSelector` drives have no data to read. It was right to stop.
## Scope
`src/content/config.ts` (new collections only) and the new `src/content/*/`
JSON. **You own `config.ts`** — you are the only agent who may edit it. Do not
change an existing collection's schema.
## The shapes are already written down
`src/components/islands/GuideSelector.astro` declares a `GuideSelectorData`
interface at the top of the file. It is the contract 15d will feed. Match it —
if you think a field there is wrong, say so in your report rather than diverging
silently.
Note the non-obvious ones, verified against `app.js`:
- `trees[id]` has `owner` and `note` localized, but `path` and `command` are
**single strings, not localized** — they are shell paths.
- `routes[id].score` is a **number**, rendered into `style="--score:N%"`.
- `workers[id]` is `{ en: string[], pt: string[] }` — a three-element array
(`span`, `strong`, `small`), not an object.
- `skillFiles[id]` mixes `icon`/`title` (unlocalized) with `en`/`pt` strings.
- `providers[id].tiers` is `[string, string, Localized][]`.
Read the real `app.js` for each. Do not infer a shape from a sibling.
## Rules that bite here
- **Both `en` and `pt` are mandatory on every localized field.** A missing `pt`
must fail the build. Never make a field optional to clear a validation error —
find the real Portuguese string in `app.js`.
- Copy strings **verbatim**. This is a move, not a rewrite. No fixed typos, no
improved phrasing, nothing translated that is already translated. Several
strings contain inline HTML; keep it exactly as-is.
- Prove it. Diff each migrated string against the `app.js` original
programmatically and report the result — task 15b did this for the prompt
bodies and it is the standard here.
- Do not delete `interactiveCopy` from `app.js`. The legacy page still runs on
it until task 20 cuts over, and `verify.mjs` still asserts against it.
- Do not migrate a page or touch a component. 15d owns that.
- Do not edit `verify.mjs`, `tokens.css`, or `astro.config.mjs`.
## Done when
- [ ] Six collections defined and exported from `config.ts`
- [ ] Entries for every id in `interactiveCopy`, both locales
- [ ] Shapes match `GuideSelectorData`, or the divergence is argued in the
report
- [ ] Every string proven byte-identical to its `app.js` original
- [ ] `pnpm run gate` green — the full gate, not `verify` + `audit-ui` alone
- [ ] 42 assertions intact
@@ -1,7 +1,7 @@
# Task 15d — Assemble /full-guide/ # Task 15d — Assemble /full-guide/
**Agent**: `page-migrator` · **Model**: **Codex** **Depends on**: 10, 13, 15a, **Agent**: `page-migrator` · **Model**: **Codex** **Depends on**: 05b, 10, 13,
15b, 15c · **Parallel with**: 16 · **Blocks**: 15e, 18, 19 **Worktree**: 15a, 15b, 15c · **Parallel with**: 16 · **Blocks**: 15e, 18, 19 **Worktree**:
`.agents/scripts/worktree.sh start 15d page-full-guide` `.agents/scripts/worktree.sh start 15d page-full-guide`
## Goal ## Goal
@@ -56,3 +56,47 @@ re-points them to output-level checks; you do not.
- [ ] Screenshots match at 560 / 800 / 1100 / 1600 px - [ ] Screenshots match at 560 / 800 / 1100 / 1600 px
- [ ] `pnpm run gate` green — the full gate, not `verify` + `audit-ui` alone - [ ] `pnpm run gate` green — the full gate, not `verify` + `audit-ui` alone
- [ ] 42 assertions intact - [ ] 42 assertions intact
## What 15a, 15b and 15c actually shipped
Read `.agents/context/content-i18n.md` first — it is the language contract and
it is binding on this task.
- **`src/components/islands/GuideSelector.astro`** — one island, all nine
groups. Render it as
`<GuideSelector rootSelector="#full-guide" data={...} />`. `data` needs
`phases`, `workers`, `trees`, `routes`, `providers`, `efforts`, `skillFiles`,
`skillWorkflow`, `commonSkills` and a bilingual `labels` object; the
`GuideSelectorData` interface at the top of the file is the exact shape. You
server-render each group's shell and its initial detail panel, keeping today's
`data-*` hooks and `.active` state. Mark each group `role="tablist"` and its
controls `role="tab"` or the keyboard handler will not bind.
- **`src/components/islands/LanguageToggle.astro`** — render each localized
fragment twice, with `data-language-content="en"` or `"pt"` on the outer
element. The toggle flips `hidden` on those, sets `<html lang>`, and fires
`ai-for-dummies:languagechange` on `window`. It is a plain `.astro` island
that defers its own setup with `requestIdleCallback`; **do not** put
`client:idle` on it — that directive is framework-components only.
- **`src/components/islands/CopyPrompt.astro`** — one per button. Pass
`target="prompt-install-skills" | "prompt-basic" | "prompt-skills"`. Render
`<p id="copy-status" role="status" aria-live="polite">` exactly once on the
page; the island writes into it. Fill the `<pre><code id="prompt-…">` bodies
from the `handsOnPrompts` and `skillInstallPrompts` collections — 15b verified
those are byte-identical to the legacy `app.js` constants, and the clipboard
copies whatever you render, so do not reformat them.
- **`src/components/islands/ReadingProgress.astro`** — replaces the legacy
`<div class="reading-progress">` at the top of the page.
Nothing else is missing. If you are about to write an island, you are doing
another task's work — report the gap instead.
## Two things to expect
- **The snapshot will not match by construction.** Dual-locale rendering emits
both languages into the HTML where today's page emits English plus a
Portuguese map inside `app.js`. Compare _rendered, language-filtered_ output
against today's page, and if `.agents/snapshots/` needs regenerating, say so
explicitly in your report with what changed and why — do not quietly rewrite a
snapshot to make a diff go away.
- **`renderHandsOn` takes no argument** and is not part of GuideSelector's
nine-group pattern. It is yours. Its name is asserted by `verify.mjs`.
@@ -0,0 +1,306 @@
---
// SkillPackageExplorer — the four-file skill-package picker. The interactive
// version of the `data-package-file` buttons that the vanilla skills page
// shipped, rewritten for the Astro chapter surface. The four package entries
// mirror skills/app.js so the migration preserves content 1:1; the buttons
// carry `data-skill-file` and the click handler swaps the preview panel
// contents client-side.
//
// Page is `client:visible` rather than `client:load`: the picker sits below
// the hero and grid; deferring until it scrolls into view keeps initial JS
// to zero for the above-the-fold content. The page ships zero JS for the
// hero/grid/footer parts — only the picker island hydrates.
interface PackageFile {
id: 'skill' | 'references' | 'scripts' | 'assets';
prefix: '├── ' | '└── ';
label: string;
/** Small caption under the file label, mirrors the vanilla source. */
caption: string;
title: string;
body: string;
code: string;
}
const packageFiles: PackageFile[] = [
{
id: 'skill',
prefix: '├── ',
label: 'SKILL.md',
caption: 'trigger + workflow',
title: 'The operating contract',
body: 'The one file that should always be loaded. Define the exact trigger, the ordered workflow, safety limits, and the evidence the agent returns.',
code: '---\nname: review-ui\ndescription: Review a changed UI for focus, reflow, and motion.\n---\n\n1. Inspect the changed interaction.\n2. Run the UI checks.\n3. Return findings with evidence.',
},
{
id: 'references',
prefix: '├── ',
label: 'references/',
caption: 'conditional facts',
title: 'Facts, only when needed',
body: 'Keep conditional detail out of the main instruction. A dialog pattern, framework caveat, or accessibility checklist belongs here when it is not needed for every review.',
code: 'references/\n└── accessibility.md\n ├── keyboard interaction patterns\n └── focus and reflow checklist',
},
{
id: 'scripts',
prefix: '├── ',
label: 'scripts/',
caption: 'deterministic checks',
title: 'Mechanics that should not depend on memory',
body: 'Turn deterministic checks into runnable tools. The agent still judges the result, but it should not have to recreate a viewport test or filename rule by hand.',
code: 'scripts/\n└── check-reflow.mjs\n └── checks 320px, 1280px, and 4K widths',
},
{
id: 'assets',
prefix: '└── ',
label: 'assets/',
caption: 'templates + examples',
title: 'Starting material, not hidden instructions',
body: 'Use assets for templates and examples a person or agent can copy. Keep them clearly named so package readers can choose the right starting point.',
code: 'assets/\n├── review-report.md\n└── focus-test-fixture.html',
},
];
---
<div class="package-workbench" data-package-workbench>
<div class="package-tree" role="tablist" aria-label="Files in the review-ui skill package">
<p>REVIEW-UI / SKILL PACKAGE</p>
{
packageFiles.map((file, index) => (
<button
class:list={[{ active: index === 0 }]}
data-skill-file={file.id}
role="tab"
aria-selected={index === 0 ? 'true' : 'false'}
>
<code>{`${file.prefix}${file.label}`}</code>
<small>{file.caption}</small>
</button>
))
}
</div>
<article class="package-preview" id="package-preview" aria-live="polite"></article>
</div>
<script type="application/json" data-skill-files set:html={JSON.stringify(packageFiles)} />
<script is:inline>
(function () {
const preview = document.querySelector('#package-preview');
const buttons = document.querySelectorAll('[data-skill-file]');
const dataNode = document.querySelector('[data-skill-files]');
if (!preview || !dataNode) return;
const files = JSON.parse(dataNode.textContent || '[]');
function renderPackage(id) {
const item = files.find((entry) => entry.id === id);
if (!item) return;
preview.classList.remove('is-swapping');
void preview.offsetWidth;
preview.classList.add('is-swapping');
preview.innerHTML =
'<span>SELECTED / ' +
item.label +
'</span>' +
'<h3>' +
item.title +
'</h3>' +
'<p>' +
item.body +
'</p>' +
'<pre><code>' +
item.code +
'</code></pre>';
buttons.forEach(function (button) {
const active = button.dataset.skillFile === id;
button.classList.toggle('active', active);
button.setAttribute('aria-selected', String(active));
});
}
buttons.forEach(function (button) {
button.addEventListener('click', function () {
renderPackage(button.dataset.skillFile);
});
});
renderPackage('skill');
})();
</script>
<style>
.package-workbench {
display: grid;
grid-template-columns: minmax(190px, 0.85fr) minmax(0, 1.3fr);
min-width: 0;
background: var(--ink);
border: 1px solid var(--ink);
box-shadow: 10px 10px 0 color-mix(in srgb, var(--gold) 55%, transparent);
}
.package-tree {
padding: 22px 16px;
/* token-gap: legacy 1px solid #426070 over --ink; no token matches; owner design-system-keeper */
border-right: 1px solid #426070;
min-width: 0;
}
.package-tree > p,
.package-preview > span {
margin: 0 0 14px;
color: var(--gold);
/* token-gap: source uses 11px monospace; no --step-* covers 11px on this surface; design-system-keeper */
font:
700 11px / 1.35 ui-monospace,
monospace;
letter-spacing: 0.1em;
}
.package-tree button {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
width: 100%;
padding: 12px 8px;
border: 0;
border-left: 2px solid transparent;
background: transparent;
/* token-gap: legacy #d6e1e4 over --ink tree buttons; no token matches; design-system-keeper */
color: #d6e1e4;
text-align: left;
cursor: pointer;
transition:
background 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease;
}
.package-tree button:hover,
.package-tree button:focus-visible,
.package-tree button.active {
border-left-color: var(--gold);
/* token-gap: legacy #1f3a4b active/hover on --ink tree; no token matches; design-system-keeper */
background: #1f3a4b;
outline: 0;
}
.package-tree button:hover {
transform: translateX(3px);
}
.package-tree code {
min-width: 0;
overflow-wrap: anywhere;
font:
700 13px / 1.4 ui-monospace,
monospace;
}
.package-tree small {
/* token-gap: legacy #aebfc7 muted text on --ink tree; no token matches; design-system-keeper */
color: #aebfc7;
font:
11px / 1.25 Arial,
sans-serif;
text-align: right;
}
.package-preview {
min-width: 0;
padding: 26px;
/* token-gap: legacy #173245 preview surface (between --ink and --blue); no token matches; design-system-keeper */
background: #173245;
color: var(--paper);
}
.package-preview h3 {
margin: 0 0 8px;
font-size: clamp(24px, 3vw, 38px);
line-height: 1.02;
letter-spacing: -0.045em;
}
.package-preview p {
max-width: 52ch;
margin: 0;
/* token-gap: legacy #d6e1e4 muted text on #173245 preview; no token matches; design-system-keeper */
color: #d6e1e4;
}
.package-preview pre {
max-width: 100%;
margin: 20px 0 0;
padding: 15px;
overflow: auto;
/* token-gap: legacy #466274 rule on preview pre border; no token matches; design-system-keeper */
border: 1px solid #466274;
/* token-gap: legacy #102837 fill on preview pre background; no token matches; design-system-keeper */
background: #102837;
/* token-gap: legacy #d6e1e4 code text on #102837; no token matches; design-system-keeper */
color: #d6e1e4;
font:
12px / 1.55 ui-monospace,
monospace;
}
.package-preview.is-swapping {
animation: package-preview-in 0.34s ease both;
}
@keyframes package-preview-in {
from {
opacity: 0.25;
transform: translateY(7px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 800px) {
.package-workbench {
grid-template-columns: 1fr;
}
.package-tree {
border-right: 0;
/* token-gap: legacy #426070 mobile layout rule over --ink; no token matches; design-system-keeper */
border-bottom: 1px solid #426070;
}
.package-preview {
padding: 22px;
}
}
/* token-gap: legacy 520px breakpoint from skills/styles.css (mobile phone), not in named set 560/800/1100/1600/2200; design-system-keeper */
@media (max-width: 520px) {
.package-tree {
padding: 18px 10px;
}
.package-tree button {
padding: 12px 6px;
}
.package-tree small {
display: none;
}
.package-preview {
padding: 18px;
}
.package-preview pre {
/* token-gap: legacy 11px mobile font-size from skills/styles.css; no --step-* covers 11px; design-system-keeper */
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
+106
View File
@@ -0,0 +1,106 @@
---
// /agents/ — chapter page. Migrated from agents/index.html in task 13.
// Identical URL (/agents/), zero client JS, copy lives in
// src/content/chapters/agents.json.
import { getEntry } from 'astro:content';
import ChapterLayout from '../layouts/ChapterLayout.astro';
import ChapterHero from '../components/blocks/ChapterHero.astro';
// Required-field guard. The chapters schema marks section eyebrow /
// panelLabel / panelCode / steps / copy as optional because the schema
// does not know which page consumes which shape. These four pages do
// consume them — fail loudly here rather than rendering a blank section.
function requireField<T>(value: T | undefined, name: string): T {
if (value === undefined) {
throw new Error(`agents chapter: missing required field "${name}"`);
}
return value;
}
const base = import.meta.env.BASE_URL;
const chapter = await getEntry('chapters', 'agents');
if (!chapter) {
throw new Error('agents chapter: missing collection entry');
}
const lede = chapter.data.lede.en;
const title = chapter.data.title.en;
const eyebrow = chapter.data.eyebrow.en;
const cards = chapter.data.cards ?? [];
const sections = chapter.data.sections ?? [];
const treeSection = sections[0];
const handoffSection = sections[1];
if (!treeSection) {
throw new Error('agents chapter: missing sections[0]');
}
if (!handoffSection) {
throw new Error('agents chapter: missing sections[1]');
}
// Narrow the section fields this page renders. Each `requireField` either
// returns a non-null value or throws — TS narrows from `T | undefined` to `T`.
const treeEyebrow = requireField(treeSection.eyebrow, 'sections[0].eyebrow');
const treePanelLabel = requireField(treeSection.panelLabel, 'sections[0].panelLabel');
const treePanelCode = requireField(treeSection.panelCode, 'sections[0].panelCode');
const handoffEyebrow = requireField(handoffSection.eyebrow, 'sections[1].eyebrow');
const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
---
<ChapterLayout title="AI For Dummies — Agents and trees" description={lede.replace(/<[^>]+>/g, '')}>
<a slot="top-previous" href={`${base}summary/`}> ROUTE MAP</a>
<span slot="top-center">02 / AGENTS & TREES</span>
<a slot="top-next" href={`${base}full-guide/`}>field guide </a>
<ChapterHero eyebrow={eyebrow}>
<span slot="title" set:html={title} />
<p>{lede}</p>
</ChapterHero>
<section class="pipeline">
<div>
<p class="eyebrow">{treeEyebrow.en}</p>
<h2 set:html={treeSection.title.en} />
</div>
<div class="panel">
<strong>{treePanelLabel.en}</strong>
<code>{treePanelCode.en}</code>
</div>
</section>
<section class="grid">
{
cards.map((card) => (
<article class="card">
<b>{card.label.en}</b>
<h2>{card.title.en}</h2>
<p>{card.copy.en}</p>
</article>
))
}
</section>
<section class="practice">
<div>
<p class="eyebrow">{handoffEyebrow.en}</p>
<h2 set:html={handoffSection.title.en} />
</div>
<div class="steps">
{
handoffSteps.map((step, index) => (
<article>
<b>{String(index + 1).padStart(2, '0')}</b>
<div>
<strong>{step.label.en}</strong>
<span>{step.copy.en}</span>
</div>
</article>
))
}
</div>
</section>
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
<a href={`${base}models/`}>Previous: models </a>
<a href={`${base}rules/`}>Rules case study </a>
<a href={`${base}hands-on/rules/`}>Try the rules lab </a>
</nav>
</ChapterLayout>
+105
View File
@@ -0,0 +1,105 @@
---
// /models/ — chapter page. Migrated from models/index.html in task 13.
// Identical URL (/models/), zero client JS, copy lives in
// src/content/chapters/models.json. The grid + panel + steps sections
// render the chapter-collections content with bilingual `en` strings.
import { getEntry } from 'astro:content';
import ChapterLayout from '../layouts/ChapterLayout.astro';
import ChapterHero from '../components/blocks/ChapterHero.astro';
// Required-field guard. The chapters schema marks section eyebrow /
// panelLabel / panelCode / steps / copy as optional because the schema
// does not know which page consumes which shape. These four pages do
// consume them — fail loudly here rather than rendering a blank section.
function requireField<T>(value: T | undefined, name: string): T {
if (value === undefined) {
throw new Error(`models chapter: missing required field "${name}"`);
}
return value;
}
const base = import.meta.env.BASE_URL;
const chapter = await getEntry('chapters', 'models');
if (!chapter) {
throw new Error('models chapter: missing collection entry');
}
const lede = chapter.data.lede.en;
const title = chapter.data.title.en;
const eyebrow = chapter.data.eyebrow.en;
const cards = chapter.data.cards ?? [];
const sections = chapter.data.sections ?? [];
// First section carries the routing-rule panel, second carries the steps.
const ruleSection = sections[0];
const sequenceSection = sections[1];
if (!ruleSection) {
throw new Error('models chapter: missing sections[0]');
}
if (!sequenceSection) {
throw new Error('models chapter: missing sections[1]');
}
const ruleEyebrow = requireField(ruleSection.eyebrow, 'sections[0].eyebrow');
const rulePanelLabel = requireField(ruleSection.panelLabel, 'sections[0].panelLabel');
const rulePanelCode = requireField(ruleSection.panelCode, 'sections[0].panelCode');
const sequenceEyebrow = requireField(sequenceSection.eyebrow, 'sections[1].eyebrow');
const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
---
<ChapterLayout title="AI For Dummies — Models" description={lede.replace(/<[^>]+>/g, '')}>
<a slot="top-previous" href={`${base}summary/`}> ROUTE MAP</a>
<span slot="top-center">01 / MODELS</span>
<a slot="top-next" href={`${base}full-guide/`}>field guide </a>
<ChapterHero eyebrow={eyebrow}>
<span slot="title" set:html={title} />
<p>{lede}</p>
</ChapterHero>
<section class="grid">
{
cards.map((card) => (
<article class="card">
<b>{card.label.en}</b>
<h2>{card.title.en}</h2>
<p>{card.copy.en}</p>
</article>
))
}
</section>
<section class="model">
<div>
<p class="eyebrow">{ruleEyebrow.en}</p>
<h2 set:html={ruleSection.title.en} />
</div>
<div class="panel">
<strong>{rulePanelLabel.en}</strong>
<code>{rulePanelCode.en}</code>
</div>
</section>
<section class="practice">
<div>
<p class="eyebrow">{sequenceEyebrow.en}</p>
<h2 set:html={sequenceSection.title.en} />
</div>
<div class="steps">
{
sequenceSteps.map((step, index) => (
<article>
<b>{String(index + 1).padStart(2, '0')}</b>
<div>
<strong>{step.label.en}</strong>
<span>{step.copy.en}</span>
</div>
</article>
))
}
</div>
</section>
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
<a href={`${base}agents/`}>Next: agents & trees </a>
<a href={`${base}rules/`}>Rules case study </a>
</nav>
</ChapterLayout>
+93
View File
@@ -0,0 +1,93 @@
---
// /skills/ — chapter page. Migrated from skills/index.html in task 13.
// Identical URL (/skills/). The package-anatomy picker is an Astro island
// (SkillPackageExplorer) — the only JS the page ships. Copy lives in
// src/content/chapters/skills.json.
import { getEntry } from 'astro:content';
import ChapterLayout from '../layouts/ChapterLayout.astro';
import ChapterHero from '../components/blocks/ChapterHero.astro';
import SkillPackageExplorer from '../components/islands/SkillPackageExplorer.astro';
import skillsStylesheet from '../../skills/styles.css?url';
// Required-field guard. The chapters schema marks section eyebrow /
// panelLabel / panelCode / steps / copy as optional because the schema
// does not know which page consumes which shape. These four pages do
// consume them — fail loudly here rather than rendering a blank section.
function requireField<T>(value: T | undefined, name: string): T {
if (value === undefined) {
throw new Error(`skills chapter: missing required field "${name}"`);
}
return value;
}
const base = import.meta.env.BASE_URL;
const chapter = await getEntry('chapters', 'skills');
if (!chapter) {
throw new Error('skills chapter: missing collection entry');
}
const lede = chapter.data.lede.en;
const title = chapter.data.title.en;
const eyebrow = chapter.data.eyebrow.en;
const sections = chapter.data.sections ?? [];
const anatomySection = sections[0];
const createSection = sections[1];
if (!anatomySection) {
throw new Error('skills chapter: missing sections[0]');
}
if (!createSection) {
throw new Error('skills chapter: missing sections[1]');
}
const anatomyEyebrow = requireField(anatomySection.eyebrow, 'sections[0].eyebrow');
const anatomyCopy = requireField(anatomySection.copy, 'sections[0].copy');
const createEyebrow = requireField(createSection.eyebrow, 'sections[1].eyebrow');
const createSteps = requireField(createSection.steps, 'sections[1].steps');
---
<ChapterLayout title="AI For Dummies — Skills" description={lede.replace(/<[^>]+>/g, '')}>
<link slot="styles" rel="stylesheet" href={skillsStylesheet} />
<a slot="top-previous" href={`${base}summary/`}> ROUTE MAP</a>
<span slot="top-center">03 / SKILLS</span>
<a slot="top-next" href={`${base}skills-review/`}>review desk </a>
<ChapterHero eyebrow={eyebrow}>
<span slot="title" set:html={title} />
<p set:html={lede} />
</ChapterHero>
<section class="pipeline package-anatomy">
<div>
<p class="eyebrow">{anatomyEyebrow.en}</p>
<h2 set:html={anatomySection.title.en} />
<p class="package-hint">{anatomyCopy.en}</p>
</div>
<SkillPackageExplorer />
</section>
<section class="practice">
<div>
<p class="eyebrow">{createEyebrow.en}</p>
<h2 set:html={createSection.title.en} />
</div>
<div class="steps">
{
createSteps.map((step, index) => (
<article>
<b>{String(index + 1).padStart(2, '0')}</b>
<div>
<strong>{step.label.en}</strong>
<span>{step.copy.en}</span>
</div>
</article>
))
}
</div>
</section>
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
<a href={`${base}agents/`}>Agents & trees </a>
<a href={`${base}rules/`}>Rules case study </a>
<a href={`${base}skills-review/`}>Review submitted skills </a>
<a href={`${base}full-guide/#create-skill`}>Full guide: skill forge </a>
</nav>
</ChapterLayout>