@brandon_m_behring/book-scaffold-astro 4.7.0 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -25,6 +25,8 @@ When in doubt, run `grep BOOK_PROFILE .env astro.config.mjs src/content.config.t
25
25
 
26
26
  ## Frontmatter schemas
27
27
 
28
+ **Universal field (v4.9.0):** every profile accepts an optional `slug:` string that overrides the URL. A file `99-appendix.mdx` with `slug: appendix` is served at `/chapters/appendix/` — Astro's glob loader maps frontmatter `slug` → `entry.id`, and cross-references (`<XRef>`, via `build-labels`) resolve to the same path. Omit it and the URL falls back to the filename. Use it to keep numbered filenames for ordering while publishing clean URLs.
29
+
28
30
  ### Academic profile (`src/content.config.ts:academicChapterSchema`)
29
31
 
30
32
  ```yaml
@@ -34,6 +36,7 @@ part: foundations # required: foundations|ssm-core|beyond-ssm|integration
34
36
  title: "..." # string, required
35
37
  status: implemented # required: implemented|chapter_only|prose_only|code_only|reading_only|scaffolded|planned
36
38
  # optional:
39
+ slug: ch01-introduction # clean URL override; else filename → /chapters/<slug>/
37
40
  roadmap_lines: [10, 42] # [start, end] line refs into roadmap.md
38
41
  code_path: experiments/jax/week01/foo.py
39
42
  tests_path: experiments/jax/week01/test_foo.py
@@ -54,7 +57,7 @@ volatility: architectural-pattern # required: stable-principle|architectural-pa
54
57
  tools_compared: [claude-code] # required, ≥1 of: claude-code|gemini-cli|codex-cli|cross-tool
55
58
  last_verified: 2026-05-18 # date, required
56
59
  sources: [] # array of source-manifest keys
57
- # optional: description, draft, updated
60
+ # optional: slug (clean URL override), description, draft, updated
58
61
  ---
