@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/components/AICollaborationDisclosure.astro +81 -0
- package/components/BlockedByCallout.astro +82 -0
- package/components/PolicyRef.astro +46 -0
- package/components/PreReleaseBanner.astro +73 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +120 -16
- package/dist/schemas.d.ts +35 -2
- package/dist/schemas.mjs +111 -16
- package/examples/chapter-template-research-portfolio.mdx +79 -0
- package/package.json +7 -2
- package/pages/frontmatter/[...slug].astro +48 -0
- package/recipes/12-where-to-file-issues.md +58 -0
- package/recipes/13-research-portfolio-getting-started.md +179 -0
- package/scripts/build-bib.mjs +18 -0
- package/scripts/build-figures.mjs +19 -0
- package/scripts/build-labels.mjs +20 -0
- package/scripts/render-notebooks.mjs +19 -0
- package/scripts/validate.mjs +51 -37
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
|
|
248
|
-
let candidate =
|
|
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 (
|
|
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
|
|
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 (!
|
|
345
|
+
if (!BOOK_PRESETS.includes(candidate)) {
|
|
261
346
|
throw new BookConfigError(
|
|
262
|
-
`
|
|
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:
|
|
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 =
|
|
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.
|
|
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
|
package/scripts/build-bib.mjs
CHANGED
|
@@ -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
|
|