@brandon_m_behring/book-scaffold-astro 4.16.0 → 4.18.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 +4 -1
- package/components/ObjectiveMap.astro +113 -0
- package/components/Rationale.astro +38 -0
- package/components/Theorem.astro +34 -9
- package/dist/index.d.ts +70 -51
- package/dist/index.mjs +211 -3
- package/dist/lib/theorem-label.d.ts +64 -0
- package/dist/lib/theorem-label.mjs +49 -0
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +123 -0
- package/package.json +3 -1
- package/pages/practice-exam.astro +202 -0
- package/recipes/04-component-library.md +30 -0
- package/scripts/build-labels.mjs +58 -10
- package/scripts/validate.mjs +87 -3
- package/src/lib/exam-domains.ts +36 -0
- package/src/lib/questions-derive.ts +105 -0
- package/src/lib/questions.ts +28 -0
- package/src/lib/theorem-label.ts +19 -0
- package/src/profile-kit.ts +10 -0
- package/src/profiles/academic.ts +1 -0
- package/src/profiles/course-notes.ts +1 -0
- package/src/profiles/minimal.ts +1 -0
- package/src/profiles/research-portfolio.ts +1 -0
- package/src/profiles/tools.ts +1 -0
- package/src/schemas.ts +167 -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). **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.
|
|
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. **Numbering (v4.18.0, #126):** a theorem with an `id` auto-numbers from `labels.json` — the same index `<XRef>` reads — so the heading number equals every cross-reference to it by construction; explicit `n=` is a fallback for un-id'd theorems. `build-labels` indexes the kind-accurate word (`Proposition 8.1`, not a kind-blind `Theorem 8.1`) and throws on an unknown kind.
|
|
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
|
|
|
@@ -104,6 +104,8 @@ Two callout families coexist. Authors import what they need.
|
|
|
104
104
|
|
|
105
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
106
|
|
|
107
|
+
**Study-guide (v4.17.0+, #112; opt-in).** A schema-validated `questions` content collection drives an exam-prep "question bank". Author questions under `src/content/questions/**.{md,mdx}` — frontmatter `id` (unique, required) / `type` (`mcq`|`free`|`cloze`) / `chapter` / `domain` (+ optional `part`/`bloom_level`/`objective_id`/`difficulty`), MDX body = the stem. MCQ carries `options: [{ id, text, correct }]` (exactly one `correct: true`); free-response carries an `answer` (model answer). An MCQ must **not** set `answer` — its answer is the option marked `correct`, and explanations for any type go in a `<Rationale>` body block. Declare the per-book domain taxonomy in `defineBookConfig({ examDomains: ['…'] })` — a question whose `domain` isn't registered **throws at build** (fail-loud, like `<BookLink>`'s `siblingBooks`). Enable the static `/practice-exam` route with `defineBookConfig({ routes: { practiceExam: true } })` (renders the bank grouped by domain with answers behind a `<details>` reveal; `cloze` is reserved/render-deferred). `<ObjectiveMap />` renders the exam-domain → chapter coverage matrix auto-derived from the collection (no separate data file). `<Rationale>` is a collapsible answer/explanation marker for a question's MDX body. (Scoring + diagnostics + appendix + flashcards are later increments — see `docs/plans/active/study-guide-epic_*.md`.)
|
|
108
|
+
|
|
107
109
|
Full reference in `recipes/04-component-library.md`.
|
|
108
110
|
|
|
109
111
|
### Theme-change event (v4.14.2)
|
|
@@ -168,6 +170,7 @@ For monorepo Astro projects (Astro project in subdir), prefix build + deploy com
|
|
|
168
170
|
- Unknown `<XRef id>` — id not in `labels.json` (XRef silently renders `[?label]` otherwise)
|
|
169
171
|
- Missing `<Figure src>` files under `public/`
|
|
170
172
|
- Internal markdown links that don't resolve
|
|
173
|
+
- Study-guide questions (v4.17.0, #112) — a question whose frontmatter `domain` isn't in `examDomains`, and duplicate question `id`s
|
|
171
174
|
|
|
172
175
|
See `recipes/09-validation.md` to extend.
|
|
173
176
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <ObjectiveMap> — exam-domain → chapter coverage matrix (v4.17.0, Tier 3 #117).
|
|
4
|
+
*
|
|
5
|
+
* The reader-facing twin of a CompTIA/Cisco objective-coverage table. AUTO-
|
|
6
|
+
* DERIVED from getCollection('questions'): for each configured exam domain
|
|
7
|
+
* (defineBookConfig({ examDomains })), it shows which chapters carry ≥1 question
|
|
8
|
+
* in that domain. There is no separate coverage data file — the matrix is
|
|
9
|
+
* computed from the question bank, so it cannot drift from reality (and it can't
|
|
10
|
+
* render at all unless the per-book examDomains taxonomy is populated and
|
|
11
|
+
* consistent, which is the cheapest proof that taxonomy pays off).
|
|
12
|
+
*
|
|
13
|
+
* A domain with no questions renders an honest "—" gap rather than being
|
|
14
|
+
* omitted, so the table never implies coverage the bank doesn't have (same
|
|
15
|
+
* coverage-honesty discipline as /convergence).
|
|
16
|
+
*
|
|
17
|
+
* Drop into a front-matter / intro page. Any profile. Twin-gated: with no
|
|
18
|
+
* questions collection, renders an honest note instead of touching the
|
|
19
|
+
* collection (which would throw when unregistered).
|
|
20
|
+
*/
|
|
21
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
22
|
+
import { getCollection } from 'astro:content';
|
|
23
|
+
import { deriveObjectiveMap, distinctChaptersSorted } from '../src/lib/questions';
|
|
24
|
+
import { assertKnownDomain } from '../src/lib/exam-domains';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** Heading above the table. Defaults to "Exam objective coverage". */
|
|
28
|
+
title?: string;
|
|
29
|
+
}
|
|
30
|
+
const { title = 'Exam objective coverage' } = Astro.props;
|
|
31
|
+
|
|
32
|
+
// Presence-gate: only touch the collection when the directory holds files.
|
|
33
|
+
const questionModules = import.meta.glob('/src/content/questions/**/*.{md,mdx}', {
|
|
34
|
+
query: '?raw',
|
|
35
|
+
import: 'default',
|
|
36
|
+
eager: true,
|
|
37
|
+
});
|
|
38
|
+
const hasQuestions = Object.keys(questionModules).length > 0;
|
|
39
|
+
|
|
40
|
+
const entries = hasQuestions
|
|
41
|
+
? await getCollection('questions', (e) => !e.data.draft)
|
|
42
|
+
: [];
|
|
43
|
+
// Fail loud (like /practice-exam): a question with an unregistered / typo'd
|
|
44
|
+
// domain must throw at build, not silently vanish from the coverage matrix.
|
|
45
|
+
for (const e of entries) {
|
|
46
|
+
assertKnownDomain(bookConfig.examDomains, e.data.domain, { id: e.data.id });
|
|
47
|
+
}
|
|
48
|
+
const coverage = deriveObjectiveMap(entries);
|
|
49
|
+
const domains = bookConfig.examDomains ?? [];
|
|
50
|
+
// Column set: chapters that carry ≥1 question, in numeric-aware order
|
|
51
|
+
// (distinctChaptersSorted — a bare .sort() would order "10" before "2").
|
|
52
|
+
const chapters = distinctChaptersSorted(entries);
|
|
53
|
+
---
|
|
54
|
+
{domains.length === 0 ? (
|
|
55
|
+
<p class="objective-map-empty">
|
|
56
|
+
No <code>examDomains</code> configured. Declare them in
|
|
57
|
+
<code>defineBookConfig({'{'} examDomains: […] {'}'})</code> to render the coverage map.
|
|
58
|
+
</p>
|
|
59
|
+
) : chapters.length === 0 ? (
|
|
60
|
+
<p class="objective-map-empty">
|
|
61
|
+
No questions found under <code>src/content/questions/</code> yet — the coverage map
|
|
62
|
+
populates as you add questions tagged with these domains.
|
|
63
|
+
</p>
|
|
64
|
+
) : (
|
|
65
|
+
<figure class="objective-map">
|
|
66
|
+
<figcaption class="objective-map-title">{title}</figcaption>
|
|
67
|
+
<table class="objective-map-table">
|
|
68
|
+
<thead>
|
|
69
|
+
<tr>
|
|
70
|
+
<th scope="col">Domain</th>
|
|
71
|
+
{chapters.map((c) => <th scope="col">{c}</th>)}
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody>
|
|
75
|
+
{domains.map((domain) => {
|
|
76
|
+
const covered = coverage.get(domain) ?? new Set();
|
|
77
|
+
const isGap = covered.size === 0;
|
|
78
|
+
return (
|
|
79
|
+
<tr class={isGap ? 'objective-map-gap' : undefined}>
|
|
80
|
+
<th scope="row">{domain}</th>
|
|
81
|
+
{chapters.map((c) => (
|
|
82
|
+
<td aria-label={covered.has(c) ? `${domain} covered in ${c}` : undefined}>
|
|
83
|
+
{covered.has(c) ? '✓' : '—'}
|
|
84
|
+
</td>
|
|
85
|
+
))}
|
|
86
|
+
</tr>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
</figure>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<style>
|
|
95
|
+
.objective-map { margin-block: 1.5rem; overflow-x: auto; }
|
|
96
|
+
.objective-map-title { font-weight: 600; margin-block-end: 0.5rem; }
|
|
97
|
+
.objective-map-empty { color: var(--color-text-muted, #6b7280); }
|
|
98
|
+
.objective-map-table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
|
99
|
+
.objective-map-table th,
|
|
100
|
+
.objective-map-table td {
|
|
101
|
+
border: 1px solid var(--color-border, #e5e7eb);
|
|
102
|
+
padding: 0.35rem 0.55rem;
|
|
103
|
+
text-align: center;
|
|
104
|
+
}
|
|
105
|
+
.objective-map-table th[scope='row'] {
|
|
106
|
+
text-align: start;
|
|
107
|
+
text-transform: capitalize;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
.objective-map-table td { color: var(--color-accent, #4f46e5); }
|
|
111
|
+
.objective-map-gap th[scope='row'] { color: var(--color-text-muted, #6b7280); }
|
|
112
|
+
.objective-map-gap td { color: var(--color-text-muted, #6b7280); }
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Rationale> — collapsible answer/explanation for a study-guide question
|
|
4
|
+
* (v4.17.0, Tier 3). Renders its slot inside a <details> so the answer stays
|
|
5
|
+
* hidden until the reader chooses to reveal it (Bjork desirable-difficulties:
|
|
6
|
+
* delayed feedback aids retention). Authored in a question's MDX body; the
|
|
7
|
+
* answer-rationale back-appendix (#114) will later hoist these into an appendix
|
|
8
|
+
* using this same marker — so a rich, cited rationale has a stable home now.
|
|
9
|
+
*
|
|
10
|
+
* Any profile. Register it in src/mdx-components.ts (defineMdxComponents) to use
|
|
11
|
+
* inside src/content/questions/**.mdx. Slot freely: prose, math, <Cite>, code.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Summary label for the reveal. Defaults to "Answer & rationale". */
|
|
15
|
+
title?: string;
|
|
16
|
+
}
|
|
17
|
+
const { title = 'Answer & rationale' } = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
<details class="question-rationale">
|
|
20
|
+
<summary>{title}</summary>
|
|
21
|
+
<div class="question-rationale-body"><slot /></div>
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
.question-rationale {
|
|
26
|
+
margin-block: 0.75rem;
|
|
27
|
+
border-inline-start: 3px solid var(--color-accent, #4f46e5);
|
|
28
|
+
padding-inline-start: 0.75rem;
|
|
29
|
+
}
|
|
30
|
+
.question-rationale > summary {
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
color: var(--color-accent, #4f46e5);
|
|
34
|
+
}
|
|
35
|
+
.question-rationale-body {
|
|
36
|
+
margin-block-start: 0.5rem;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
package/components/Theorem.astro
CHANGED
|
@@ -10,29 +10,54 @@
|
|
|
10
10
|
* (ssm-foundations ch1–11 + the LaTeX-env mental model pass `type=`). `name`
|
|
11
11
|
* is canonical with `title=`/`label=` accepted. An absent or unknown kind
|
|
12
12
|
* THROWS at build (see `src/lib/theorem-label`) — it never renders an empty
|
|
13
|
-
* label.
|
|
13
|
+
* label.
|
|
14
14
|
*
|
|
15
|
-
* Auto-numbering
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Auto-numbering (#126): when the theorem has an `id`, its number is read from
|
|
16
|
+
* `src/data/labels.json` — the same map <XRef> resolves — so the heading number
|
|
17
|
+
* EQUALS every cross-reference to it by construction. No hand-passed `n=`, no
|
|
18
|
+
* drift; inserting a theorem renumbers the chapter from one counter
|
|
19
|
+
* (`book-scaffold build-labels`). Explicit `n=` stays a fallback for an un-id'd
|
|
20
|
+
* theorem (or before labels.json is built); when an id resolves, the labels.json
|
|
21
|
+
* number wins so the two surfaces can never disagree.
|
|
18
22
|
*
|
|
19
23
|
* Usage:
|
|
20
|
-
* <Theorem kind="theorem"
|
|
21
|
-
* For …
|
|
24
|
+
* <Theorem kind="theorem" name="Stable continuous-time eigenvalues" id="thm-w4-stability">
|
|
25
|
+
* For … // number auto-resolved from labels.json
|
|
22
26
|
* </Theorem>
|
|
23
27
|
*
|
|
24
28
|
* <Theorem kind="definition" n="4.1" name="HiPPO-LegS">
|
|
25
|
-
* The HiPPO-LegS state matrix …
|
|
29
|
+
* The HiPPO-LegS state matrix … // explicit n= (un-cross-referenced)
|
|
26
30
|
* </Theorem>
|
|
27
31
|
*/
|
|
28
|
-
import { theoremLabel, type TheoremLabelProps } from '../src/lib/theorem-label';
|
|
32
|
+
import { theoremLabel, resolveTheoremNumber, type TheoremLabelProps } from '../src/lib/theorem-label';
|
|
29
33
|
|
|
30
34
|
interface Props extends TheoremLabelProps {
|
|
31
35
|
id?: string;
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// #126: resolve THIS theorem's number from the same labels.json that <XRef>
|
|
39
|
+
// reads, so the heading number == the cross-reference display by construction.
|
|
40
|
+
// build-labels.mjs writes { href, display, number } keyed by id. Mirror XRef's
|
|
41
|
+
// project-root glob (Vite resolves `/` to the consumer root, not the package);
|
|
42
|
+
// a missing file → empty map → fall back to explicit n= (the same soft-degrade
|
|
43
|
+
// path as XRef — the validator catches unknown ids at CI).
|
|
44
|
+
type LabelEntry = { href: string; display: string; number?: string | null };
|
|
45
|
+
const labelsModules = import.meta.glob<{ default: Record<string, LabelEntry> }>(
|
|
46
|
+
'/src/data/labels.json',
|
|
47
|
+
{ eager: true },
|
|
48
|
+
);
|
|
49
|
+
const labelsMap = (labelsModules['/src/data/labels.json']?.default ?? {}) as Record<
|
|
50
|
+
string,
|
|
51
|
+
LabelEntry
|
|
52
|
+
>;
|
|
53
|
+
|
|
34
54
|
const { id } = Astro.props;
|
|
35
|
-
|
|
55
|
+
// labels.json (by id) is the number source; explicit n= is the fallback when no
|
|
56
|
+
// id resolves. When both exist the shared source wins — that is what guarantees
|
|
57
|
+
// heading == xref (a stale n= can't reintroduce drift).
|
|
58
|
+
const entry = id ? labelsMap[id] : undefined;
|
|
59
|
+
const resolvedN = resolveTheoremNumber(entry, Astro.props.n);
|
|
60
|
+
const { kind, fullLabel } = theoremLabel({ ...Astro.props, n: resolvedN });
|
|
36
61
|
---
|
|
37
62
|
<div class="theorem" data-kind={kind} id={id}>
|
|
38
63
|
<span class="theorem-label">{fullLabel}.</span>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import {
|
|
3
|
-
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError,
|
|
2
|
+
import { d as BookConfigOptions, g as BookScaffoldIntegrationOptions, a5 as volatilityLevels, i as ChaptersRenderer, t as academicParts, Q as Question, q as Style } from './types-CMPuyZGP.js';
|
|
3
|
+
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BloomLevel, c as BookConfigError, e as BookPreset, f as BookProfile, h as BookSchemasOptions, C as ChapterFor, j as CourseNotesChapter, F as FreshnessAffordance, k as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, l as PartialRouteToggles, m as ProfileDefinition, n as Provenance, o as QuestionType, R as ResearchPortfolioChapter, p as RouteToggles, S as StatusBadge, r as StyleInput, T as ToolsChapter, V as VolatilityBadge, s as academicChapterSchema, u as bloomLevels, v as changeKinds, w as changelogSchema, x as chapterStatus, y as citationBackstops, z as composeStyles, D as courseNotesChapterSchema, E as defineProfile, G as defineStyle, H as minimalChapterSchema, I as normalizeFrontmatterConfig, J as patternCategories, K as patternsSchema, L as provenanceObject, N as provenanceSchema, O as questionDifficulties, U as questionSchema, W as questionTypes, X as refineQuestion, Y as refinedQuestionSchema, Z as researchPortfolioChapterSchema, _ as resolvePreset, $ as resolveProfile, a0 as sourceTiers, a1 as sourceTiersResearch, a2 as sourcesSchema, a3 as toolSlugs, a4 as toolsChapterSchema } from './types-CMPuyZGP.js';
|
|
4
|
+
export { KIND_LABEL, ResolvedTheoremLabel, THEOREM_KINDS, TheoremKind, TheoremLabelProps, resolveTheoremNumber, theoremLabel } from './lib/theorem-label.js';
|
|
4
5
|
import 'astro/zod';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -204,54 +205,6 @@ declare function academicPartOrdinal(part: string): number;
|
|
|
204
205
|
*/
|
|
205
206
|
declare function academicPartHeading(part: string): string;
|
|
206
207
|
|
|
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
|
-
|
|
255
208
|
/**
|
|
256
209
|
* repo-url — resolve and build GitHub source links for CodeRef / CodeBlock.
|
|
257
210
|
*
|
|
@@ -337,6 +290,72 @@ declare function assertEnumProp<T extends string>(value: unknown, allowed: reado
|
|
|
337
290
|
*/
|
|
338
291
|
declare function resolveBookHref(siblingBooks: Record<string, string> | null | undefined, book: string, to: string): string;
|
|
339
292
|
|
|
293
|
+
/**
|
|
294
|
+
* exam-domains — validate a question's `domain` against the consumer's closed
|
|
295
|
+
* `examDomains` taxonomy (Tier 3, #112).
|
|
296
|
+
*
|
|
297
|
+
* Exam domains are PER-BOOK (Cisco security domains ≠ CompTIA objectives ≠ a
|
|
298
|
+
* math syllabus's topics), so they can't be a hardcoded `z.enum` in the schema.
|
|
299
|
+
* The consumer declares them once — `defineBookConfig({ examDomains: [...] })`,
|
|
300
|
+
* threaded through the `virtual:book-scaffold/book-config` module — and a
|
|
301
|
+
* question whose `domain` is not in that list THROWS at build rather than
|
|
302
|
+
* silently mis-weighting a blueprint or dropping a row from the objective-map.
|
|
303
|
+
*
|
|
304
|
+
* Membership can't be a Zod invariant: `questionSchema` is constructed at
|
|
305
|
+
* package-load time, outside any consumer context, so it can't see
|
|
306
|
+
* `examDomains` (the same constraint that put `siblingBooks` validation in
|
|
307
|
+
* lib/book-link.ts rather than the schema). So Zod checks `domain` is a
|
|
308
|
+
* non-empty string; THIS runs at the route/build layer where the resolved
|
|
309
|
+
* config is available. Mirrors `resolveBookHref` (lib/book-link.ts).
|
|
310
|
+
*/
|
|
311
|
+
declare function assertKnownDomain(examDomains: readonly string[] | null | undefined, domain: string, ctx: {
|
|
312
|
+
id: string;
|
|
313
|
+
}): string;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* src/lib/questions-derive.ts — PURE grouping + objective-map derivation for the
|
|
317
|
+
* study-guide `questions` collection (Tier 3, #112).
|
|
318
|
+
*
|
|
319
|
+
* No `astro:content` import, so this is safe in the Node-loaded main entry and
|
|
320
|
+
* unit-tested directly from dist/ (tests/questions.test.mjs). The Astro-context
|
|
321
|
+
* `getCollection` wrapper lives in questions.ts — same split as
|
|
322
|
+
* chapter-sort.ts (pure) / chapters.ts (Astro wrapper).
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
/** Stable string label for a chapter ref (number or academic-style string). */
|
|
326
|
+
declare function chapterLabel(chapter: number | string): string;
|
|
327
|
+
/** Sort questions by chapter then id (stable, deterministic). Pure. */
|
|
328
|
+
declare function sortQuestions<T extends {
|
|
329
|
+
data: Pick<Question, 'chapter' | 'id'>;
|
|
330
|
+
}>(entries: readonly T[]): T[];
|
|
331
|
+
/** Group entries by `domain`, preserving input order within each bucket. */
|
|
332
|
+
declare function groupByDomain<T extends {
|
|
333
|
+
data: Pick<Question, 'domain'>;
|
|
334
|
+
}>(entries: readonly T[]): Map<string, T[]>;
|
|
335
|
+
/** Group entries by `chapter` (string label), preserving input order. */
|
|
336
|
+
declare function groupByChapter<T extends {
|
|
337
|
+
data: Pick<Question, 'chapter'>;
|
|
338
|
+
}>(entries: readonly T[]): Map<string, T[]>;
|
|
339
|
+
/**
|
|
340
|
+
* Domain → set of chapter labels that have ≥1 question in that domain. The data
|
|
341
|
+
* backbone of `<ObjectiveMap>` (#117): the coverage matrix is *derived* from the
|
|
342
|
+
* question bank, so it can't drift from a separately-maintained table — the
|
|
343
|
+
* cheapest proof the per-book `examDomains` taxonomy is populated + consistent.
|
|
344
|
+
*/
|
|
345
|
+
declare function deriveObjectiveMap<T extends {
|
|
346
|
+
data: Pick<Question, 'domain' | 'chapter'>;
|
|
347
|
+
}>(entries: readonly T[]): Map<string, Set<string>>;
|
|
348
|
+
/**
|
|
349
|
+
* Distinct chapter labels across the entries, in numeric-aware order (numeric
|
|
350
|
+
* chapters before string slugs, each ordered) — the column set for
|
|
351
|
+
* `<ObjectiveMap>`. Dedupes by label, sorts the underlying chapter values via
|
|
352
|
+
* the same `chapterOrder` as `sortQuestions`, then labels. A bare `.sort()` on
|
|
353
|
+
* the labels would put "10" before "2" (the chapter-column sort bug).
|
|
354
|
+
*/
|
|
355
|
+
declare function distinctChaptersSorted<T extends {
|
|
356
|
+
data: Pick<Question, 'chapter'>;
|
|
357
|
+
}>(entries: readonly T[]): string[];
|
|
358
|
+
|
|
340
359
|
/**
|
|
341
360
|
* src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
|
|
342
361
|
*
|
|
@@ -438,4 +457,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
438
457
|
*/
|
|
439
458
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
440
459
|
|
|
441
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus,
|
|
460
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, Question, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, assertEnumProp, assertKnownDomain, bookScaffoldIntegration, buildGithubUrl, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, minimalStyle, originUrlFromGitConfig, parseRepoSlug, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, sortQuestions, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|