59
62
  ```
60
63
 
@@ -99,6 +102,8 @@ Two callout families coexist. Authors import what they need.
99
102
 
100
103
  **Utility components** (`src/components/`, any profile): `Cite`, `XRef`, `Figure`, `MarginNote`, `Sidenote`, `WeekRef`, `CodeRef`, `CodeBlock`, `Tag`, `StatusBadge`, `PocLayout` (v4.1.0+; wraps slot in a per-`kind` layout shell — 5 closed-union kinds; see `recipes/15-defining-styles.md`).
101
104
 
105
+ **Provenance** (v4.8.0, any profile, **auto-injected by the chapter route — not author-imported**): per-chapter "How this was made" audit-trail block, rendered from the optional `provenance` frontmatter (`ai_tools`, `prompts_archive`, `decisions_log`, `audit_history`, `citation_backstop`). **Opt-out**: a chapter with no `provenance` shows a fallback ("Audit history not yet recorded"). Distinct from `AICollaborationDisclosure` (book-level, manual model+role disclosure). Repo-relative path fields render as `<code>`; only `http(s)` values link.
106
+
102
107
  Full reference in `recipes/04-component-library.md`.
103
108
 
104
109
  ## Citation patterns
@@ -0,0 +1,206 @@
1
+ ---
2
+ /**
3
+ * Provenance — per-chapter "How this was made" audit-trail disclosure (v4.8.0).
4
+ *
5
+ * Renders the optional `provenance` chapter frontmatter (see provenanceObject in
6
+ * src/schemas.ts) as a collapsed <details> block, family-styled with the warm-plum
7
+ * disclosure language (cf. AICollaborationDisclosure.astro) and the ▸/▾ summary
8
+ * marker (cf. .chapter-toc in styles/chapter.css). No JavaScript.
9
+ *
10
+ * Auto-injected by the chapter route (pages/chapters/[...slug].astro) on EVERY
11
+ * chapter of EVERY profile — so it is OPT-OUT, not opt-in: a chapter with no
12
+ * `provenance` still renders, showing a fallback ("Audit history not yet
13
+ * recorded — initial draft"). This forces awareness and signals to readers that
14
+ * an audit trail is expected.
15
+ *
16
+ * Scope (deliberate): this block owns the PROCESS audit trail (ai_tools, prompts,
17
+ * decisions, audits, citation backstop). It does NOT render freshness/volatility —
18
+ * ChapterHeader.astro owns that "is this current?" badge. And it is distinct from
19
+ * AICollaborationDisclosure (book-level, manual model+role disclosure).
20
+ *
21
+ * Claim-safety: references are repo-relative paths (e.g. "audits/AUDIT_x.md",
22
+ * "DECISIONS.md#anchor") that do NOT resolve on the built static site, so they
23
+ * render as <code> text. Only genuine http(s) URLs become clickable links — the
24
+ * site's "no dead links" thesis applied to its own chrome.
25
+ */
26
+ import type { Provenance } from '../src/schemas.js';
27
+
28
+ interface Props {
29
+ data: Provenance | null;
30
+ }
31
+ const { data } = Astro.props;
32
+
33
+ const isUrl = (s: string) => /^https?:\/\//i.test(s);
34
+ const fmtDate = (d: Date | string) =>
35
+ (d instanceof Date ? d : new Date(d)).toISOString().slice(0, 10);
36
+
37
+ const aiTools = data?.ai_tools ?? [];
38
+ const audits = data?.audit_history ?? [];
39
+ const backstop = data?.citation_backstop;
40
+ const promptsArchive = data?.prompts_archive;
41
+ const decisionsLog = data?.decisions_log;
42
+
43
+ // Opt-out (D3): the block always renders; "empty" → fallback.
44
+ const hasContent =
45
+ aiTools.length > 0 ||
46
+ audits.length > 0 ||
47
+ Boolean(backstop) ||
48
+ Boolean(promptsArchive) ||
49
+ Boolean(decisionsLog);
50
+
51
+ // Teaser digest shown in the <summary> so the signal reads while collapsed.
52
+ const teaserParts: string[] = [];
53
+ if (audits.length) teaserParts.push(`${audits.length} audit${audits.length > 1 ? 's' : ''}`);
54
+ if (backstop) teaserParts.push(`${backstop}-backed`);
55
+ if (!teaserParts.length && aiTools.length) {
56
+ teaserParts.push(`${aiTools.length} tool${aiTools.length > 1 ? 's' : ''}`);
57
+ }
58
+ const teaser = teaserParts.length ? `How this was made · ${teaserParts.join(' · ')}` : 'How this was made';
59
+ ---
60
+ <aside class="provenance" role="note" aria-labelledby="provenance-h">
61
+ <details class="provenance-details">
62
+ <summary id="provenance-h">{teaser}</summary>
63
+
64
+ {hasContent ? (
65
+ <div class="provenance-body">
66
+ {aiTools.length > 0 && (
67
+ <p class="provenance-row">
68
+ <span class="provenance-label">AI tools</span>
69
+ {aiTools.map((t) => <span class="provenance-chip">{t}</span>)}
70
+ </p>
71
+ )}
72
+
73
+ {(promptsArchive || decisionsLog) && (
74
+ <p class="provenance-row">
75
+ <span class="provenance-label">References</span>
76
+ {promptsArchive && (
77
+ <span class="provenance-ref">prompts: {isUrl(promptsArchive)
78
+ ? <a href={promptsArchive}>{promptsArchive}</a>
79
+ : <code>{promptsArchive}</code>}</span>
80
+ )}
81
+ {decisionsLog && (
82
+ <span class="provenance-ref">decisions: {isUrl(decisionsLog)
83
+ ? <a href={decisionsLog}>{decisionsLog}</a>
84
+ : <code>{decisionsLog}</code>}</span>
85
+ )}
86
+ </p>
87
+ )}
88
+
89
+ {audits.length > 0 && (
90
+ <div class="provenance-row">
91
+ <span class="provenance-label">Audit history</span>
92
+ <ul class="provenance-audits">
93
+ {audits.map((a) => (
94
+ <li>
95
+ <time datetime={fmtDate(a.date)}>{fmtDate(a.date)}</time>
96
+ <span class="provenance-audit-type">{a.type}</span>
97
+ {isUrl(a.file) ? <a href={a.file}>{a.file}</a> : <code>{a.file}</code>}
98
+ </li>
99
+ ))}
100
+ </ul>
101
+ </div>
102
+ )}
103
+
104
+ {backstop && (
105
+ <p class="provenance-row">
106
+ <span class="provenance-label">Citation backstop</span>
107
+ <span class={`provenance-badge backstop-${backstop}`}>{backstop}</span>
108
+ </p>
109
+ )}
110
+ </div>
111
+ ) : (
112
+ <p class="provenance-fallback">
113
+ Audit history not yet recorded — initial draft.
114
+ </p>
115
+ )}
116
+ </details>
117
+ </aside>
118
+
119
+ <style>
120
+ .provenance {
121
+ display: block;
122
+ margin: var(--space-6) 0;
123
+ padding: var(--space-3) var(--space-4);
124
+ border-left: var(--border-bar) solid var(--warm-plum);
125
+ background: var(--warm-plum-tint);
126
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
127
+ font-size: var(--text-sm);
128
+ line-height: var(--leading-normal);
129
+ max-width: var(--measure-main);
130
+ }
131
+ .provenance-details > summary {
132
+ cursor: pointer;
133
+ font-weight: 500;
134
+ color: var(--warm-plum);
135
+ letter-spacing: 0.02em;
136
+ list-style: none;
137
+ }
138
+ .provenance-details > summary::-webkit-details-marker {
139
+ display: none;
140
+ }
141
+ .provenance-details > summary::before {
142
+ content: '▸ ';
143
+ display: inline-block;
144
+ }
145
+ .provenance-details[open] > summary::before {
146
+ content: '▾ ';
147
+ }
148
+ .provenance-body {
149
+ margin-top: var(--space-3);
150
+ }
151
+ .provenance-row {
152
+ margin: var(--space-2) 0;
153
+ text-indent: 0;
154
+ }
155
+ .provenance-label {
156
+ display: inline-block;
157
+ min-width: 9rem;
158
+ font-size: var(--text-xs);
159
+ text-transform: uppercase;
160
+ letter-spacing: 0.04em;
161
+ color: var(--warm-plum);
162
+ vertical-align: top;
163
+ }
164
+ .provenance-chip,
165
+ .provenance-badge {
166
+ display: inline-block;
167
+ margin: 0 var(--space-1) var(--space-1) 0;
168
+ padding: 0.1em 0.5em;
169
+ border-radius: var(--radius-sm);
170
+ background: color-mix(in srgb, var(--warm-plum) 12%, var(--paper));
171
+ font-size: var(--text-xs);
172
+ }
173
+ .provenance-ref {
174
+ display: inline-block;
175
+ margin-right: var(--space-3);
176
+ }
177
+ .provenance-ref code,
178
+ .provenance-audits code {
179
+ font-family: var(--font-code);
180
+ font-size: 0.95em;
181
+ background: var(--color-code-bg);
182
+ padding: 0.1em 0.4em;
183
+ border-radius: var(--radius-sm);
184
+ }
185
+ .provenance-audits {
186
+ list-style: none;
187
+ margin: var(--space-1) 0 0;
188
+ padding-left: 0;
189
+ }
190
+ .provenance-audits li {
191
+ margin: var(--space-1) 0;
192
+ }
193
+ .provenance-audits time {
194
+ font-family: var(--font-code);
195
+ margin-right: var(--space-2);
196
+ }
197
+ .provenance-audit-type {
198
+ margin-right: var(--space-2);
199
+ font-style: italic;
200
+ }
201
+ .provenance-fallback {
202
+ margin: var(--space-3) 0 0;
203
+ font-style: italic;
204
+ opacity: 0.8;
205
+ }
206
+ </style>
@@ -1,28 +1,31 @@
1
1
  ---
2
- /**
3
- * XRef — resolves a `\cref{label}` LaTeX reference to a hyperlink.
4
- *
5
- * Reads src/data/labels.json built by scripts/build-labels.mjs
6
- * (Phase 2.6). For each known id, the map provides:
7
- * { href: "/chapters/week04#thm-w4-stability", display: "Theorem 4.2" }
8
- *
9
- * Runtime: renders `<a href>` for known ids; renders an inline `[?id]`
10
- * placeholder for unknown ids so the Astro dev server stays running while
11
- * chapters are being authored or labels are being added.
12
- *
13
- * CI: `book-scaffold validate` (Phase 2.6, shipped) catches unknown ids
14
- * and **fails the build with a non-zero exit code** before unresolved
15
- * placeholders can reach production. The placeholder is a dev-ergonomic
16
- * affordance, not a soft-degradation path on the deploy critical line.
17
- *
18
- * Bootstrapping note: when porting a book chapter-by-chapter, early
19
- * chapters that reference yet-to-be-ported targets need either plain-prose
20
- * substitutes or a temporarily-commented `{/* <XRef …/> */}` until the
21
- * target chapter (and its `id="…"` attributes) exists.
22
- *
23
- * Usage:
24
- * By <XRef id="thm:w4:stability" />, the discretized eigenvalues
25
- */
2
+ // XRef — resolves a `\cref{label}` LaTeX reference to a hyperlink.
3
+ //
4
+ // Reads src/data/labels.json built by scripts/build-labels.mjs
5
+ // (Phase 2.6). For each known id, the map provides:
6
+ // { href: "/chapters/week04#thm-w4-stability", display: "Theorem 4.2" }
7
+ //
8
+ // Runtime: renders `<a href>` for known ids; renders an inline `[?id]`
9
+ // placeholder for unknown ids so the Astro dev server stays running while
10
+ // chapters are being authored or labels are being added.
11
+ //
12
+ // CI: `book-scaffold validate` (Phase 2.6, shipped) catches unknown ids
13
+ // and **fails the build with a non-zero exit code** before unresolved
14
+ // placeholders can reach production. The placeholder is a dev-ergonomic
15
+ // affordance, not a soft-degradation path on the deploy critical line.
16
+ //
17
+ // Bootstrapping note: when porting a book chapter-by-chapter, early
18
+ // chapters that reference yet-to-be-ported targets need either plain-prose
19
+ // substitutes or a temporarily-commented `{/* <XRef …/> */}` until the
20
+ // target chapter (and its `id="…"` attributes) exists. MDX uses JSX-style
21
+ // expression comments the HTML `<` + bang + `--` form is NOT valid MDX.
22
+ //
23
+ // NB: these are `//` line comments, not a `/** */` block, on purpose — the
24
+ // literal `*/` in the example above would otherwise close a block comment
25
+ // early and break esbuild on every MDX import (the v4.9.0 fix).
26
+ //
27
+ // Usage:
28
+ // By <XRef id="thm:w4:stability" />, the discretized eigenvalues …
26
29
  type LabelEntry = { href: string; display: string };
