@brandon_m_behring/book-scaffold-astro 4.6.1 → 4.8.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 +2 -0
- package/components/Provenance.astro +206 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +27 -4
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +24 -4
- package/package.json +2 -1
- package/pages/chapters/[...slug].astro +9 -0
- package/recipes/04-component-library.md +18 -0
- package/recipes/09-validation.md +18 -1
- package/scripts/validate.mjs +11 -2
- package/scripts/walk-mdx.mjs +83 -0
- package/src/schemas.ts +49 -0
package/CLAUDE.md
CHANGED
|
@@ -99,6 +99,8 @@ Two callout families coexist. Authors import what they need.
|
|
|
99
99
|
|
|
100
100
|
**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
101
|
|
|
102
|
+
**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.
|
|
103
|
+
|
|
102
104
|
Full reference in `recipes/04-component-library.md`.
|
|
103
105
|
|
|
104
106
|
## 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/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-DTQ2l6B6.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-DTQ2l6B6.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -168,6 +168,18 @@ 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),
|
|
@@ -185,7 +197,9 @@ var academicChapterSchema = z.object({
|
|
|
185
197
|
published: z.date().optional(),
|
|
186
198
|
updated: z.date().optional(),
|
|
187
199
|
tags: z.array(z.string()).default([]),
|
|
188
|
-
image: z.string().optional()
|
|
200
|
+
image: z.string().optional(),
|
|
201
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
202
|
+
provenance: provenanceSchema
|
|
189
203
|
});
|
|
190
204
|
var toolsChapterSchema = z.object({
|
|
191
205
|
title: z.string().min(1),
|
|
@@ -203,7 +217,9 @@ var toolsChapterSchema = z.object({
|
|
|
203
217
|
author: z.string().optional(),
|
|
204
218
|
published: z.date().optional(),
|
|
205
219
|
tags: z.array(z.string()).default([]),
|
|
206
|
-
image: z.string().optional()
|
|
220
|
+
image: z.string().optional(),
|
|
221
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
222
|
+
provenance: provenanceSchema
|
|
207
223
|
});
|
|
208
224
|
var minimalChapterSchema = toolsChapterSchema;
|
|
209
225
|
var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
|
|
@@ -237,7 +253,9 @@ var courseNotesChapterSchema = z.object({
|
|
|
237
253
|
author: z.string().optional(),
|
|
238
254
|
published: z.date().optional(),
|
|
239
255
|
updated: z.date().optional(),
|
|
240
|
-
image: z.string().optional()
|
|
256
|
+
image: z.string().optional(),
|
|
257
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
258
|
+
provenance: provenanceSchema
|
|
241
259
|
});
|
|
242
260
|
var researchPortfolioChapterSchema = z.object({
|
|
243
261
|
// Identity
|
|
@@ -295,7 +313,9 @@ var researchPortfolioChapterSchema = z.object({
|
|
|
295
313
|
// `tags` + `updated` already existed; `author` + `published` + `image` are new.
|
|
296
314
|
author: z.string().optional(),
|
|
297
315
|
published: z.date().optional(),
|
|
298
|
-
image: z.string().optional()
|
|
316
|
+
image: z.string().optional(),
|
|
317
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
318
|
+
provenance: provenanceSchema
|
|
299
319
|
});
|
|
300
320
|
var sourcesSchema = z.object({
|
|
301
321
|
url: z.string().url(),
|
|
@@ -1262,6 +1282,7 @@ export {
|
|
|
1262
1282
|
changelogSchema,
|
|
1263
1283
|
chapterSortKey,
|
|
1264
1284
|
chapterStatus,
|
|
1285
|
+
citationBackstops,
|
|
1265
1286
|
composeStyles,
|
|
1266
1287
|
courseNotesChapterSchema,
|
|
1267
1288
|
courseNotesStyle,
|
|
@@ -1278,6 +1299,8 @@ export {
|
|
|
1278
1299
|
normalizeFrontmatterConfig,
|
|
1279
1300
|
patternCategories,
|
|
1280
1301
|
patternsSchema,
|
|
1302
|
+
provenanceObject,
|
|
1303
|
+
provenanceSchema,
|
|
1281
1304
|
researchPortfolioChapterSchema,
|
|
1282
1305
|
researchPortfolioStyle,
|
|
1283
1306
|
resolvePreset,
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -52,6 +52,18 @@ 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),
|
|
@@ -69,7 +81,9 @@ var academicChapterSchema = z.object({
|
|
|
69
81
|
published: z.date().optional(),
|
|
70
82
|
updated: z.date().optional(),
|
|
71
83
|
tags: z.array(z.string()).default([]),
|
|
72
|
-
image: z.string().optional()
|
|
84
|
+
image: z.string().optional(),
|
|
85
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
86
|
+
provenance: provenanceSchema
|
|
73
87
|
});
|
|
74
88
|
var toolsChapterSchema = z.object({
|
|
75
89
|
title: z.string().min(1),
|
|
@@ -87,7 +101,9 @@ var toolsChapterSchema = z.object({
|
|
|
87
101
|
author: z.string().optional(),
|
|
88
102
|
published: z.date().optional(),
|
|
89
103
|
tags: z.array(z.string()).default([]),
|
|
90
|
-
image: z.string().optional()
|
|
104
|
+
image: z.string().optional(),
|
|
105
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
106
|
+
provenance: provenanceSchema
|
|
91
107
|
});
|
|
92
108
|
var minimalChapterSchema = toolsChapterSchema;
|
|
93
109
|
var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
|
|
@@ -121,7 +137,9 @@ var courseNotesChapterSchema = z.object({
|
|
|
121
137
|
author: z.string().optional(),
|
|
122
138
|
published: z.date().optional(),
|
|
123
139
|
updated: z.date().optional(),
|
|
124
|
-
image: z.string().optional()
|
|
140
|
+
image: z.string().optional(),
|
|
141
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
142
|
+
provenance: provenanceSchema
|
|
125
143
|
});
|
|
126
144
|
var researchPortfolioChapterSchema = z.object({
|
|
127
145
|
// Identity
|
|
@@ -179,7 +197,9 @@ var researchPortfolioChapterSchema = z.object({
|
|
|
179
197
|
// `tags` + `updated` already existed; `author` + `published` + `image` are new.
|
|
180
198
|
author: z.string().optional(),
|
|
181
199
|
published: z.date().optional(),
|
|
182
|
-
image: z.string().optional()
|
|
200
|
+
image: z.string().optional(),
|
|
201
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
202
|
+
provenance: provenanceSchema
|
|
183
203
|
});
|
|
184
204
|
var sourcesSchema = z.object({
|
|
185
205
|
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.8.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/recipes/09-validation.md
CHANGED
|
@@ -49,8 +49,25 @@ For pre-commit: add to `.pre-commit-config.yaml`:
|
|
|
49
49
|
|
|
50
50
|
## Environment variables
|
|
51
51
|
|
|
52
|
-
- `BOOK_PROFILE` — `academic` enables Cite-key checking
|
|
52
|
+
- `BOOK_PRESET` (canonical) / `BOOK_PROFILE` (alias) — which preset to validate against. `academic` enables Cite-key checking.
|
|
53
53
|
- `BOOK_REPO_ROOT` — absolute path to the paired code repo for CodeRef checks. Unset → skipped (the scaffold default; minimal/tools books rarely have a paired code repo).
|
|
54
|
+
- `BOOK_CHAPTERS_DIR` — override the chapters directory (default: read from `content.config.ts`, fallback `src/content/chapters`).
|
|
55
|
+
|
|
56
|
+
## Preset / chaptersBase resolution (v4.7.0+, #75)
|
|
57
|
+
|
|
58
|
+
The validator resolves both `preset` and `chaptersBase` by consulting multiple sources in a documented order. Notable v4.7.0 addition: the v4.5+ canonical form
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// src/content.config.ts
|
|
62
|
+
export const { collections } = defineBookSchemas({
|
|
63
|
+
preset: 'research-portfolio',
|
|
64
|
+
chaptersBase: './src/content/textbook',
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
is now read by the CLI (previously it was silently ignored — the CLI defaulted to `profile=minimal` and walked `./src/content/chapters/` while `astro build` applied the correct settings, masking real schema drift).
|
|
69
|
+
|
|
70
|
+
Full precedence chain documented in [`PACKAGE_DESIGN.md §8 — Preset + chaptersBase resolution`](../../PACKAGE_DESIGN.md#preset--chaptersbase-resolution-v470-closes-75).
|
|
54
71
|
|
|
55
72
|
## Output
|
|
56
73
|
|
package/scripts/validate.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { readFile, access } from 'node:fs/promises';
|
|
29
29
|
import { existsSync, readFileSync } from 'node:fs';
|
|
30
30
|
import { resolve, dirname, join } from 'node:path';
|
|
31
|
-
import { walkMdx, readChaptersBase } from './walk-mdx.mjs';
|
|
31
|
+
import { walkMdx, readChaptersBase, readBookSchemaConfig } from './walk-mdx.mjs';
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Best-effort .env reader. Mirrors `readEnvFile` in src/types.ts; kept inline
|
|
@@ -105,17 +105,26 @@ const DATA_DIR = resolve(ROOT, 'src/data');
|
|
|
105
105
|
|
|
106
106
|
// Preset resolution (matches resolvePreset in src/types.ts):
|
|
107
107
|
// --preset flag > BOOK_PRESET env > BOOK_PROFILE env >
|
|
108
|
-
// .env BOOK_PRESET > .env BOOK_PROFILE >
|
|
108
|
+
// .env BOOK_PRESET > .env BOOK_PROFILE >
|
|
109
|
+
// defineBookSchemas({ preset }) in content.config.ts >
|
|
110
|
+
// defineBookSchemas({ profile }) in content.config.ts (alias) >
|
|
111
|
+
// 'minimal'.
|
|
109
112
|
// .env fallback closes #20 — without it, consumers who set BOOK_PROFILE in
|
|
110
113
|
// .env (the documented convenience in SKILL.md + create-book defaults) saw
|
|
111
114
|
// the CLI silently default to minimal, hiding academic-profile errors.
|
|
115
|
+
// content.config.ts fallback closes #75 — without it, consumers using the
|
|
116
|
+
// canonical v4.5+ defineBookSchemas({ preset, chaptersBase }) form had the
|
|
117
|
+
// CLI silently default to minimal, hiding research-portfolio (and any
|
|
118
|
+
// non-env-set) profile errors while astro build applied the correct settings.
|
|
112
119
|
const dotenv = readEnvFile(resolve(ROOT, '.env'));
|
|
120
|
+
const schemaConfig = await readBookSchemaConfig(ROOT);
|
|
113
121
|
const PRESET =
|
|
114
122
|
presetFromFlag ??
|
|
115
123
|
process.env.BOOK_PRESET ??
|
|
116
124
|
process.env.BOOK_PROFILE ??
|
|
117
125
|
dotenv.BOOK_PRESET ??
|
|
118
126
|
dotenv.BOOK_PROFILE ??
|
|
127
|
+
schemaConfig.preset ??
|
|
119
128
|
'minimal';
|
|
120
129
|
// Alias kept for downstream message text only; the resolution above is canonical.
|
|
121
130
|
const PROFILE = PRESET;
|
package/scripts/walk-mdx.mjs
CHANGED
|
@@ -34,6 +34,79 @@ export async function* walkMdx(dir, baseDir = dir) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Read the consumer's `content.config.ts` (or `.mjs` / `.js`) and extract
|
|
39
|
+
* `defineBookSchemas({ preset?, profile?, chaptersBase? })` options.
|
|
40
|
+
*
|
|
41
|
+
* v4.7.0 (closes #75): consumers using the v4.5+ canonical form
|
|
42
|
+
*
|
|
43
|
+
* export const { collections } = defineBookSchemas({
|
|
44
|
+
* preset: 'research-portfolio',
|
|
45
|
+
* chaptersBase: './src/content/textbook',
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* previously had BOTH options ignored by the CLI scripts. `validate` and
|
|
49
|
+
* `build-labels` resolved preset only from env vars and walked the default
|
|
50
|
+
* chapters dir, silently checking the wrong directory under the wrong
|
|
51
|
+
* profile while astro build applied the correct settings — a divergence
|
|
52
|
+
* masking real schema drift.
|
|
53
|
+
*
|
|
54
|
+
* Strategy: regex-parse the source file (avoid runtime import; the file
|
|
55
|
+
* imports from `astro:content` / `astro/loaders` which don't resolve in
|
|
56
|
+
* plain Node). Captures the entire `defineBookSchemas({ ... })` options
|
|
57
|
+
* object, then field-by-field regex within that scope for `preset:`,
|
|
58
|
+
* `profile:` (alias), and `chaptersBase:`.
|
|
59
|
+
*
|
|
60
|
+
* Returns `{ preset, chaptersBase }` — both nullable. Returns `null` for
|
|
61
|
+
* either field when:
|
|
62
|
+
* - content.config.{ts,mjs,js} doesn't exist
|
|
63
|
+
* - no `defineBookSchemas(...)` call found
|
|
64
|
+
* - the field is absent or uses a dynamic form (variable, template literal)
|
|
65
|
+
*
|
|
66
|
+
* `preset` and `profile` are aliases (canonical name flipped in v3.7+);
|
|
67
|
+
* `preset` wins when both are present.
|
|
68
|
+
*/
|
|
69
|
+
export async function readBookSchemaConfig(projectRoot) {
|
|
70
|
+
const result = { preset: null, chaptersBase: null };
|
|
71
|
+
for (const ext of ['ts', 'mjs', 'js']) {
|
|
72
|
+
const configPath = join(projectRoot, `src/content.config.${ext}`);
|
|
73
|
+
if (!existsSync(configPath)) continue;
|
|
74
|
+
let source;
|
|
75
|
+
try {
|
|
76
|
+
source = await readFile(configPath, 'utf8');
|
|
77
|
+
} catch {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// Match `defineBookSchemas({ ... })` — capture the options object body.
|
|
81
|
+
// Non-greedy `[\s\S]*?` matches the smallest balanced-enough scope; for
|
|
82
|
+
// typical configs the options object is small (≤200 chars) and any
|
|
83
|
+
// nested braces (uncommon in this API) would terminate the match early.
|
|
84
|
+
// Acceptable trade-off: simple regex over a real parser.
|
|
85
|
+
const callMatch = source.match(
|
|
86
|
+
/\bdefineBookSchemas\s*\(\s*\{([\s\S]*?)\}\s*\)/,
|
|
87
|
+
);
|
|
88
|
+
if (callMatch) {
|
|
89
|
+
const optsBody = callMatch[1];
|
|
90
|
+
// preset is canonical (v3.7+); profile is backward-compat alias.
|
|
91
|
+
const presetMatch =
|
|
92
|
+
optsBody.match(/\bpreset\s*:\s*'([^']+)'/) ||
|
|
93
|
+
optsBody.match(/\bpreset\s*:\s*"([^"]+)"/);
|
|
94
|
+
const profileMatch =
|
|
95
|
+
optsBody.match(/\bprofile\s*:\s*'([^']+)'/) ||
|
|
96
|
+
optsBody.match(/\bprofile\s*:\s*"([^"]+)"/);
|
|
97
|
+
result.preset = presetMatch?.[1] ?? profileMatch?.[1] ?? null;
|
|
98
|
+
const chaptersBaseMatch =
|
|
99
|
+
optsBody.match(/\bchaptersBase\s*:\s*'([^']+)'/) ||
|
|
100
|
+
optsBody.match(/\bchaptersBase\s*:\s*"([^"]+)"/);
|
|
101
|
+
result.chaptersBase = chaptersBaseMatch?.[1] ?? null;
|
|
102
|
+
}
|
|
103
|
+
// First existing file wins (priority: .ts > .mjs > .js).
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
// No content.config.{ts,mjs,js} at all.
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
37
110
|
/**
|
|
38
111
|
* Read the consumer's `content.config.ts` (or `.mjs` / `.js`) and extract
|
|
39
112
|
* the `loader.base` path for the `chapters` content collection.
|
|
@@ -46,6 +119,9 @@ export async function* walkMdx(dir, baseDir = dir) {
|
|
|
46
119
|
* consumer's config file and returns the actual base path so both scripts
|
|
47
120
|
* discover the consumer's chapter files.
|
|
48
121
|
*
|
|
122
|
+
* v4.7.0 (closes #75): when the raw Astro form isn't present, also consult
|
|
123
|
+
* `readBookSchemaConfig()` for `defineBookSchemas({ chaptersBase })`.
|
|
124
|
+
*
|
|
49
125
|
* Strategy: regex-parse the source file (avoid runtime import; the file
|
|
50
126
|
* imports from `astro:content` / `astro/loaders` which don't resolve in
|
|
51
127
|
* plain Node). Matches both single- and double-quoted string literals;
|
|
@@ -55,6 +131,7 @@ export async function* walkMdx(dir, baseDir = dir) {
|
|
|
55
131
|
* `${projectRoot}/src/content/chapters` when:
|
|
56
132
|
* - content.config.{ts,mjs,js} doesn't exist
|
|
57
133
|
* - the file exists but no `chapters` collection or `loader.base` found
|
|
134
|
+
* AND no `defineBookSchemas({ chaptersBase: ... })` form found
|
|
58
135
|
* - the matched base path uses dynamic forms (variables, template literals)
|
|
59
136
|
* instead of a string literal
|
|
60
137
|
*
|
|
@@ -91,6 +168,12 @@ export async function readChaptersBase(projectRoot) {
|
|
|
91
168
|
if (captured) {
|
|
92
169
|
return resolve(projectRoot, captured);
|
|
93
170
|
}
|
|
171
|
+
// v4.7.0 (closes #75): no raw Astro form match — try the v4.5+ form
|
|
172
|
+
// `defineBookSchemas({ chaptersBase: '...' })`.
|
|
173
|
+
const schemaConfig = await readBookSchemaConfig(projectRoot);
|
|
174
|
+
if (schemaConfig.chaptersBase) {
|
|
175
|
+
return resolve(projectRoot, schemaConfig.chaptersBase);
|
|
176
|
+
}
|
|
94
177
|
// File exists but no override found — assume the consumer uses the
|
|
95
178
|
// scaffold's defineBookSchemas() default.
|
|
96
179
|
return DEFAULT_BASE;
|
package/src/schemas.ts
CHANGED
|
@@ -70,6 +70,46 @@ 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({
|
|
@@ -90,6 +130,8 @@ export const academicChapterSchema = z.object({
|
|
|
90
130
|
updated: z.date().optional(),
|
|
91
131
|
tags: z.array(z.string()).default([]),
|
|
92
132
|
image: z.string().optional(),
|
|
133
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
134
|
+
provenance: provenanceSchema,
|
|
93
135
|
});
|
|
94
136
|
|
|
95
137
|
export const toolsChapterSchema = z.object({
|
|
@@ -109,6 +151,8 @@ export const toolsChapterSchema = z.object({
|
|
|
109
151
|
published: z.date().optional(),
|
|
110
152
|
tags: z.array(z.string()).default([]),
|
|
111
153
|
image: z.string().optional(),
|
|
154
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
155
|
+
provenance: provenanceSchema,
|
|
112
156
|
});
|
|
113
157
|
|
|
114
158
|
/** Minimal profile currently aliases the tools schema. */
|
|
@@ -168,6 +212,8 @@ export const courseNotesChapterSchema = z.object({
|
|
|
168
212
|
published: z.date().optional(),
|
|
169
213
|
updated: z.date().optional(),
|
|
170
214
|
image: z.string().optional(),
|
|
215
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
216
|
+
provenance: provenanceSchema,
|
|
171
217
|
});
|
|
172
218
|
|
|
173
219
|
/**
|
|
@@ -255,6 +301,8 @@ export const researchPortfolioChapterSchema = z.object({
|
|
|
255
301
|
author: z.string().optional(),
|
|
256
302
|
published: z.date().optional(),
|
|
257
303
|
image: z.string().optional(),
|
|
304
|
+
// v4.8.0: optional process-as-artifact audit trail (Provenance.astro).
|
|
305
|
+
provenance: provenanceSchema,
|
|
258
306
|
});
|
|
259
307
|
|
|
260
308
|
// ===== Inferred chapter types — one per schema =====
|
|
@@ -269,6 +317,7 @@ export type ToolsChapter = z.infer<typeof toolsChapterSchema>;
|
|
|
269
317
|
export type MinimalChapter = z.infer<typeof minimalChapterSchema>;
|
|
270
318
|
export type CourseNotesChapter = z.infer<typeof courseNotesChapterSchema>;
|
|
271
319
|
export type ResearchPortfolioChapter = z.infer<typeof researchPortfolioChapterSchema>;
|
|
320
|
+
export type Provenance = z.infer<typeof provenanceObject>;
|
|
272
321
|
|
|
273
322
|
// ===== Collateral collection schemas (tools-profile; always-defined) =====
|
|
274
323
|
|