@brandon_m_behring/book-scaffold-astro 3.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +179 -0
- package/bin/book-scaffold.mjs +61 -0
- package/components/CaseStudy.astro +36 -0
- package/components/ChapterHeader.astro +61 -0
- package/components/ChapterNav.astro +29 -0
- package/components/ChapterTOC.astro +33 -0
- package/components/Citation.astro +94 -0
- package/components/Cite.astro +71 -0
- package/components/CodeBlock.astro +115 -0
- package/components/CodeRef.astro +49 -0
- package/components/ConceptBox.astro +26 -0
- package/components/Convergence.astro +41 -0
- package/components/CounterBox.astro +15 -0
- package/components/Divergence.astro +32 -0
- package/components/DynConnect.astro +15 -0
- package/components/ExampleBox.astro +15 -0
- package/components/Figure.astro +35 -0
- package/components/InsightBox.astro +15 -0
- package/components/KeyIdea.astro +21 -0
- package/components/MarginNote.astro +37 -0
- package/components/NoteBox.astro +15 -0
- package/components/OpenQuestion.astro +15 -0
- package/components/PaperBox.astro +15 -0
- package/components/PatternTimeline.astro +133 -0
- package/components/Recovery.astro +34 -0
- package/components/ResultBox.astro +15 -0
- package/components/Sidebar.astro +268 -0
- package/components/Sidenote.astro +26 -0
- package/components/SkillBox.astro +24 -0
- package/components/SourceArchive.astro +285 -0
- package/components/StatusBadge.astro +51 -0
- package/components/Tag.astro +60 -0
- package/components/Theorem.astro +65 -0
- package/components/TipBox.astro +15 -0
- package/components/ToolFilter.tsx +160 -0
- package/components/TryThis.astro +23 -0
- package/components/VersionSelector.tsx +85 -0
- package/components/WarnBox.astro +15 -0
- package/components/WeekRef.astro +51 -0
- package/components/XRef.astro +40 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.mjs +369 -0
- package/dist/lib/katex-macros.d.ts +26 -0
- package/dist/lib/katex-macros.mjs +98 -0
- package/dist/schemas.d.ts +17 -0
- package/dist/schemas.mjs +160 -0
- package/dist/types-Cz-pwE1N.d.ts +61 -0
- package/examples/chapter-template-academic.mdx +100 -0
- package/examples/chapter-template-tools.mdx +90 -0
- package/layouts/Base.astro +250 -0
- package/layouts/Chapter.astro +37 -0
- package/package.json +137 -0
- package/pages/chapters.astro +371 -0
- package/pages/convergence.astro +96 -0
- package/pages/print.astro +39 -0
- package/pages/references.astro +160 -0
- package/pages/search.astro +87 -0
- package/pedagogy/kf-chapter-shape.md +96 -0
- package/pedagogy/source-tiers.md +121 -0
- package/pedagogy/volatility-classes.md +110 -0
- package/recipes/00-getting-started.md +77 -0
- package/recipes/01-add-math.md +71 -0
- package/recipes/02-bibliography-pipeline.md +82 -0
- package/recipes/03-asset-pipelines.md +84 -0
- package/recipes/04-component-library.md +118 -0
- package/recipes/05-deploy-cloudflare.md +74 -0
- package/recipes/06-mobile-first-layout.md +73 -0
- package/recipes/07-chapter-shapes.md +84 -0
- package/recipes/08-decisions-ledger.md +110 -0
- package/recipes/09-validation.md +106 -0
- package/recipes/10-custom-domain.md +72 -0
- package/recipes/README.md +43 -0
- package/scripts/build-bib.mjs +99 -0
- package/scripts/build-figures.mjs +179 -0
- package/scripts/render-notebooks.mjs +223 -0
- package/scripts/validate.mjs +179 -0
- package/styles/callouts.css +303 -0
- package/styles/chapter.css +209 -0
- package/styles/convergence.css +349 -0
- package/styles/layout.css +156 -0
- package/styles/print.css +203 -0
- package/styles/tokens.css +194 -0
- package/styles/tool-filter.css +135 -0
- package/styles/typography.css +147 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# CLAUDE.md — Authoring guide for AI assistants
|
|
2
|
+
|
|
3
|
+
This file is auto-loaded by Claude Code (and cross-tool agents via the symmetric `AGENTS.md`) when working in a repo bootstrapped from `book-scaffold-astro`. Read this first; the patterns below are pre-tested.
|
|
4
|
+
|
|
5
|
+
## Inherits from
|
|
6
|
+
|
|
7
|
+
Cross-project conventions live in the hub at `~/Claude/lever_of_archimedes/patterns/`. Defer to those for:
|
|
8
|
+
|
|
9
|
+
- **Git commit format** — `~/Claude/lever_of_archimedes/patterns/git.md`
|
|
10
|
+
- **Testing patterns** — `~/Claude/lever_of_archimedes/patterns/testing.md`
|
|
11
|
+
- **Session workflows** — `~/Claude/lever_of_archimedes/patterns/sessions.md`
|
|
12
|
+
|
|
13
|
+
If the hub isn't available in your environment (e.g. external contributor), the scaffold's `CHANGELOG.md` documents commit conventions inline.
|
|
14
|
+
|
|
15
|
+
## Profile
|
|
16
|
+
|
|
17
|
+
Read `BOOK_PROFILE` from the environment or `.env`. It controls:
|
|
18
|
+
|
|
19
|
+
- Which content-collection schema is enforced (`academic` / `tools` / `minimal`)
|
|
20
|
+
- Which markdown integrations run (KaTeX gated on `academic`)
|
|
21
|
+
- Which callout family is the "default" import in templates
|
|
22
|
+
- Whether ToolFilter / VersionSelector Preact islands mount in the chrome row
|
|
23
|
+
|
|
24
|
+
When in doubt, run `grep BOOK_PROFILE .env astro.config.mjs src/content.config.ts` to see the wiring.
|
|
25
|
+
|
|
26
|
+
## Frontmatter schemas
|
|
27
|
+
|
|
28
|
+
### Academic profile (`src/content.config.ts:academicChapterSchema`)
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
---
|
|
32
|
+
week: 1 # int, required, 1-99
|
|
33
|
+
part: foundations # required: foundations|ssm-core|beyond-ssm|integration|synthesis
|
|
34
|
+
title: "..." # string, required
|
|
35
|
+
status: implemented # required: implemented|chapter_only|prose_only|code_only|reading_only|scaffolded|planned
|
|
36
|
+
# optional:
|
|
37
|
+
roadmap_lines: [10, 42] # [start, end] line refs into roadmap.md
|
|
38
|
+
code_path: experiments/jax/week01/foo.py
|
|
39
|
+
tests_path: experiments/jax/week01/test_foo.py
|
|
40
|
+
notebook_path: notebooks/week01.ipynb
|
|
41
|
+
description: "..." # SEO/meta
|
|
42
|
+
draft: false
|
|
43
|
+
---
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Tools profile (`src/content.config.ts:toolsChapterSchema`)
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
---
|
|
50
|
+
title: "..." # required
|
|
51
|
+
part: 1 # int, required, 0-10
|
|
52
|
+
chapter: 1 # int, required, 0-99
|
|
53
|
+
volatility: architectural-pattern # required: stable-principle|architectural-pattern|feature-surface
|
|
54
|
+
tools_compared: [claude-code] # required, ≥1 of: claude-code|gemini-cli|codex-cli|cross-tool
|
|
55
|
+
last_verified: 2026-05-18 # date, required
|
|
56
|
+
sources: [] # array of source-manifest keys
|
|
57
|
+
# optional: description, draft, updated
|
|
58
|
+
---
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Component reference
|
|
62
|
+
|
|
63
|
+
Two callout families coexist. Authors import what they need.
|
|
64
|
+
|
|
65
|
+
**Tools family** (`src/components/callouts/`, 8 components): `SkillBox`, `CaseStudy`, `ConceptBox`, `KeyIdea`, `TryThis`, `Recovery`, `Convergence`, `Divergence`.
|
|
66
|
+
|
|
67
|
+
**Academic family** (`src/components/callouts/`, 10 components): `NoteBox`, `ExampleBox`, `DynConnect`, `InsightBox`, `WarnBox`, `CounterBox`, `TipBox`, `OpenQuestion`, `PaperBox`, `ResultBox`. Plus `Theorem` (unified for theorem/proposition/lemma/corollary/definition/example/exercise/remark/proof).
|
|
68
|
+
|
|
69
|
+
**Utility components** (`src/components/`, any profile): `Cite`, `XRef`, `Figure`, `MarginNote`, `Sidenote`, `WeekRef`, `CodeRef`, `CodeBlock`, `Tag`, `StatusBadge`.
|
|
70
|
+
|
|
71
|
+
Full reference in `recipes/04-component-library.md`.
|
|
72
|
+
|
|
73
|
+
## Citation patterns
|
|
74
|
+
|
|
75
|
+
Academic profile uses BibTeX → `references.json`:
|
|
76
|
+
|
|
77
|
+
```mdx
|
|
78
|
+
The HiPPO theory <Cite key="gu2020hippo" /> shows that …
|
|
79
|
+
For the kernel decomposition see <Cite key="gu2024mamba" page="3" />.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Build: `npm run build:bib` reads `bibliography.bib` and writes `src/data/references.json`. Run after any `.bib` edit. The pre-build hook handles this automatically.
|
|
83
|
+
|
|
84
|
+
Tools profile uses the YAML source manifest (`sources/manifest.yaml`); cite via `sources` array in frontmatter, rendered by `SourceArchive.astro`.
|
|
85
|
+
|
|
86
|
+
## Build + dev commands
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install # once after clone
|
|
90
|
+
npm run dev # localhost:4321
|
|
91
|
+
npm run build # astro build + pagefind index → dist/
|
|
92
|
+
npm run validate # pre-flight check (recipe 09)
|
|
93
|
+
npm run build:bib # rebuild references.json after .bib edit
|
|
94
|
+
npm run pdf # render dist-pdf/book.pdf via Paged.js
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`prebuild` chains: `build:assets` (bib + figures + notebooks) → `validate` → `astro build`.
|
|
98
|
+
|
|
99
|
+
## Deploy
|
|
100
|
+
|
|
101
|
+
Cloudflare Workers + Static Assets via `wrangler.toml`. Recipe 05 has the dashboard flow. URL after first deploy: `https://<book-name>.<account>.workers.dev`.
|
|
102
|
+
|
|
103
|
+
For monorepo Astro projects (Astro project in subdir), prefix build + deploy commands with `cd <subdir> &&`.
|
|
104
|
+
|
|
105
|
+
## Validation
|
|
106
|
+
|
|
107
|
+
`npm run validate` (also runs in prebuild) catches:
|
|
108
|
+
|
|
109
|
+
- Unknown `<Cite key>` (academic) — bibkey not in `references.json`
|
|
110
|
+
- Unknown `<XRef id>` — id not in `labels.json` (XRef silently renders `[?label]` otherwise)
|
|
111
|
+
- Missing `<Figure src>` files under `public/`
|
|
112
|
+
- Internal markdown links that don't resolve
|
|
113
|
+
|
|
114
|
+
See `recipes/09-validation.md` to extend.
|
|
115
|
+
|
|
116
|
+
## Common authoring tasks
|
|
117
|
+
|
|
118
|
+
### Add a new chapter
|
|
119
|
+
|
|
120
|
+
1. Copy `examples/chapter-template-{academic,tools}.mdx` to `src/content/chapters/`.
|
|
121
|
+
2. Edit frontmatter (title, week/chapter, status/volatility).
|
|
122
|
+
3. Write.
|
|
123
|
+
4. `npm run dev` to preview at `/chapters/<slug>/`.
|
|
124
|
+
|
|
125
|
+
### Add a citation
|
|
126
|
+
|
|
127
|
+
1. Edit `bibliography.bib` (academic profile) — add the BibTeX entry.
|
|
128
|
+
2. `npm run build:bib` regenerates `src/data/references.json`.
|
|
129
|
+
3. Use `<Cite key="<bibkey>" />` in chapter.
|
|
130
|
+
|
|
131
|
+
### Add a figure
|
|
132
|
+
|
|
133
|
+
1. Drop PDF in `figures/<topic>/<name>.pdf` (or set `BOOK_FIGURES_PATH`).
|
|
134
|
+
2. `npm run build:figures` produces `public/figures/<topic>/<name>.svg`.
|
|
135
|
+
3. Reference: `<Figure src="/figures/<topic>/<name>.svg" caption="..." id="..." />`.
|
|
136
|
+
|
|
137
|
+
### Add a new component
|
|
138
|
+
|
|
139
|
+
1. Create `src/components/<Foo>.astro`.
|
|
140
|
+
2. Add an entry to `recipes/04-component-library.md`.
|
|
141
|
+
3. Update this file's "Component reference" section.
|
|
142
|
+
|
|
143
|
+
## Commit conventions
|
|
144
|
+
|
|
145
|
+
Inherit from the hub's `git.md`. Format:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
type(scope): Short imperative subject
|
|
149
|
+
|
|
150
|
+
Body paragraphs explaining what and why.
|
|
151
|
+
|
|
152
|
+
- Bullet for each significant change
|
|
153
|
+
- Another bullet
|
|
154
|
+
|
|
155
|
+
Generated with Claude Code
|
|
156
|
+
|
|
157
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Types: `feat` / `fix` / `refactor` / `docs` / `test` / `chore` / `release`. One commit per logical unit; small commits over big ones.
|
|
161
|
+
|
|
162
|
+
## Don't
|
|
163
|
+
|
|
164
|
+
- Don't use `npm create astro@latest` to bootstrap a fresh repo — the scaffold is not vanilla Astro.
|
|
165
|
+
- Don't bypass the validator with `--no-verify` on a commit. If validate fails, fix the underlying issue.
|
|
166
|
+
- Don't commit large binaries (PDFs > 5 MB, model checkpoints) — keep them in research-kb or a separate asset host.
|
|
167
|
+
- Don't auto-import from both callout families in the same chapter unless you have a reason. Pick a default family and stay with it.
|
|
168
|
+
|
|
169
|
+
## Reference repos
|
|
170
|
+
|
|
171
|
+
- `~/Claude/post_transformers/guides/web/` — academic-profile reference, deployed at `post-transformers-guide.brandon-m-behring.workers.dev`
|
|
172
|
+
- `~/Claude/book-template-astro/` — tools-profile reference, "Agentic Coding" book in production
|
|
173
|
+
- `~/Claude/book-scaffold-astro/` — this canonical scaffold
|
|
174
|
+
|
|
175
|
+
## Reading this guide didn't help?
|
|
176
|
+
|
|
177
|
+
- `recipes/README.md` — index of all 11 recipes
|
|
178
|
+
- `recipes/08-decisions-ledger.md` — why everything is shaped the way it is
|
|
179
|
+
- `~/.claude/plans/i-want-to-investigate-recursive-yao.md` — full design discussion
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* book-scaffold — single-dispatcher CLI for the book-scaffold-astro toolkit.
|
|
4
|
+
*
|
|
5
|
+
* Per PACKAGE_DESIGN.md §8: one bin entry, sub-command parsed from argv[2].
|
|
6
|
+
* Sub-commands import the corresponding script module; the script's own
|
|
7
|
+
* argv parsing handles flags.
|
|
8
|
+
*/
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
const handlers = {
|
|
16
|
+
validate: '../scripts/validate.mjs',
|
|
17
|
+
'build-labels': '../scripts/build-labels.mjs',
|
|
18
|
+
'build-bib': '../scripts/build-bib.mjs',
|
|
19
|
+
'build-figures': '../scripts/build-figures.mjs',
|
|
20
|
+
'render-notebooks': '../scripts/render-notebooks.mjs',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const HELP = `Usage: book-scaffold <sub-command> [args...]
|
|
24
|
+
|
|
25
|
+
Sub-commands:
|
|
26
|
+
validate Pre-flight content validator (XRef ids, Cite keys, Figure srcs).
|
|
27
|
+
build-labels Emit src/data/labels.json for cross-references (Phase C).
|
|
28
|
+
build-bib BibTeX -> CSL JSON for the <Cite> component.
|
|
29
|
+
build-figures PDF -> SVG via pdftocairo / pdftoppm fallback.
|
|
30
|
+
render-notebooks ipynb -> HTML via Jupyter nbconvert.
|
|
31
|
+
|
|
32
|
+
--help, -h This message.
|
|
33
|
+
--version, -v Print the package version.
|
|
34
|
+
|
|
35
|
+
See: https://github.com/brandon-behring/book-scaffold-astro
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const [, , sub, ...rest] = process.argv;
|
|
39
|
+
|
|
40
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
41
|
+
process.stdout.write(HELP);
|
|
42
|
+
process.exit(sub ? 0 : 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (sub === '--version' || sub === '-v') {
|
|
46
|
+
const pkgPath = resolve(__dirname, '../package.json');
|
|
47
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
48
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!(sub in handlers)) {
|
|
53
|
+
process.stderr.write(`book-scaffold: unknown sub-command '${sub}'.\n\n${HELP}`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Hand off argv to the sub-command's script. The script reads process.argv
|
|
58
|
+
// directly so flag parsing stays consistent with v2.0 standalone usage.
|
|
59
|
+
const scriptPath = resolve(__dirname, handlers[sub]);
|
|
60
|
+
process.argv = [process.argv[0], scriptPath, ...rest];
|
|
61
|
+
await import(scriptPath);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* CaseStudy — concrete anecdote, dated. Grounds a principle in a real
|
|
4
|
+
* event: "When Gemini's 1M window launched (Feb 2026)...", "A team hit
|
|
5
|
+
* context rot at 60K tokens in Q3 2025..."
|
|
6
|
+
*
|
|
7
|
+
* The date is required and rendered prominently to help readers
|
|
8
|
+
* calibrate relevance against current tool state. Stale case studies
|
|
9
|
+
* don't pretend to be recent.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* <CaseStudy date="2026-02">
|
|
13
|
+
* When Gemini shipped the 1M-token window, teams initially filled
|
|
14
|
+
* it to ~800K and watched retrieval accuracy collapse by Q3...
|
|
15
|
+
* </CaseStudy>
|
|
16
|
+
*/
|
|
17
|
+
interface Props {
|
|
18
|
+
date: string; // human-readable or ISO — not parsed, just displayed
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
21
|
+
const { date, title } = Astro.props;
|
|
22
|
+
---
|
|
23
|
+
<aside class="callout callout-case" role="note" aria-label="Case study">
|
|
24
|
+
<strong class="callout-title">
|
|
25
|
+
Case study{title ? ` · ${title}` : ''} <span class="case-date">· {date}</span>
|
|
26
|
+
</strong>
|
|
27
|
+
<div class="callout-body"><slot /></div>
|
|
28
|
+
</aside>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.case-date {
|
|
32
|
+
font-weight: 400;
|
|
33
|
+
color: var(--color-text-muted);
|
|
34
|
+
font-size: var(--text-sm);
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ChapterHeader — renders the metadata block at the top of every chapter.
|
|
4
|
+
* Surfaces provenance signals the LaTeX book conveyed implicitly:
|
|
5
|
+
* - Part + chapter number (position in the book)
|
|
6
|
+
* - Volatility class (freshness calibration for the reader)
|
|
7
|
+
* - Tools compared (scope signal)
|
|
8
|
+
* - Last verified date (how stale are the claims?)
|
|
9
|
+
*
|
|
10
|
+
* Driven entirely from the chapter's frontmatter (see content.config.ts).
|
|
11
|
+
*/
|
|
12
|
+
import type { CollectionEntry } from 'astro:content';
|
|
13
|
+
import { getFreshness, freshnessLabel } from '../src/lib/freshness';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
data: CollectionEntry<'chapters'>['data'];
|
|
17
|
+
}
|
|
18
|
+
const { data } = Astro.props;
|
|
19
|
+
|
|
20
|
+
function formatDate(d: Date): string {
|
|
21
|
+
return d.toISOString().slice(0, 10);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const freshness = getFreshness(data.last_verified, data.volatility);
|
|
25
|
+
const freshnessText =
|
|
26
|
+
freshness.status === 'fresh'
|
|
27
|
+
? 'Fresh'
|
|
28
|
+
: freshness.status === 'verify-soon'
|
|
29
|
+
? 'Verify soon'
|
|
30
|
+
: 'Stale';
|
|
31
|
+
---
|
|
32
|
+
<header class="chapter-header">
|
|
33
|
+
<div class="chapter-meta">
|
|
34
|
+
<span>Part {data.part}</span>
|
|
35
|
+
<span>Chapter {data.chapter}</span>
|
|
36
|
+
<span>
|
|
37
|
+
Last verified {formatDate(data.last_verified)}
|
|
38
|
+
<span
|
|
39
|
+
class="freshness-badge"
|
|
40
|
+
data-status={freshness.status}
|
|
41
|
+
aria-label={freshnessLabel(freshness)}
|
|
42
|
+
title={freshnessLabel(freshness)}
|
|
43
|
+
>{freshnessText}</span>
|
|
44
|
+
</span>
|
|
45
|
+
{data.updated && <span>Updated {formatDate(data.updated)}</span>}
|
|
46
|
+
</div>
|
|
47
|
+
<h1>{data.title}</h1>
|
|
48
|
+
{data.description && <p class="chapter-description">{data.description}</p>}
|
|
49
|
+
<div class="chapter-badge-row">
|
|
50
|
+
<span class="chapter-badge-row-label">Volatility:</span>
|
|
51
|
+
<span class={`volatility-badge volatility-${data.volatility}`}>
|
|
52
|
+
{data.volatility}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
{data.tools_compared.length > 0 && (
|
|
56
|
+
<div class="chapter-badge-row">
|
|
57
|
+
<span class="chapter-badge-row-label">Tools compared:</span>
|
|
58
|
+
{data.tools_compared.map((t) => <span class="tool-badge">{t}</span>)}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</header>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ChapterNav — prev / next chapter links at the bottom of each chapter.
|
|
4
|
+
* Derived from the ordered chapters collection via getNeighbors.
|
|
5
|
+
*/
|
|
6
|
+
import { getNeighbors } from '../src/lib/chapters';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
currentId: string;
|
|
10
|
+
}
|
|
11
|
+
const { currentId } = Astro.props;
|
|
12
|
+
const { prev, next } = await getNeighbors(currentId);
|
|
13
|
+
---
|
|
14
|
+
{(prev || next) && (
|
|
15
|
+
<nav class="chapter-nav" aria-label="Chapter navigation">
|
|
16
|
+
{prev && (
|
|
17
|
+
<a href={`/${prev.id}/`} class="prev">
|
|
18
|
+
<span class="nav-label">← Previous</span>
|
|
19
|
+
<span class="nav-title">{prev.data.title}</span>
|
|
20
|
+
</a>
|
|
21
|
+
)}
|
|
22
|
+
{next && (
|
|
23
|
+
<a href={`/${next.id}/`} class="next">
|
|
24
|
+
<span class="nav-label">Next →</span>
|
|
25
|
+
<span class="nav-title">{next.data.title}</span>
|
|
26
|
+
</a>
|
|
27
|
+
)}
|
|
28
|
+
</nav>
|
|
29
|
+
)}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ChapterTOC — collapsible "On this page" anchor list.
|
|
4
|
+
*
|
|
5
|
+
* Uses native <details> — collapsed by default; no JavaScript required.
|
|
6
|
+
* Shows h2 and h3 headings only (h1 is the chapter title; h4+ is noise
|
|
7
|
+
* in a TOC). Each heading link uses the slug Astro's MDX generates.
|
|
8
|
+
*
|
|
9
|
+
* If the chapter has fewer than 3 matching headings, the TOC renders
|
|
10
|
+
* nothing — an anchor list for a short chapter is clutter.
|
|
11
|
+
*/
|
|
12
|
+
import type { MarkdownHeading } from 'astro';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
headings: MarkdownHeading[];
|
|
16
|
+
}
|
|
17
|
+
const { headings } = Astro.props;
|
|
18
|
+
|
|
19
|
+
const toc = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
|
20
|
+
const showToc = toc.length >= 3;
|
|
21
|
+
---
|
|
22
|
+
{showToc && (
|
|
23
|
+
<details class="chapter-toc">
|
|
24
|
+
<summary>On this page</summary>
|
|
25
|
+
<ol>
|
|
26
|
+
{toc.map((h) => (
|
|
27
|
+
<li class={`toc-h${h.depth}`}>
|
|
28
|
+
<a href={`#${h.slug}`}>{h.text}</a>
|
|
29
|
+
</li>
|
|
30
|
+
))}
|
|
31
|
+
</ol>
|
|
32
|
+
</details>
|
|
33
|
+
)}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Citation — renders a reference to a source from sources/manifest.yaml.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the source slug at build time via Astro's content collections.
|
|
6
|
+
* An unknown slug fails the build with a precise error, not at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Two display modes:
|
|
9
|
+
* as="sidenote" (default) — wraps the citation in a <Sidenote>. Shows
|
|
10
|
+
* in the desktop right margin / mobile inline aside. Contains title,
|
|
11
|
+
* author, year, tier badge, plus links to original and Perma.cc.
|
|
12
|
+
* as="inline" — renders the citation inline in the main text as a
|
|
13
|
+
* subtle reference (title + author + year). For cases where the
|
|
14
|
+
* citation is part of the sentence rather than a reference.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* Context rot<Citation src="anthropic-context-2026" /> is observed
|
|
18
|
+
* in all three tools.
|
|
19
|
+
*
|
|
20
|
+
* Or inline:
|
|
21
|
+
* See <Citation src="gwern-sidenote" as="inline" /> for the argument.
|
|
22
|
+
*/
|
|
23
|
+
import { getEntry } from 'astro:content';
|
|
24
|
+
import Sidenote from './Sidenote.astro';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
src: string;
|
|
28
|
+
as?: 'sidenote' | 'inline';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { src, as = 'sidenote' } = Astro.props;
|
|
32
|
+
const source = await getEntry('sources', src);
|
|
33
|
+
|
|
34
|
+
if (!source) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Citation: source slug "${src}" not found in sources/manifest.yaml. ` +
|
|
37
|
+
`Add an entry with id: ${src}, or correct the typo.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { data } = source;
|
|
42
|
+
const publishYear = data.publish_date
|
|
43
|
+
? new Date(data.publish_date).getUTCFullYear()
|
|
44
|
+
: null;
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
{as === 'sidenote' ? (
|
|
48
|
+
<Sidenote>
|
|
49
|
+
<strong>{data.title}</strong>
|
|
50
|
+
{data.author && <>{' · '}{data.author}</>}
|
|
51
|
+
{publishYear && <>{' ('}{publishYear}{')'}</>}
|
|
52
|
+
<span class="citation-tier" title={`Source tier: ${data.tier}`}>{data.tier}</span>
|
|
53
|
+
{' '}
|
|
54
|
+
<a href={data.url} rel="external noopener" class="citation-link">original</a>
|
|
55
|
+
{data.perma_cc && (
|
|
56
|
+
<>
|
|
57
|
+
{' · '}
|
|
58
|
+
<a href={data.perma_cc} rel="external noopener" class="citation-link">archived</a>
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
61
|
+
</Sidenote>
|
|
62
|
+
) : (
|
|
63
|
+
<span class="citation-inline">
|
|
64
|
+
<a href={data.url} rel="external noopener">{data.title}</a>
|
|
65
|
+
{data.author && <> · {data.author}</>}
|
|
66
|
+
{publishYear && <> ({publishYear})</>}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
<style is:global>
|
|
71
|
+
/* Tier badge styling — muted, uppercase, small, monospaced */
|
|
72
|
+
.citation-tier {
|
|
73
|
+
display: inline-block;
|
|
74
|
+
margin-left: 0.4em;
|
|
75
|
+
padding: 0 0.4em;
|
|
76
|
+
font-family: var(--font-code);
|
|
77
|
+
font-size: 0.7em;
|
|
78
|
+
font-style: normal;
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
color: var(--color-text-muted);
|
|
81
|
+
background: var(--color-bg);
|
|
82
|
+
border: 1px solid var(--color-border);
|
|
83
|
+
border-radius: var(--radius-sm);
|
|
84
|
+
vertical-align: 0.1em;
|
|
85
|
+
letter-spacing: 0.05em;
|
|
86
|
+
}
|
|
87
|
+
.citation-link {
|
|
88
|
+
font-style: normal;
|
|
89
|
+
}
|
|
90
|
+
.citation-inline {
|
|
91
|
+
font-style: italic;
|
|
92
|
+
color: var(--color-text-muted);
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Cite — renders a `\cite{bibkey}` LaTeX citation as a clickable
|
|
4
|
+
* inline reference to the bibliography page.
|
|
5
|
+
*
|
|
6
|
+
* Reads src/data/references.json (built by scripts/build-bib.mjs from
|
|
7
|
+
* guides/shared/references.bib). Unknown bibkeys fail the build with
|
|
8
|
+
* a precise error so typos surface at the same point biber would have
|
|
9
|
+
* caught them.
|
|
10
|
+
*
|
|
11
|
+
* Display:
|
|
12
|
+
* - Single author: "Gu (2024)"
|
|
13
|
+
* - Two authors: "Gu & Dao (2024)"
|
|
14
|
+
* - 3+ authors: "Gu et al. (2024)"
|
|
15
|
+
* - With page: "Gu et al. (2024, p. 42)"
|
|
16
|
+
*
|
|
17
|
+
* The link target is /references#<bibkey>, where the references page
|
|
18
|
+
* (src/pages/references.astro) renders all entries with anchor IDs
|
|
19
|
+
* matching their bibkeys.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* The HiPPO theory <Cite key="gu2020hippo" /> shows that …
|
|
23
|
+
* See <Cite key="gu2024mamba" page="3" /> for the kernel decomposition.
|
|
24
|
+
*/
|
|
25
|
+
type CslAuthor = { family?: string; given?: string; literal?: string };
|
|
26
|
+
type CslEntry = {
|
|
27
|
+
id: string;
|
|
28
|
+
author?: CslAuthor[];
|
|
29
|
+
issued?: { 'date-parts'?: number[][] };
|
|
30
|
+
title?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Resolve references.json from consumer's project root. Missing file -> empty
|
|
34
|
+
// map -> any <Cite> throws below (strict per D3 — bibkeys must resolve at build).
|
|
35
|
+
const refsModules = import.meta.glob<{ default: Record<string, CslEntry> }>(
|
|
36
|
+
'/src/data/references.json',
|
|
37
|
+
{ eager: true },
|
|
38
|
+
);
|
|
39
|
+
const refsModule = refsModules['/src/data/references.json'];
|
|
40
|
+
const map = (refsModule?.default ?? {}) as Record<string, CslEntry>;
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
key: string;
|
|
44
|
+
page?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { key, page } = Astro.props;
|
|
48
|
+
const entry = map[key];
|
|
49
|
+
if (!entry) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Cite: unknown bibkey "${key}". Check guides/shared/references.bib ` +
|
|
52
|
+
`for the expected key, or re-run build:bib if the .bib was just edited.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const authors = entry.author ?? [];
|
|
57
|
+
const surname = (a: CslAuthor) => a.family ?? a.literal ?? '(anon)';
|
|
58
|
+
|
|
59
|
+
let authorText: string;
|
|
60
|
+
if (authors.length === 0) authorText = '(anon)';
|
|
61
|
+
else if (authors.length === 1) authorText = surname(authors[0]);
|
|
62
|
+
else if (authors.length === 2)
|
|
63
|
+
authorText = `${surname(authors[0])} & ${surname(authors[1])}`;
|
|
64
|
+
else authorText = `${surname(authors[0])} et al.`;
|
|
65
|
+
|
|
66
|
+
const year = entry.issued?.['date-parts']?.[0]?.[0] ?? 'n.d.';
|
|
67
|
+
const yearText = page ? `${year}, p. ${page}` : `${year}`;
|
|
68
|
+
---
|
|
69
|
+
<a href={`/references#${key}`} class="cite" title={entry.title ?? key}>
|
|
70
|
+
{authorText} ({yearText})
|
|
71
|
+
</a>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* CodeBlock — embed a slice of a real source file from the repo at
|
|
4
|
+
* build time, with syntax highlighting and a "View on GitHub" header
|
|
5
|
+
* link pointing to the exact line range.
|
|
6
|
+
*
|
|
7
|
+
* Reads the file from the repo root (resolved relative to
|
|
8
|
+
* guides/web/), slices `lines` ("N-M" or "N"), and hands the snippet
|
|
9
|
+
* to Astro's `<Code>` component for Shiki rendering.
|
|
10
|
+
*
|
|
11
|
+
* Build fails if:
|
|
12
|
+
* - The file does not exist on disk
|
|
13
|
+
* - The requested line range is out of bounds
|
|
14
|
+
* - `lines` is malformed
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* <CodeBlock
|
|
18
|
+
* src="experiments/jax/week04/s4_hippo.py"
|
|
19
|
+
* lines="42-58"
|
|
20
|
+
* lang="python"
|
|
21
|
+
* />
|
|
22
|
+
*
|
|
23
|
+
* <CodeBlock src="experiments/jax/week04/s4_hippo.py" lines="42" lang="python" />
|
|
24
|
+
*
|
|
25
|
+
* If `lang` is omitted, it is inferred from the file extension.
|
|
26
|
+
*/
|
|
27
|
+
import { Code } from 'astro:components';
|
|
28
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
29
|
+
import { resolve } from 'node:path';
|
|
30
|
+
import { buildGithubUrl } from '../src/lib/repo-url';
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
src: string;
|
|
34
|
+
lines: string; // "N" or "N-M" (1-indexed, inclusive)
|
|
35
|
+
lang?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { src, lines, lang } = Astro.props;
|
|
39
|
+
|
|
40
|
+
// Resolve src relative to repo root. Astro's build runs with cwd =
|
|
41
|
+
// guides/web/, so the repo root is two levels up. Using process.cwd()
|
|
42
|
+
// (not import.meta.dirname) survives Astro's bundling — at runtime
|
|
43
|
+
// import.meta.dirname points to the emitted .mjs chunk, not the source.
|
|
44
|
+
const REPO_ROOT = resolve(process.cwd(), '..', '..');
|
|
45
|
+
const absPath = resolve(REPO_ROOT, src);
|
|
46
|
+
|
|
47
|
+
if (!existsSync(absPath)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`CodeBlock: file not found at ${absPath} (src="${src}"). ` +
|
|
50
|
+
`Check the path is relative to the repo root.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rangeMatch = lines.match(/^(\d+)(?:-(\d+))?$/);
|
|
55
|
+
if (!rangeMatch) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`CodeBlock: lines="${lines}" is malformed. Use "N" or "N-M" (1-indexed).`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const startLine = parseInt(rangeMatch[1], 10);
|
|
62
|
+
const endLine = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : startLine;
|
|
63
|
+
|
|
64
|
+
if (endLine < startLine) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`CodeBlock: lines="${lines}" — end line ${endLine} is before start ${startLine}.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const fileLines = readFileSync(absPath, 'utf8').split('\n');
|
|
71
|
+
if (startLine < 1 || endLine > fileLines.length) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`CodeBlock: requested lines ${startLine}-${endLine} fall outside ` +
|
|
74
|
+
`file extent (1-${fileLines.length}) for ${src}.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const snippet = fileLines.slice(startLine - 1, endLine).join('\n');
|
|
79
|
+
|
|
80
|
+
const inferLang = (path: string): string => {
|
|
81
|
+
const ext = path.split('.').pop()?.toLowerCase() ?? '';
|
|
82
|
+
const map: Record<string, string> = {
|
|
83
|
+
py: 'python',
|
|
84
|
+
jl: 'julia',
|
|
85
|
+
js: 'javascript',
|
|
86
|
+
ts: 'typescript',
|
|
87
|
+
mjs: 'javascript',
|
|
88
|
+
sh: 'bash',
|
|
89
|
+
yaml: 'yaml',
|
|
90
|
+
yml: 'yaml',
|
|
91
|
+
json: 'json',
|
|
92
|
+
md: 'markdown',
|
|
93
|
+
mdx: 'mdx',
|
|
94
|
+
rs: 'rust',
|
|
95
|
+
go: 'go',
|
|
96
|
+
cpp: 'cpp',
|
|
97
|
+
c: 'c',
|
|
98
|
+
tex: 'latex',
|
|
99
|
+
};
|
|
100
|
+
return map[ext] ?? 'text';
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const language = (lang ?? inferLang(src)) as Parameters<typeof Code>[0]['lang'];
|
|
104
|
+
|
|
105
|
+
const githubUrl = buildGithubUrl(src, startLine, endLine);
|
|
106
|
+
const rangeText = startLine === endLine ? `:${startLine}` : `:${startLine}-${endLine}`;
|
|
107
|
+
const displayPath = `${src}${rangeText}`;
|
|
108
|
+
---
|
|
109
|
+
<div class="codeblock-frame">
|
|
110
|
+
<div class="codeblock-header">
|
|
111
|
+
<code>{displayPath}</code>
|
|
112
|
+
<a href={githubUrl} rel="external noopener">View on GitHub →</a>
|
|
113
|
+
</div>
|
|
114
|
+
<Code code={snippet} lang={language} theme="css-variables" wrap={false} />
|
|
115
|
+
</div>
|