27
30
 
28
31
  // Resolve labels.json from the consumer's project root (Vite resolves `/`
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AstroUserConfig, AstroIntegration } from 'astro';
2
- import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-B2bO9Nga.js';
3
- export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-B2bO9Nga.js';
2
+ import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, o as Style } from './types-CULHImU4.js';
3
+ export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, r as academicParts, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-CULHImU4.js';
4
4
  import 'astro/zod';
5
5
 
6
6
  /**
package/dist/index.mjs CHANGED
@@ -168,10 +168,24 @@ var chapterStatus = [
168
168
  "scaffolded",
169
169
  "planned"
170
170
  ];
171
+ var citationBackstops = ["research-kb", "manual", "unverified"];
172
+ var provenanceObject = z.object({
173
+ ai_tools: z.array(z.string()).default([]),
174
+ prompts_archive: z.string().optional(),
175
+ decisions_log: z.string().optional(),
176
+ audit_history: z.array(z.object({ date: z.date(), type: z.string(), file: z.string() })).default([]),
177
+ citation_backstop: z.enum(citationBackstops).optional()
178
+ }).strict();
179
+ var provenanceSchema = provenanceObject.refine(
180
+ (p) => p.ai_tools.length > 0 || p.audit_history.length > 0 || Boolean(p.citation_backstop) || Boolean(p.prompts_archive) || Boolean(p.decisions_log),
181
+ { message: "provenance is present but empty \u2014 omit the key, or set at least one field" }
182
+ ).optional();
171
183
  var academicChapterSchema = z.object({
172
184
  week: z.number().int().min(1).max(99),
173
185
  part: z.enum(academicParts),
174
186
  title: z.string().min(1),
187
+ slug: z.string().optional(),
188
+ // v4.9.0: explicit URL slug override (else filename → entry.id)
175
189
  status: z.enum(chapterStatus),
176
190
  roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
177
191
  code_path: z.string().optional(),
@@ -185,10 +199,14 @@ var academicChapterSchema = z.object({
185
199
  published: z.date().optional(),
186
200
  updated: z.date().optional(),
187
201
  tags: z.array(z.string()).default([]),
188
- image: z.string().optional()
202
+ image: z.string().optional(),
203
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
204
+ provenance: provenanceSchema
189
205
  });
190
206
  var toolsChapterSchema = z.object({
191
207
  title: z.string().min(1),
208
+ slug: z.string().optional(),
209
+ // v4.9.0: explicit URL slug override (else filename → entry.id)
192
210
  part: z.number().int().min(0).max(10),
193
211
  chapter: z.number().int().min(0).max(99),
194
212
  volatility: z.enum(volatilityLevels),
@@ -203,13 +221,17 @@ var toolsChapterSchema = z.object({
203
221
  author: z.string().optional(),
204
222
  published: z.date().optional(),
205
223
  tags: z.array(z.string()).default([]),
206
- image: z.string().optional()
224
+ image: z.string().optional(),
225
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
226
+ provenance: provenanceSchema
207
227
  });
208
228
  var minimalChapterSchema = toolsChapterSchema;
209
229
  var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
210
230
  var courseNotesChapterSchema = z.object({
211
231
  // Identity
212
232
  title: z.string().min(1),
233
+ slug: z.string().optional(),
234
+ // v4.9.0: explicit URL slug override (else filename → entry.id)
213
235
  chapter: z.number().int().min(0).max(99),
214
236
  part: z.number().int().min(0).max(20).default(1),
215
237
  description: z.string().optional(),
@@ -237,7 +259,9 @@ var courseNotesChapterSchema = z.object({
237
259
  author: z.string().optional(),
238
260
  published: z.date().optional(),
239
261
  updated: z.date().optional(),
240
- image: z.string().optional()
262
+ image: z.string().optional(),
263
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
264
+ provenance: provenanceSchema
241
265
  });
242
266
  var researchPortfolioChapterSchema = z.object({
243
267
  // Identity
@@ -295,7 +319,9 @@ var researchPortfolioChapterSchema = z.object({
295
319
  // `tags` + `updated` already existed; `author` + `published` + `image` are new.
296
320
  author: z.string().optional(),
297
321
  published: z.date().optional(),
298
- image: z.string().optional()
322
+ image: z.string().optional(),
323
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
324
+ provenance: provenanceSchema
299
325
  });
300
326
  var sourcesSchema = z.object({
301
327
  url: z.string().url(),
@@ -1262,6 +1288,7 @@ export {
1262
1288
  changelogSchema,
1263
1289
  chapterSortKey,
1264
1290
  chapterStatus,
1291
+ citationBackstops,
1265
1292
  composeStyles,
1266
1293
  courseNotesChapterSchema,
1267
1294
  courseNotesStyle,
@@ -1278,6 +1305,8 @@ export {
1278
1305
  normalizeFrontmatterConfig,
1279
1306
  patternCategories,
1280
1307
  patternsSchema,
1308
+ provenanceObject,
1309
+ provenanceSchema,
1281
1310
  researchPortfolioChapterSchema,
1282
1311
  researchPortfolioStyle,
1283
1312
  resolvePreset,
package/dist/schemas.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCollection } from 'astro:content';
2
- import { g as BookSchemasOptions } from './types-B2bO9Nga.js';
2
+ import { g as BookSchemasOptions } from './types-CULHImU4.js';
3
3
  import 'astro';
4
4
  import 'astro/zod';
5
5
 
package/dist/schemas.mjs CHANGED
@@ -52,10 +52,24 @@ var chapterStatus = [
52
52
  "scaffolded",
53
53
  "planned"
54
54
  ];
55
+ var citationBackstops = ["research-kb", "manual", "unverified"];
56
+ var provenanceObject = z.object({
57
+ ai_tools: z.array(z.string()).default([]),
58
+ prompts_archive: z.string().optional(),
59
+ decisions_log: z.string().optional(),
60
+ audit_history: z.array(z.object({ date: z.date(), type: z.string(), file: z.string() })).default([]),
61
+ citation_backstop: z.enum(citationBackstops).optional()
62
+ }).strict();
63
+ var provenanceSchema = provenanceObject.refine(
64
+ (p) => p.ai_tools.length > 0 || p.audit_history.length > 0 || Boolean(p.citation_backstop) || Boolean(p.prompts_archive) || Boolean(p.decisions_log),
65
+ { message: "provenance is present but empty \u2014 omit the key, or set at least one field" }
66
+ ).optional();
55
67
  var academicChapterSchema = z.object({
56
68
  week: z.number().int().min(1).max(99),
57
69
  part: z.enum(academicParts),
58
70
  title: z.string().min(1),
71
+ slug: z.string().optional(),
72
+ // v4.9.0: explicit URL slug override (else filename → entry.id)
59
73
  status: z.enum(chapterStatus),
60
74
  roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
61
75
  code_path: z.string().optional(),
@@ -69,10 +83,14 @@ var academicChapterSchema = z.object({
69
83
  published: z.date().optional(),
70
84
  updated: z.date().optional(),
71
85
  tags: z.array(z.string()).default([]),
72
- image: z.string().optional()
86
+ image: z.string().optional(),
87
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
88
+ provenance: provenanceSchema
73
89
  });
74
90
  var toolsChapterSchema = z.object({
75
91
  title: z.string().min(1),
92
+ slug: z.string().optional(),
93
+ // v4.9.0: explicit URL slug override (else filename → entry.id)
76
94
  part: z.number().int().min(0).max(10),
77
95
  chapter: z.number().int().min(0).max(99),
78
96
  volatility: z.enum(volatilityLevels),
@@ -87,13 +105,17 @@ var toolsChapterSchema = z.object({
87
105
  author: z.string().optional(),
88
106
  published: z.date().optional(),
89
107
  tags: z.array(z.string()).default([]),
90
- image: z.string().optional()
108
+ image: z.string().optional(),
109
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
110
+ provenance: provenanceSchema
91
111
  });
92
112
  var minimalChapterSchema = toolsChapterSchema;
93
113
  var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
94
114
  var courseNotesChapterSchema = z.object({
95
115
  // Identity
96
116
  title: z.string().min(1),
117
+ slug: z.string().optional(),
118
+ // v4.9.0: explicit URL slug override (else filename → entry.id)
97
119
  chapter: z.number().int().min(0).max(99),
98
120
  part: z.number().int().min(0).max(20).default(1),
99
121
  description: z.string().optional(),
@@ -121,7 +143,9 @@ var courseNotesChapterSchema = z.object({
121
143
  author: z.string().optional(),
122
144
  published: z.date().optional(),
123
145
  updated: z.date().optional(),
124
- image: z.string().optional()
146
+ image: z.string().optional(),
147
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
148
+ provenance: provenanceSchema
125
149
  });
126
150
  var researchPortfolioChapterSchema = z.object({
127
151
  // Identity
@@ -179,7 +203,9 @@ var researchPortfolioChapterSchema = z.object({
179
203
  // `tags` + `updated` already existed; `author` + `published` + `image` are new.
180
204
  author: z.string().optional(),
181
205
  published: z.date().optional(),
182
- image: z.string().optional()
206
+ image: z.string().optional(),
207
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
208
+ provenance: provenanceSchema
183
209
  });
184
210
  var sourcesSchema = z.object({
185
211
  url: z.string().url(),
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": "4.7.0",
4
+ "version": "4.9.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -72,6 +72,7 @@
72
72
  "./components/PolicyRef.astro": "./components/PolicyRef.astro",
73
73
  "./components/Practice.astro": "./components/Practice.astro",
74
74
  "./components/PreReleaseBanner.astro": "./components/PreReleaseBanner.astro",
75
+ "./components/Provenance.astro": "./components/Provenance.astro",
75
76
  "./components/Recovery.astro": "./components/Recovery.astro",
76
77
  "./components/ResultBox.astro": "./components/ResultBox.astro",
77
78
  "./components/Sidebar.astro": "./components/Sidebar.astro",
@@ -17,8 +17,10 @@
17
17
  * [...slug].astro for the canonical pattern).
18
18
  */
