@brandon_m_behring/book-scaffold-astro 4.17.0 → 4.19.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 +3 -3
- package/components/Diagnostic.astro +56 -0
- package/components/PartReview.astro +81 -0
- package/components/Term.astro +33 -0
- package/components/Theorem.astro +34 -9
- package/dist/index.d.ts +109 -51
- package/dist/index.mjs +105 -0
- 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 +31 -0
- package/dist/types-DJgLQGP9.d.ts +1248 -0
- package/package.json +7 -3
- package/pages/glossary.astro +103 -0
- package/recipes/04-component-library.md +4 -1
- package/scripts/build-labels.mjs +58 -10
- package/scripts/validate.mjs +17 -2
- package/src/lib/exam-engine.ts +155 -0
- package/src/lib/part-review.ts +56 -0
- package/src/lib/theorem-label.ts +19 -0
- package/src/profile-kit.ts +8 -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 +22 -0
- package/styles/callouts.css +84 -0
- package/styles/tokens.css +1 -0
package/CLAUDE.md
CHANGED
|
@@ -96,15 +96,15 @@ 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
|
-
**Pedagogy family** (v4.1.0+, any profile,
|
|
101
|
+
**Pedagogy family** (v4.1.0+, any profile, 4 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), `Diagnostic` (v4.19.0, #110; teal; pre-reading "Do I Know This Already?" DIKTA self-check — a slotted question list + a skip/skim/read routing rubric via `skimTo`, plus an optional collapsible answer key via `slot="answers"`). Slot bullets/code freely; render at any preset.
|
|
102
102
|
|
|
103
103
|
**Utility components** (`src/components/`, any profile): `Cite`, `XRef`, `Figure`, `MarginNote`, `Sidenote`, `WeekRef`, `CodeRef`, `CodeBlock`, `Tag`, `StatusBadge`, `BookLink` (v4.16.0+; cross-book link — `<BookLink book="design" to="…"/>` resolves `book` against `defineBookConfig({ siblingBooks })` and throws on an unknown book; `<XRef>` is in-book only — #96), `PocLayout` (v4.1.0+; wraps slot in a per-`kind` layout shell — 5 closed-union kinds; see `recipes/15-defining-styles.md`).
|
|
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. (
|
|
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. `<Diagnostic>` (v4.19.0, #110) renders a per-chapter pre-reading "Do I Know This Already?" self-check (pedagogy family above; static `<details>` answer reveal). `<PartReview part={N} />` (v4.19.0, #111) aggregates a Part's `<Exercise>` items for interleaved review — reusing the `build-exercises` index + the chapters' `part` field (run `book-scaffold build-exercises` first; presence-gated otherwise). A **searchable glossary** (v4.19.0, #115): author terms under `src/content/glossary/**` (frontmatter `term` + an MDX-body definition), enable the static `/glossary` route via `defineBookConfig({ routes: { glossary: true } })`, and link inline with `<Term id="…">…</Term>` (→ `/glossary#term-<id>`). (Scoring, the rationale appendix, and flashcards are later increments — see `docs/plans/active/study-guide-epic_*.md`.)
|
|
108
108
|
|
|
109
109
|
Full reference in `recipes/04-component-library.md`.
|
|
110
110
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Diagnostic — pre-reading "Do I Know This Already?" self-check (#110).
|
|
4
|
+
*
|
|
5
|
+
* Pearson / Cisco-Press DIKTA pattern: a short retrieval list the reader
|
|
6
|
+
* attempts BEFORE reading, plus a skip/skim/read routing rubric — visually
|
|
7
|
+
* distinct from the post-chapter <Exercise>/<Practice>. Static: the optional
|
|
8
|
+
* answer key reveals via native <details> (no JS; the scored engine is the
|
|
9
|
+
* separate #112 increment). Pedagogically this is pre-testing (Bjork): a
|
|
10
|
+
* retrieval attempt before study strengthens later recall even when the guess
|
|
11
|
+
* is wrong, so the reader should try before peeking.
|
|
12
|
+
*
|
|
13
|
+
* Slots:
|
|
14
|
+
* default — the numbered question list (the retrieval items)
|
|
15
|
+
* "answers" — OPTIONAL collapsible answer key (rendered only when provided)
|
|
16
|
+
*
|
|
17
|
+
* Family: teal (--callout-diagnostic, a blue+green blend) — a cool hue held
|
|
18
|
+
* distinct from the warm pedagogy callouts (Pitfall crimson, WorkedExample
|
|
19
|
+
* plum, YouWillLearn gold) and the plum Exercise/Practice.
|
|
20
|
+
*/
|
|
21
|
+
interface Props {
|
|
22
|
+
/** Heading. Default "Do I know this already?". */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** Section to skim ahead to when confident. Default "Exam essentials". */
|
|
25
|
+
skimTo?: string;
|
|
26
|
+
/** Full override of the routing sentence (else built from skimTo). */
|
|
27
|
+
routing?: string;
|
|
28
|
+
}
|
|
29
|
+
const {
|
|
30
|
+
title = 'Do I know this already?',
|
|
31
|
+
skimTo = 'Exam essentials',
|
|
32
|
+
routing,
|
|
33
|
+
} = Astro.props;
|
|
34
|
+
const hasAnswers = Astro.slots.has('answers');
|
|
35
|
+
---
|
|
36
|
+
<aside class="callout callout-diagnostic" role="note">
|
|
37
|
+
<div class="callout-diagnostic-header">
|
|
38
|
+
<strong class="callout-title">{title}</strong>
|
|
39
|
+
<span class="callout-chip">Diagnostic</span>
|
|
40
|
+
</div>
|
|
41
|
+
<p class="callout-routing">
|
|
42
|
+
{routing ? routing : (
|
|
43
|
+
<Fragment>
|
|
44
|
+
Answer these confidently and you can skim ahead to <strong>{skimTo}</strong>;
|
|
45
|
+
if any is shaky, read closely — each is developed below.
|
|
46
|
+
</Fragment>
|
|
47
|
+
)}
|
|
48
|
+
</p>
|
|
49
|
+
<div class="callout-body"><slot /></div>
|
|
50
|
+
{hasAnswers && (
|
|
51
|
+
<details class="callout-diagnostic-answers">
|
|
52
|
+
<summary>Check your answers</summary>
|
|
53
|
+
<div class="callout-body"><slot name="answers" /></div>
|
|
54
|
+
</details>
|
|
55
|
+
)}
|
|
56
|
+
</aside>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PartReview — aggregate a Part's <Exercise> items for interleaved review (#111).
|
|
4
|
+
*
|
|
5
|
+
* Cisco-Press / Pearson "Part Review": after a group of chapters (a Part — e.g.
|
|
6
|
+
* a cert domain) the reader gets that part's exercises collected in one place
|
|
7
|
+
* for spaced, interleaved practice. Reuses `src/data/exercises.json` (emitted by
|
|
8
|
+
* `book-scaffold build-exercises`) plus the chapters collection's `part` field —
|
|
9
|
+
* no new build step. Drop `<PartReview part={N} />` at the end of a part's last
|
|
10
|
+
* chapter or on a part-summary page.
|
|
11
|
+
*
|
|
12
|
+
* `part` accepts a number (tools/minimal/course-notes/research-portfolio
|
|
13
|
+
* profiles, where `part` is numeric) or a string (academic `part` enum).
|
|
14
|
+
*
|
|
15
|
+
* Presence-gated (twin-gate, like /exercises): with no exercises.json it renders
|
|
16
|
+
* a build hint rather than failing; with the index but no items in this part it
|
|
17
|
+
* says so honestly. `<Practice>` aggregation + a book-level `/review` route are
|
|
18
|
+
* later increments (see docs/plans/active/study-guide-epic_*.md).
|
|
19
|
+
*/
|
|
20
|
+
import { getCollection } from 'astro:content';
|
|
21
|
+
import { selectPartExercises } from '../src/lib/part-review';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** The part to aggregate. Number (tools/minimal/…) or string (academic enum). */
|
|
25
|
+
part: number | string;
|
|
26
|
+
/** Heading override. Default "Part {part} Review". */
|
|
27
|
+
title?: string;
|
|
28
|
+
}
|
|
29
|
+
const { part, title } = Astro.props;
|
|
30
|
+
|
|
31
|
+
// Reuse the build-exercises index (keyed by chapter slug). Project-root-relative
|
|
32
|
+
// glob (the tips.astro / exercises.astro lesson) so it resolves across consumers.
|
|
33
|
+
const exModules = import.meta.glob<{ default: Record<string, Array<{ id: string; problem: string }>> }>(
|
|
34
|
+
'/src/data/exercises.json',
|
|
35
|
+
{ eager: true },
|
|
36
|
+
);
|
|
37
|
+
const exEntry = exModules['/src/data/exercises.json'];
|
|
38
|
+
const byChapter = exEntry?.default ?? {};
|
|
39
|
+
const hasIndex = Boolean(exEntry);
|
|
40
|
+
|
|
41
|
+
// Filter this part's chapters, sort to book order, and join the index — pure +
|
|
42
|
+
// unit-tested in src/lib/part-review.ts (`part` is String-coerced, so a
|
|
43
|
+
// number-vs-string prop can't silently miss). This component just renders it.
|
|
44
|
+
const chapters = await getCollection('chapters', (e) => !e.data.draft);
|
|
45
|
+
const { groups, total } = selectPartExercises(chapters, byChapter, part);
|
|
46
|
+
const heading = title ?? `Part ${part} Review`;
|
|
47
|
+
---
|
|
48
|
+
<aside class="part-review" aria-label={heading}>
|
|
49
|
+
<h2 class="part-review-title">{heading}</h2>
|
|
50
|
+
|
|
51
|
+
{!hasIndex && (
|
|
52
|
+
<p class="part-review-empty">
|
|
53
|
+
No <code>src/data/exercises.json</code> — run <code>npx book-scaffold build-exercises</code> to populate the review.
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{hasIndex && total === 0 && (
|
|
58
|
+
<p class="part-review-empty">No exercises found in this part's chapters yet.</p>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{total > 0 && (
|
|
62
|
+
<Fragment>
|
|
63
|
+
<p class="part-review-summary">
|
|
64
|
+
{total} exercise{total === 1 ? '' : 's'} across {groups.length} chapter{groups.length === 1 ? '' : 's'} — interleaved review.
|
|
65
|
+
</p>
|
|
66
|
+
{groups.map((g) => (
|
|
67
|
+
<section class="part-review-chapter">
|
|
68
|
+
<h3 class="part-review-chapter-title"><a href={`/chapters/${g.chapter}/`}>{g.chapter}</a></h3>
|
|
69
|
+
<ol class="part-review-list">
|
|
70
|
+
{g.exercises.map((ex) => (
|
|
71
|
+
<li id={`review-${ex.id}`} class="part-review-item">
|
|
72
|
+
<span class="part-review-item-id">{ex.id}</span>
|
|
73
|
+
<span class="part-review-item-problem">{ex.problem}</span>
|
|
74
|
+
</li>
|
|
75
|
+
))}
|
|
76
|
+
</ol>
|
|
77
|
+
</section>
|
|
78
|
+
))}
|
|
79
|
+
</Fragment>
|
|
80
|
+
)}
|
|
81
|
+
</aside>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Term — inline glossary cross-link (v4.19.0, #115).
|
|
4
|
+
*
|
|
5
|
+
* <Term id="agentic-loop">agent loop</Term> renders the slot text as a link to
|
|
6
|
+
* the glossary entry's anchor (/glossary#term-<id>). `id` is the glossary file's
|
|
7
|
+
* entry id (filename slug, e.g. `agentic-loop` for agentic-loop.mdx). Static
|
|
8
|
+
* link — no tooltip/definition preview in v1. Pairs with the `glossary`
|
|
9
|
+
* collection + the /glossary route (defineBookConfig({ routes: { glossary: true } })).
|
|
10
|
+
*
|
|
11
|
+
* Like <XRef>, this does not validate the target at render — a dangling id
|
|
12
|
+
* produces a link to a missing anchor, caught by review (a future `validate`
|
|
13
|
+
* check can resolve <Term id> against the glossary collection).
|
|
14
|
+
*/
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Glossary entry id (filename slug) — the /glossary#term-<id> anchor target. */
|
|
17
|
+
id: string;
|
|
18
|
+
}
|
|
19
|
+
const { id } = Astro.props;
|
|
20
|
+
---
|
|
21
|
+
<a href={`/glossary#term-${id}`} class="term-link"><slot /></a>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.term-link {
|
|
25
|
+
color: inherit;
|
|
26
|
+
text-decoration: underline dotted;
|
|
27
|
+
text-decoration-color: var(--callout-info, #3B6FA0);
|
|
28
|
+
text-underline-offset: 0.15em;
|
|
29
|
+
}
|
|
30
|
+
.term-link:hover {
|
|
31
|
+
text-decoration-style: solid;
|
|
32
|
+
}
|
|
33
|
+
</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 { d as BookConfigOptions, g as BookScaffoldIntegrationOptions,
|
|
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,
|
|
2
|
+
import { d as BookConfigOptions, g as BookScaffoldIntegrationOptions, a7 as volatilityLevels, i as ChaptersRenderer, t as academicParts, Q as Question, q as Style } from './types-DJgLQGP9.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, G as GlossaryTerm, 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, H as defineStyle, I as glossarySchema, J as minimalChapterSchema, K as normalizeFrontmatterConfig, L as patternCategories, N as patternsSchema, O as provenanceObject, U as provenanceSchema, W as questionDifficulties, X as questionSchema, Y as questionTypes, Z as refineQuestion, _ as refinedQuestionSchema, $ as researchPortfolioChapterSchema, a0 as resolvePreset, a1 as resolveProfile, a2 as sourceTiers, a3 as sourceTiersResearch, a4 as sourcesSchema, a5 as toolSlugs, a6 as toolsChapterSchema } from './types-DJgLQGP9.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
|
*
|
|
@@ -403,6 +356,111 @@ declare function distinctChaptersSorted<T extends {
|
|
|
403
356
|
data: Pick<Question, 'chapter'>;
|
|
404
357
|
}>(entries: readonly T[]): string[];
|
|
405
358
|
|
|
359
|
+
/**
|
|
360
|
+
* exam-engine.ts — PURE sampling + scoring for the interactive practice exam
|
|
361
|
+
* (#112) and the cross-domain assessment test (#113).
|
|
362
|
+
*
|
|
363
|
+
* No DOM, no Preact: this is the testable core both the PracticeExam island and
|
|
364
|
+
* the AssessmentTest reuse, exercised in tests/exam-engine.test.mjs (node:test,
|
|
365
|
+
* no browser). The islands are thin UI over these functions — selection state +
|
|
366
|
+
* rendering only — so the *logic* (which questions, what score, which weak
|
|
367
|
+
* domains) is verified without a headless browser.
|
|
368
|
+
*
|
|
369
|
+
* Randomness is injected (`rng`, default Math.random) so the sampler is
|
|
370
|
+
* deterministic under test (pass a seeded rng) and varied in the browser.
|
|
371
|
+
*/
|
|
372
|
+
/** The minimum a question contributes to sampling + scoring. The stem (MDX
|
|
373
|
+
* body) is rendered server-side, NOT carried here. */
|
|
374
|
+
interface ExamQuestion {
|
|
375
|
+
id: string;
|
|
376
|
+
domain: string;
|
|
377
|
+
/** MCQ options; exactly one carries `correct: true` (enforced by the schema). */
|
|
378
|
+
options: ReadonlyArray<{
|
|
379
|
+
id: string;
|
|
380
|
+
correct?: boolean;
|
|
381
|
+
}>;
|
|
382
|
+
}
|
|
383
|
+
interface ExamBlueprint {
|
|
384
|
+
/** Total questions to sample (clamped to pool size). */
|
|
385
|
+
count: number;
|
|
386
|
+
/** Optional per-domain quota (a Bloom/weight blueprint). Filled first, then
|
|
387
|
+
* topped up from the rest of the pool to reach `count`. */
|
|
388
|
+
perDomain?: Readonly<Record<string, number>>;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Fisher–Yates shuffle with an injectable rng (default Math.random). Pure: it
|
|
392
|
+
* copies the input rather than mutating it.
|
|
393
|
+
*
|
|
394
|
+
* `rng` MUST return a value in `[0, 1)` (Math.random's contract). `j` is
|
|
395
|
+
* additionally clamped to `i` so a defective rng returning exactly `1.0` can't
|
|
396
|
+
* index out of bounds (`Math.floor(1.0 * (i+1)) === i+1`), which would otherwise
|
|
397
|
+
* punch a hole into `a` and grow its length.
|
|
398
|
+
*/
|
|
399
|
+
declare function shuffle<T>(arr: readonly T[], rng?: () => number): T[];
|
|
400
|
+
/**
|
|
401
|
+
* Sample a question form from the pool. With a `perDomain` blueprint, each
|
|
402
|
+
* domain's quota is filled first (clamped to availability), then the form is
|
|
403
|
+
* topped up from the remaining pool to reach `count`. Never returns duplicates;
|
|
404
|
+
* never more than the pool holds. The result is shuffled so domain-grouped
|
|
405
|
+
* picks aren't clustered.
|
|
406
|
+
*
|
|
407
|
+
* Caller contracts: `rng` returns `[0, 1)` (passed through to `shuffle`); and
|
|
408
|
+
* `Σ perDomain ≤ count` — quotas summing past `count` are honored in
|
|
409
|
+
* `Object.entries` order until the budget is spent, so later-listed domains are
|
|
410
|
+
* silently starved. Validating/clamping the blueprint sum is the caller's job;
|
|
411
|
+
* a guard can graduate here once a real consumer needs it.
|
|
412
|
+
*/
|
|
413
|
+
declare function sampleExam(pool: readonly ExamQuestion[], blueprint: ExamBlueprint, rng?: () => number): ExamQuestion[];
|
|
414
|
+
interface DomainScore {
|
|
415
|
+
domain: string;
|
|
416
|
+
correct: number;
|
|
417
|
+
total: number;
|
|
418
|
+
}
|
|
419
|
+
interface ExamResult {
|
|
420
|
+
correct: number;
|
|
421
|
+
total: number;
|
|
422
|
+
/** Whole-percent score, 0–100 (0 when no questions). */
|
|
423
|
+
pct: number;
|
|
424
|
+
/** Per-domain rollup, in first-seen order. */
|
|
425
|
+
byDomain: DomainScore[];
|
|
426
|
+
/** Domains scoring below `passMark` — what the assessment routes the reader to. */
|
|
427
|
+
weakDomains: string[];
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Score a set of answered questions. `answers` maps question id → chosen option
|
|
431
|
+
* id; a question is correct when the chosen option is the one flagged
|
|
432
|
+
* `correct`. Unanswered or wrongly-answered questions count as incorrect.
|
|
433
|
+
* Rolls up per domain and flags domains below `passMark` (default 0.7) as weak.
|
|
434
|
+
*/
|
|
435
|
+
declare function scoreExam(questions: readonly ExamQuestion[], answers: Readonly<Record<string, string>>, passMark?: number): ExamResult;
|
|
436
|
+
|
|
437
|
+
/** One exercise as emitted by `book-scaffold build-exercises` (src/data/exercises.json). */
|
|
438
|
+
interface ReviewExercise {
|
|
439
|
+
id: string;
|
|
440
|
+
problem: string;
|
|
441
|
+
}
|
|
442
|
+
/** A chapter entry — structurally what `getCollection('chapters')` yields. */
|
|
443
|
+
interface ReviewChapter {
|
|
444
|
+
id: string;
|
|
445
|
+
data: Record<string, unknown>;
|
|
446
|
+
}
|
|
447
|
+
interface PartReviewGroup {
|
|
448
|
+
chapter: string;
|
|
449
|
+
exercises: ReviewExercise[];
|
|
450
|
+
}
|
|
451
|
+
interface PartReviewSelection {
|
|
452
|
+
groups: PartReviewGroup[];
|
|
453
|
+
total: number;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Select a Part's exercises, in book order, joining `chapters` (filtered by
|
|
457
|
+
* `part`) against the `byChapter` index (keyed by chapter id). `part` is matched
|
|
458
|
+
* with `String()` coercion so `<PartReview part="2" />` finds numeric-`part`
|
|
459
|
+
* chapters and vice versa (the fail-silent trap a strict `===` left). Chapters
|
|
460
|
+
* with no exercises in the index are skipped; `total` sums the rest.
|
|
461
|
+
*/
|
|
462
|
+
declare function selectPartExercises(chapters: readonly ReviewChapter[], byChapter: Readonly<Record<string, ReviewExercise[]>>, part: number | string): PartReviewSelection;
|
|
463
|
+
|
|
406
464
|
/**
|
|
407
465
|
* src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
|
|
408
466
|
*
|
|
@@ -504,4 +562,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
504
562
|
*/
|
|
505
563
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
506
564
|
|
|
507
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type
|
|
565
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type DomainScore, type ExamBlueprint, type ExamQuestion, type ExamResult, type Freshness, type FreshnessStatus, type PartReviewGroup, type PartReviewSelection, Question, type ReviewChapter, type ReviewExercise, 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, sampleExam, scoreExam, selectPartExercises, shuffle, sortQuestions, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -462,6 +462,18 @@ function refineQuestion(q, ctx) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
var refinedQuestionSchema = questionSchema.superRefine(refineQuestion);
|
|
465
|
+
var glossarySchema = z.object({
|
|
466
|
+
term: z.string().trim().min(1),
|
|
467
|
+
// display term (required)
|
|
468
|
+
aliases: z.array(z.string()).default([]),
|
|
469
|
+
// synonyms / alternate spellings (searchable)
|
|
470
|
+
domain: z.string().optional(),
|
|
471
|
+
// optional grouping (e.g. an exam domain)
|
|
472
|
+
see: z.array(z.string()).default([]),
|
|
473
|
+
// related glossary entry ids (cross-links)
|
|
474
|
+
tags: z.array(z.string()).default([]),
|
|
475
|
+
draft: z.boolean().default(false)
|
|
476
|
+
}).strict();
|
|
465
477
|
|
|
466
478
|
// src/lib/academic-parts.ts
|
|
467
479
|
var ACADEMIC_PART_NAMES = {
|
|
@@ -555,6 +567,8 @@ var academicProfile = defineProfile({
|
|
|
555
567
|
// v4.4.0: opt-in per book; requires build-exercises
|
|
556
568
|
practiceExam: false,
|
|
557
569
|
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
570
|
+
glossary: false,
|
|
571
|
+
// v4.19.0 #115: opt-in per book; requires src/content/glossary/
|
|
558
572
|
landing: true
|
|
559
573
|
// v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
|
|
560
574
|
},
|
|
@@ -680,6 +694,8 @@ var toolsProfile = defineProfile({
|
|
|
680
694
|
// v4.4.0: opt-in per book
|
|
681
695
|
practiceExam: false,
|
|
682
696
|
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
697
|
+
glossary: false,
|
|
698
|
+
// v4.19.0 #115: opt-in per book; requires src/content/glossary/
|
|
683
699
|
landing: true
|
|
684
700
|
// v4.5.0: auto-inject minimal root landing
|
|
685
701
|
},
|
|
@@ -767,6 +783,8 @@ var minimalProfile = defineProfile({
|
|
|
767
783
|
// v4.4.0: opt-in per book
|
|
768
784
|
practiceExam: false,
|
|
769
785
|
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
786
|
+
glossary: false,
|
|
787
|
+
// v4.19.0 #115: opt-in per book; requires src/content/glossary/
|
|
770
788
|
landing: true
|
|
771
789
|
// v4.5.0: auto-inject minimal root landing
|
|
772
790
|
},
|
|
@@ -794,6 +812,8 @@ var courseNotesProfile = defineProfile({
|
|
|
794
812
|
// v4.4.0: opt-in per book
|
|
795
813
|
practiceExam: false,
|
|
796
814
|
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
815
|
+
glossary: false,
|
|
816
|
+
// v4.19.0 #115: opt-in per book; requires src/content/glossary/
|
|
797
817
|
landing: true
|
|
798
818
|
// v4.5.0: auto-inject minimal root landing
|
|
799
819
|
},
|
|
@@ -825,6 +845,8 @@ var researchPortfolioProfile = defineProfile({
|
|
|
825
845
|
// v4.4.0: opt-in per book
|
|
826
846
|
practiceExam: false,
|
|
827
847
|
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
848
|
+
glossary: false,
|
|
849
|
+
// v4.19.0 #115: opt-in per book; requires src/content/glossary/
|
|
828
850
|
landing: true
|
|
829
851
|
// v4.5.0: auto-inject minimal root landing
|
|
830
852
|
},
|
|
@@ -1108,6 +1130,9 @@ var ROUTE_REGISTRY = {
|
|
|
1108
1130
|
// v4.17.0 (Tier 3, #112): static practice question-bank. Opt-in via
|
|
1109
1131
|
// routes.practiceExam: true; reads the `questions` collection + examDomains.
|
|
1110
1132
|
practiceExam: { pattern: "/practice-exam", file: "practice-exam.astro" },
|
|
1133
|
+
// v4.19.0 (#115): searchable key-terms glossary. Opt-in via routes.glossary:
|
|
1134
|
+
// true; reads the `glossary` collection (src/content/glossary/).
|
|
1135
|
+
glossary: { pattern: "/glossary", file: "glossary.astro" },
|
|
1111
1136
|
// v4.5.0: minimal root landing page. Reads title/description/portfolio/routes
|
|
1112
1137
|
// from vite.define-injected import.meta.env vars. Default-on per profile;
|
|
1113
1138
|
// consumers with their own src/pages/index.astro override (file-system route
|
|
@@ -1498,6 +1523,9 @@ function theoremLabel(props) {
|
|
|
1498
1523
|
const fullLabel = name ? `${numbered} (${name})` : numbered;
|
|
1499
1524
|
return { kind: raw, fullLabel };
|
|
1500
1525
|
}
|
|
1526
|
+
function resolveTheoremNumber(entry, n) {
|
|
1527
|
+
return entry?.number ?? n;
|
|
1528
|
+
}
|
|
1501
1529
|
|
|
1502
1530
|
// src/lib/assert-prop.ts
|
|
1503
1531
|
function assertEnumProp(value, allowed, ctx) {
|
|
@@ -1588,6 +1616,77 @@ function distinctChaptersSorted(entries) {
|
|
|
1588
1616
|
}).map(chapterLabel);
|
|
1589
1617
|
}
|
|
1590
1618
|
|
|
1619
|
+
// src/lib/exam-engine.ts
|
|
1620
|
+
function shuffle(arr, rng = Math.random) {
|
|
1621
|
+
const a = arr.slice();
|
|
1622
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
1623
|
+
const j = Math.min(i, Math.floor(rng() * (i + 1)));
|
|
1624
|
+
const ai = a[i];
|
|
1625
|
+
a[i] = a[j];
|
|
1626
|
+
a[j] = ai;
|
|
1627
|
+
}
|
|
1628
|
+
return a;
|
|
1629
|
+
}
|
|
1630
|
+
function sampleExam(pool, blueprint, rng = Math.random) {
|
|
1631
|
+
const count = Math.max(0, Math.min(blueprint.count, pool.length));
|
|
1632
|
+
const picked = [];
|
|
1633
|
+
const used = /* @__PURE__ */ new Set();
|
|
1634
|
+
if (blueprint.perDomain) {
|
|
1635
|
+
for (const [domain, quota] of Object.entries(blueprint.perDomain)) {
|
|
1636
|
+
const inDomain = shuffle(
|
|
1637
|
+
pool.filter((q) => q.domain === domain && !used.has(q.id)),
|
|
1638
|
+
rng
|
|
1639
|
+
);
|
|
1640
|
+
for (const q of inDomain.slice(0, Math.max(0, quota))) {
|
|
1641
|
+
if (picked.length >= count) break;
|
|
1642
|
+
picked.push(q);
|
|
1643
|
+
used.add(q.id);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
for (const q of shuffle(pool.filter((q2) => !used.has(q2.id)), rng)) {
|
|
1648
|
+
if (picked.length >= count) break;
|
|
1649
|
+
picked.push(q);
|
|
1650
|
+
used.add(q.id);
|
|
1651
|
+
}
|
|
1652
|
+
return shuffle(picked, rng);
|
|
1653
|
+
}
|
|
1654
|
+
function scoreExam(questions, answers, passMark = 0.7) {
|
|
1655
|
+
const order = [];
|
|
1656
|
+
const tally = /* @__PURE__ */ new Map();
|
|
1657
|
+
let correct = 0;
|
|
1658
|
+
for (const q of questions) {
|
|
1659
|
+
const chosen = answers[q.id];
|
|
1660
|
+
const right = chosen !== void 0 && q.options.some((o) => o.correct === true && o.id === chosen);
|
|
1661
|
+
if (right) correct++;
|
|
1662
|
+
if (!tally.has(q.domain)) {
|
|
1663
|
+
tally.set(q.domain, { correct: 0, total: 0 });
|
|
1664
|
+
order.push(q.domain);
|
|
1665
|
+
}
|
|
1666
|
+
const t = tally.get(q.domain);
|
|
1667
|
+
t.total++;
|
|
1668
|
+
if (right) t.correct++;
|
|
1669
|
+
}
|
|
1670
|
+
const total = questions.length;
|
|
1671
|
+
const byDomain = order.map((domain) => ({ domain, ...tally.get(domain) }));
|
|
1672
|
+
const weakDomains = byDomain.filter((d) => d.total > 0 && d.correct / d.total < passMark).map((d) => d.domain);
|
|
1673
|
+
return {
|
|
1674
|
+
correct,
|
|
1675
|
+
total,
|
|
1676
|
+
pct: total === 0 ? 0 : Math.round(correct / total * 100),
|
|
1677
|
+
byDomain,
|
|
1678
|
+
weakDomains
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/lib/part-review.ts
|
|
1683
|
+
function selectPartExercises(chapters, byChapter, part) {
|
|
1684
|
+
const want = String(part);
|
|
1685
|
+
const groups = chapters.filter((c) => String(c.data.part ?? "") === want).sort((a, b) => chapterSortKey(a.data) - chapterSortKey(b.data)).map((c) => ({ chapter: c.id, exercises: byChapter[c.id] ?? [] })).filter((g) => g.exercises.length > 0);
|
|
1686
|
+
const total = groups.reduce((n, g) => n + g.exercises.length, 0);
|
|
1687
|
+
return { groups, total };
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1591
1690
|
// src/styles/built-in.ts
|
|
1592
1691
|
var academicStyle = defineStyle({
|
|
1593
1692
|
name: "academic",
|
|
@@ -1669,6 +1768,7 @@ export {
|
|
|
1669
1768
|
fallbackChaptersRenderer,
|
|
1670
1769
|
freshnessLabel,
|
|
1671
1770
|
getFreshness,
|
|
1771
|
+
glossarySchema,
|
|
1672
1772
|
groupByChapter,
|
|
1673
1773
|
groupByDomain,
|
|
1674
1774
|
minimalChapterSchema,
|
|
@@ -1691,6 +1791,11 @@ export {
|
|
|
1691
1791
|
resolveGithubRepo,
|
|
1692
1792
|
resolvePreset,
|
|
1693
1793
|
resolveProfile,
|
|
1794
|
+
resolveTheoremNumber,
|
|
1795
|
+
sampleExam,
|
|
1796
|
+
scoreExam,
|
|
1797
|
+
selectPartExercises,
|
|
1798
|
+
shuffle,
|
|
1694
1799
|
sortQuestions,
|
|
1695
1800
|
sourceTiers,
|
|
1696
1801
|
sourceTiersResearch,
|