@brandon_m_behring/book-scaffold-astro 3.3.0 → 3.5.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/dist/schemas.mjs CHANGED
@@ -77,6 +77,7 @@ var toolsChapterSchema = z.object({
77
77
  updated: z.date().optional()
78
78
  });
79
79
  var minimalChapterSchema = toolsChapterSchema;
80
+ var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
80
81
  var courseNotesChapterSchema = z.object({
81
82
  // Identity
82
83
  title: z.string().min(1),
@@ -102,6 +103,59 @@ var courseNotesChapterSchema = z.object({
102
103
  sources: z.array(z.string()).default([]),
103
104
  draft: z.boolean().default(false)
104
105
  });
106
+ var researchPortfolioChapterSchema = z.object({
107
+ // Identity
108
+ title: z.string().min(1),
109
+ slug: z.string().optional(),
110
+ // explicit slug override (otherwise filename)
111
+ description: z.string().optional(),
112
+ // Hierarchy — accept either academic-style or tools-style; all optional.
113
+ // The academic 'part' field is a string enum; tools 'part' is a number.
114
+ // Use z.union to permit either type.
115
+ part: z.union([z.number().int().min(0).max(20), z.string()]).optional(),
116
+ week: z.number().int().min(0).max(99).optional(),
117
+ chapter: z.number().int().min(0).max(99).optional(),
118
+ // Academic-style status (optional for research-portfolio — books may track
119
+ // chapters as 'prose_only' / 'experimental-result' / etc.).
120
+ status: z.enum([
121
+ "implemented",
122
+ "chapter_only",
123
+ "reading_only",
124
+ "prose_only",
125
+ "code_only",
126
+ "scaffolded",
127
+ "planned"
128
+ ]).optional(),
129
+ // Research-portfolio specific: nature of the chapter's content.
130
+ // Distinct from academic's 'status' (which tracks authoring state) — this
131
+ // describes the EVIDENCE TYPE the chapter rests on.
132
+ freshness: z.enum([
133
+ "experimental-result",
134
+ // primary data the author produced
135
+ "literature-survey",
136
+ // synthesis of others' work
137
+ "theoretical",
138
+ // analytical / mathematical argument
139
+ "reference"
140
+ // canonical material (definitions, taxonomy)
141
+ ]).optional(),
142
+ // Provenance (tools-style — overlap with tools/course-notes profiles).
143
+ volatility: z.enum(volatilityLevels).optional(),
144
+ tags: z.array(z.string()).default([]),
145
+ // freeform; replaces tools_compared
146
+ // Structured inline sources with T1-T4 tiers.
147
+ sources: z.array(
148
+ z.object({
149
+ tier: z.enum(sourceTiersResearch),
150
+ url: z.string().url(),
151
+ label: z.string().min(1)
152
+ })
153
+ ).default([]),
154
+ // Status + dates.
155
+ last_verified: z.date(),
156
+ updated: z.date().optional(),
157
+ draft: z.boolean().default(false)
158
+ });
105
159
  var sourcesSchema = z.object({
106
160
  url: z.string().url(),
107
161
  title: z.string().min(1),
@@ -148,8 +202,10 @@ var academicProfile = defineProfile({
148
202
  print: true,
149
203
  chapters: false,
150
204
  // academic consumers ship their own week-based /chapters listing
151
- convergence: false
205
+ convergence: false,
152
206
  // tools-profile-specific
207
+ frontmatter: false
208
+ // opt-in per book; see #7
153
209
  },
154
210
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
155
211
  katex: true
@@ -165,8 +221,10 @@ var toolsProfile = defineProfile({
165
221
  print: true,
166
222
  chapters: true,
167
223
  // tools profile ships a flat chapter index
168
- convergence: true
224
+ convergence: true,
169
225
  // tools profile ships convergence dashboard
226
+ frontmatter: false
227
+ // opt-in per book; see #7
170
228
  },
171
229
  styles: [
172
230
  "tokens.css",
@@ -189,7 +247,9 @@ var minimalProfile = defineProfile({
189
247
  search: true,
190
248
  print: true,
191
249
  chapters: false,
192
- convergence: false
250
+ convergence: false,
251
+ frontmatter: false
252
+ // opt-in per book; see #7
193
253
  },
194
254
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
195
255
  });
@@ -204,22 +264,46 @@ var courseNotesProfile = defineProfile({
204
264
  print: true,
205
265
  chapters: false,
206
266
  // multi-book consumers route via [book]/[slug] themselves
207
- convergence: false
267
+ convergence: false,
268
+ frontmatter: false
269
+ // opt-in per book; see #7
208
270
  },
209
271
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
210
272
  });
211
273
 
274
+ // src/profiles/research-portfolio.ts
275
+ var researchPortfolioProfile = defineProfile({
276
+ name: "research-portfolio",
277
+ schema: researchPortfolioChapterSchema,
278
+ routes: {
279
+ references: true,
280
+ search: true,
281
+ print: true,
282
+ chapters: false,
283
+ // portfolio books ship their own landing/index
284
+ convergence: false,
285
+ // tools-profile-specific
286
+ frontmatter: true
287
+ // portfolios universally need title/disclosure/banner pages
288
+ },
289
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
290
+ katex: true
291
+ // math is common in research content
292
+ });
293
+
212
294
  // src/profiles/index.ts
213
295
  var PROFILES = {
214
296
  academic: academicProfile,
215
297
  tools: toolsProfile,
216
298
  minimal: minimalProfile,
217
- "course-notes": courseNotesProfile
299
+ "course-notes": courseNotesProfile,
300
+ "research-portfolio": researchPortfolioProfile
218
301
  };
219
302
  var BOOK_PROFILES = Object.keys(PROFILES);
220
303
 
221
304
  // src/types.ts
222
305
  import { existsSync, readFileSync } from "fs";
306
+ var BOOK_PRESETS = BOOK_PROFILES;
223
307
  var BookConfigError = class extends Error {
224
308
  constructor(message) {
225
309
  super(message);
@@ -244,35 +328,45 @@ function readEnvFile(path = ".env") {
244
328
  return {};
245
329
  }
246
330
  }
247
- function resolveProfile(explicit) {
248
- let candidate = explicit ?? process.env.BOOK_PROFILE;
331
+ function resolvePreset(explicitPreset, explicitProfile) {
332
+ let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
249
333
  let source = "default";
250
- if (explicit) source = "param";
251
- else if (process.env.BOOK_PROFILE) source = "env";
334
+ if (explicitPreset || explicitProfile) source = "param";
335
+ else if (process.env.BOOK_PRESET || process.env.BOOK_PROFILE) source = "env";
252
336
  if (!candidate) {
253
- const fromFile = readEnvFile().BOOK_PROFILE;
337
+ const env = readEnvFile();
338
+ const fromFile = env.BOOK_PRESET ?? env.BOOK_PROFILE;
254
339
  if (fromFile) {
255
340
  candidate = fromFile;
256
341
  source = "dotenv";
257
342
  }
258
343
  }
259
344
  candidate = candidate ?? "minimal";
260
- if (!BOOK_PROFILES.includes(candidate)) {
345
+ if (!BOOK_PRESETS.includes(candidate)) {
261
346
  throw new BookConfigError(
262
- `profile must be one of ${BOOK_PROFILES.join(" | ")} (got ${JSON.stringify(candidate)})`
347
+ `preset must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(candidate)})`
263
348
  );
264
349
  }
265
350
  if (source === "default") {
266
- console.warn("book-scaffold-astro: BOOK_PROFILE not set; falling back to 'minimal'.");
351
+ console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
267
352
  }
268
353
  return candidate;
269
354
  }
270
355
 
271
356
  // src/schemas-entry.ts
357
+ function frontmatterCollection(schema, base = "./src/content/frontmatter") {
358
+ return defineCollection({
359
+ loader: glob({
360
+ pattern: ["**/*.{md,mdx}", "!**/_*"],
361
+ base
362
+ }),
363
+ schema
364
+ });
365
+ }
272
366
  function defineBookSchemas(opts = {}) {
273
- const profile = resolveProfile(opts.profile);
367
+ const profile = resolvePreset(opts.preset, opts.profile);
274
368
  const chaptersBase = opts.chaptersBase ?? "./src/content/chapters";
275
- const schemaForProfile = profile === "academic" ? academicChapterSchema : profile === "course-notes" ? courseNotesChapterSchema : profile === "minimal" ? minimalChapterSchema : toolsChapterSchema;
369
+ const schemaForProfile = profile === "academic" ? academicChapterSchema : profile === "course-notes" ? courseNotesChapterSchema : profile === "research-portfolio" ? researchPortfolioChapterSchema : profile === "minimal" ? minimalChapterSchema : toolsChapterSchema;
276
370
  const chapters = defineCollection({
277
371
  loader: glob({
278
372
  // Exclude underscore-prefixed files (standard "hidden" convention).
@@ -305,5 +399,6 @@ function defineBookSchemas(opts = {}) {
305
399
  return { collections };
306
400
  }
307
401
  export {
308
- defineBookSchemas
402
+ defineBookSchemas,
403
+ frontmatterCollection
309
404
  };
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: "Chapter N — Title goes here"
3
+ slug: chN-short-slug
4
+ chapter: 1
5
+ part: 1
6
+ week: 1 # optional; omit if not on a weekly cadence
7
+ status: prose_only # 'prose_only' | 'code_only' | 'implemented' | ...
8
+ freshness: experimental-result # 'experimental-result' | 'literature-survey' | 'theoretical' | 'reference'
9
+ volatility: feature-surface # 'stable-principle' | 'architectural-pattern' | 'feature-surface'
10
+ tags:
11
+ - replace-me
12
+ - with
13
+ - real-tags
14
+ sources:
15
+ - tier: T1
16
+ url: https://example.invalid/primary-source
17
+ label: Primary source (e.g., NVD CVE / arXiv paper / official spec)
18
+ - tier: T2
19
+ url: https://example.invalid/secondary
20
+ label: Secondary corroboration
21
+ last_verified: 2026-05-19
22
+ draft: true
23
+ ---
24
+
25
+ import PreReleaseBanner from '@brandon_m_behring/book-scaffold-astro/components/PreReleaseBanner.astro';
26
+ import PolicyRef from '@brandon_m_behring/book-scaffold-astro/components/PolicyRef.astro';
27
+ import AICollaborationDisclosure from '@brandon_m_behring/book-scaffold-astro/components/AICollaborationDisclosure.astro';
28
+ import BlockedByCallout from '@brandon_m_behring/book-scaffold-astro/components/BlockedByCallout.astro';
29
+ import Theorem from '@brandon_m_behring/book-scaffold-astro/components/Theorem.astro';
30
+ import Sidenote from '@brandon_m_behring/book-scaffold-astro/components/Sidenote.astro';
31
+ import Cite from '@brandon_m_behring/book-scaffold-astro/components/Cite.astro';
32
+
33
+ <PreReleaseBanner state="alpha" />
34
+
35
+ ## Introduction
36
+
37
+ This chapter template exercises the four research-portfolio-specific components.
38
+ Delete sections that don't apply to your chapter.
39
+
40
+ ## Section: a result with provenance
41
+
42
+ Per <Cite key="example2024" />, the result holds under conditions $\Phi \subset \mathbb{R}^n$.
43
+ <Sidenote>Sidenotes float to the right margin on desktop and inline below 768px.</Sidenote>
44
+
45
+ <Theorem kind="theorem" n="1" name="Existence">
46
+ Let $f: X \to Y$ be a map satisfying ... Then there exists ...
47
+ </Theorem>
48
+
49
+ ## Section: policy + AI disclosure references
50
+
51
+ See <PolicyRef file="ETHICS.md" section="§1 Dual-use disclosure" label="our ethics policy" />
52
+ for the dual-use review process applied to this chapter.
53
+
54
+ <AICollaborationDisclosure
55
+ model="Claude Opus 4.7 (Anthropic)"
56
+ role="research collaborator + writing collaborator"
57
+ commit_attribution="Co-Authored-By: Claude <noreply@anthropic.com>"
58
+ >
59
+ All factual claims and numeric results in this chapter independently verified
60
+ by the human author; AI contributions reviewed line-by-line before merge.
61
+ </AICollaborationDisclosure>
62
+
63
+ ## Section: an upstream blocker
64
+
65
+ <BlockedByCallout
66
+ upstream="dataset X v2.0"
67
+ url="https://example.invalid/dataset-x/issues/42"
68
+ reason="full evaluation suite (current: smoke subset only)"
69
+ unblockedAt="2026-Q3"
70
+ >
71
+ This section's numerical claims will be re-verified once dataset X v2.0 ships
72
+ with the missing splits. Current numbers are from the smoke subset and should
73
+ be treated as upper bounds.
74
+ </BlockedByCallout>
75
+
76
+ ## Section: conclusion
77
+
78
+ Wrap up here. Common pattern: 1-2 sentences summarizing the result, 1 sentence
79
+ on limitations, 1 sentence pointing at the next chapter.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@brandon_m_behring/book-scaffold-astro",
3
3
  "description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
4
- "version": "3.3.0",
4
+ "version": "3.5.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -41,6 +41,8 @@
41
41
  "import": "./dist/schemas.mjs"
42
42
  },
43
43
  "./package.json": "./package.json",
44
+ "./components/AICollaborationDisclosure.astro": "./components/AICollaborationDisclosure.astro",
45
+ "./components/BlockedByCallout.astro": "./components/BlockedByCallout.astro",
44
46
  "./components/CaseStudy.astro": "./components/CaseStudy.astro",
45
47
  "./components/ChapterHeader.astro": "./components/ChapterHeader.astro",
46
48
  "./components/ChapterNav.astro": "./components/ChapterNav.astro",
@@ -63,6 +65,8 @@
63
65
  "./components/OpenQuestion.astro": "./components/OpenQuestion.astro",
64
66
  "./components/PaperBox.astro": "./components/PaperBox.astro",
65
67
  "./components/PatternTimeline.astro": "./components/PatternTimeline.astro",
68
+ "./components/PolicyRef.astro": "./components/PolicyRef.astro",
69
+ "./components/PreReleaseBanner.astro": "./components/PreReleaseBanner.astro",
66
70
  "./components/Recovery.astro": "./components/Recovery.astro",
67
71
  "./components/ResultBox.astro": "./components/ResultBox.astro",
68
72
  "./components/Sidebar.astro": "./components/Sidebar.astro",
@@ -114,7 +118,8 @@
114
118
  "examples",
115
119
  "CLAUDE.md",
116
120
  "README.md",
117
- "LATEX_TO_MDX_MAPPING.md"
121
+ "LATEX_TO_MDX_MAPPING.md",
122
+ "examples"
118
123
  ],
119
124
  "scripts": {
120
125
  "build": "tsup && rm -f dist/types-*.d.ts",
@@ -0,0 +1,48 @@
1
+ ---
2
+ /**
3
+ * pages/frontmatter/[...slug].astro — auto-injected route for the
4
+ * consumer-defined `frontmatter` content collection. v3.4.0 closes #7.
5
+ *
6
+ * Opt-in: consumer enables via defineBookConfig({ routes: { frontmatter: true } })
7
+ * AND defines the collection in src/content.config.ts via the
8
+ * `frontmatterCollection(schema)` helper. Drops MDX files under
9
+ * src/content/frontmatter/; each renders at /frontmatter/<slug>/.
10
+ *
11
+ * Why a single template route (not consumer-owned): the rendering shape
12
+ * is uniform (Base layout + prose + Content) — every consumer would write
13
+ * the same file. Centralizing it keeps the consumer-side surface to just
14
+ * the schema definition + the routes-toggle flip.
15
+ *
16
+ * mdx-components plumbing (issue #2): the consumer's src/mdx-components.ts
17
+ * components are imported via the virtual module and threaded through
18
+ * <Content components={mdxComponents} />, so custom MDX components render
19
+ * here exactly as they do on /print and chapter routes.
20
+ */
21
+ import { getCollection, render } from 'astro:content';
22
+ import Base from '../../layouts/Base.astro';
23
+ import mdxComponents from 'virtual:book-scaffold/mdx-components';
24
+
25
+ export async function getStaticPaths() {
26
+ const entries = await getCollection('frontmatter');
27
+ return entries.map((entry) => ({
28
+ params: { slug: entry.id },
29
+ props: { entry },
30
+ }));
31
+ }
32
+
33
+ const { entry } = Astro.props;
34
+ const { Content } = await render(entry);
35
+
36
+ // The schema is consumer-defined; we read defensively to avoid crashes
37
+ // if the consumer skipped title/description fields. The frontmatterCollection
38
+ // helper documents the recommended shape.
39
+ const data = entry.data as Record<string, unknown>;
40
+ const title = typeof data.title === 'string' ? data.title : entry.id;
41
+ const description = typeof data.description === 'string' ? data.description : undefined;
42
+ ---
43
+
44
+ <Base title={title} description={description ?? ''}>
45
+ <article class="prose frontmatter-page">
46
+ <Content components={mdxComponents} />
47
+ </article>
48
+ </Base>
@@ -0,0 +1,58 @@
1
+ # Recipe 12 — Where to file issues (consumer-driven evolution)
2
+
3
+ This toolkit grows through cross-consumer dogfooding. Each new book project you stand up — academic curriculum, AI-CLI comparison, course notes, research portfolio, or something new — is both content work *and* a structured test of the scaffold's abstraction.
4
+
5
+ ## When to file an issue
6
+
7
+ File against [`brandon-behring/book-scaffold-astro/issues`](https://github.com/brandon-behring/book-scaffold-astro/issues) when:
8
+
9
+ - The scaffold's current schemas don't fit your book's content shape (e.g. course notes needing freeform `tags` instead of the `tools_compared` enum).
10
+ - An auto-injected route conflicts with your book's URL structure (e.g. multi-book corpus that routes via `[book]/[slug]/`).
11
+ - A scaffold-injected route can't render your custom MDX components (e.g. you have `<AnkiCard>` that needs to appear on `/print`).
12
+ - A CLI subcommand crashes or behaves unexpectedly (e.g. `validate` reports zero chapters).
13
+ - A scaffold component you rebuilt has an exact equivalent already shipped (waste signal — file as `docs: missing in LATEX_TO_MDX_MAPPING.md`).
14
+ - An API decision blocks one of your downstream projects.
15
+
16
+ ## Issue shape
17
+
18
+ Mirror the pattern used by issues [#1–#14](https://github.com/brandon-behring/book-scaffold-astro/issues?q=is%3Aissue+sort%3Acreated-desc):
19
+
20
+ ```markdown
21
+ ## Problem
22
+ <observed behavior + repro steps + which consumer surfaced it>
23
+
24
+ ## Evidence
25
+ <command output, file paths, version pin (`npm view @brandon_m_behring/book-scaffold-astro version`)>
26
+
27
+ ## Suggested fix
28
+ <one or more concrete options; trade-offs noted>
29
+
30
+ ## Acceptance criteria
31
+ <bulleted checklist a reviewer can verify>
32
+ ```
33
+
34
+ Label with `bug` / `enhancement` / `documentation`. Reference the consumer repo + line where the friction was hit.
35
+
36
+ ## Why this matters (the loop)
37
+
38
+ Each batch of cross-consumer issues drives a minor toolkit release:
39
+
40
+ - **v3.0–v3.2** absorbed Phase B/C/D feedback from `post_transformers` + `book-template-astro`.
41
+ - **v3.3.0** closed 5 issues surfaced from the DLAI knowledge-graphs-rag pilot (course-notes profile + defineMdxComponents + per-route override + LaTeX migration doc).
42
+ - **v3.4.0** closed 8 more (preset vocabulary + propagation + frontmatter helper + validate root fix + CI hygiene + docs).
43
+ - **v3.5.0** (future) is expected to add the `research-portfolio` preset per issue #6 once cross-repo coordination with `prompt-injection-portfolio` is ready.
44
+
45
+ Profile-by-profile growth is the explicit strategy: the toolkit gets a new profile when a real consumer needs one, not before.
46
+
47
+ ## What NOT to file
48
+
49
+ - Bug reports from external users of a single book — file those against the book's repo, not the scaffold's.
50
+ - Style preferences that already have an escape hatch (e.g. `extraStyles` array, consumer-side `<style>` blocks).
51
+ - Speculative features ("we might one day want X"). Wait for the second consumer to actually need it.
52
+
53
+ ## Where to find prior decisions
54
+
55
+ - [`CHANGELOG.md`](../../CHANGELOG.md) — release-by-release breakdown.
56
+ - [`PACKAGE_DESIGN.md`](../PACKAGE_DESIGN.md) §1 Q1–Q6 — original Phase A locked decisions.
57
+ - [`LATEX_TO_MDX_MAPPING.md`](../LATEX_TO_MDX_MAPPING.md) — 38-component reference.
58
+ - [Closed issues](https://github.com/brandon-behring/book-scaffold-astro/issues?q=is%3Aissue+is%3Aclosed) — many problems already have rejected-alternative discussion attached.
@@ -0,0 +1,179 @@
1
+ # Recipe 13 — Research-portfolio getting started
2
+
3
+ The `research-portfolio` preset (v3.5.0+) is for books that combine:
4
+
5
+ - **Academic structure**: week/part/status, KaTeX math, BibTeX citations, Theorem family
6
+ - **Tools-style provenance**: volatility class, T1–T4 tier-tagged sources, `last_verified` freshness
7
+ - **Portfolio-specific affordances**: pre-release banner, AI collaboration disclosure, blocked-by-upstream callouts, structured ethics/policy references
8
+
9
+ If your book is primarily a weekly curriculum, use [`academic`](07-chapter-shapes.md#academic). If primarily AI-CLI comparison content, use [`tools`](07-chapter-shapes.md#tools). If a course-derived study notebook, use [`course-notes`](07-chapter-shapes.md#course-notes). Research portfolios sit at the intersection of all three and get their own preset.
10
+
11
+ ## When to use this preset
12
+
13
+ Choose `research-portfolio` if your book:
14
+
15
+ - Reports on research the author conducted directly (experimental results, theoretical analysis, literature surveys with original synthesis)
16
+ - Has an evolving release state — chapters land at different times, the book passes through alpha → beta → rc states
17
+ - Cites primary sources directly inline per-chapter (vs the tools-profile pattern of a central sources collection)
18
+ - Discloses AI collaboration / dual-use considerations / governance per repo policy
19
+ - Tracks upstream blockers (waiting on a tool release, a paper publication, a dataset)
20
+
21
+ Reference (forthcoming) consumer: [`prompt-injection-portfolio`](https://github.com/brandon-behring/prompt-injection-portfolio).
22
+
23
+ ## Quickstart
24
+
25
+ ```bash
26
+ npx @brandon_m_behring/create-book my-portfolio --preset=research-portfolio
27
+ cd my-portfolio
28
+ npm install
29
+ npm run dev
30
+ ```
31
+
32
+ This scaffolds:
33
+
34
+ - `astro.config.mjs` with `defineBookConfig({ preset: 'research-portfolio' })`
35
+ - `src/content.config.ts` with the `researchPortfolioChapterSchema`
36
+ - A sample chapter at `src/content/chapters/01-introduction.mdx`
37
+ - Frontmatter pages at `src/content/frontmatter/` (title-page, ai-disclosure, banner)
38
+ - A `bibliography.bib` stub for citations
39
+
40
+ ## Chapter frontmatter shape
41
+
42
+ ```yaml
43
+ ---
44
+ title: "Chapter title"
45
+ slug: ch01-introduction # optional; defaults to filename
46
+ chapter: 1 # tools-style numeric
47
+ part: 1 # either number OR academic-style string enum
48
+ week: 1 # optional; only if you use weekly cadence
49
+ status: prose_only # academic 7-state (optional)
50
+ freshness: experimental-result # 'experimental-result' | 'literature-survey' | 'theoretical' | 'reference'
51
+ volatility: feature-surface # tools-style: 'stable-principle' | 'architectural-pattern' | 'feature-surface'
52
+ tags: # freeform string array (NOT the tools_compared enum)
53
+ - prompt-injection
54
+ - red-team
55
+ - CVE-2025-32711
56
+ sources:
57
+ - tier: T1
58
+ url: https://nvd.nist.gov/vuln/detail/CVE-2025-32711
59
+ label: NVD CVE-2025-32711 (primary advisory)
60
+ - tier: T2
61
+ url: https://arxiv.org/abs/2406.00799
62
+ label: TaskTracker (Wallace et al. 2024)
63
+ last_verified: 2026-05-19
64
+ draft: false
65
+ ---
66
+ ```
67
+
68
+ All hierarchy fields (`part`, `week`, `chapter`) are optional — chapters can use whichever shape fits. The route templates dispatch on which is set.
69
+
70
+ ## The 4 portfolio-specific components
71
+
72
+ Shipped in v3.5.0 alongside the preset:
73
+
74
+ ### `<PreReleaseBanner>` — declare release state
75
+
76
+ ```astro
77
+ ---
78
+ import PreReleaseBanner from '@brandon_m_behring/book-scaffold-astro/components/PreReleaseBanner.astro';
79
+ ---
80
+ <PreReleaseBanner state="alpha" />
81
+ <PreReleaseBanner state="beta" dismissAt="v0.7.0" />
82
+ <PreReleaseBanner state="rc" message="Final review pass; please file issues." />
83
+ <PreReleaseBanner state="locked" />
84
+ ```
85
+
86
+ Place at the top of a layout to surface site-wide, or inline at the top of a specific chapter. Four states: `'alpha' | 'beta' | 'rc' | 'locked'`. Each has a default message + color treatment; override with `message`.
87
+
88
+ ### `<PolicyRef>` — inline link to a repo-root policy doc
89
+
90
+ ```astro
91
+ ---
92
+ import PolicyRef from '@brandon_m_behring/book-scaffold-astro/components/PolicyRef.astro';
93
+ ---
94
+ See <PolicyRef file="ETHICS.md" section="§1 Dual-use disclosure" label="our ethics policy" />
95
+ for the dual-use review process.
96
+
97
+ Per <PolicyRef file="SECURITY.md" /> the disclosure timeline is 90 days.
98
+ ```
99
+
100
+ Resolves to `/<file>#<slug-of-section>` by default (assumes consumer ships the markdown at site root via `public/` or an Astro page). Override the href with `href="..."`.
101
+
102
+ ### `<AICollaborationDisclosure>` — render an AI-collab paragraph
103
+
104
+ ```astro
105
+ ---
106
+ import AICollaborationDisclosure from '@brandon_m_behring/book-scaffold-astro/components/AICollaborationDisclosure.astro';
107
+ ---
108
+ <AICollaborationDisclosure
109
+ model="Claude Opus 4.7 + Sonnet 4.6 (Anthropic)"
110
+ role="research collaborator + writing collaborator"
111
+ commit_attribution="Co-Authored-By: Claude <noreply@anthropic.com>"
112
+ >
113
+ All factual claims independently verified by the human author; AI contributions
114
+ reviewed line-by-line before merge.
115
+ </AICollaborationDisclosure>
116
+ ```
117
+
118
+ Three required props (`model`, `role`, `commit_attribution`); optional slot for prose. For YAML-driven config, load the YAML consumer-side and spread props:
119
+
120
+ ```astro
121
+ ---
122
+ import disclosure from '../data/ai-collaboration.yaml';
123
+ ---
124
+ <AICollaborationDisclosure {...disclosure} />
125
+ ```
126
+
127
+ (The scaffold doesn't bundle a YAML parser; use `astro:content` file loader with `yaml()` or similar consumer-side.)
128
+
129
+ ### `<BlockedByCallout>` — declare upstream blockers
130
+
131
+ ```astro
132
+ ---
133
+ import BlockedByCallout from '@brandon_m_behring/book-scaffold-astro/components/BlockedByCallout.astro';
134
+ ---
135
+ <BlockedByCallout
136
+ upstream="book-scaffold-astro v3.5.0"
137
+ url="https://github.com/brandon-behring/book-scaffold-astro/issues/6"
138
+ reason="research-portfolio preset + 3 new components"
139
+ unblockedAt="2026-05-19"
140
+ >
141
+ Once the preset ships, this chapter's frontmatter migrates from the
142
+ hand-rolled schema to the upstream `research-portfolio` shape.
143
+ </BlockedByCallout>
144
+ ```
145
+
146
+ Use for chapters/sections waiting on external work — a tool release, a paper publication, a dataset acquisition. The structured fields produce a scannable card; slot content holds migration notes / workaround prose.
147
+
148
+ ## Frontmatter pages
149
+
150
+ The `research-portfolio` preset enables `/frontmatter/[slug]/` by default. Drop MDX files under `src/content/frontmatter/` (each needs `slug`, `title`, `order` per `frontmatterCollection()` — see [recipe 04](04-component-library.md) or PACKAGE_DESIGN.md §17).
151
+
152
+ Common frontmatter pages for a portfolio:
153
+
154
+ - `title-page.mdx` — book title + author + version + license
155
+ - `ai-collaboration-disclosure.mdx` — wraps `<AICollaborationDisclosure>`
156
+ - `pre-alpha-banner.mdx` (or similar) — author's note on release state
157
+ - `executive-summary.mdx` — 1-page overview for skim readers
158
+ - `acknowledgments.mdx` — collaborators + funding + dataset providers
159
+ - `ethics-policy.mdx` — wraps `<PolicyRef>` to other ETHICS docs
160
+
161
+ ## Migrating from a hand-rolled schema
162
+
163
+ If you previously rolled your own schema (e.g., for `prompt-injection-portfolio` pre-v3.5.0), migration is mostly mechanical:
164
+
165
+ 1. **Replace your `defineCollection` for chapters** with `defineBookSchemas({ preset: 'research-portfolio' }).collections.chapters`.
166
+ 2. **Rename `tools_compared` → `tags`** in frontmatter across chapters (the new schema uses freeform `tags`; the rename is a global find-and-replace).
167
+ 3. **Restructure `sources`** to the new inline shape `{ tier: 'T1', url, label }` — if you were using the tools-profile `sources` collection (referenced by string keys), inline them per chapter.
168
+ 4. **Replace ad-hoc PreReleaseBanner / EthicsRef / AIAssistanceDisclosure** components with the scaffold-shipped versions (delete your local copies; update imports).
169
+ 5. **Bump pin to `^3.5.0`** in your `package.json`.
170
+
171
+ See `package/CHANGELOG.md` §3.5.0 for the full additive list.
172
+
173
+ ## See also
174
+
175
+ - [Recipe 04 — Component library](04-component-library.md) — full component reference (38+ now with v3.5.0 additions)
176
+ - [Recipe 07 — Chapter shapes](07-chapter-shapes.md) — choosing between presets
177
+ - [Recipe 12 — Where to file issues](12-where-to-file-issues.md) — feedback loop for new portfolios
178
+ - [`LATEX_TO_MDX_MAPPING.md`](../LATEX_TO_MDX_MAPPING.md) — converting a LaTeX research book
179
+ - [`PACKAGE_DESIGN.md`](../PACKAGE_DESIGN.md) — full API contract
@@ -33,6 +33,24 @@
33
33
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
34
34
  import { dirname, resolve } from 'node:path';
35
35
  import { fileURLToPath } from 'node:url';
36
+
37
+ // --help / -h: non-mutating (closes #14).
38
+ const USAGE = `Usage: book-scaffold build-bib
39
+
40
+ Bibliography pipeline (academic profile). Reads bibliography.bib (or
41
+ BOOK_BIB_PATH if set), parses via @citation-js, emits src/data/references.json.
42
+
43
+ Env:
44
+ BOOK_BIB_PATH Override path to .bib file (default: ./bibliography.bib).
45
+
46
+ Options:
47
+ --help, -h Print this message and exit (non-mutating).
48
+ `;
49
+
50
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
51
+ process.stdout.write(USAGE);
52
+ process.exit(0);
53
+ }
36
54
  import { Cite } from '@citation-js/core';
37
55
  import '@citation-js/plugin-bibtex';
38
56
 
@@ -30,6 +30,25 @@ import { dirname, resolve, basename } from 'node:path';
30
30
  import { fileURLToPath } from 'node:url';
31
31
  import { spawnSync } from 'node:child_process';
32
32
 
33
+ // --help / -h: non-mutating (closes #14).
34
+ const USAGE = `Usage: book-scaffold build-figures
35
+
36
+ Figure pipeline. PDF -> SVG via pdftocairo (PNG fallback via pdftoppm at
37
+ 200dpi). Walks figures/ (or BOOK_FIGURES_PATH), emits to public/figures/.
38
+ Graceful-skip if pdftocairo / pdftoppm not on PATH.
39
+
40
+ Env:
41
+ BOOK_FIGURES_PATH Override figures source (default: figures/).
42
+
43
+ Options:
44
+ --help, -h Print this message and exit (non-mutating).
45
+ `;
46
+
47
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
48
+ process.stdout.write(USAGE);
49
+ process.exit(0);
50
+ }
51
+
33
52
  const __dirname = dirname(fileURLToPath(import.meta.url));
34
53
  const PROJECT_ROOT = process.cwd();
35
54