@brandon_m_behring/book-scaffold-astro 4.26.3 → 4.27.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 (49) hide show
  1. package/AGENTS.md +6 -0
  2. package/CLAUDE.md +35 -10
  3. package/LATEX_TO_MDX_MAPPING.md +1 -1
  4. package/LICENSE +27 -0
  5. package/LICENSE-CONTENT +19 -0
  6. package/README.md +33 -0
  7. package/components/AssessmentTest.astro +2 -1
  8. package/components/ChapterNav.astro +2 -2
  9. package/components/Cite.astro +2 -1
  10. package/components/NavContent.astro +2 -2
  11. package/components/PartReview.astro +2 -1
  12. package/components/Rationale.astro +3 -2
  13. package/components/Sidebar.astro +2 -1
  14. package/components/Term.astro +2 -1
  15. package/components/TipsCard.astro +2 -1
  16. package/components/VersionSelector.tsx +39 -36
  17. package/components/WeekRef.astro +2 -1
  18. package/components/XRef.astro +2 -1
  19. package/dist/components/VersionSelector.d.ts +16 -2
  20. package/dist/components/VersionSelector.mjs +18 -17
  21. package/dist/index.d.ts +24 -6
  22. package/dist/index.mjs +89 -5
  23. package/dist/schemas.d.ts +1 -1
  24. package/dist/schemas.mjs +8 -1
  25. package/dist/{types-CWXP1S4b.d.ts → types-CZrkqzpC.d.ts} +58 -26
  26. package/layouts/Base.astro +20 -19
  27. package/package.json +7 -4
  28. package/pages/answers.astro +2 -1
  29. package/pages/chapters.astro +2 -1
  30. package/pages/exercises.astro +2 -1
  31. package/pages/index.astro +4 -3
  32. package/pages/search.astro +2 -1
  33. package/pages/tips.astro +2 -1
  34. package/recipes/01-add-math.md +40 -28
  35. package/recipes/04-component-library.md +14 -4
  36. package/recipes/05-deploy-cloudflare.md +44 -0
  37. package/recipes/06-mobile-first-layout.md +25 -2
  38. package/recipes/09-validation.md +18 -9
  39. package/recipes/15-defining-styles.md +5 -2
  40. package/recipes/19-prevalidate-hook.md +29 -83
  41. package/scripts/build-bib.mjs +7 -3
  42. package/scripts/build-labels.mjs +33 -16
  43. package/scripts/read-env.mjs +25 -0
  44. package/scripts/resolve-book-config.mjs +96 -0
  45. package/scripts/validate.mjs +125 -84
  46. package/src/lib/define-style.ts +15 -6
  47. package/src/lib/nav-href.ts +24 -2
  48. package/styles/layout.css +14 -8
  49. package/styles/tokens.css +8 -10
@@ -11,10 +11,11 @@
11
11
  * `astro dev`. See README for dev-search workflow.
12
12
  */
13
13
  import Base from '../layouts/Base.astro';
14
+ import { normalizeBase } from '../src/lib/nav-href';
14
15
 
15
16
  // #142: Pagefind writes its bundle to dist/pagefind/, served under the deploy
16
17
  // base — so the asset URLs must prefix BASE_URL or they 404 under a non-root base.
17
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
18
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
18
19
  ---
19
20
 
20
21
  <Base
