@brandon_m_behring/book-scaffold-astro 4.7.0 → 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 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, 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-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
@@ -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-DTQ2l6B6.js';
3
3
  import 'astro';
4
4
  import 'astro/zod';
5
5
 
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.7.0",
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/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