@brandon_m_behring/book-scaffold-astro 4.14.1 → 4.14.3
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 +23 -1
- package/components/Theorem.astro +13 -38
- package/dist/index.d.ts +49 -1
- package/dist/index.mjs +44 -0
- package/layouts/Base.astro +38 -12
- package/package.json +1 -1
- package/recipes/08-decisions-ledger.md +17 -0
- package/scripts/validate.mjs +18 -0
- package/src/lib/theorem-label.ts +98 -0
package/CLAUDE.md
CHANGED
|
@@ -96,7 +96,7 @@ Two callout families coexist. Authors import what they need.
|
|
|
96
96
|
|
|
97
97
|
**Tools family** (`src/components/callouts/`, 8 components): `SkillBox`, `CaseStudy`, `ConceptBox`, `KeyIdea`, `TryThis`, `Recovery`, `Convergence`, `Divergence`.
|
|
98
98
|
|
|
99
|
-
**Academic family** (`src/components/callouts/`, 10 components): `NoteBox`, `ExampleBox`, `DynConnect`, `InsightBox`, `WarnBox`, `CounterBox`, `TipBox`, `OpenQuestion`, `PaperBox`, `ResultBox`. Plus `Theorem` (unified for theorem/proposition/lemma/corollary/definition/example/exercise/remark/proof).
|
|
99
|
+
**Academic family** (`src/components/callouts/`, 10 components): `NoteBox`, `ExampleBox`, `DynConnect`, `InsightBox`, `WarnBox`, `CounterBox`, `TipBox`, `OpenQuestion`, `PaperBox`, `ResultBox`. Plus `Theorem` (unified for theorem/proposition/lemma/corollary/definition/example/exercise/remark/proof). **Props (v4.14.3, #121):** `kind=` is canonical; `type=` is accepted as a legacy alias (likewise `title=`/`label=` alias `name=`). An absent or unknown kind **throws at build** (via `src/lib/theorem-label`) rather than rendering an empty label; `book-scaffold validate` flags a `<Theorem>` with neither `kind=` nor `type=` even earlier.
|
|
100
100
|
|
|
101
101
|
**Pedagogy family** (v4.1.0+, any profile, 3 components): `Pitfall` (rose; "common mistake" — distinct from `WarnBox`'s preemptive warning), `WorkedExample` (plum; collapsible `<details>` block with `#worked-example-{id}` anchor for deep links), `YouWillLearn` (gold; chapter-opener with optional `prerequisites` prop). Slot bullets/code freely; render at any preset.
|
|
102
102
|
|
|
@@ -106,6 +106,28 @@ Two callout families coexist. Authors import what they need.
|
|
|
106
106
|
|
|
107
107
|
Full reference in `recipes/04-component-library.md`.
|
|
108
108
|
|
|
109
|
+
### Theme-change event (v4.14.2)
|
|
110
|
+
|
|
111
|
+
`Base.astro` emits `book:theme:change` on `window` whenever the **effective** theme changes — both the chrome's dark-mode toggle and a system `prefers-color-scheme` flip (the latter only when no explicit theme is pinned). Use it for **canvas / JS islands** that can't recolor via CSS alone; CSS-token elements recolor automatically from the `[data-theme]` attribute.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// inside a Preact island (client:visible / client:idle)
|
|
115
|
+
function currentTheme(): 'light' | 'dark' {
|
|
116
|
+
const t = document.documentElement.getAttribute('data-theme');
|
|
117
|
+
return t === 'light' || t === 'dark'
|
|
118
|
+
? t
|
|
119
|
+
: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
120
|
+
}
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
draw(currentTheme()); // initial paint
|
|
123
|
+
const onChange = (e: Event) => draw((e as CustomEvent).detail.theme);
|
|
124
|
+
window.addEventListener('book:theme:change', onChange);
|
|
125
|
+
return () => window.removeEventListener('book:theme:change', onChange);
|
|
126
|
+
}, []);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`detail.theme` is `'light' | 'dark'`. Pull design-token colors via `getComputedStyle(document.documentElement).getPropertyValue('--…')` so the canvas matches the page, and respect `prefers-reduced-motion` for any redraw animation. (Event-only by design — a `useThemeColors` helper graduates with the demo kit, #103.)
|
|
130
|
+
|
|
109
131
|
## Citation patterns
|
|
110
132
|
|
|
111
133
|
Academic profile uses BibTeX → `references.json`:
|
package/components/Theorem.astro
CHANGED
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Maps LaTeX `\begin{theorem}`, `\begin{proposition}`, `\begin{lemma}`,
|
|
6
6
|
* `\begin{corollary}`, `\begin{definition}`, `\begin{example}`,
|
|
7
|
-
* `\begin{exercise}`, `\begin{remark}` to a single component
|
|
8
|
-
*
|
|
7
|
+
* `\begin{exercise}`, `\begin{remark}`, `\begin{proof}` to a single component.
|
|
8
|
+
*
|
|
9
|
+
* Props (#121): `kind` is canonical; `type=` is an accepted **legacy alias**
|
|
10
|
+
* (ssm-foundations ch1–11 + the LaTeX-env mental model pass `type=`). `name`
|
|
11
|
+
* is canonical with `title=`/`label=` accepted. An absent or unknown kind
|
|
12
|
+
* THROWS at build (see `src/lib/theorem-label`) — it never renders an empty
|
|
13
|
+
* label. Pass `n` to number the environment.
|
|
9
14
|
*
|
|
10
15
|
* Auto-numbering note: the LaTeX preamble shares a counter between
|
|
11
|
-
* theorem/proposition/lemma/corollary
|
|
12
|
-
*
|
|
13
|
-
* In MDX we accept that as a contract: per-chapter counters are
|
|
14
|
-
* implemented in Phase 2.6 (label scanner) — for now, the author
|
|
15
|
-
* passes `n` explicitly when numbering matters.
|
|
16
|
+
* theorem/proposition/lemma/corollary and gives definition/example/exercise/
|
|
17
|
+
* remark their own; in MDX the author passes `n` explicitly when it matters.
|
|
16
18
|
*
|
|
17
19
|
* Usage:
|
|
18
20
|
* <Theorem kind="theorem" n="4.2" name="Stable continuous-time eigenvalues" id="thm-w4-stability">
|
|
@@ -23,41 +25,14 @@
|
|
|
23
25
|
* The HiPPO-LegS state matrix …
|
|
24
26
|
* </Theorem>
|
|
25
27
|
*/
|
|
26
|
-
type
|
|
27
|
-
| 'theorem'
|
|
28
|
-
| 'proposition'
|
|
29
|
-
| 'lemma'
|
|
30
|
-
| 'corollary'
|
|
31
|
-
| 'definition'
|
|
32
|
-
| 'example'
|
|
33
|
-
| 'exercise'
|
|
34
|
-
| 'remark'
|
|
35
|
-
| 'proof';
|
|
28
|
+
import { theoremLabel, type TheoremLabelProps } from '../src/lib/theorem-label';
|
|
36
29
|
|
|
37
|
-
interface Props {
|
|
38
|
-
kind: Kind;
|
|
39
|
-
n?: string;
|
|
40
|
-
name?: string;
|
|
30
|
+
interface Props extends TheoremLabelProps {
|
|
41
31
|
id?: string;
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
const {
|
|
45
|
-
|
|
46
|
-
const KIND_LABEL: Record<Kind, string> = {
|
|
47
|
-
theorem: 'Theorem',
|
|
48
|
-
proposition: 'Proposition',
|
|
49
|
-
lemma: 'Lemma',
|
|
50
|
-
corollary: 'Corollary',
|
|
51
|
-
definition: 'Definition',
|
|
52
|
-
example: 'Example',
|
|
53
|
-
exercise: 'Exercise',
|
|
54
|
-
remark: 'Remark',
|
|
55
|
-
proof: 'Proof',
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const label = KIND_LABEL[kind];
|
|
59
|
-
const numbered = n ? `${label} ${n}` : label;
|
|
60
|
-
const fullLabel = name ? `${numbered} (${name})` : numbered;
|
|
34
|
+
const { id } = Astro.props;
|
|
35
|
+
const { kind, fullLabel } = theoremLabel(Astro.props);
|
|
61
36
|
---
|
|
62
37
|
<div class="theorem" data-kind={kind} id={id}>
|
|
63
38
|
<span class="theorem-label">{fullLabel}.</span>
|
package/dist/index.d.ts
CHANGED
|
@@ -204,6 +204,54 @@ declare function academicPartOrdinal(part: string): number;
|
|
|
204
204
|
*/
|
|
205
205
|
declare function academicPartHeading(part: string): string;
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* theorem-label — resolve a `<Theorem>` component's label from its props,
|
|
209
|
+
* failing loud instead of rendering a silent empty label (#121).
|
|
210
|
+
*
|
|
211
|
+
* Vocabulary: `kind` is canonical; `type` is an accepted **legacy alias** (many
|
|
212
|
+
* consumer books — ssm-foundations ch1–11 — and the LaTeX `\begin{<env>}`
|
|
213
|
+
* mental model pass `type=`). Likewise `name` is canonical with `title`/`label`
|
|
214
|
+
* accepted as aliases. Aliases keep existing content valid; they are synonyms,
|
|
215
|
+
* not deprecations, so they neither warn nor throw.
|
|
216
|
+
*
|
|
217
|
+
* Failure mode: an absent or unrecognized kind THROWS a build-failing,
|
|
218
|
+
* actionable error rather than computing `KIND_LABEL[undefined]` → a bare "."
|
|
219
|
+
* (the v4.8–4.14 silent defect, live across 32+ ssm-foundations theorems). A
|
|
220
|
+
* typo'd kind — silent before — now stops the build with the offending value.
|
|
221
|
+
*
|
|
222
|
+
* Extracted from the `Theorem.astro` frontmatter so the contract is unit-tested
|
|
223
|
+
* in the pure `node --test` suite (see tests/theorem-label.test.mjs) — the same
|
|
224
|
+
* single-source-of-truth move as `academic-parts.ts` (#95).
|
|
225
|
+
*/
|
|
226
|
+
declare const THEOREM_KINDS: readonly ["theorem", "proposition", "lemma", "corollary", "definition", "example", "exercise", "remark", "proof"];
|
|
227
|
+
type TheoremKind = (typeof THEOREM_KINDS)[number];
|
|
228
|
+
declare const KIND_LABEL: Record<TheoremKind, string>;
|
|
229
|
+
interface TheoremLabelProps {
|
|
230
|
+
/** Canonical environment selector. */
|
|
231
|
+
kind?: string;
|
|
232
|
+
/** Legacy alias for `kind` (LaTeX-env mental model; ssm-foundations ch1–11). */
|
|
233
|
+
type?: string;
|
|
234
|
+
/** Explicit number, e.g. "4.2"; omit for an unnumbered environment. */
|
|
235
|
+
n?: string;
|
|
236
|
+
/** Canonical display name shown in parentheses. */
|
|
237
|
+
name?: string;
|
|
238
|
+
/** Legacy aliases for `name`. */
|
|
239
|
+
title?: string;
|
|
240
|
+
label?: string;
|
|
241
|
+
}
|
|
242
|
+
interface ResolvedTheoremLabel {
|
|
243
|
+
/** The validated, canonical kind (for `data-kind`). */
|
|
244
|
+
kind: TheoremKind;
|
|
245
|
+
/** The composed "Kind N (Name)" label (never empty). */
|
|
246
|
+
fullLabel: string;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Resolve a `<Theorem>`'s `kind` (canonical or legacy `type=`) and compose its
|
|
250
|
+
* full label. Throws — never returns an empty label — when the kind is absent
|
|
251
|
+
* or not one of {@link THEOREM_KINDS}.
|
|
252
|
+
*/
|
|
253
|
+
declare function theoremLabel(props: TheoremLabelProps): ResolvedTheoremLabel;
|
|
254
|
+
|
|
207
255
|
/**
|
|
208
256
|
* src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
|
|
209
257
|
*
|
|
@@ -305,4 +353,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
305
353
|
*/
|
|
306
354
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
307
355
|
|
|
308
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
|
356
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, KIND_LABEL, type ResolvedTheoremLabel, Style, THEOREM_KINDS, type TheoremKind, type TheoremLabelProps, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, theoremLabel, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -1253,6 +1253,47 @@ function chapterSortKey(data) {
|
|
|
1253
1253
|
return partOrdinal * 1e3 + within;
|
|
1254
1254
|
}
|
|
1255
1255
|
|
|
1256
|
+
// src/lib/theorem-label.ts
|
|
1257
|
+
var THEOREM_KINDS = [
|
|
1258
|
+
"theorem",
|
|
1259
|
+
"proposition",
|
|
1260
|
+
"lemma",
|
|
1261
|
+
"corollary",
|
|
1262
|
+
"definition",
|
|
1263
|
+
"example",
|
|
1264
|
+
"exercise",
|
|
1265
|
+
"remark",
|
|
1266
|
+
"proof"
|
|
1267
|
+
];
|
|
1268
|
+
var KIND_LABEL = {
|
|
1269
|
+
theorem: "Theorem",
|
|
1270
|
+
proposition: "Proposition",
|
|
1271
|
+
lemma: "Lemma",
|
|
1272
|
+
corollary: "Corollary",
|
|
1273
|
+
definition: "Definition",
|
|
1274
|
+
example: "Example",
|
|
1275
|
+
exercise: "Exercise",
|
|
1276
|
+
remark: "Remark",
|
|
1277
|
+
proof: "Proof"
|
|
1278
|
+
};
|
|
1279
|
+
function isTheoremKind(value) {
|
|
1280
|
+
return value !== void 0 && THEOREM_KINDS.includes(value);
|
|
1281
|
+
}
|
|
1282
|
+
function theoremLabel(props) {
|
|
1283
|
+
const raw = props.kind ?? props.type;
|
|
1284
|
+
if (!isTheoremKind(raw)) {
|
|
1285
|
+
const detail = raw === void 0 ? "no kind= (or legacy type=) prop was passed" : `kind="${raw}" is not one of ${THEOREM_KINDS.join(", ")}`;
|
|
1286
|
+
const hint = props.type !== void 0 && props.kind === void 0 ? " (you passed type=; kind= is canonical, type= is accepted)" : "";
|
|
1287
|
+
throw new Error(
|
|
1288
|
+
`<Theorem>: cannot resolve a label \u2014 ${detail}. Pass a valid kind, e.g. kind="theorem"${hint}.`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
const name = props.name ?? props.title ?? props.label;
|
|
1292
|
+
const numbered = props.n ? `${KIND_LABEL[raw]} ${props.n}` : KIND_LABEL[raw];
|
|
1293
|
+
const fullLabel = name ? `${numbered} (${name})` : numbered;
|
|
1294
|
+
return { kind: raw, fullLabel };
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1256
1297
|
// src/styles/built-in.ts
|
|
1257
1298
|
var academicStyle = defineStyle({
|
|
1258
1299
|
name: "academic",
|
|
@@ -1299,6 +1340,8 @@ export {
|
|
|
1299
1340
|
BRANDON_PORTFOLIO_DEFAULT,
|
|
1300
1341
|
BUILTIN_STYLES,
|
|
1301
1342
|
BookConfigError,
|
|
1343
|
+
KIND_LABEL,
|
|
1344
|
+
THEOREM_KINDS,
|
|
1302
1345
|
UNKNOWN_PART_ORDINAL,
|
|
1303
1346
|
academicChapterSchema,
|
|
1304
1347
|
academicChaptersRenderer,
|
|
@@ -1338,6 +1381,7 @@ export {
|
|
|
1338
1381
|
sourceTiers,
|
|
1339
1382
|
sourceTiersResearch,
|
|
1340
1383
|
sourcesSchema,
|
|
1384
|
+
theoremLabel,
|
|
1341
1385
|
toolSlugs,
|
|
1342
1386
|
toolsChapterSchema,
|
|
1343
1387
|
toolsChaptersRenderer,
|
package/layouts/Base.astro
CHANGED
|
@@ -176,21 +176,47 @@ const ogDescription = description ?? bookConfig.description ?? '';
|
|
|
176
176
|
) : (
|
|
177
177
|
<main><slot /></main>
|
|
178
178
|
)}
|
|
179
|
+
{/*
|
|
180
|
+
Theme-change hook (#103, v4.14.2). Whenever the EFFECTIVE theme changes,
|
|
181
|
+
emit `book:theme:change` on `window` with `detail.theme` ∈ {'light','dark'}.
|
|
182
|
+
CSS recolors via the `data-theme` attribute alone; canvas/JS islands can't,
|
|
183
|
+
so they subscribe to this event (and read the attribute / matchMedia on
|
|
184
|
+
mount for the initial value) to redraw. Contract documented in the package
|
|
185
|
+
CLAUDE.md. Event-only by design — a `useThemeColors` helper graduates later
|
|
186
|
+
with the demo kit.
|
|
187
|
+
*/}
|
|
179
188
|
<script is:inline>
|
|
180
189
|
(function () {
|
|
190
|
+
var root = document.documentElement;
|
|
191
|
+
function effectiveTheme() {
|
|
192
|
+
var explicit = root.getAttribute('data-theme');
|
|
193
|
+
if (explicit === 'light' || explicit === 'dark') return explicit;
|
|
194
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
195
|
+
}
|
|
196
|
+
function emitThemeChange() {
|
|
197
|
+
window.dispatchEvent(new CustomEvent('book:theme:change', { detail: { theme: effectiveTheme() } }));
|
|
198
|
+
}
|
|
181
199
|
var btn = document.getElementById('theme-toggle');
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
if (btn) {
|
|
201
|
+
btn.addEventListener('click', function () {
|
|
202
|
+
var current = root.getAttribute('data-theme');
|
|
203
|
+
// If no explicit theme, pick the opposite of prefers-color-scheme.
|
|
204
|
+
if (!current) {
|
|
205
|
+
current = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
206
|
+
}
|
|
207
|
+
var next = current === 'dark' ? 'light' : 'dark';
|
|
208
|
+
root.setAttribute('data-theme', next);
|
|
209
|
+
try { localStorage.setItem('theme', next); } catch (e) {}
|
|
210
|
+
emitThemeChange();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// A system-preference flip only changes the effective theme when no
|
|
214
|
+
// explicit theme is pinned — emit then so islands can redraw too.
|
|
215
|
+
try {
|
|
216
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
|
|
217
|
+
if (!root.getAttribute('data-theme')) emitThemeChange();
|
|
218
|
+
});
|
|
219
|
+
} catch (e) { /* older browsers: no media-query change events */ }
|
|
194
220
|
})();
|
|
195
221
|
</script>
|
|
196
222
|
</body>
|
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.14.
|
|
4
|
+
"version": "4.14.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -105,6 +105,23 @@ Not in v2.0:
|
|
|
105
105
|
|
|
106
106
|
Until v3.0 triggers, accept that bug fixes don't auto-flow to existing books. Both books are currently low-churn.
|
|
107
107
|
|
|
108
|
+
## Visual-suite migration → gallery + Playwright (v5.x infra, 2026-06)
|
|
109
|
+
|
|
110
|
+
### D18 — Replace run.sh with a component gallery + Playwright
|
|
111
|
+
**Decision**: A standalone `gallery/` app renders every component × state × theme; Playwright (`channel:'chrome'`, full-page, pixelmatch) drives it (`playwright.config.ts`) **and** the 6 profile fixtures' 24 routes (`playwright.fixtures.config.ts`). `run.sh` + its 96 baselines + `visual-regression.yml` + `update-baselines.yml` are retired; `visual-pw.yml` is the sole visual gate.
|
|
112
|
+
**Reasoning**: run.sh's `--window-size=Wx2000` cap left below-fold content untested (root of #82); full-page `toHaveScreenshot` removes the fold + adds interaction (expand `<details>`, dark-mode toggle, canvas recolor). `channel:'chrome'` uses system Chrome — dissolves the "chromium-headless-shell won't build on every distro" objection that drove run.sh.
|
|
113
|
+
**Deviate when**: Never reintroduce viewport-capped screenshots. New components → gallery pages; new routes → fixture-suite entries.
|
|
114
|
+
|
|
115
|
+
### D19 — Baselines are CI-generated (glyph pages especially)
|
|
116
|
+
**Decision**: Commit baselines from the `visual-pw` `update` dispatch (ubuntu + system chrome). ASCII/common-glyph pages are byte-identical CI==local, but math/Unicode-glyph pages (λ, ℂ, Λ) render with platform fallback fonts and MUST be CI-generated.
|
|
117
|
+
**Reasoning**: Cross-OS font rendering diverges; pixelmatch `maxDiffPixels` absorbs AA, not glyph substitution.
|
|
118
|
+
**Deviate when**: Never hand-commit a glyph-page baseline from a dev box.
|
|
119
|
+
|
|
120
|
+
### D20 — Full route parity before retiring run.sh (Q3)
|
|
121
|
+
**Decision**: The fixture suite covers run.sh's 24 routes (2 widths: mobile 768 + desktop 1280; run.sh's 1440/1920 were redundant) before deletion. Porting caught a stale run.sh route (research-portfolio `/chapters/example/` was a silently-screenshotted 404 — real slug `ch01-fixture`) + the hardcoded-`GITHUB_REPO` CodeRef/CodeBlock wart (#109).
|
|
122
|
+
**Reasoning**: #82 itself was a coverage gap — don't trade coverage for speed.
|
|
123
|
+
**Deviate when**: Never delete a visual gate without equivalent-or-better coverage in the replacement.
|
|
124
|
+
|
|
108
125
|
## How to use this ledger
|
|
109
126
|
|
|
110
127
|
When a future change in the scaffold contradicts a decision above, update this ledger first. Don't change behavior silently — the ledger is the durable record of "why this is shaped like it is."
|
package/scripts/validate.mjs
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* 4. Internal markdown links [text](/foo) — target resolves.
|
|
14
14
|
* 5. <CodeRef path="..." line={N} /> — when BOOK_REPO_ROOT set,
|
|
15
15
|
* path exists + line in bounds.
|
|
16
|
+
* 6. <Theorem> — has a resolvable kind= (or legacy type=); else it would
|
|
17
|
+
* render an empty label and throw at build (#121).
|
|
16
18
|
*
|
|
17
19
|
* Run from the consumer's project root. Closes #8 (was resolving paths
|
|
18
20
|
* from the package's own directory inside node_modules — false negatives
|
|
@@ -209,6 +211,9 @@ const RE_XREF = /<XRef[^>]+id=["']([^"']+)["']/g;
|
|
|
209
211
|
const RE_FIGURE = /<Figure[^>]+src=["']([^"']+)["']/g;
|
|
210
212
|
const RE_CODEREF = /<CodeRef[^>]+path=["']([^"']+)["'](?:[^>]*line=\{(\d+)\})?(?:[^>]*lineEnd=\{(\d+)\})?/g;
|
|
211
213
|
const RE_MD_LINK = /\[(?:[^\]]*)\]\((\/[^)\s#]+)(?:#[^)]*)?\)/g;
|
|
214
|
+
// #121: a <Theorem> opening tag — capture its attributes to assert a
|
|
215
|
+
// resolvable kind= (or legacy type=) is present.
|
|
216
|
+
const RE_THEOREM = /<Theorem\b([^>]*)>/g;
|
|
212
217
|
|
|
213
218
|
async function fileExists(p) {
|
|
214
219
|
try {
|
|
@@ -278,6 +283,19 @@ for (const rel of chapterFiles) {
|
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
285
|
}
|
|
286
|
+
|
|
287
|
+
// 6. Theorem requires a resolvable kind (#121) — kind= canonical, type=
|
|
288
|
+
// legacy alias. Catches the silent-empty-label / build-throw case at the
|
|
289
|
+
// earliest gate. (Value typos are caught at build by theoremLabel's throw.)
|
|
290
|
+
for (const m of content.matchAll(RE_THEOREM)) {
|
|
291
|
+
if (!/\b(?:kind|type)\s*=/.test(m[1])) {
|
|
292
|
+
fail(
|
|
293
|
+
rel,
|
|
294
|
+
lineOf(content, m.index),
|
|
295
|
+
`<Theorem> has no kind= (or legacy type=) — renders an empty label / throws at build. Add e.g. kind="theorem".`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
281
299
|
}
|
|
282
300
|
|
|
283
301
|
// ===== v4.6.0 (issue #77): missing-prereq re-framing =====
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* theorem-label — resolve a `<Theorem>` component's label from its props,
|
|
3
|
+
* failing loud instead of rendering a silent empty label (#121).
|
|
4
|
+
*
|
|
5
|
+
* Vocabulary: `kind` is canonical; `type` is an accepted **legacy alias** (many
|
|
6
|
+
* consumer books — ssm-foundations ch1–11 — and the LaTeX `\begin{<env>}`
|
|
7
|
+
* mental model pass `type=`). Likewise `name` is canonical with `title`/`label`
|
|
8
|
+
* accepted as aliases. Aliases keep existing content valid; they are synonyms,
|
|
9
|
+
* not deprecations, so they neither warn nor throw.
|
|
10
|
+
*
|
|
11
|
+
* Failure mode: an absent or unrecognized kind THROWS a build-failing,
|
|
12
|
+
* actionable error rather than computing `KIND_LABEL[undefined]` → a bare "."
|
|
13
|
+
* (the v4.8–4.14 silent defect, live across 32+ ssm-foundations theorems). A
|
|
14
|
+
* typo'd kind — silent before — now stops the build with the offending value.
|
|
15
|
+
*
|
|
16
|
+
* Extracted from the `Theorem.astro` frontmatter so the contract is unit-tested
|
|
17
|
+
* in the pure `node --test` suite (see tests/theorem-label.test.mjs) — the same
|
|
18
|
+
* single-source-of-truth move as `academic-parts.ts` (#95).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const THEOREM_KINDS = [
|
|
22
|
+
'theorem',
|
|
23
|
+
'proposition',
|
|
24
|
+
'lemma',
|
|
25
|
+
'corollary',
|
|
26
|
+
'definition',
|
|
27
|
+
'example',
|
|
28
|
+
'exercise',
|
|
29
|
+
'remark',
|
|
30
|
+
'proof',
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type TheoremKind = (typeof THEOREM_KINDS)[number];
|
|
34
|
+
|
|
35
|
+
export const KIND_LABEL: Record<TheoremKind, string> = {
|
|
36
|
+
theorem: 'Theorem',
|
|
37
|
+
proposition: 'Proposition',
|
|
38
|
+
lemma: 'Lemma',
|
|
39
|
+
corollary: 'Corollary',
|
|
40
|
+
definition: 'Definition',
|
|
41
|
+
example: 'Example',
|
|
42
|
+
exercise: 'Exercise',
|
|
43
|
+
remark: 'Remark',
|
|
44
|
+
proof: 'Proof',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface TheoremLabelProps {
|
|
48
|
+
/** Canonical environment selector. */
|
|
49
|
+
kind?: string;
|
|
50
|
+
/** Legacy alias for `kind` (LaTeX-env mental model; ssm-foundations ch1–11). */
|
|
51
|
+
type?: string;
|
|
52
|
+
/** Explicit number, e.g. "4.2"; omit for an unnumbered environment. */
|
|
53
|
+
n?: string;
|
|
54
|
+
/** Canonical display name shown in parentheses. */
|
|
55
|
+
name?: string;
|
|
56
|
+
/** Legacy aliases for `name`. */
|
|
57
|
+
title?: string;
|
|
58
|
+
label?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ResolvedTheoremLabel {
|
|
62
|
+
/** The validated, canonical kind (for `data-kind`). */
|
|
63
|
+
kind: TheoremKind;
|
|
64
|
+
/** The composed "Kind N (Name)" label (never empty). */
|
|
65
|
+
fullLabel: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isTheoremKind(value: string | undefined): value is TheoremKind {
|
|
69
|
+
return value !== undefined && (THEOREM_KINDS as readonly string[]).includes(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a `<Theorem>`'s `kind` (canonical or legacy `type=`) and compose its
|
|
74
|
+
* full label. Throws — never returns an empty label — when the kind is absent
|
|
75
|
+
* or not one of {@link THEOREM_KINDS}.
|
|
76
|
+
*/
|
|
77
|
+
export function theoremLabel(props: TheoremLabelProps): ResolvedTheoremLabel {
|
|
78
|
+
const raw = props.kind ?? props.type;
|
|
79
|
+
if (!isTheoremKind(raw)) {
|
|
80
|
+
const detail =
|
|
81
|
+
raw === undefined
|
|
82
|
+
? 'no kind= (or legacy type=) prop was passed'
|
|
83
|
+
: `kind="${raw}" is not one of ${THEOREM_KINDS.join(', ')}`;
|
|
84
|
+
const hint =
|
|
85
|
+
props.type !== undefined && props.kind === undefined
|
|
86
|
+
? ' (you passed type=; kind= is canonical, type= is accepted)'
|
|
87
|
+
: '';
|
|
88
|
+
throw new Error(
|
|
89
|
+
`<Theorem>: cannot resolve a label — ${detail}. ` +
|
|
90
|
+
`Pass a valid kind, e.g. kind="theorem"${hint}.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const name = props.name ?? props.title ?? props.label;
|
|
95
|
+
const numbered = props.n ? `${KIND_LABEL[raw]} ${props.n}` : KIND_LABEL[raw];
|
|
96
|
+
const fullLabel = name ? `${numbered} (${name})` : numbered;
|
|
97
|
+
return { kind: raw, fullLabel };
|
|
98
|
+
}
|