@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 +6 -1
- package/components/Provenance.astro +206 -0
- package/components/XRef.astro +27 -24
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +33 -4
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +30 -4
- package/package.json +2 -1
- package/pages/chapters/[...slug].astro +9 -0
- package/recipes/04-component-library.md +18 -0
- package/scripts/build-labels.mjs +8 -5
- package/src/schemas.ts +52 -0
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>
|
package/components/XRef.astro
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
|
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,
|
|
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
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.
|
|
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
|
|
package/scripts/build-labels.mjs
CHANGED
|
@@ -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
|
|
19
|
-
* the consumer's pages
|
|
20
|
-
* using `[...slug].astro`
|
|
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 =
|
|
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
|
|