package/pages/tips.astro CHANGED
@@ -10,6 +10,7 @@
10
10
  * page (doesn't fail the build).
11
11
  */
12
12
  import Base from '../layouts/Base.astro';
13
+ import { normalizeBase } from '../src/lib/nav-href';
13
14
 
14
15
  // v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
15
16
  // path. The previous `../../../src/data/tips.json` resolution failed when
@@ -27,7 +28,7 @@ const loadError = tipsEntry
27
28
  : 'src/data/tips.json not found — run `npx book-scaffold build-tips` to generate.';
28
29
 
29
30
  // #142: chapter back-links must prefix the deploy base.
30
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
31
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
31
32
  ---
32
33
  <Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters.">
33
34
  <article class="prose">
@@ -1,27 +1,26 @@
1
1
  # Recipe 01 — Add math to your book (KaTeX)
2
2
 
3
- **Profile**: academic (gated by `BOOK_PROFILE=academic`)
3
+ **Profiles**: academic + research-portfolio (the presets that flag `katex: true`)
4
4
 
5
- **TL;DR**: Math is wired but disabled by default. Set `BOOK_PROFILE=academic` and the build adds `remark-math` + `rehype-katex` (strict mode) with the SSM macro library at `src/lib/katex-macros.ts`.
5
+ **TL;DR**: Math is wired but profile-gated. Compose your book from `academicStyle` (or `researchPortfolioStyle`) and `defineBookConfig` adds `remark-math` + `rehype-katex` (strict mode) with the scaffold's 37-macro `ssmMacros` library. Extend per-book with `defineBookConfig({ katexMacros })` — never by editing package source.
6
6
 
7
7
  ## How it works
8
8
 
9
- The conditional integration lives at the top of `astro.config.mjs`:
10
- - Reads `process.env.BOOK_PROFILE ?? 'minimal'`
11
- - For `academic`: dynamically imports `remark-math`, `rehype-katex`, and `ssmMacros`; adds them to the markdown pipeline
12
- - KaTeX CSS (`katex/dist/katex.min.css`) is always loaded by `Base.astro` academic books need it; minimal/tools books carry the cost (~60 KB) without rendering math (the CSS is inert without matching DOM)
9
+ All the wiring lives inside the package (`defineBookConfig`), not in your config file:
10
+
11
+ - `defineBookConfig` resolves the composed preset from your `styles` chain (falling back to `BOOK_PRESET`/`BOOK_PROFILE` from the environment or `.env`).
12
+ - For katex-flagged presets it dynamically imports `remark-math` + `rehype-katex` and registers them with `strict: 'error'` and the merged macro set (`ssmMacros` + your `katexMacros`).
13
+ - The integration injects `katex/dist/katex.min.css` **only** for katex presets (`package/src/integration.ts`) — tools/minimal books don't carry the ~60 KB.
14
+ - `katex`, `remark-math`, `rehype-katex` are **optional peerDependencies**: math books install them (`npm i katex remark-math rehype-katex`); other profiles skip them entirely.
13
15
 
14
16
  ## Enable math in your book
15
17
 
16
- 1. Set `BOOK_PROFILE=academic` at run time:
17
- ```bash
18
- BOOK_PROFILE=academic npm run dev # local hot-reload
19
- BOOK_PROFILE=academic npm run build # production build
20
- ```
21
- Or persist it in `.env`:
22
- ```
23
- BOOK_PROFILE=academic
18
+ 1. Use a katex preset in `astro.config.mjs` (a fresh `create-book --preset=academic` scaffold already does):
19
+ ```js
20
+ import { defineBookConfig, academicStyle } from '@brandon_m_behring/book-scaffold-astro';
21
+ export default await defineBookConfig({ styles: [academicStyle], site: 'https://…' });
24
22
  ```
23
+ Keep `.env`'s `BOOK_PRESET`/`BOOK_PROFILE` in sync — the content-collection schemas resolve the preset from it.
25
24
 
26
25
  2. Author math in MDX with `$...$` (inline) and `$$...$$` (display):
27
26
  ```mdx
@@ -33,39 +32,52 @@ The conditional integration lives at the top of `astro.config.mjs`:
33
32
  $$
34
33
  ```
35
34
 
36
- 3. Strict mode is on by default (`strict: 'error'` in `rehypeKatex` options). Unknown macros, malformed expressions, and unsupported AMS environments fail the build with a precise error. This is intentional — catch typos at write-time, not in production.
35
+ 3. Strict mode is on by default (`strict: 'error'`). Unknown macros, malformed expressions, and unsupported AMS environments fail the build with a precise error. This is intentional — catch typos at write-time, not in production.
37
36
 
38
37
  ## Macro library
39
38
 
40
- `src/lib/katex-macros.ts` defines 36 macros:
39
+ The package ships `ssmMacros` — **37 macros** (`package/src/lib/katex-macros.ts`):
40
+
41
41
  - 20 SSM-specific from the post-transformers academic reference: `\statevec`, `\statemat`, `\inputmat`, `\outputmat`, `\feedmat`, `\stepsize`, `\discA`, `\discB`, `\seqlen`, `\statedim`, `\inputdim`, `\scanop`, `\elemwise`, `\monodromy`, `\floquet`, `\lyapexp`, `\jacobian`, `\ddt`, `\pderiv`, `\spectralradius`
42
42
  - 16 general math: `\R`, `\C`, `\N`, `\Z`, `\E`, `\Prob`, `\norm`, `\ip`, `\abs`, `\argmax`, `\argmin`, `\diag`, `\tr`, `\spec`, `\rank`, `\bigO`
43
43
  - 1 compatibility alias: `\bm` → `\boldsymbol` (KaTeX doesn't ship `\bm`)
44
44
 
45
- To extend for your book, edit `src/lib/katex-macros.ts` and add new entries to the `ssmMacros` object:
45
+ **Read or spread it** (v4.27.0+, #177) from the main entry, or the `./lib` subpath:
46
+
46
47
  ```ts
47
- export const ssmMacros = {
48
- // existing macros...
49
- '\\mybook': '\\mathbb{B}', // 0-arg macro
50
- '\\myfunc': '\\mathrm{my}(#1)', // 1-arg macro
51
- };
48
+ import { ssmMacros } from '@brandon_m_behring/book-scaffold-astro';
49
+ ```
50
+
51
+ **Extend for your book — the supported path** (#22). `katexMacros` is shallow-merged **on top of** `ssmMacros`, so your entries win on collision:
52
+
53
+ ```js
54
+ export default await defineBookConfig({
55
+ styles: [academicStyle],
56
+ site: 'https://…',
57
+ katexMacros: {
58
+ '\\ate': '\\tau', // 0-arg macro
59
+ '\\propensity': 'e(#1)', // 1-arg macro
60
+ },
61
+ });
52
62
  ```
53
63
 
64
+ Do **not** edit `katex-macros.ts` — it lives inside `node_modules` and your changes vanish on the next install. If a macro is general enough for every book, file an issue instead.
65
+
54
66
  ## Common gotchas
55
67
 
56
68
  - **`\bm{x}` doesn't ship with KaTeX.** The macro library aliases it to `\boldsymbol{x}` (visually identical in stix-two / Computer Modern fonts).
57
69
  - **`\psmallmatrix` not supported by KaTeX.** Convert to `\begin{pmatrix} ... \end{pmatrix}` in your MDX source.
58
- - **Equation auto-numbering across a document is not supported by KaTeX.** Each `$$...$$` block is independent. Use `\tag{N}` for explicit numbering, or a per-chapter remark plugin (deferred; see PROFILES_DESIGN.md §6).
70
+ - **Equation auto-numbering across a document is not supported by KaTeX.** Each `$$...$$` block is independent. Use `\tag{N}` for explicit numbering; a per-chapter remark plugin is tracked as #146.
59
71
  - **`{,}` (LaTeX thousands separator) breaks MDX** — MDX parses `{...}` as a JSX expression. Use `1,000` not `1{,}000`.
60
72
  - **Math inside JSX components needs care.** `<Theorem>$x^2$</Theorem>` works because remark-math runs after MDX parses JSX, but escape any `{` or `}` in arguments.
61
73
 
62
74
  ## Canonical files
63
75
 
64
- - `astro.config.mjs:14-32` — profile branch + plugin wiring
65
- - `src/lib/katex-macros.ts` — full macro library (36 entries)
66
- - `src/layouts/Base.astro:24-30` — KaTeX CSS import
67
- - `package.json` — `katex ^0.16`, `remark-math ^6`, `rehype-katex ^7` deps
76
+ - `package/src/lib/katex-macros.ts` — the 37-entry `ssmMacros` library
77
+ - `package/src/config.ts` — profile-gated remark/rehype wiring + the `katexMacros` merge
78
+ - `package/src/integration.ts` — KaTeX CSS injection for katex presets
79
+ - your `astro.config.mjs` — `styles: [academicStyle]` + optional `katexMacros`
68
80
 
69
81
  ## Reference implementation
70
82
 
71
- [`~/Claude/post_transformers/guides/web/`](../) at commit `111ba26` (math first wired) through `2eaef5d` (current). The reference book has 6 academic chapters using these macros and exercises every edge case the scaffold supports.
83
+ [`~/Claude/post_transformers/guides/web/`](../) the academic reference book has 6 chapters using these macros and exercises every edge case the scaffold supports.
@@ -42,19 +42,29 @@ For full theorem-like environments (proof scaffolding, numbering), use `<Theorem
42
42
 
43
43
  ## Theorem family (academic profile)
44
44
 
45
- `src/components/Theorem.astro` — unified component for nine LaTeX-style environments via the `type` prop:
45
+ `src/components/Theorem.astro` — unified component for nine LaTeX-style environments via the canonical `kind` prop (`type` remains a legacy alias):
46
46
 
47
47
  ```mdx
48
- <Theorem type="theorem" id="thm:zoh-stability" label="ZOH stability">
48
+ <Theorem kind="theorem" id="thm:zoh-stability" name="ZOH stability">
49
49
  The bilinear discretization preserves stability iff $|\lambda \Delta t| < 1$.
50
50
  </Theorem>
51
51
 
52
- <Theorem type="proof">
52
+ <Theorem kind="proof">
53
53
  Direct algebra on the bilinear map.
54
54
  </Theorem>
55
55
  ```
56
56
 
57
- Supported `type` values: `theorem`, `proposition`, `lemma`, `corollary`, `definition`, `example`, `exercise`, `remark`, `proof`. Each gets its own bar color and numbering counter.
57
+ Supported kinds: `theorem`, `proposition`, `lemma`, `corollary`, `definition`, `example`, `exercise`, `remark`, `proof`. Each gets its own bar color. By default all kinds share one amsthm-style sequence, preserving the v4.26-and-earlier contract. To give each kind an independent sequence, set the persistent book/style option:
58
+
59
+ ```js
60
+ export default await defineBookConfig({
61
+ styles: [academicStyle],
62
+ numberStyle: 'per-kind', // or declare it in a composed defineStyle(...)
63
+ site: 'https://example.invalid',
64
+ });
65
+ ```
66
+
67
+ `build-labels` evaluates the real Astro config and records the resolved strategy. An explicit `label=` override is custom and unnumbered, so it consumes no counter in either mode. Omission defaults to `shared`, including legacy projects with no resolvable scaffold integration.
58
68
 
59
69
  ## Utility components (any profile)
60
70
 
@@ -28,6 +28,50 @@
28
28
 
29
29
  URL: `https://<project-name>.<your-account>.workers.dev`.
30
30
 
31
+ ## Default security headers
32
+
33
+ The scaffold integration emits `dist/_headers` at the end of every Astro
34
+ build. Cloudflare Static Assets reads that file for both Workers and Pages
35
+ deployments. The default applies:
36
+
37
+ - `Strict-Transport-Security`
38
+ - `X-Content-Type-Options`
39
+ - `Referrer-Policy`
40
+ - `Permissions-Policy`
41
+ - `Content-Security-Policy`
42
+
43
+ The CSP includes the allowances the shipped book UI needs: inline theme and
44
+ drawer scripts, Astro inline component styles, Pagefind WebAssembly,
45
+ Cloudflare Web Analytics, and images from the book itself, data URIs, or any
46
+ HTTPS origin.
47
+
48
+ Use a consumer file when you need route-specific rules or full ownership:
49
+
50
+ ```text
51
+ # public/_headers
52
+ /private/*
53
+ Cache-Control: no-store
54
+ ```
55
+
56
+ Astro copies `public/_headers` to `dist/_headers`; the integration detects it
57
+ and leaves it unchanged. To retain the four non-CSP defaults but replace the
58
+ entire CSP, configure the book:
59
+
60
+ ```js
61
+ export default await defineBookConfig({
62
+ styles: [academicStyle],
63
+ site: 'https://my-book.example',
64
+ securityHeaders: {
65
+ contentSecurityPolicy:
66
+ "default-src 'self'; img-src 'self' data: https://images.example; object-src 'none'",
67
+ },
68
+ });
69
+ ```
70
+
71
+ That value is a complete replacement, not an additive fragment. Set
72
+ `securityHeaders: false` if the deployment platform or another integration
73
+ owns the headers and you do not ship `public/_headers`.
74
+
31
75
  ## The `cd` prefix for monorepo Astro projects
32
76
 
33
77
  When `wrangler.toml` lives in a subdirectory (e.g. `guides/web/wrangler.toml`), Cloudflare runs commands from the repo root by default. Wrangler can't find its config there → deploy fails with "Could not detect a directory containing static files".
@@ -2,11 +2,11 @@
2
2
 
3
3
  **Profile**: any (layout is profile-agnostic; sidebar groups chapters differently per profile).
4
4
 
5
- **TL;DR**: Three-tier Tufte width (65/80/90ch) + 28/24/26ch sidenote column + left chapter-nav sidebar at ≥1024px. Mobile (<48rem) collapses to single column with inline sidenote asides; sidebar hides. All pure CSS, zero JS.
5
+ **TL;DR**: Three-tier Tufte width (65/80/90ch) + 28/24/26ch sidenote column + left chapter-nav sidebar at ≥1024px. Mobile (<48rem) collapses to single column with inline sidenote asides; sidebar hides. The layout is pure CSS; optional chrome controls hydrate independently.
6
6
 
7
7
  ## The three layers
8
8
 
9
- 1. **Floating chrome** (top-right, `position: fixed`): theme toggle + search + tools-profile islands (`ToolFilter`, `VersionSelector`) when `BOOK_PROFILE !== 'academic'`.
9
+ 1. **Floating chrome** (top-right, `position: fixed`): theme toggle + search + the `ToolFilter` island when `BOOK_PROFILE !== 'academic'`. `VersionSelector` is manual opt-in because its links must come from a real deployment manifest (see below).
10
10
  2. **Left sidebar** (`Sidebar.astro`): chapter nav grouped by part. Sticky to top, ≤100vh, independently scrollable. Hidden below 64rem (1024px).
11
11
  3. **Main content** (`.prose`): Tufte 2-column. Main text at `--measure-main`, sidenote column at `--measure-side`, both responsive to viewport tier.
12
12
 
@@ -45,6 +45,29 @@ Re-tune with Playwright + `browser_take_screenshot` at the four viewports above
45
45
 
46
46
  Default is `showSidebar={true}`. Set it false on full-bleed surfaces — landing pages, splash screens, search results — that have no chapter context (`Base.astro` still emits the page's single `<main>` landmark in that branch). Every chapter route inherits the default (true).
47
47
 
48
+ ## Opt-in version selector
49
+
50
+ `Base.astro` does not mount a version selector or manufacture version URLs. If
51
+ your deployment publishes multiple versions, mount the exported Preact island
52
+ where your book's navigation belongs and pass the real destinations directly:
53
+
54
+ ```astro
55
+ ---
56
+ import VersionSelector from '@brandon_m_behring/book-scaffold-astro/components/VersionSelector';
57
+
58
+ const versions = [
59
+ { href: '/', label: 'Latest', date: '2026-07-13', current: true },
60
+ { href: '/versions/v4.26/', label: 'v4.26', date: '2026-06-30' },
61
+ ];
62
+ ---
63
+
64
+ <VersionSelector versions={versions} client:idle />
65
+ ```
66
+
67
+ Each entry requires `href`, `label`, and `date`; `current` is optional. An
68
+ omitted or empty `versions` array renders nothing, so incomplete deployment
69
+ metadata cannot turn placeholder releases into live navigation.
70
+
48
71
  ## Customizing the sidebar
49
72
 
50
73
  `Sidebar.astro` reads `import.meta.env.BOOK_PROFILE` to decide grouping:
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Profile**: any (Cite checks skip under non-academic).
4
4
 
5
- **TL;DR**: `npm run validate` runs `scripts/validate.mjs` against all chapter MDX files. Catches typo'd bibkeys / XRef ids / Figure paths / internal links that `astro build` would either miss or surface with poor context. Auto-runs as `prebuild`; recommend wiring into pre-commit too.
5
+ **TL;DR**: `npm run validate` runs `scripts/validate.mjs` against all chapter MDX files. It first regenerates missing `labels.json` and `references.json`, then catches typo'd bibkeys / XRef ids / Figure paths / internal links that `astro build` would either miss or surface with poor context. Auto-runs as `prebuild`; recommend wiring into pre-commit too.
6
6
 
7
7
  ## What gets checked
8
8
 
@@ -13,13 +13,19 @@
13
13
  | `<Figure src="/...">` file exists under `public/` | all | Figure.astro emits a broken-image icon for missing files; build doesn't fail. |
14
14
  | `[text](/internal-link)` resolves to known chapter slug or top-level route | all | Astro won't fail on dead internal links. Warning, not error (regex misses dynamic routes). |
15
15
  | `<CodeRef path="..." line={N} />` path exists + line in bounds | all, if `BOOK_REPO_ROOT` set | Catches stale line numbers after code refactors in the paired experiments/ repo. |
16
- | `<Theorem>` has a resolvable `kind=` (or legacy `type=`); an id'd theorem resolves in `labels.json` (#121, #126) | all | An absent kind throws at build with less context; an unindexed id silently renders the heading unnumbered. |
16
+ | `<Theorem>` has a resolvable `kind=` (or legacy `type=`); an id'd theorem resolves in `labels.json`; a literal `n=` agrees with the index (#121, #126, #176) | all | An absent kind throws at build with less context; an unindexed id silently renders the heading unnumbered; a stale literal `n=` contradicts the heading/XRefs. Dynamic expressions and `label=` overrides are skipped. |
17
17
  | `<BookLink book= to=>` both present; `book=` registered in `siblingBooks` (#96) | all | Pre-flights the component's build-time throw across all files at once. |
18
18
  | Questions collection: unique `id`s + `domain` in `examDomains` (#112); `<Rationale appendix>` carries `for=` (#114, v4.21.0) | all, when `src/content/questions/` exists | Duplicate ids break the appendix/flashcards cross-ref key; an unregistered domain throws one-at-a-time at build; an appendix rationale without its anchor target throws at build. |
19
19
  | `los[].anchor` ↔ `{/* anchor: <slug> */}` prose markers agree both ways (#130, v4.20.0) | all, when frontmatter has `los:` | A declared objective whose prose marker is missing/misspelled (or an orphan marker) builds green otherwise — frontmatter↔prose drift only a hand audit would catch. |
20
20
 
21
21
  Validate also emits two **non-blocking shadow-route warnings** (exit code unaffected): a consumer-owned `src/pages/chapters/[...slug].astro` without `routes: { chapters: false }` (v4.6.0, #76), and a consumer-owned `src/pages/index.astro` without `routes: { landing: false }` (v4.20.0, #129 — Astro has announced this collision becomes a hard error). See [recipe 18](./18-chapter-route-ownership.md).
22
22
 
23
+ ## Missing generated artifacts
24
+
25
+ `src/data/labels.json` and `src/data/references.json` are derived and normally gitignored. v4.27+ `validate` self-heals either missing file unconditionally by invoking the package's own `build-labels` or `build-bib` command before loading data. Existing files are untouched. A failed child command stops validation with the child's original diagnostic and exit status.
26
+
27
+ `build-bib` resolves `BOOK_BIB_PATH` from the process environment first, then the project-root `.env`, then `./bibliography.bib`. A project with no bibliography still gets a deterministic empty `references.json`.
28
+
23
29
  ## What is NOT checked (already covered elsewhere)
24
30
 
25
31
  - **Frontmatter Zod validation** — `astro build` syncs content collections first; Zod errors there.
@@ -34,8 +40,9 @@ The validator's job is to fill the gaps `astro build` leaves, not duplicate it.
34
40
  // package.json
35
41
  {
36
42
  "scripts": {
37
- "validate": "node scripts/validate.mjs",
38
- "prebuild": "npm run build:assets && npm run validate"
43
+ "prevalidate": "npm run build:bib --if-present && npm run build:labels --if-present",
44
+ "validate": "book-scaffold validate",
45
+ "prebuild": "npm run validate --if-present"
39
46
  }
40
47
  }
41
48
  ```
@@ -61,7 +68,9 @@ For pre-commit: add to `.pre-commit-config.yaml`:
61
68
 
62
69
  ## Preset / chaptersBase resolution (v4.7.0+, #75)
63
70
 
64
- The validator resolves both `preset` and `chaptersBase` by consulting multiple sources in a documented order. Notable v4.7.0 addition: the v4.5+ canonical form
71
+ The validator evaluates `astro.config.*` through Vite first. A resolved scaffold integration supplies the composed preset and `numberStyle`, so CLI tooling sees the same Style chain as the Astro build. Without such an integration, the legacy preset chain remains: `--preset` process `BOOK_PRESET`/`BOOK_PROFILE` root `.env` the literal value in `defineBookSchemas` → a warned `minimal` v4 compatibility fallback. Every selected value is checked against the five-preset enum; config-evaluation failures stop validation instead of silently using defaults.
72
+
73
+ `chaptersBase` resolution still consults `BOOK_CHAPTERS_DIR`, content configuration, then `src/content/chapters`. The v4.5+ canonical form is:
65
74
 
66
75
  ```ts
67
76
  // src/content.config.ts
@@ -80,13 +89,13 @@ Full precedence chain documented in [`PACKAGE_DESIGN.md §8 — Preset + chapter
80
89
  Exit code = total error count. On success:
81
90
 
82
91
  ```
83
- validate: ✓ 6 chapter(s) checked (profile=academic); no errors.
92
+ validate: ✓ 6 chapter(s) checked (profile=academic, number-style=shared); no errors.
84
93
  ```
85
94
 
86
95
  On failure, all issues listed at once with `file:line msg`:
87
96
 
88
97
  ```
89
- validate: ✗ 17 error(s) in 6 chapter(s) (profile=academic):
98
+ validate: ✗ 17 error(s) in 6 chapter(s) (profile=academic, number-style=shared):
90
99
  week05.mdx:102 Unknown XRef id "w4:prop:zoh-stability" — not in labels.json
91
100
  week11.mdx:37 Unknown XRef id "ch:week13" — not in labels.json
92
101
  ...
@@ -115,13 +124,13 @@ Keep the script regex-based; resist the urge to pull in MDX AST parsing. The scr
115
124
 
116
125
  - **Regex false negatives**: multi-line `<Cite\n key="...">` won't match. Authors should keep component attributes on one line; the build catches the residual cases.
117
126
  - **`<Figure src>` with `BOOK_FIGURES_PATH` override**: the validator checks `public/figures/<...>` (post-build location), not the source `figures/` directory. Run `npm run build:figures` before `npm run validate` if assets are stale.
118
- - **`labels.json` empty**: every XRef fires an error. Until you have a labels-building step (Phase 2.6 in post-transformers, deferred at scaffold v2.0), avoid `<XRef>` in chapter content — use direct markdown links instead.
127
+ - **`labels.json` exists but is stale**: self-healing only regenerates missing artifacts. Run `npm run build:labels` after changing IDs, kinds, slugs, or `numberStyle`.
119
128
 
120
129
  ## Canonical files
121
130
 
122
131
  - `scripts/validate.mjs` — the validator
123
132
  - `src/data/references.json` — emitted by `scripts/build-bib.mjs` (recipe 02)
124
- - `src/data/labels.json` — placeholder; populated by future labels-building step
133
+ - `src/data/labels.json` — emitted by `scripts/build-labels.mjs`
125
134
  - `src/components/{Cite,XRef,Figure,CodeRef}.astro` — components whose contracts validate.mjs enforces
126
135
 
127
136
  ## Reference implementation
@@ -46,7 +46,7 @@ defineStyle({
46
46
  extraIntegrations?: readonly AstroIntegration[];
47
47
  mdxComponentsModule?: string;
48
48
  markdown?: AstroUserConfig['markdown'];
49
- deploy?: 'pages' | 'workers'; // v4.0.0 NEW (#50)
49
+ deploy?: 'pages' | 'workers'; // RESERVED (#50, #180) — no runtime effect; wrangler shape is set at scaffold time by profile
50
50
  extra?: Record<string, unknown>; // scoped consumer-side metadata
51
51
  });
52
52
  ```
@@ -158,7 +158,10 @@ defineBookConfig({ styles: [researchPortfolioStyle], ... });
158
158
  defineBookConfig({ styles: [BUILTIN_STYLES['research-portfolio']], ... });
159
159
  ```
160
160
 
161
- Each built-in style has a `name` matching its preset, a `preset` field, and sane `deploy` defaults (academic/tools/minimal → 'workers'; course-notes/research-portfolio → 'pages').
161
+ Each built-in style has a `name` matching its preset and a `preset` field. Its
162
+ historical `deploy` value is reserved metadata only; it does not alter a
163
+ generated or existing deployment. `create-book --preset` selects the initial
164
+ `wrangler.toml`, after which the consumer owns that file (#180).
162
165
 
163
166
  ---
164
167
 
@@ -1,113 +1,59 @@
1
- # Recipe 19 — `prevalidate` npm hook (v4.6.0+)
1
+ # Recipe 19 — generated data before validation (v4.27+)
2
2
 
3
- `book-scaffold validate` checks `<Cite key="...">` against `src/data/references.json` (academic profile) and `<XRef id="...">` against `src/data/labels.json`. Both JSON files are **derived artifacts** regenerated from `bibliography.bib` + chapter MDX by `book-scaffold build-bib` + `book-scaffold build-labels`. Both are gitignored.
3
+ `book-scaffold validate` checks citations and cross-references against two derived, normally gitignored files:
4
4
 
5
- When `npm run validate` runs **standalone** (e.g., a reusable deploy workflow runs only the validate command, without the full `npm run build` chain), the prereq scripts don't fire and the validator chokes on apparently-missing keys.
5
+ - `src/data/references.json`, emitted by `book-scaffold build-bib`
6
+ - `src/data/labels.json`, emitted by `book-scaffold build-labels`
6
7
 
7
- The `prevalidate` npm lifecycle hook is the canonical fix: it auto-runs the prereqs whenever `npm run validate` is invoked, regardless of how validate is called.
8
-
9
- ---
10
-
11
- ## TL;DR
12
-
13
- ```json
14
- {
15
- "scripts": {
16
- "prevalidate": "npm run build:bib && npm run build:labels --if-present",
17
- "validate": "book-scaffold validate"
18
- }
19
- }
20
- ```
21
-
22
- `npm run validate` → automatically runs `prevalidate` first (build:bib + build:labels) → then runs `validate`. CI and local behave identically; no separate `ci:validate` wrapper script needed.
23
-
24
- ---
25
-
26
- ## Why the workaround was needed
27
-
28
- `brandon-behring/deploy-workflows@v1` (the reusable Cloudflare Workers deploy) runs `npm run <validate-command>` between `npm ci` and `npm run build`. Without the `prevalidate` hook OR an explicit wrapper script, the validate step ran against missing artifacts.
29
-
30
- The Phase 1c first-deploy of `ssm-foundations` (2026-05-26) hit this — validate emitted 25+ "Unknown bibkey" errors that pointed at chapter content instead of the missing `references.json` artifact. The deploy-time fix shipped as a `ci:validate` wrapper:
8
+ Every profile can use theorem/Figure IDs and XRefs, so every generated book now carries the same npm lifecycle:
31
9
 
32
10
  ```json
33
11
  {
34
12
  "scripts": {
35
- "ci:validate": "npm run build:bib && npm run build:labels --if-present && npm run validate"
13
+ "prevalidate": "npm run build:bib --if-present && npm run build:labels --if-present",
14
+ "validate": "book-scaffold validate",
15
+ "prebuild": "npm run validate --if-present"
36
16
  }
37
17
  }
38
18
  ```
39
19
 
40
- …with `validate-command: ci:validate` in `.github/workflows/deploy.yml`. This worked but introduced consumer-side ceremony for what is structurally a scaffold-level convention.
41
-
42
- The cleaner long-term fix is the `prevalidate` npm-lifecycle hook (this recipe). v4.6.0's `create-book` automatically scaffolds it for academic + research-portfolio profiles.
43
-
44
- ---
20
+ `npm run validate` runs `prevalidate` automatically. `npm run build` runs `prebuild`, which delegates to that same path. Academic, tools, minimal, course-notes, and research-portfolio scaffolds use this identical contract.
45
21
 
46
- ## Migration: from `ci:validate` to `prevalidate`
22
+ ## Direct CLI calls self-heal too
47
23
 
48
- Existing consumers (DML, ssm, dlai when it ships) that adopted the `ci:validate` wrapper during 2026-05-26 deploys can migrate when they bump to scaffold `^4.6.0`. Three-file mechanical change per consumer:
24
+ An npm hook cannot protect `npx book-scaffold validate` or a direct bin invocation. Starting in v4.27, the validator itself regenerates each missing artifact before loading it:
49
25
 
50
- ### 1. `package.json` rename `ci:validate` → `prevalidate`
26
+ 1. Missing `labels.json` invokes `build-labels`.
27
+ 2. Missing `references.json` invokes `build-bib`, even when no default `bibliography.bib` exists; that valid no-bibliography case emits `{}`.
28
+ 3. Existing files are not rewritten.
29
+ 4. A child failure stops validation and preserves the original child diagnostic and non-zero status.
51
30
 
52
- ```diff
53
- {
54
- "scripts": {
55
- - "ci:validate": "npm run build:bib && npm run build:labels --if-present && npm run validate",
56
- + "prevalidate": "npm run build:bib && npm run build:labels --if-present",
57
- "validate": "book-scaffold validate"
58
- }
59
- }
60
- ```
31
+ This makes fresh checkouts deterministic without hiding stale existing data. After changing theorem IDs, kinds, slugs, bibliography entries, or `numberStyle`, explicitly rerun the corresponding build command.
61
32
 
62
- Note the renamed script no longer needs to call `validate` itself — npm's lifecycle invokes it automatically after `prevalidate` completes.
33
+ ## Bibliography path precedence
63
34
 
64
- ### 2. `.github/workflows/deploy.yml` revert `validate-command`
35
+ Books with a shared or non-root bibliography can set:
65
36
 
66
- ```diff
67
- jobs:
68
- deploy:
69
- uses: brandon-behring/deploy-workflows/.github/workflows/deploy-astro-worker.yml@v2
70
- secrets: inherit
71
- with:
72
- working-directory: web
73
- - validate-command: ci:validate
74
- + validate-command: validate
75
- enable-pr-previews: true
37
+ ```dotenv
38
+ BOOK_BIB_PATH=../shared/references.bib
76
39
  ```
77
40
 
78
- The reusable workflow's `validate-command` now points at the native `validate` script; the `prevalidate` hook handles its own prereqs transparently.
41
+ `build-bib` resolves the process environment first, then the project-root `.env`, then `./bibliography.bib`. Relative paths are resolved from the book root. This same precedence applies when validation invokes `build-bib` during self-healing.
79
42
 
80
- ### 3. Simplify `prebuild` (optional)
43
+ ## Migrating older consumers
81
44
 
82
- If the consumer's `prebuild` still has the long chain:
45
+ Replace custom `ci:validate` wrappers and profile-conditional hooks with the uniform scripts above. A reusable deploy workflow can call the normal `validate` script; no wrapper needs to repeat the artifact chain.
83
46
 
84
47
  ```diff
85
48
  {
86
49
  "scripts": {
87
- - "prebuild": "npm run build:bib --if-present && npm run build:labels --if-present && npm run validate --if-present",
88
- + "prebuild": "npm run validate --if-present",
89
- "build": "astro build && pagefind --site dist"
50
+ - "ci:validate": "npm run build:bib && npm run build:labels && npm run validate",
51
+ + "prevalidate": "npm run build:bib --if-present && npm run build:labels --if-present",
52
+ "validate": "book-scaffold validate",
53
+ - "prebuild": "npm run build:bib && npm run build:labels && npm run validate"
54
+ + "prebuild": "npm run validate --if-present"
90
55
  }
91
56
  }
92
57
  ```
93
58
 
94
- Now `npm run build` triggers `prebuild` (which runs `validate`) triggers `prevalidate` (which runs the prereqs) validate runs cleanly. Single source of truth for the prereq chain.
95
-
96
- ---
97
-
98
- ## When `prevalidate` is NOT needed
99
-
100
- Profiles that don't run cite-key or XRef validation don't need `prevalidate`. Specifically:
101
-
102
- - `tools`, `minimal`: no `<Cite>` resolution against `references.json`.
103
- - `course-notes`: depends on whether the consumer uses `<Cite>` (some do for source-tier attribution).
104
-
105
- For these profiles, `book-scaffold validate` operates on chapter structure + XRef IDs only; `prevalidate` would be a no-op. The v4.6.0 create-book template adds `prevalidate` ONLY for academic + research-portfolio.
106
-
107
- ---
108
-
109
- ## Why this matters
110
-
111
- Recipe 19 is part of [issue #76](https://github.com/brandon-behring/book-scaffold-astro/issues/76)'s v4.6 bundle. Companion: [recipe 18 — chapter-route ownership](./18-chapter-route-ownership.md), which fixes another silent-CI surface from the same Phase 1c first-deploys.
112
-
113
- The `prevalidate` convention also closes [issue #77](https://github.com/brandon-behring/book-scaffold-astro/issues/77) — the v4.6 validator now emits a single re-framed error pointing at this recipe when `references.json` is missing, replacing the noisy 25-symptom output.
59
+ The historical profile split is intentionally gone: non-academic profiles still need labels, and an empty bibliography build is a supported no-op that creates the importable empty artifact.
@@ -39,6 +39,7 @@
39
39
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
40
40
  import { dirname, resolve } from 'node:path';
41
41
  import { fileURLToPath } from 'node:url';
42
+ import { readEnvFile } from './read-env.mjs';
42
43
 
43
44
  // --help / -h: non-mutating (closes #14).
44
45
  const USAGE = `Usage: book-scaffold build-bib
@@ -49,7 +50,8 @@ AND sources/manifest.yaml -> src/data/sources.json (tools-profile sources for
49
50
  the /references page). Either input may be absent.
50
51
 
51
52
  Env:
52
- BOOK_BIB_PATH Override path to .bib file (default: ./bibliography.bib).
53
+ BOOK_BIB_PATH Override path to .bib file (process env wins, then root
54
+ .env; default: ./bibliography.bib).
53
55
 
54
56
  Options:
55
57
  --help, -h Print this message and exit (non-mutating).
@@ -64,11 +66,13 @@ import '@citation-js/plugin-bibtex';
64
66
 
65
67
  const __dirname = dirname(fileURLToPath(import.meta.url));
66
68
  const PROJECT_ROOT = process.cwd();
69
+ const dotenv = readEnvFile(PROJECT_ROOT);
67
70
 
68
71
  // Default: bibliography.bib at scaffold root.
69
72
  // Override via BOOK_BIB_PATH=path/to/your.bib (absolute or relative to cwd).
70
- const BIB_PATH = process.env.BOOK_BIB_PATH
71
- ? resolve(process.cwd(), process.env.BOOK_BIB_PATH)
73
+ const configuredBibPath = process.env.BOOK_BIB_PATH ?? dotenv.BOOK_BIB_PATH;
74
+ const BIB_PATH = configuredBibPath
75
+ ? resolve(PROJECT_ROOT, configuredBibPath)
72
76
  : resolve(PROJECT_ROOT, 'bibliography.bib');
73
77
  const OUT_PATH = resolve(PROJECT_ROOT, 'src/data/references.json');
74
78
  const SOURCES_PATH = resolve(PROJECT_ROOT, 'sources/manifest.yaml');