19
19
  import { getCollection, render } from 'astro:content';
20
+ import type { Provenance } from '../../src/schemas.js';
20
21
  import Chapter from '../../layouts/Chapter.astro';
21
22
  import Base from '../../layouts/Base.astro';
23
+ import ProvenanceBlock from '../../components/Provenance.astro';
22
24
 
23
25
  const BOOK_PRESET = import.meta.env.BOOK_PRESET ?? 'minimal';
24
26
  const USE_CHAPTER_LAYOUT = ['academic', 'research-portfolio'].includes(BOOK_PRESET);
@@ -34,7 +36,14 @@ export async function getStaticPaths() {
34
36
  const { entry } = Astro.props;
35
37
  const { Content, headings } = await render(entry);
36
38
  const Layout = USE_CHAPTER_LAYOUT ? Chapter : Base;
39
+
40
+ // v4.8.0: per-chapter provenance audit trail. Rendered here (not in a single
41
+ // layout) so it reaches BOTH layout paths — Chapter.astro (academic/
42
+ // research-portfolio) AND Base.astro (tools/minimal/course-notes). Opt-out:
43
+ // always rendered; absent `provenance` → the component's fallback.
44
+ const provenance = (entry.data as { provenance?: Provenance }).provenance ?? null;
37
45
  ---
38
46
  <Layout entry={entry} headings={headings}>
39
47
  <Content />
48
+ <ProvenanceBlock data={provenance} />
40
49
  </Layout>
@@ -73,6 +73,24 @@ Supported `type` values: `theorem`, `proposition`, `lemma`, `corollary`, `defini
73
73
  | `Tag` | Inline volatility/topic tag | `<Tag>stable-principle</Tag>` |
74
74
  | `StatusBadge` | Render frontmatter `status` value with color | `<StatusBadge status={frontmatter.status} />` |
75
75
  | `ChapterHeader` | Auto-rendered metadata block (week, part, status, companion links) | placed at top of each chapter automatically by Chapter.astro |
76
+ | `Provenance` | Auto-rendered per-chapter audit trail (v4.8.0) | placed at the end of each chapter by the chapter route — not imported |
77
+
78
+ ## Per-chapter provenance (v4.8.0, auto-injected)
79
+
80
+ `Provenance` renders a collapsible "How this was made" block on **every** chapter — you don't import or place it. It reads the optional `provenance` frontmatter and is **opt-out**: a chapter with no `provenance` shows a fallback ("Audit history not yet recorded"). It surfaces *process* (the audit trail); `ChapterHeader` still owns *freshness*. Distinct from `AICollaborationDisclosure` (book-level, manual model+role disclosure).
81
+
82
+ ```yaml
83
+ provenance:
84
+ ai_tools: ['Claude Code (Opus 4.8)', 'research-kb']
85
+ prompts_archive: docs/sessions/2026-05-22--ch07.md # repo-relative path or URL
86
+ decisions_log: DECISIONS.md#ch07-derivation # repo-relative path or URL
87
+ audit_history:
88
+ - { date: 2026-05-15, type: routine, file: audits/AUDIT_2026-05-15.md }
89
+ - { date: 2026-05-22, type: independent, file: audits/AUDIT_2026-05-22.md }
90
+ citation_backstop: research-kb # research-kb | manual | unverified
91
+ ```
92
+
93
+ Repo-relative paths render as `<code>`; only `http(s)` values become links (no dead links). If present, the `provenance` object must be non-empty (omit the key to opt out — unknown keys are rejected). `citation_backstop` is a closed set; `audit_history[].type` is free text.
76
94
 
77
95
  ## Conditional imports
78
96
 
@@ -15,10 +15,11 @@
15
15
  * - tools profile: `chapter` field (number).
16
16
  * - academic profile: `week` field (number).
17
17
  *
18
- * Slug used for the href = filename minus `.mdx`. The href shape mirrors
19
- * the consumer's pages router: `/chapters/<slug>#<id>`. Academic books
20
- * using `[...slug].astro` get the same shape since Astro slugifies
21
- * filenames identically.
18
+ * Slug used for the href: the chapter's frontmatter `slug:` if set,
19
+ * else filename minus `.mdx`. The href shape mirrors the consumer's pages
20
+ * router: `/chapters/<slug>#<id>`. Academic books using `[...slug].astro`
21
+ * get the same shape since Astro slugifies filenames identically when no
22
+ * frontmatter override is present.
22
23
  *
23
24
  * Optional override:
24
25
  * <Theorem id="…" label="Custom display" />
@@ -178,7 +179,9 @@ async function main() {
178
179
  const source = await readFile(file, 'utf8');
179
180
  const fm = parseFrontmatter(source);
180
181
  const chapterNum = chapterNumberOf(fm);
181
- const slug = basename(file).replace(/\.mdx?$/, '');
182
+ const slug = (typeof fm.slug === 'string' && fm.slug.length > 0)
183
+ ? fm.slug
184
+ : basename(file).replace(/\.mdx?$/, '');
182
185
 
183
186
  // Per-chapter counters reset for each file.
184
187
  const counters = {};
package/src/schemas.ts CHANGED
@@ -70,12 +70,53 @@ export const chapterStatus = [
70
70
  'planned',
71
71
  ] as const;
72
72
 
73
+ // ===== Provenance (v4.8.0) — process-as-artifact audit trail =====
74
+ //
75
+ // Optional per-chapter block attached to EVERY profile schema below.
76
+ // components/Provenance.astro renders it as a collapsible "How this was made"
77
+ // disclosure (opt-out: absent → fallback). Distinct from AICollaborationDisclosure
78
+ // (book-level, manual). Paths are repo-relative, so prompts_archive / decisions_log
79
+ // use plain z.string() — NOT .url() (which would reject "DECISIONS.md#anchor").
80
+ // audit_history.type is a free string (real audit types vary: 'routine',
81
+ // 'independent', 'first-deploy', ...); citation_backstop is a controlled vocabulary.
82
+ export const citationBackstops = ['research-kb', 'manual', 'unverified'] as const;
83
+
84
+ export const provenanceObject = z
85
+ .object({
86
+ ai_tools: z.array(z.string()).default([]),
87
+ prompts_archive: z.string().optional(),
88
+ decisions_log: z.string().optional(),
89
+ audit_history: z
90
+ .array(z.object({ date: z.date(), type: z.string(), file: z.string() }))
91
+ .default([]),
92
+ citation_backstop: z.enum(citationBackstops).optional(),
93
+ })
94
+ // .strict(): a misspelled key (e.g. `desisions_log`) must fail loud at build,
95
+ // not be silently stripped — silent data loss is the opposite of an audit trail.
96
+ .strict();
97
+
98
+ // Attached to every chapter schema as an optional field. The `.refine` makes
99
+ // "present ⇒ non-empty": a bare `provenance: {}` is author error (omit the key
100
+ // to opt out instead), so it fails fast rather than rendering a meaningless block.
101
+ export const provenanceSchema = provenanceObject
102
+ .refine(
103
+ (p) =>
104
+ p.ai_tools.length > 0 ||
105
+ p.audit_history.length > 0 ||
106
+ Boolean(p.citation_backstop) ||
107
+ Boolean(p.prompts_archive) ||
108
+ Boolean(p.decisions_log),
109
+ { message: 'provenance is present but empty — omit the key, or set at least one field' },
110
+ )
111
+ .optional();
112
+
73
113
  // ===== Chapter schemas — one per profile =====
74
114
 
75
115
  export const academicChapterSchema = z.object({
76
116
  week: z.number().int().min(1).max(99),
77
117
  part: z.enum(academicParts),
78
118
  title: z.string().min(1),
119
+ slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
79
120
  status: z.enum(chapterStatus),
80
121
  roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
81
122
  code_path: z.string().optional(),
@@ -90,10 +131,13 @@ export const academicChapterSchema = z.object({
90
131
  updated: z.date().optional(),
91
132
  tags: z.array(z.string()).default([]),
92
133
  image: z.string().optional(),
134
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
135
+ provenance: provenanceSchema,
93
136
  });
94
137
 
95
138
  export const toolsChapterSchema = z.object({
96
139
  title: z.string().min(1),
140
+ slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
97
141
  part: z.number().int().min(0).max(10),
98
142
  chapter: z.number().int().min(0).max(99),
99
143
  volatility: z.enum(volatilityLevels),
@@ -109,6 +153,8 @@ export const toolsChapterSchema = z.object({
109
153
  published: z.date().optional(),
110
154
  tags: z.array(z.string()).default([]),
111
155
  image: z.string().optional(),
156
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
157
+ provenance: provenanceSchema,
112
158
  });
113
159
 
114
160
  /** Minimal profile currently aliases the tools schema. */
@@ -134,6 +180,7 @@ export const sourceTiersResearch = ['T1', 'T2', 'T3', 'T4'] as const;
134
180
  export const courseNotesChapterSchema = z.object({
135
181
  // Identity
136
182
  title: z.string().min(1),
183
+ slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
137
184
  chapter: z.number().int().min(0).max(99),
138
185
  part: z.number().int().min(0).max(20).default(1),
139
186
  description: z.string().optional(),
@@ -168,6 +215,8 @@ export const courseNotesChapterSchema = z.object({
168
215
  published: z.date().optional(),
169
216
  updated: z.date().optional(),
170
217
  image: z.string().optional(),
218
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
219
+ provenance: provenanceSchema,
171
220
  });
172
221
 
173
222
  /**
@@ -255,6 +304,8 @@ export const researchPortfolioChapterSchema = z.object({
255
304
  author: z.string().optional(),
256
305
  published: z.date().optional(),
257
306
  image: z.string().optional(),
307
+ // v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
308
+ provenance: provenanceSchema,
258
309
  });
259
310
 
260
311
  // ===== Inferred chapter types — one per schema =====
@@ -269,6 +320,7 @@ export type ToolsChapter = z.infer<typeof toolsChapterSchema>;
269
320
  export type MinimalChapter = z.infer<typeof minimalChapterSchema>;
270
321
  export type CourseNotesChapter = z.infer<typeof courseNotesChapterSchema>;
271
322
  export type ResearchPortfolioChapter = z.infer<typeof researchPortfolioChapterSchema>;
323
+ export type Provenance = z.infer<typeof provenanceObject>;
272
324
 
273
325
  // ===== Collateral collection schemas (tools-profile; always-defined) =====
274
326