@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.
Files changed (84) hide show
  1. package/CLAUDE.md +179 -0
  2. package/bin/book-scaffold.mjs +61 -0
  3. package/components/CaseStudy.astro +36 -0
  4. package/components/ChapterHeader.astro +61 -0
  5. package/components/ChapterNav.astro +29 -0
  6. package/components/ChapterTOC.astro +33 -0
  7. package/components/Citation.astro +94 -0
  8. package/components/Cite.astro +71 -0
  9. package/components/CodeBlock.astro +115 -0
  10. package/components/CodeRef.astro +49 -0
  11. package/components/ConceptBox.astro +26 -0
  12. package/components/Convergence.astro +41 -0
  13. package/components/CounterBox.astro +15 -0
  14. package/components/Divergence.astro +32 -0
  15. package/components/DynConnect.astro +15 -0
  16. package/components/ExampleBox.astro +15 -0
  17. package/components/Figure.astro +35 -0
  18. package/components/InsightBox.astro +15 -0
  19. package/components/KeyIdea.astro +21 -0
  20. package/components/MarginNote.astro +37 -0
  21. package/components/NoteBox.astro +15 -0
  22. package/components/OpenQuestion.astro +15 -0
  23. package/components/PaperBox.astro +15 -0
  24. package/components/PatternTimeline.astro +133 -0
  25. package/components/Recovery.astro +34 -0
  26. package/components/ResultBox.astro +15 -0
  27. package/components/Sidebar.astro +268 -0
  28. package/components/Sidenote.astro +26 -0
  29. package/components/SkillBox.astro +24 -0
  30. package/components/SourceArchive.astro +285 -0
  31. package/components/StatusBadge.astro +51 -0
  32. package/components/Tag.astro +60 -0
  33. package/components/Theorem.astro +65 -0
  34. package/components/TipBox.astro +15 -0
  35. package/components/ToolFilter.tsx +160 -0
  36. package/components/TryThis.astro +23 -0
  37. package/components/VersionSelector.tsx +85 -0
  38. package/components/WarnBox.astro +15 -0
  39. package/components/WeekRef.astro +51 -0
  40. package/components/XRef.astro +40 -0
  41. package/dist/index.d.ts +135 -0
  42. package/dist/index.mjs +369 -0
  43. package/dist/lib/katex-macros.d.ts +26 -0
  44. package/dist/lib/katex-macros.mjs +98 -0
  45. package/dist/schemas.d.ts +17 -0
  46. package/dist/schemas.mjs +160 -0
  47. package/dist/types-Cz-pwE1N.d.ts +61 -0
  48. package/examples/chapter-template-academic.mdx +100 -0
  49. package/examples/chapter-template-tools.mdx +90 -0
  50. package/layouts/Base.astro +250 -0
  51. package/layouts/Chapter.astro +37 -0
  52. package/package.json +137 -0
  53. package/pages/chapters.astro +371 -0
  54. package/pages/convergence.astro +96 -0
  55. package/pages/print.astro +39 -0
  56. package/pages/references.astro +160 -0
  57. package/pages/search.astro +87 -0
  58. package/pedagogy/kf-chapter-shape.md +96 -0
  59. package/pedagogy/source-tiers.md +121 -0
  60. package/pedagogy/volatility-classes.md +110 -0
  61. package/recipes/00-getting-started.md +77 -0
  62. package/recipes/01-add-math.md +71 -0
  63. package/recipes/02-bibliography-pipeline.md +82 -0
  64. package/recipes/03-asset-pipelines.md +84 -0
  65. package/recipes/04-component-library.md +118 -0
  66. package/recipes/05-deploy-cloudflare.md +74 -0
  67. package/recipes/06-mobile-first-layout.md +73 -0
  68. package/recipes/07-chapter-shapes.md +84 -0
  69. package/recipes/08-decisions-ledger.md +110 -0
  70. package/recipes/09-validation.md +106 -0
  71. package/recipes/10-custom-domain.md +72 -0
  72. package/recipes/README.md +43 -0
  73. package/scripts/build-bib.mjs +99 -0
  74. package/scripts/build-figures.mjs +179 -0
  75. package/scripts/render-notebooks.mjs +223 -0
  76. package/scripts/validate.mjs +179 -0
  77. package/styles/callouts.css +303 -0
  78. package/styles/chapter.css +209 -0
  79. package/styles/convergence.css +349 -0
  80. package/styles/layout.css +156 -0
  81. package/styles/print.css +203 -0
  82. package/styles/tokens.css +194 -0
  83. package/styles/tool-filter.css +135 -0
  84. 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>