@brandon_m_behring/book-scaffold-astro 4.15.0 → 4.17.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/BookLink.astro +32 -0
- package/components/ObjectiveMap.astro +113 -0
- package/components/Rationale.astro +38 -0
- package/dist/index.d.ts +86 -3
- package/dist/index.mjs +228 -3
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +123 -0
- package/package.json +4 -1
- package/pages/practice-exam.astro +202 -0
- package/recipes/04-component-library.md +30 -0
- package/scripts/validate.mjs +111 -1
- package/src/lib/book-link.ts +32 -0
- 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/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
|
@@ -100,10 +100,12 @@ Two callout families coexist. Authors import what they need.
|
|
|
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
|
|
|
103
|
-
**Utility components** (`src/components/`, any profile): `Cite`, `XRef`, `Figure`, `MarginNote`, `Sidenote`, `WeekRef`, `CodeRef`, `CodeBlock`, `Tag`, `StatusBadge`, `PocLayout` (v4.1.0+; wraps slot in a per-`kind` layout shell — 5 closed-union kinds; see `recipes/15-defining-styles.md`).
|
|
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. (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,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* BookLink — cross-book link to a sibling scaffold book (#96).
|
|
4
|
+
*
|
|
5
|
+
* Each scaffold book is a separate Astro app with its own `labels.json` and
|
|
6
|
+
* deploy origin, so `<XRef>` can't reach a sibling — a cross-book ref resolves
|
|
7
|
+
* against the wrong labels and dies. `<BookLink book="design" to="…">` resolves
|
|
8
|
+
* `book` against the consumer's `siblingBooks` registry
|
|
9
|
+
* (`defineBookConfig({ siblingBooks })`) — the single place to update when a
|
|
10
|
+
* sibling redeploys or extracts to its own repo. An unknown `book` throws at
|
|
11
|
+
* build (fail-loud, see `src/lib/book-link`), never a dead cross-origin link.
|
|
12
|
+
*
|
|
13
|
+
* `to` is a path within the sibling book (its router shape), e.g.
|
|
14
|
+
* `chapters/<slug>/#<id>`. Phase 1 builds the href; Phase 2 (deferred) will
|
|
15
|
+
* validate `to` against a vendored sibling `labels.json`.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* For design depth see
|
|
19
|
+
* <BookLink book="design" to="chapters/patterns/#layered">the layered pattern</BookLink>.
|
|
20
|
+
*/
|
|
21
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
22
|
+
import { resolveBookHref } from '../src/lib/book-link';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
book: string;
|
|
26
|
+
to: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { book, to } = Astro.props;
|
|
30
|
+
const href = resolveBookHref(bookConfig.siblingBooks, book, to);
|
|
31
|
+
---
|
|
32
|
+
<a href={href} rel="external noopener" class="book-link"><slot /></a>
|
|
@@ -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/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -320,6 +320,89 @@ declare function assertEnumProp<T extends string>(value: unknown, allowed: reado
|
|
|
320
320
|
prop: string;
|
|
321
321
|
}): T;
|
|
322
322
|
|
|
323
|
+
/**
|
|
324
|
+
* book-link — resolve a cross-book `<BookLink>` href from the consumer's
|
|
325
|
+
* sibling-book registry (#96).
|
|
326
|
+
*
|
|
327
|
+
* Each scaffold book is a separate Astro app with its own `labels.json` and
|
|
328
|
+
* deploy origin, so `<XRef>` can't reach a sibling book — a cross-book ref
|
|
329
|
+
* resolves against the wrong labels and dies. `<BookLink book to>` instead
|
|
330
|
+
* resolves `book` against a per-consumer registry of sibling base URLs
|
|
331
|
+
* (`defineBookConfig({ siblingBooks })`) — the single place to update when a
|
|
332
|
+
* sibling redeploys or extracts to its own repo. An unknown `book` THROWS
|
|
333
|
+
* rather than emitting a dead cross-origin link (fail-loud, like #109).
|
|
334
|
+
*
|
|
335
|
+
* Phase 1 (this): registry-backed href + fail-loud on unknown book. Phase 2
|
|
336
|
+
* (deferred): validate the `to` id against a vendored sibling `labels.json`.
|
|
337
|
+
*/
|
|
338
|
+
declare function resolveBookHref(siblingBooks: Record<string, string> | null | undefined, book: string, to: string): string;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* exam-domains — validate a question's `domain` against the consumer's closed
|
|
342
|
+
* `examDomains` taxonomy (Tier 3, #112).
|
|
343
|
+
*
|
|
344
|
+
* Exam domains are PER-BOOK (Cisco security domains ≠ CompTIA objectives ≠ a
|
|
345
|
+
* math syllabus's topics), so they can't be a hardcoded `z.enum` in the schema.
|
|
346
|
+
* The consumer declares them once — `defineBookConfig({ examDomains: [...] })`,
|
|
347
|
+
* threaded through the `virtual:book-scaffold/book-config` module — and a
|
|
348
|
+
* question whose `domain` is not in that list THROWS at build rather than
|
|
349
|
+
* silently mis-weighting a blueprint or dropping a row from the objective-map.
|
|
350
|
+
*
|
|
351
|
+
* Membership can't be a Zod invariant: `questionSchema` is constructed at
|
|
352
|
+
* package-load time, outside any consumer context, so it can't see
|
|
353
|
+
* `examDomains` (the same constraint that put `siblingBooks` validation in
|
|
354
|
+
* lib/book-link.ts rather than the schema). So Zod checks `domain` is a
|
|
355
|
+
* non-empty string; THIS runs at the route/build layer where the resolved
|
|
356
|
+
* config is available. Mirrors `resolveBookHref` (lib/book-link.ts).
|
|
357
|
+
*/
|
|
358
|
+
declare function assertKnownDomain(examDomains: readonly string[] | null | undefined, domain: string, ctx: {
|
|
359
|
+
id: string;
|
|
360
|
+
}): string;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* src/lib/questions-derive.ts — PURE grouping + objective-map derivation for the
|
|
364
|
+
* study-guide `questions` collection (Tier 3, #112).
|
|
365
|
+
*
|
|
366
|
+
* No `astro:content` import, so this is safe in the Node-loaded main entry and
|
|
367
|
+
* unit-tested directly from dist/ (tests/questions.test.mjs). The Astro-context
|
|
368
|
+
* `getCollection` wrapper lives in questions.ts — same split as
|
|
369
|
+
* chapter-sort.ts (pure) / chapters.ts (Astro wrapper).
|
|
370
|
+
*/
|
|
371
|
+
|
|
372
|
+
/** Stable string label for a chapter ref (number or academic-style string). */
|
|
373
|
+
declare function chapterLabel(chapter: number | string): string;
|
|
374
|
+
/** Sort questions by chapter then id (stable, deterministic). Pure. */
|
|
375
|
+
declare function sortQuestions<T extends {
|
|
376
|
+
data: Pick<Question, 'chapter' | 'id'>;
|
|
377
|
+
}>(entries: readonly T[]): T[];
|
|
378
|
+
/** Group entries by `domain`, preserving input order within each bucket. */
|
|
379
|
+
declare function groupByDomain<T extends {
|
|
380
|
+
data: Pick<Question, 'domain'>;
|
|
381
|
+
}>(entries: readonly T[]): Map<string, T[]>;
|
|
382
|
+
/** Group entries by `chapter` (string label), preserving input order. */
|
|
383
|
+
declare function groupByChapter<T extends {
|
|
384
|
+
data: Pick<Question, 'chapter'>;
|
|
385
|
+
}>(entries: readonly T[]): Map<string, T[]>;
|
|
386
|
+
/**
|
|
387
|
+
* Domain → set of chapter labels that have ≥1 question in that domain. The data
|
|
388
|
+
* backbone of `<ObjectiveMap>` (#117): the coverage matrix is *derived* from the
|
|
389
|
+
* question bank, so it can't drift from a separately-maintained table — the
|
|
390
|
+
* cheapest proof the per-book `examDomains` taxonomy is populated + consistent.
|
|
391
|
+
*/
|
|
392
|
+
declare function deriveObjectiveMap<T extends {
|
|
393
|
+
data: Pick<Question, 'domain' | 'chapter'>;
|
|
394
|
+
}>(entries: readonly T[]): Map<string, Set<string>>;
|
|
395
|
+
/**
|
|
396
|
+
* Distinct chapter labels across the entries, in numeric-aware order (numeric
|
|
397
|
+
* chapters before string slugs, each ordered) — the column set for
|
|
398
|
+
* `<ObjectiveMap>`. Dedupes by label, sorts the underlying chapter values via
|
|
399
|
+
* the same `chapterOrder` as `sortQuestions`, then labels. A bare `.sort()` on
|
|
400
|
+
* the labels would put "10" before "2" (the chapter-column sort bug).
|
|
401
|
+
*/
|
|
402
|
+
declare function distinctChaptersSorted<T extends {
|
|
403
|
+
data: Pick<Question, 'chapter'>;
|
|
404
|
+
}>(entries: readonly T[]): string[];
|
|
405
|
+
|
|
323
406
|
/**
|
|
324
407
|
* src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
|
|
325
408
|
*
|
|
@@ -421,4 +504,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
421
504
|
*/
|
|
422
505
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
423
506
|
|
|
424
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, 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, assertEnumProp, bookScaffoldIntegration, buildGithubUrl, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, originUrlFromGitConfig, parseRepoSlug, researchPortfolioStyle, resolveGithubRepo, theoremLabel, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
|
507
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, KIND_LABEL, Question, type ResolvedTheoremLabel, Style, THEOREM_KINDS, type TheoremKind, type TheoremLabelProps, 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, theoremLabel, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -358,6 +358,110 @@ var patternsSchema = z.object({
|
|
|
358
358
|
category: z.enum(patternCategories).optional(),
|
|
359
359
|
convergence_date: z.date().nullable().optional()
|
|
360
360
|
});
|
|
361
|
+
var questionTypes = ["mcq", "free", "cloze"];
|
|
362
|
+
var bloomLevels = [
|
|
363
|
+
"remember",
|
|
364
|
+
"understand",
|
|
365
|
+
"apply",
|
|
366
|
+
"analyze",
|
|
367
|
+
"evaluate",
|
|
368
|
+
"create"
|
|
369
|
+
];
|
|
370
|
+
var questionDifficulties = ["1", "2", "3", "4"];
|
|
371
|
+
var CHAPTER_SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
372
|
+
var mcqOptionObject = z.object({
|
|
373
|
+
id: z.string().min(1),
|
|
374
|
+
// stable per-option key (e.g. 'a') — anchors + future scoring
|
|
375
|
+
correct: z.boolean().default(false),
|
|
376
|
+
text: z.string().optional()
|
|
377
|
+
// short option prose inline; long ones via body
|
|
378
|
+
}).strict();
|
|
379
|
+
var questionSchema = z.object({
|
|
380
|
+
// ----- identity -----
|
|
381
|
+
id: z.string().trim().min(1),
|
|
382
|
+
// EXPLICIT cross-ref key (#114 appendix / #116 cards); distinct from file-derived entry.id. trim → whitespace-only fails loud.
|
|
383
|
+
type: z.enum(questionTypes),
|
|
384
|
+
// ----- placement (every surface keys on these) -----
|
|
385
|
+
chapter: z.union([z.number().int().min(0).max(99), z.string().regex(CHAPTER_SLUG)]),
|
|
386
|
+
// number OR a kebab-slug string
|
|
387
|
+
part: z.union([z.number().int().min(0).max(20), z.string().regex(CHAPTER_SLUG)]).optional(),
|
|
388
|
+
domain: z.string().trim().min(1),
|
|
389
|
+
// value validated at route/build (assertKnownDomain), NOT here; trim → whitespace-only fails loud
|
|
390
|
+
// ----- pedagogy metadata (used-when-present) -----
|
|
391
|
+
bloom_level: z.enum(bloomLevels).optional(),
|
|
392
|
+
// #112/#113/#116
|
|
393
|
+
objective_id: z.string().min(1).optional(),
|
|
394
|
+
// #117 objective-map rows + #116 cards
|
|
395
|
+
difficulty: z.enum(questionDifficulties).optional(),
|
|
396
|
+
// #112 blueprint, #113 routing
|
|
397
|
+
// ----- type-specific payloads (validated per-type in refineQuestion) -----
|
|
398
|
+
options: z.array(mcqOptionObject).optional(),
|
|
399
|
+
// MCQ only
|
|
400
|
+
answer: z.string().optional(),
|
|
401
|
+
// free-response model answer (prose)
|
|
402
|
+
// ----- lifecycle (mirrors chapter schemas) -----
|
|
403
|
+
draft: z.boolean().default(false),
|
|
404
|
+
tags: z.array(z.string()).default([])
|
|
405
|
+
}).strict();
|
|
406
|
+
function refineQuestion(q, ctx) {
|
|
407
|
+
if (q.type === "mcq") {
|
|
408
|
+
if (q.answer !== void 0) {
|
|
409
|
+
ctx.addIssue({
|
|
410
|
+
code: "custom",
|
|
411
|
+
path: ["answer"],
|
|
412
|
+
message: `MCQ "${q.id}": the answer is the option marked correct: true \u2014 remove \`answer\` and put any explanation in a <Rationale> body block.`
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (!q.options || q.options.length < 2) {
|
|
416
|
+
ctx.addIssue({
|
|
417
|
+
code: "custom",
|
|
418
|
+
path: ["options"],
|
|
419
|
+
message: `MCQ "${q.id}" needs \u22652 options (got ${q.options?.length ?? 0}).`
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const correct = q.options.filter((o) => o.correct).length;
|
|
424
|
+
if (correct !== 1) {
|
|
425
|
+
ctx.addIssue({
|
|
426
|
+
code: "custom",
|
|
427
|
+
path: ["options"],
|
|
428
|
+
message: `MCQ "${q.id}" must have EXACTLY ONE option with correct: true (got ${correct}).`
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const ids = q.options.map((o) => o.id);
|
|
432
|
+
if (new Set(ids).size !== ids.length) {
|
|
433
|
+
ctx.addIssue({
|
|
434
|
+
code: "custom",
|
|
435
|
+
path: ["options"],
|
|
436
|
+
message: `MCQ "${q.id}" has duplicate option ids (${ids.join(", ")}).`
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
} else if (q.type === "free") {
|
|
440
|
+
if (q.options) {
|
|
441
|
+
ctx.addIssue({
|
|
442
|
+
code: "custom",
|
|
443
|
+
path: ["options"],
|
|
444
|
+
message: `Free-response "${q.id}" must not define MCQ options.`
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
if (!q.answer || q.answer.trim() === "") {
|
|
448
|
+
ctx.addIssue({
|
|
449
|
+
code: "custom",
|
|
450
|
+
path: ["answer"],
|
|
451
|
+
message: `Free-response "${q.id}" needs an "answer" (model answer) for the appendix.`
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
} else if (q.type === "cloze") {
|
|
455
|
+
if (q.options) {
|
|
456
|
+
ctx.addIssue({
|
|
457
|
+
code: "custom",
|
|
458
|
+
path: ["options"],
|
|
459
|
+
message: `Cloze "${q.id}" must not define MCQ options.`
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
var refinedQuestionSchema = questionSchema.superRefine(refineQuestion);
|
|
361
465
|
|
|
362
466
|
// src/lib/academic-parts.ts
|
|
363
467
|
var ACADEMIC_PART_NAMES = {
|
|
@@ -449,6 +553,8 @@ var academicProfile = defineProfile({
|
|
|
449
553
|
// v4.3.0 #70: opt-in per book; requires build-tips
|
|
450
554
|
exercises: false,
|
|
451
555
|
// v4.4.0: opt-in per book; requires build-exercises
|
|
556
|
+
practiceExam: false,
|
|
557
|
+
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
452
558
|
landing: true
|
|
453
559
|
// v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
|
|
454
560
|
},
|
|
@@ -572,6 +678,8 @@ var toolsProfile = defineProfile({
|
|
|
572
678
|
// v4.3.0 #70: opt-in per book
|
|
573
679
|
exercises: false,
|
|
574
680
|
// v4.4.0: opt-in per book
|
|
681
|
+
practiceExam: false,
|
|
682
|
+
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
575
683
|
landing: true
|
|
576
684
|
// v4.5.0: auto-inject minimal root landing
|
|
577
685
|
},
|
|
@@ -657,6 +765,8 @@ var minimalProfile = defineProfile({
|
|
|
657
765
|
// v4.3.0 #70: opt-in per book
|
|
658
766
|
exercises: false,
|
|
659
767
|
// v4.4.0: opt-in per book
|
|
768
|
+
practiceExam: false,
|
|
769
|
+
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
660
770
|
landing: true
|
|
661
771
|
// v4.5.0: auto-inject minimal root landing
|
|
662
772
|
},
|
|
@@ -682,6 +792,8 @@ var courseNotesProfile = defineProfile({
|
|
|
682
792
|
// v4.3.0 #70: opt-in per book
|
|
683
793
|
exercises: false,
|
|
684
794
|
// v4.4.0: opt-in per book
|
|
795
|
+
practiceExam: false,
|
|
796
|
+
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
685
797
|
landing: true
|
|
686
798
|
// v4.5.0: auto-inject minimal root landing
|
|
687
799
|
},
|
|
@@ -711,6 +823,8 @@ var researchPortfolioProfile = defineProfile({
|
|
|
711
823
|
// v4.3.0 #70: opt-in per book
|
|
712
824
|
exercises: false,
|
|
713
825
|
// v4.4.0: opt-in per book
|
|
826
|
+
practiceExam: false,
|
|
827
|
+
// v4.17.0 #112: opt-in per book; requires src/content/questions/
|
|
714
828
|
landing: true
|
|
715
829
|
// v4.5.0: auto-inject minimal root landing
|
|
716
830
|
},
|
|
@@ -991,6 +1105,9 @@ var ROUTE_REGISTRY = {
|
|
|
991
1105
|
// v4.4.0: exercises index by chapter. Opt-in via routes.exercises: true;
|
|
992
1106
|
// pairs with build-exercises script + <ExerciseSolutions auto /> mode.
|
|
993
1107
|
exercises: { pattern: "/exercises", file: "exercises.astro" },
|
|
1108
|
+
// v4.17.0 (Tier 3, #112): static practice question-bank. Opt-in via
|
|
1109
|
+
// routes.practiceExam: true; reads the `questions` collection + examDomains.
|
|
1110
|
+
practiceExam: { pattern: "/practice-exam", file: "practice-exam.astro" },
|
|
994
1111
|
// v4.5.0: minimal root landing page. Reads title/description/portfolio/routes
|
|
995
1112
|
// from vite.define-injected import.meta.env vars. Default-on per profile;
|
|
996
1113
|
// consumers with their own src/pages/index.astro override (file-system route
|
|
@@ -1028,7 +1145,11 @@ function bookScaffoldIntegration(opts) {
|
|
|
1028
1145
|
seo,
|
|
1029
1146
|
// v4.15.0 (#109): optional GitHub repo/branch override for CodeRef/CodeBlock.
|
|
1030
1147
|
githubRepo,
|
|
1031
|
-
githubBranch
|
|
1148
|
+
githubBranch,
|
|
1149
|
+
// v4.16.0 (#96): sibling-book registry for cross-book <BookLink>.
|
|
1150
|
+
siblingBooks,
|
|
1151
|
+
// v4.17.0 (#112): exam-domain taxonomy for the questions collection.
|
|
1152
|
+
examDomains
|
|
1032
1153
|
} = opts;
|
|
1033
1154
|
const def = PROFILES[profile];
|
|
1034
1155
|
const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
|
|
@@ -1088,7 +1209,9 @@ function bookScaffoldIntegration(opts) {
|
|
|
1088
1209
|
twitterHandle: seo?.twitterHandle ?? null
|
|
1089
1210
|
},
|
|
1090
1211
|
githubRepo: resolvedGithubRepo,
|
|
1091
|
-
githubBranch: resolvedGithubBranch
|
|
1212
|
+
githubBranch: resolvedGithubBranch,
|
|
1213
|
+
siblingBooks: siblingBooks ?? {},
|
|
1214
|
+
examDomains: examDomains ?? []
|
|
1092
1215
|
})
|
|
1093
1216
|
],
|
|
1094
1217
|
define: {
|
|
@@ -1224,7 +1347,11 @@ async function defineBookConfig(opts) {
|
|
|
1224
1347
|
seo: opts.seo ? { ogImage: opts.seo.ogImage, twitterHandle: opts.seo.twitterHandle } : void 0,
|
|
1225
1348
|
// v4.15.0 (#109): repo/branch override; integration auto-detects when undefined.
|
|
1226
1349
|
githubRepo: opts.githubRepo,
|
|
1227
|
-
githubBranch: opts.githubBranch
|
|
1350
|
+
githubBranch: opts.githubBranch,
|
|
1351
|
+
// v4.16.0 (#96): cross-book link registry.
|
|
1352
|
+
siblingBooks: opts.siblingBooks,
|
|
1353
|
+
// v4.17.0 (#112): per-book exam-domain taxonomy for the questions collection.
|
|
1354
|
+
examDomains: opts.examDomains
|
|
1228
1355
|
}),
|
|
1229
1356
|
...mergedExtraIntegrations
|
|
1230
1357
|
];
|
|
@@ -1269,6 +1396,10 @@ async function defineBookConfig(opts) {
|
|
|
1269
1396
|
// v4.15.0: strip repo opts so they don't leak into AstroUserConfig.
|
|
1270
1397
|
githubRepo: _githubRepo,
|
|
1271
1398
|
githubBranch: _githubBranch,
|
|
1399
|
+
// v4.16.0: strip cross-book registry.
|
|
1400
|
+
siblingBooks: _siblingBooks,
|
|
1401
|
+
// v4.17.0: strip exam-domain taxonomy.
|
|
1402
|
+
examDomains: _examDomains,
|
|
1272
1403
|
...rest
|
|
1273
1404
|
} = opts;
|
|
1274
1405
|
void _styles;
|
|
@@ -1287,6 +1418,8 @@ async function defineBookConfig(opts) {
|
|
|
1287
1418
|
void _seo;
|
|
1288
1419
|
void _githubRepo;
|
|
1289
1420
|
void _githubBranch;
|
|
1421
|
+
void _siblingBooks;
|
|
1422
|
+
void _examDomains;
|
|
1290
1423
|
const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
|
|
1291
1424
|
const restVite = rest.vite ?? {};
|
|
1292
1425
|
const restSsr = restVite.ssr ?? {};
|
|
@@ -1377,6 +1510,84 @@ function assertEnumProp(value, allowed, ctx) {
|
|
|
1377
1510
|
);
|
|
1378
1511
|
}
|
|
1379
1512
|
|
|
1513
|
+
// src/lib/book-link.ts
|
|
1514
|
+
function resolveBookHref(siblingBooks, book, to) {
|
|
1515
|
+
const base = siblingBooks?.[book];
|
|
1516
|
+
if (!base) {
|
|
1517
|
+
const known = siblingBooks ? Object.keys(siblingBooks) : [];
|
|
1518
|
+
throw new Error(
|
|
1519
|
+
`<BookLink book="${book}">: unknown sibling book. Register it in defineBookConfig({ siblingBooks: { "${book}": "https://\u2026" } })` + (known.length ? ` (known: ${known.join(", ")})` : "") + "."
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
return `${base.replace(/\/+$/, "")}/${to.replace(/^\/+/, "")}`;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/lib/exam-domains.ts
|
|
1526
|
+
function assertKnownDomain(examDomains, domain, ctx) {
|
|
1527
|
+
if (!examDomains || !examDomains.includes(domain)) {
|
|
1528
|
+
const known = examDomains ?? [];
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
`question "${ctx.id}": unknown exam domain "${domain}". Register it in defineBookConfig({ examDomains: [${JSON.stringify(domain)}, \u2026] })` + (known.length ? ` (known: ${known.join(", ")})` : " (no examDomains configured)") + "."
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
return domain;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/lib/questions-derive.ts
|
|
1537
|
+
function chapterLabel(chapter) {
|
|
1538
|
+
return String(chapter);
|
|
1539
|
+
}
|
|
1540
|
+
function chapterOrder(chapter) {
|
|
1541
|
+
return typeof chapter === "number" ? [0, String(chapter).padStart(6, "0")] : [1, chapter];
|
|
1542
|
+
}
|
|
1543
|
+
function sortQuestions(entries) {
|
|
1544
|
+
return [...entries].sort((a, b) => {
|
|
1545
|
+
const [ar, as] = chapterOrder(a.data.chapter);
|
|
1546
|
+
const [br, bs] = chapterOrder(b.data.chapter);
|
|
1547
|
+
if (ar !== br) return ar - br;
|
|
1548
|
+
if (as !== bs) return as < bs ? -1 : 1;
|
|
1549
|
+
return a.data.id.localeCompare(b.data.id);
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
function groupByDomain(entries) {
|
|
1553
|
+
const out = /* @__PURE__ */ new Map();
|
|
1554
|
+
for (const e of entries) {
|
|
1555
|
+
const bucket = out.get(e.data.domain) ?? [];
|
|
1556
|
+
bucket.push(e);
|
|
1557
|
+
out.set(e.data.domain, bucket);
|
|
1558
|
+
}
|
|
1559
|
+
return out;
|
|
1560
|
+
}
|
|
1561
|
+
function groupByChapter(entries) {
|
|
1562
|
+
const out = /* @__PURE__ */ new Map();
|
|
1563
|
+
for (const e of entries) {
|
|
1564
|
+
const key = chapterLabel(e.data.chapter);
|
|
1565
|
+
const bucket = out.get(key) ?? [];
|
|
1566
|
+
bucket.push(e);
|
|
1567
|
+
out.set(key, bucket);
|
|
1568
|
+
}
|
|
1569
|
+
return out;
|
|
1570
|
+
}
|
|
1571
|
+
function deriveObjectiveMap(entries) {
|
|
1572
|
+
const out = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const e of entries) {
|
|
1574
|
+
const set = out.get(e.data.domain) ?? /* @__PURE__ */ new Set();
|
|
1575
|
+
set.add(chapterLabel(e.data.chapter));
|
|
1576
|
+
out.set(e.data.domain, set);
|
|
1577
|
+
}
|
|
1578
|
+
return out;
|
|
1579
|
+
}
|
|
1580
|
+
function distinctChaptersSorted(entries) {
|
|
1581
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
1582
|
+
for (const e of entries) byLabel.set(chapterLabel(e.data.chapter), e.data.chapter);
|
|
1583
|
+
return [...byLabel.values()].sort((a, b) => {
|
|
1584
|
+
const [ar, as] = chapterOrder(a);
|
|
1585
|
+
const [br, bs] = chapterOrder(b);
|
|
1586
|
+
if (ar !== br) return ar - br;
|
|
1587
|
+
return as < bs ? -1 : as > bs ? 1 : 0;
|
|
1588
|
+
}).map(chapterLabel);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1380
1591
|
// src/styles/built-in.ts
|
|
1381
1592
|
var academicStyle = defineStyle({
|
|
1382
1593
|
name: "academic",
|
|
@@ -1435,10 +1646,13 @@ export {
|
|
|
1435
1646
|
academicParts,
|
|
1436
1647
|
academicStyle,
|
|
1437
1648
|
assertEnumProp,
|
|
1649
|
+
assertKnownDomain,
|
|
1650
|
+
bloomLevels,
|
|
1438
1651
|
bookScaffoldIntegration,
|
|
1439
1652
|
buildGithubUrl,
|
|
1440
1653
|
changeKinds,
|
|
1441
1654
|
changelogSchema,
|
|
1655
|
+
chapterLabel,
|
|
1442
1656
|
chapterSortKey,
|
|
1443
1657
|
chapterStatus,
|
|
1444
1658
|
citationBackstops,
|
|
@@ -1450,9 +1664,13 @@ export {
|
|
|
1450
1664
|
defineProfile,
|
|
1451
1665
|
defineStyle,
|
|
1452
1666
|
defineTips,
|
|
1667
|
+
deriveObjectiveMap,
|
|
1668
|
+
distinctChaptersSorted,
|
|
1453
1669
|
fallbackChaptersRenderer,
|
|
1454
1670
|
freshnessLabel,
|
|
1455
1671
|
getFreshness,
|
|
1672
|
+
groupByChapter,
|
|
1673
|
+
groupByDomain,
|
|
1456
1674
|
minimalChapterSchema,
|
|
1457
1675
|
minimalStyle,
|
|
1458
1676
|
normalizeFrontmatterConfig,
|
|
@@ -1462,11 +1680,18 @@ export {
|
|
|
1462
1680
|
patternsSchema,
|
|
1463
1681
|
provenanceObject,
|
|
1464
1682
|
provenanceSchema,
|
|
1683
|
+
questionDifficulties,
|
|
1684
|
+
questionSchema,
|
|
1685
|
+
questionTypes,
|
|
1686
|
+
refineQuestion,
|
|
1687
|
+
refinedQuestionSchema,
|
|
1465
1688
|
researchPortfolioChapterSchema,
|
|
1466
1689
|
researchPortfolioStyle,
|
|
1690
|
+
resolveBookHref,
|
|
1467
1691
|
resolveGithubRepo,
|
|
1468
1692
|
resolvePreset,
|
|
1469
1693
|
resolveProfile,
|
|
1694
|
+
sortQuestions,
|
|
1470
1695
|
sourceTiers,
|
|
1471
1696
|
sourceTiersResearch,
|
|
1472
1697
|
sourcesSchema,
|