explico 0.13.0 → 0.14.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/CHANGELOG.md +894 -1
- package/README.md +200 -30
- package/dist/compiler/astro.d.ts +15 -2
- package/dist/compiler/astro.js +40 -11
- package/dist/compiler/cli-main.d.ts +43 -0
- package/dist/compiler/cli-main.js +392 -0
- package/dist/compiler/cli.d.ts +424 -9
- package/dist/compiler/cli.js +1289 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +105 -1
- package/dist/compiler/decks.d.ts +161 -0
- package/dist/compiler/decks.js +235 -0
- package/dist/compiler/exports/combined-html.d.ts +5 -1
- package/dist/compiler/exports/deck-fit.d.ts +55 -0
- package/dist/compiler/exports/deck-fit.js +90 -0
- package/dist/compiler/exports/export-chapters.d.ts +37 -6
- package/dist/compiler/exports/export-chapters.js +64 -5
- package/dist/compiler/exports/export-pages.d.ts +30 -3
- package/dist/compiler/exports/export-pages.js +24 -1
- package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/dist/compiler/exports/latex/cache.d.ts +78 -0
- package/dist/compiler/exports/latex/cache.js +129 -0
- package/dist/compiler/exports/latex/capture.d.ts +10 -1
- package/dist/compiler/exports/latex/capture.js +3 -1
- package/dist/compiler/exports/latex/compile.d.ts +13 -2
- package/dist/compiler/exports/latex/compile.js +12 -1
- package/dist/compiler/exports/latex/deck.d.ts +92 -0
- package/dist/compiler/exports/latex/deck.js +349 -0
- package/dist/compiler/exports/latex/document.d.ts +51 -6
- package/dist/compiler/exports/latex/document.js +71 -12
- package/dist/compiler/exports/latex/explico-common.sty +198 -0
- package/dist/compiler/exports/latex/explico.cls +13 -183
- package/dist/compiler/exports/latex/serialize.d.ts +60 -4
- package/dist/compiler/exports/latex/serialize.js +4 -2
- package/dist/compiler/exports/latex-export.d.ts +24 -2
- package/dist/compiler/exports/latex-export.js +107 -26
- package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
- package/dist/compiler/exports/markdown/serialize.js +85 -8
- package/dist/compiler/exports/markdown-export.d.ts +6 -1
- package/dist/compiler/exports/markdown-export.js +50 -11
- package/dist/compiler/exports/standalone-html.d.ts +10 -5
- package/dist/compiler/exports/standalone-html.js +3 -3
- package/dist/compiler/exports/static-tree.d.ts +38 -1
- package/dist/compiler/exports/static-tree.js +120 -4
- package/dist/compiler/knowledge-version.d.ts +26 -2
- package/dist/compiler/knowledge-version.js +27 -17
- package/dist/compiler/lesson-derivation.d.ts +143 -6
- package/dist/compiler/lesson-derivation.js +145 -13
- package/dist/compiler/manifest.d.ts +133 -4
- package/dist/compiler/manifest.js +359 -58
- package/dist/compiler/page-versions.d.ts +160 -0
- package/dist/compiler/page-versions.js +198 -0
- package/dist/compiler/render-diagnostics.d.ts +5 -1
- package/dist/compiler/resolution-report.d.ts +24 -6
- package/dist/compiler/resolution-report.js +22 -4
- package/dist/compiler/validate-content.d.ts +7 -0
- package/dist/compiler/validate-content.js +160 -0
- package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
- package/dist/curriculum/competency-graph.d.ts +54 -0
- package/dist/curriculum/competency-graph.js +41 -0
- package/dist/reference/base-library.d.mts +8 -0
- package/dist/reference/base-library.mjs +19 -0
- package/dist/reference/consistency.d.ts +8 -4
- package/dist/reference/consistency.js +76 -26
- package/dist/reference/deck-remark.d.mts +123 -0
- package/dist/reference/deck-remark.mjs +600 -0
- package/dist/reference/deck-source.d.ts +107 -0
- package/dist/reference/deck-source.js +140 -0
- package/dist/reference/decks-validate.d.ts +133 -0
- package/dist/reference/decks-validate.js +359 -0
- package/dist/reference/derived-entries.d.ts +5 -1
- package/dist/reference/diagram-fit.d.ts +1 -1
- package/dist/reference/figure-symbols.d.ts +23 -6
- package/dist/reference/figure-symbols.js +51 -15
- package/dist/reference/float-name.d.ts +43 -0
- package/dist/reference/float-name.js +58 -0
- package/dist/reference/folds-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-indices.js +5 -4
- package/dist/reference/gate-math.d.ts +29 -2
- package/dist/reference/gate-math.js +99 -18
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/lab-katex-validate.d.ts +10 -6
- package/dist/reference/lab-katex-validate.js +17 -9
- package/dist/reference/lab-math-scope.js +29 -4
- package/dist/reference/math-glyphs.d.mts +25 -0
- package/dist/reference/math-glyphs.mjs +189 -25
- package/dist/reference/page-notation.d.ts +22 -3
- package/dist/reference/page-notation.js +32 -10
- package/dist/reference/references.d.ts +14 -1
- package/dist/reference/references.js +13 -0
- package/dist/reference/registry-scope.d.mts +33 -0
- package/dist/reference/registry-scope.mjs +34 -0
- package/dist/reference/registry.js +29 -10
- package/dist/reference/rehype-deck.d.mts +6 -0
- package/dist/reference/rehype-deck.mjs +286 -0
- package/dist/reference/rehype-heading-anchors.d.mts +20 -0
- package/dist/reference/rehype-heading-anchors.mjs +48 -6
- package/dist/reference/remark-notation.mjs +249 -40
- package/dist/reference/source-lines.d.mts +12 -0
- package/dist/reference/source-lines.mjs +65 -0
- package/dist/reference/types.d.ts +28 -0
- package/dist/rules/catalog.d.ts +21 -4
- package/dist/rules/catalog.js +221 -2
- package/dist/rules/course-rules.d.ts +5 -1
- package/dist/rules/course-rules.js +17 -3
- package/dist/rules/course-rules.schema.json +12 -0
- package/dist/rules/in-effect.d.ts +13 -3
- package/dist/rules/in-effect.js +30 -7
- package/dist/rules/rules.schema.json +259 -1
- package/dist/rules/settings.d.ts +1 -1
- package/dist/session/PreferenceStore.d.ts +6 -0
- package/dist/session/PreferenceStore.js +18 -1
- package/dist/session/deck-memory.d.ts +116 -0
- package/dist/session/deck-memory.js +143 -0
- package/dist/session/reading-memory.d.ts +96 -0
- package/dist/session/reading-memory.js +133 -0
- package/docs/VERSIONING.md +21 -18
- package/docs/authoring/README.md +29 -9
- package/docs/authoring/agents.md +63 -16
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +588 -0
- package/docs/authoring/migrating.md +1087 -14
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +54 -2
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +37 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
- package/docs/course-agent/skills/release-content/SKILL.md +24 -0
- package/docs/diagram-scaling.md +148 -0
- package/docs/privacy.md +59 -0
- package/package.json +8 -2
- package/src/compiler/astro.ts +60 -21
- package/src/compiler/cli-main.ts +469 -0
- package/src/compiler/cli.ts +1905 -16
- package/src/compiler/collections.ts +284 -25
- package/src/compiler/course-config.ts +267 -0
- package/src/compiler/decks.ts +455 -0
- package/src/compiler/exports/deck-fit.ts +127 -0
- package/src/compiler/exports/export-chapters.ts +105 -6
- package/src/compiler/exports/export-pages.ts +46 -2
- package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/src/compiler/exports/latex/cache.ts +183 -0
- package/src/compiler/exports/latex/capture.ts +5 -2
- package/src/compiler/exports/latex/compile.ts +18 -0
- package/src/compiler/exports/latex/deck.ts +419 -0
- package/src/compiler/exports/latex/document.ts +117 -21
- package/src/compiler/exports/latex/explico-common.sty +198 -0
- package/src/compiler/exports/latex/explico.cls +13 -183
- package/src/compiler/exports/latex/serialize.ts +25 -21
- package/src/compiler/exports/latex/texlive-packages.txt +6 -2
- package/src/compiler/exports/latex-export.ts +187 -45
- package/src/compiler/exports/markdown/serialize.ts +118 -9
- package/src/compiler/exports/markdown-export.ts +56 -11
- package/src/compiler/exports/standalone-html.ts +8 -7
- package/src/compiler/exports/static-tree.ts +175 -3
- package/src/compiler/knowledge-version.ts +56 -21
- package/src/compiler/lesson-derivation.ts +282 -16
- package/src/compiler/manifest.ts +558 -63
- package/src/compiler/page-versions.ts +359 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +212 -0
- package/src/components/CourseContents.astro +124 -6
- package/src/components/CurriculumMap.astro +49 -1
- package/src/components/FloatIndex.astro +1 -1
- package/src/components/PartOverview.astro +51 -5
- package/src/components/assessments/AssessmentSet.astro +41 -15
- package/src/components/block-heading.ts +15 -6
- package/src/components/card-carry.ts +7 -4
- package/src/components/card-manager.ts +14 -2
- package/src/components/card-memory.ts +13 -9
- package/src/components/deck/DeckEnhancer.astro +13 -0
- package/src/components/deck/deck-context.ts +104 -0
- package/src/components/deck/deck-controller.ts +1287 -0
- package/src/components/deck/deck-map.ts +91 -0
- package/src/components/deck-lane.ts +163 -0
- package/src/components/diagram/Diagram.astro +131 -86
- package/src/components/disclosure/persisted-disclosure.ts +9 -1
- package/src/components/figure/Figure.astro +7 -2
- package/src/components/figure/FloatCaption.astro +5 -3
- package/src/components/fold/Fold.astro +2 -2
- package/src/components/notation/NotationFormula.astro +81 -11
- package/src/components/notation/NotationGlossary.astro +132 -1
- package/src/components/notation/NotationLayer.astro +29 -0
- package/src/components/starlight/AboutPanel.astro +186 -3
- package/src/components/starlight/CollapsibleSections.astro +24 -3
- package/src/components/starlight/DiagramEnhancer.astro +5 -1
- package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
- package/src/components/starlight/LayoutHeader.astro +17 -0
- package/src/components/starlight/LayoutPageTitle.astro +110 -0
- package/src/components/starlight/LayoutSidebar.astro +807 -21
- package/src/components/starlight/LayoutSocialIcons.astro +62 -6
- package/src/components/starlight/LessonExportActions.astro +27 -9
- package/src/components/starlight/LessonFooter.astro +59 -0
- package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
- package/src/components/starlight/TextSizeControl.astro +24 -13
- package/src/components/starlight/page-marks.ts +139 -0
- package/src/components/starlight/section-summary.ts +1 -1
- package/src/components/starlight/sidebar-editions.ts +245 -0
- package/src/components/table/Table.astro +15 -7
- package/src/content-config.ts +87 -1
- package/src/curriculum/competency-graph.ts +95 -0
- package/src/reference/base-library.mjs +19 -0
- package/src/reference/consistency.ts +126 -39
- package/src/reference/deck-remark.mjs +600 -0
- package/src/reference/deck-source.ts +273 -0
- package/src/reference/decks-validate.ts +604 -0
- package/src/reference/diagram-layout.ts +3 -1
- package/src/reference/figure-symbols.ts +108 -31
- package/src/reference/folds-validate.ts +1 -1
- package/src/reference/gate-indices.ts +5 -3
- package/src/reference/gate-math.ts +169 -25
- package/src/reference/lab-katex-validate.ts +30 -15
- package/src/reference/lab-math-scope.ts +34 -3
- package/src/reference/math-glyphs.mjs +189 -25
- package/src/reference/page-notation.ts +69 -15
- package/src/reference/references.ts +28 -0
- package/src/reference/registry-scope.mjs +34 -0
- package/src/reference/registry.ts +41 -11
- package/src/reference/rehype-deck.mjs +286 -0
- package/src/reference/rehype-heading-anchors.mjs +48 -6
- package/src/reference/remark-notation.mjs +249 -40
- package/src/reference/source-lines.mjs +65 -0
- package/src/reference/types.ts +28 -0
- package/src/rules/catalog.ts +257 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +54 -8
- package/src/session/PreferenceStore.ts +23 -0
- package/src/session/deck-memory.ts +263 -0
- package/src/session/reading-memory.ts +243 -0
- package/src/styles/deck.css +1150 -0
- package/src/styles/global.css +958 -27
- package/src/styles/zones.css +277 -0
- /package/src/{components → reference}/float-name.ts +0 -0
package/src/rules/catalog.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
/** The part of a page a rule is about. The first tag on an entry is primary. */
|
|
30
30
|
export type Element =
|
|
31
|
+
| 'slide'
|
|
31
32
|
| 'prose'
|
|
32
33
|
| 'math'
|
|
33
34
|
| 'equation'
|
|
@@ -43,6 +44,7 @@ export type Element =
|
|
|
43
44
|
|
|
44
45
|
/** Every element, in the order the rules-in-effect page lists them. */
|
|
45
46
|
export const ELEMENTS: readonly Element[] = [
|
|
47
|
+
'slide',
|
|
46
48
|
'prose',
|
|
47
49
|
'math',
|
|
48
50
|
'equation',
|
|
@@ -58,10 +60,20 @@ export const ELEMENTS: readonly Element[] = [
|
|
|
58
60
|
];
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
|
-
* The
|
|
63
|
+
* The six areas the engine ships, plus `X` — reserved for a course's own rules
|
|
62
64
|
* and never used by an engine rule (see {@link CourseRuleId}).
|
|
63
65
|
*/
|
|
64
|
-
export type RuleArea = 'N' | 'V' | 'C' | 'S' | 'P' | 'X';
|
|
66
|
+
export type RuleArea = 'N' | 'V' | 'C' | 'S' | 'P' | 'D' | 'X';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Which edition of a lesson a rule is about (`docs/authoring/deck.md`): its
|
|
70
|
+
* chapter, its deck (`<lesson>.deck.mdx`), or `both`. A rule without one is
|
|
71
|
+
* `both`. The rules-in-effect page is written per edition, so an author of
|
|
72
|
+
* either reads one list: a chapter-only rule is off on a deck, where a `D`
|
|
73
|
+
* rule says the same thing about a frame.
|
|
74
|
+
*/
|
|
75
|
+
export type Edition = 'chapter' | 'deck';
|
|
76
|
+
export type Editions = Edition | 'both';
|
|
65
77
|
export type RuleId = `${RuleArea}${number}`;
|
|
66
78
|
|
|
67
79
|
/**
|
|
@@ -145,6 +157,8 @@ export interface RuleEntry {
|
|
|
145
157
|
* absent means "shipped with the engine".
|
|
146
158
|
*/
|
|
147
159
|
readonly source?: 'course';
|
|
160
|
+
/** The edition the rule is about; absent is `both` ({@link Editions}). */
|
|
161
|
+
readonly editions?: Editions;
|
|
148
162
|
}
|
|
149
163
|
|
|
150
164
|
export interface InvariantEntry {
|
|
@@ -156,10 +170,18 @@ export interface InvariantEntry {
|
|
|
156
170
|
readonly doc: string;
|
|
157
171
|
readonly summary: string;
|
|
158
172
|
readonly checks: readonly [RuleCheck, ...RuleCheck[]];
|
|
173
|
+
/** The edition the invariant is about; absent is `both` ({@link Editions}). */
|
|
174
|
+
readonly editions?: Editions;
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
export type CatalogEntry = RuleEntry | InvariantEntry;
|
|
162
178
|
|
|
179
|
+
/** True when `entry` is in force for `edition` ({@link Editions}). */
|
|
180
|
+
export function appliesTo(entry: { readonly editions?: Editions }, edition: Edition): boolean {
|
|
181
|
+
const editions = entry.editions ?? 'both';
|
|
182
|
+
return editions === 'both' || editions === edition;
|
|
183
|
+
}
|
|
184
|
+
|
|
163
185
|
export type Enforcement = 'script' | 'partial' | 'guide';
|
|
164
186
|
|
|
165
187
|
// --- emitters ------------------------------------------------------------
|
|
@@ -169,6 +191,7 @@ const GLYPHS: Emitter = { module: 'src/reference/math-glyphs.mjs', fn: 'resolveM
|
|
|
169
191
|
const REGISTRY: Emitter = { module: 'src/reference/registry.ts', fn: 'buildNotationRegistry' };
|
|
170
192
|
const KEYED_FLOATS: Emitter = { module: 'src/reference/keyed-float-validate.ts', fn: 'validateKeyedFloats' };
|
|
171
193
|
const INDICES: Emitter = { module: 'src/reference/gate-indices.ts', fn: 'gateIndices' };
|
|
194
|
+
const DECKS: Emitter = { module: 'src/reference/decks-validate.ts', fn: 'validateDecks' };
|
|
172
195
|
|
|
173
196
|
function consistency(fn: string): Emitter {
|
|
174
197
|
return { module: 'src/reference/consistency.ts', fn };
|
|
@@ -334,12 +357,14 @@ const RULES: readonly RuleEntry[] = [
|
|
|
334
357
|
elements: ['math', 'notation'],
|
|
335
358
|
doc: 'notation.md#n4',
|
|
336
359
|
summary:
|
|
337
|
-
'Every glyph in rendered math resolves to exactly one notation entry on the page: introduce it with `[[key]]` in prose before its first `$…$`, define a page-local one in `notation.local`, or bind a compound with `\\explain{key}{latex}`. Do not reuse a glyph for a second meaning on the same page.',
|
|
360
|
+
'Every glyph in rendered math resolves to exactly one notation entry on the page: introduce it with `[[key]]` in prose before its first `$…$`, define a page-local one in `notation.local`, or bind a compound with `\\explain{key}{latex}`. Do not reuse a glyph for a second meaning on the same page. Declare a meaning once — a shared card unless the letter already means something else in the course, and only then `notation.local` or a gloss — and reference it everywhere else.',
|
|
338
361
|
checks: [
|
|
339
362
|
check('unresolved-glyph', 'error', true, GATE_MATH),
|
|
340
363
|
check('glyph-unique-in-corpus', 'warning', false, consistency('checkGlyphUniqueness')),
|
|
341
364
|
check('weak-local', 'info', false, consistency('checkWeakLocal')),
|
|
342
|
-
check('
|
|
365
|
+
check('definition-repeated', 'warning', false, consistency('checkDefinitionRepeated')),
|
|
366
|
+
check('meaning-repeated', 'info', false, consistency('checkMeaningRepeated')),
|
|
367
|
+
check('explain-redundant', 'info', true, GATE_MATH),
|
|
343
368
|
check('card-wants-demoting', 'info', false, consistency('checkCardWantsDemoting')),
|
|
344
369
|
],
|
|
345
370
|
locked:
|
|
@@ -729,6 +754,8 @@ const RULES: readonly RuleEntry[] = [
|
|
|
729
754
|
{
|
|
730
755
|
kind: 'rule',
|
|
731
756
|
id: 'V7',
|
|
757
|
+
// Chapter-only: on a deck, rule D2 (a frame fits its canvas) says it of a frame.
|
|
758
|
+
editions: 'chapter',
|
|
732
759
|
short: 'A figure’s picture fits on one screen',
|
|
733
760
|
title: 'A figure’s picture takes at most `maxPercent` of the screen height.',
|
|
734
761
|
elements: ['plot'],
|
|
@@ -755,6 +782,8 @@ const RULES: readonly RuleEntry[] = [
|
|
|
755
782
|
{
|
|
756
783
|
kind: 'rule',
|
|
757
784
|
id: 'V8',
|
|
785
|
+
// Chapter-only: on a deck, rule D7 (a float is the frame) says it of a frame.
|
|
786
|
+
editions: 'chapter',
|
|
758
787
|
short: 'A diagram fits the lesson column',
|
|
759
788
|
title: 'A diagram is no wider than the lesson column.',
|
|
760
789
|
elements: ['diagram'],
|
|
@@ -829,11 +858,36 @@ const RULES: readonly RuleEntry[] = [
|
|
|
829
858
|
unchecked:
|
|
830
859
|
'that the two edits landed in one commit — the build only ever sees the end state, so a release split across commits warns until the second half lands.',
|
|
831
860
|
},
|
|
861
|
+
{
|
|
862
|
+
kind: 'rule',
|
|
863
|
+
id: 'C5',
|
|
864
|
+
short: 'Each chapter and part page carries the release its content last changed in; `content/versions.json` is generated and committed',
|
|
865
|
+
title:
|
|
866
|
+
'Each chapter and part page carries the knowledge version its content last changed in, kept in `content/versions.json`, which `validate:content` writes and the course commits with the change.',
|
|
867
|
+
elements: ['changelog'],
|
|
868
|
+
doc: 'versioning.md#c5',
|
|
869
|
+
summary:
|
|
870
|
+
'A page’s PDF, Markdown and `[?]` panel say "unchanged since version 0.2.0", the release its content last changed in, or "with changes after version 0.4.0, not yet released" while `## [Unreleased]` holds its change. The record is `content/versions.json`: `validate:content` works it out from the committed file and the content, writes it on a developer’s machine, and in CI fails when the committed file is not the one it would write. Never edit it by hand; commit what `validate:content` wrote with the content change, and with a release.',
|
|
871
|
+
checks: [
|
|
872
|
+
check('page-versions-stale', 'error', false, {
|
|
873
|
+
module: 'src/compiler/page-versions.ts',
|
|
874
|
+
fn: 'syncPageVersions',
|
|
875
|
+
}),
|
|
876
|
+
check('page-versions-invalid', 'warning', false, {
|
|
877
|
+
module: 'src/compiler/page-versions.ts',
|
|
878
|
+
fn: 'readPageVersions',
|
|
879
|
+
}),
|
|
880
|
+
],
|
|
881
|
+
unchecked:
|
|
882
|
+
'which bullet of `## [Unreleased]` belongs to which page — every page changed while there is any bullet counts as not yet released; and a change to a lab, `content/domain/` or a part’s label, which changes the page’s PDF without changing its version.',
|
|
883
|
+
},
|
|
832
884
|
|
|
833
885
|
// ---- S — structure ----------------------------------------------------------
|
|
834
886
|
{
|
|
835
887
|
kind: 'rule',
|
|
836
888
|
id: 'S1',
|
|
889
|
+
// Chapter-only: on a deck, rule D5 (a frame title fits one line) says it of a frame.
|
|
890
|
+
editions: 'chapter',
|
|
837
891
|
short: 'No heading, short caption or short-note title over 65 characters',
|
|
838
892
|
title:
|
|
839
893
|
'No heading, no short caption (`short`, an equation’s `\\caption{}`) and no short-note title runs past 65 characters.',
|
|
@@ -945,6 +999,8 @@ const RULES: readonly RuleEntry[] = [
|
|
|
945
999
|
{
|
|
946
1000
|
kind: 'rule',
|
|
947
1001
|
id: 'S6',
|
|
1002
|
+
// Chapter-only: on a deck, rule D1 (sections of frames) says it of a frame.
|
|
1003
|
+
editions: 'chapter',
|
|
948
1004
|
short: 'Headings descend one level at a time, from `##`',
|
|
949
1005
|
title:
|
|
950
1006
|
'Headings descend one level at a time: a lesson body starts at `##` and never skips a level on the way down.',
|
|
@@ -968,6 +1024,8 @@ const RULES: readonly RuleEntry[] = [
|
|
|
968
1024
|
{
|
|
969
1025
|
kind: 'rule',
|
|
970
1026
|
id: 'S7',
|
|
1027
|
+
// Chapter-only: on a deck, rule D1 (sections of frames) says it of a frame.
|
|
1028
|
+
editions: 'chapter',
|
|
971
1029
|
short: 'A heading is followed by body content, never by another heading',
|
|
972
1030
|
title: 'A heading is followed by body content, never directly by another heading.',
|
|
973
1031
|
elements: ['heading'],
|
|
@@ -1135,6 +1193,179 @@ const RULES: readonly RuleEntry[] = [
|
|
|
1135
1193
|
'The sentence before a display equation names the quantity it yields and the step or law it follows from — not “we have:”.',
|
|
1136
1194
|
checks: [],
|
|
1137
1195
|
},
|
|
1196
|
+
// ---- D — decks --------------------------------------------------------------
|
|
1197
|
+
{
|
|
1198
|
+
kind: 'rule',
|
|
1199
|
+
id: 'D1',
|
|
1200
|
+
editions: 'deck',
|
|
1201
|
+
short: 'A deck is sections of frames: `##` a section, `###` a frame',
|
|
1202
|
+
title:
|
|
1203
|
+
'A deck is sections of frames: `##` opens a section, `###` is a frame, and nothing is deeper or outside a frame.',
|
|
1204
|
+
elements: ['slide', 'heading'],
|
|
1205
|
+
doc: 'deck.md#d1',
|
|
1206
|
+
summary:
|
|
1207
|
+
'In a `<lesson>.deck.mdx`, `##` opens a section, a divider frame that shows only its title, and `###` is a frame whose title is the heading. Write every line of content inside a `###` frame: no `#`, no `####` or deeper, nothing before the first heading and nothing between a `##` and its first `###`.',
|
|
1208
|
+
checks: [
|
|
1209
|
+
check('frame-depth', 'error', true, DECKS),
|
|
1210
|
+
check('deck-loose-content', 'error', true, DECKS),
|
|
1211
|
+
check('deck-empty', 'error', false, DECKS),
|
|
1212
|
+
],
|
|
1213
|
+
locked:
|
|
1214
|
+
'a frame is the unit the deck page, Present mode and the PDF are built from — content outside one has no frame to show in.',
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
kind: 'rule',
|
|
1218
|
+
id: 'D2',
|
|
1219
|
+
editions: 'deck',
|
|
1220
|
+
short: 'A frame fits its canvas in every step',
|
|
1221
|
+
title: 'A frame fits its canvas in every step, and never continues on the next frame.',
|
|
1222
|
+
elements: ['slide'],
|
|
1223
|
+
doc: 'deck.md#d2',
|
|
1224
|
+
summary:
|
|
1225
|
+
'Everything a frame shows — title, body, legend line, at its last step — fits the course’s canvas (`deck.aspectRatio`). `content check` warns when a frame passes `maxWords` words or `maxBullets` list items. A frame that does not fit is two frames; never shrink its type.',
|
|
1226
|
+
checks: [
|
|
1227
|
+
check('frame-dense', 'warning', true, DECKS),
|
|
1228
|
+
check('frame-overflow', 'error', false, { module: 'src/compiler/exports/deck-fit.ts', fn: 'checkFrameFit' }),
|
|
1229
|
+
],
|
|
1230
|
+
unchecked:
|
|
1231
|
+
'the paper: a frame that fits its canvas on the web can still run over in the PDF, which the export reports as a warning naming the frame.',
|
|
1232
|
+
settings: [
|
|
1233
|
+
{
|
|
1234
|
+
key: 'maxWords',
|
|
1235
|
+
title: 'Most words on a frame',
|
|
1236
|
+
description: 'Words a frame may show (its prose, not its math or notes) before `content check` warns.',
|
|
1237
|
+
schema: { type: 'integer', minimum: 10, default: 70 },
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
key: 'maxBullets',
|
|
1241
|
+
title: 'Most list items on a frame',
|
|
1242
|
+
description: 'List items a frame may show before `content check` warns.',
|
|
1243
|
+
schema: { type: 'integer', minimum: 1, default: 7 },
|
|
1244
|
+
},
|
|
1245
|
+
],
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
kind: 'rule',
|
|
1249
|
+
id: 'D3',
|
|
1250
|
+
editions: 'deck',
|
|
1251
|
+
short: 'A frame makes one claim, and its title states it',
|
|
1252
|
+
title: 'A frame makes one claim, and its title states the claim.',
|
|
1253
|
+
elements: ['slide', 'prose'],
|
|
1254
|
+
doc: 'deck.md#d3',
|
|
1255
|
+
summary:
|
|
1256
|
+
'Title a frame with the sentence it shows to be true — “Range is largest at a 45° launch angle”, not “Range”. Everything on the frame serves that one claim; a second claim is a second frame.',
|
|
1257
|
+
checks: [],
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
kind: 'rule',
|
|
1261
|
+
id: 'D4',
|
|
1262
|
+
editions: 'deck',
|
|
1263
|
+
short: 'A frame with text carries speaker notes',
|
|
1264
|
+
title: 'A frame with text carries speaker notes: the argument is in `<Notes>`, the frame shows its claims.',
|
|
1265
|
+
elements: ['slide', 'prose'],
|
|
1266
|
+
doc: 'deck.md#d4',
|
|
1267
|
+
summary:
|
|
1268
|
+
'A `###` frame whose body has words carries a `<Notes>…</Notes>` block with what the speaker says: the reasoning, the transitions, the question to ask the room. A frame that shows only a picture, an equation or a question needs none. The prose rules hold for the notes.',
|
|
1269
|
+
checks: [check('frame-notes-missing', 'warning', true, DECKS)],
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
kind: 'rule',
|
|
1273
|
+
id: 'D5',
|
|
1274
|
+
editions: 'deck',
|
|
1275
|
+
short: 'A frame title fits one line',
|
|
1276
|
+
title: 'A frame title fits one line: at most `maxLength` characters.',
|
|
1277
|
+
elements: ['slide', 'heading'],
|
|
1278
|
+
doc: 'deck.md#d5',
|
|
1279
|
+
summary:
|
|
1280
|
+
'A `##` or `###` heading in a deck runs to at most `maxLength` characters as the reader sees it (math as it reads, a `[[key]]` term as its label), so it stays on one line of the canvas. Move the rest into the frame.',
|
|
1281
|
+
checks: [check('frame-title-length', 'error', true, DECKS)],
|
|
1282
|
+
settings: [
|
|
1283
|
+
{
|
|
1284
|
+
key: 'maxLength',
|
|
1285
|
+
title: 'Longest frame title',
|
|
1286
|
+
description: 'Characters a frame or section title may run to.',
|
|
1287
|
+
schema: { type: 'integer', minimum: 10, default: 50 },
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
kind: 'rule',
|
|
1293
|
+
id: 'D6',
|
|
1294
|
+
editions: 'deck',
|
|
1295
|
+
short: 'A frame shows in at most `maxSteps` steps, each adding to the last',
|
|
1296
|
+
title: 'A frame shows in at most `maxSteps` steps, and each step adds to what is shown.',
|
|
1297
|
+
elements: ['slide'],
|
|
1298
|
+
doc: 'deck.md#d6',
|
|
1299
|
+
summary:
|
|
1300
|
+
'`<Pause />` is the one step mark: what follows it appears on the next key press, and nothing shown earlier changes or goes, so the last step is the whole frame — what the scroll view, the print and the PDF show. Keep to `maxSteps` steps; more is a second frame.',
|
|
1301
|
+
checks: [check('frame-steps', 'warning', true, DECKS)],
|
|
1302
|
+
unchecked: 'that a later step contradicts nothing already shown.',
|
|
1303
|
+
settings: [
|
|
1304
|
+
{
|
|
1305
|
+
key: 'maxSteps',
|
|
1306
|
+
title: 'Most steps in a frame',
|
|
1307
|
+
description: 'Steps a frame may show in (one more than its `<Pause />` marks).',
|
|
1308
|
+
schema: { type: 'integer', minimum: 1, default: 4 },
|
|
1309
|
+
},
|
|
1310
|
+
],
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
kind: 'rule',
|
|
1314
|
+
id: 'D7',
|
|
1315
|
+
editions: 'deck',
|
|
1316
|
+
short: 'A frame holds one figure, table or diagram',
|
|
1317
|
+
title: 'A frame holds at most one figure, table or diagram, and it is the frame’s picture.',
|
|
1318
|
+
elements: ['slide', 'plot', 'table', 'diagram'],
|
|
1319
|
+
doc: 'deck.md#d7',
|
|
1320
|
+
summary:
|
|
1321
|
+
'One `<Figure>`, `<Table>`, `<Diagram>` or recalled float per frame, laid out at the canvas width; a lab’s controls are the frame’s tools, live in Present mode. A second picture is a second frame.',
|
|
1322
|
+
checks: [check('frame-floats', 'warning', true, DECKS)],
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
kind: 'rule',
|
|
1326
|
+
id: 'D8',
|
|
1327
|
+
editions: 'deck',
|
|
1328
|
+
short: 'Recall a chapter’s result; never retype it',
|
|
1329
|
+
title:
|
|
1330
|
+
'A result the chapter numbers is recalled with `<Recall>`, never retyped; the deck’s own floats carry no number.',
|
|
1331
|
+
elements: ['slide', 'equation'],
|
|
1332
|
+
doc: 'deck.md#d8',
|
|
1333
|
+
summary:
|
|
1334
|
+
'Show the chapter’s equation, figure, table or diagram with `<Recall ref="eq-range" />` (`fig-`, `tab-`, `dia-`): it renders as in the chapter, with the chapter’s number, and links back to it. A display equation retyped from the chapter is a warning. An equation the deck labels itself numbers (1), (2) within the deck; the deck’s figures, tables and diagrams are unnumbered.',
|
|
1335
|
+
checks: [check('frame-retyped', 'warning', true, DECKS)],
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
kind: 'rule',
|
|
1339
|
+
id: 'D9',
|
|
1340
|
+
editions: 'deck',
|
|
1341
|
+
short: 'A frame introduces at most `maxNewSymbols` symbols',
|
|
1342
|
+
title: 'A frame introduces at most `maxNewSymbols` symbols the deck has not shown before.',
|
|
1343
|
+
elements: ['slide', 'math', 'notation'],
|
|
1344
|
+
doc: 'deck.md#d9',
|
|
1345
|
+
summary:
|
|
1346
|
+
'The room reads a frame once. Count the symbols it shows for the first time in the deck and keep them to `maxNewSymbols`; the legend line under the frame names them. More than that is a frame of its own.',
|
|
1347
|
+
checks: [check('frame-new-symbols', 'info', true, DECKS)],
|
|
1348
|
+
settings: [
|
|
1349
|
+
{
|
|
1350
|
+
key: 'maxNewSymbols',
|
|
1351
|
+
title: 'Most new symbols on a frame',
|
|
1352
|
+
description: 'Symbols a frame may show for the first time in its deck.',
|
|
1353
|
+
schema: { type: 'integer', minimum: 1, default: 4 },
|
|
1354
|
+
},
|
|
1355
|
+
],
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
kind: 'rule',
|
|
1359
|
+
id: 'D10',
|
|
1360
|
+
editions: 'deck',
|
|
1361
|
+
short: 'A deck opens on its title and closes on what the student can now do',
|
|
1362
|
+
title: 'A deck opens on its title frame and closes on what the student can now do.',
|
|
1363
|
+
elements: ['slide'],
|
|
1364
|
+
doc: 'deck.md#d10',
|
|
1365
|
+
summary:
|
|
1366
|
+
'The title frame (the lesson, the course, its authors and version) and the closing frame (the outcomes of the competencies the lesson teaches, `deck.outcomesFrame`) are generated: do not write them. With `deck.outcomesFrame` off, end on a `###` frame that says what the student can now do.',
|
|
1367
|
+
checks: [],
|
|
1368
|
+
},
|
|
1138
1369
|
];
|
|
1139
1370
|
|
|
1140
1371
|
/**
|
|
@@ -1424,6 +1655,28 @@ const INVARIANTS: readonly InvariantEntry[] = [
|
|
|
1424
1655
|
}),
|
|
1425
1656
|
],
|
|
1426
1657
|
},
|
|
1658
|
+
{
|
|
1659
|
+
kind: 'invariant',
|
|
1660
|
+
id: 'decks',
|
|
1661
|
+
editions: 'deck',
|
|
1662
|
+
title: 'A deck belongs to a lesson or a part, and recalls what its chapters number.',
|
|
1663
|
+
elements: ['slide'],
|
|
1664
|
+
doc: 'deck.md#the-deck-file',
|
|
1665
|
+
summary:
|
|
1666
|
+
'A deck is `content/docs/<part>/<lesson>.deck.mdx`, or a part’s `content/docs/<part>/index.deck.mdx`. Beside a chapter, and as a part’s, it sets only `title` and `description` — the chapter holds the lesson’s `teaches`, `assumptions`, `studyMinutes` and `notation`; alone, it is the lesson and carries them. On a part’s deck, `## Title [chapter: <name>]` names the chapter a section covers: a chapter of the part, once, and only on a `##` of a part’s deck. `<Recall ref>` names an equation, figure, table or diagram of the deck’s own chapter, or of a chapter a part’s deck covers; `<AssessmentSet>` on a part’s deck sits in a section that names a chapter; `<Pause>` takes only `control`, `from`, `to`, `seconds` (a driven figure) or `highlight` (a spotlight).',
|
|
1667
|
+
checks: [
|
|
1668
|
+
check('deck-frontmatter', 'error', false, DECKS),
|
|
1669
|
+
check('deck-outside-part', 'error', false, DECKS),
|
|
1670
|
+
check('recall-unresolved', 'error', true, DECKS),
|
|
1671
|
+
check('pause-invalid', 'error', true, DECKS),
|
|
1672
|
+
check('deck-chapter-unknown', 'error', true, DECKS),
|
|
1673
|
+
check('deck-chapter-repeated', 'error', true, DECKS),
|
|
1674
|
+
check('deck-chapter-outside-part', 'error', true, DECKS),
|
|
1675
|
+
check('deck-marker-misplaced', 'error', true, DECKS),
|
|
1676
|
+
check('deck-check-no-chapter', 'error', true, DECKS),
|
|
1677
|
+
check('deck-route-taken', 'error', false, DECKS),
|
|
1678
|
+
],
|
|
1679
|
+
},
|
|
1427
1680
|
];
|
|
1428
1681
|
|
|
1429
1682
|
export const CATALOG: readonly CatalogEntry[] = [...RULES, ...INVARIANTS];
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type Element,
|
|
43
43
|
type RuleCheck,
|
|
44
44
|
type RuleEntry,
|
|
45
|
+
type Editions,
|
|
45
46
|
} from './catalog';
|
|
46
47
|
|
|
47
48
|
/** Where a `forbid` pattern is matched. */
|
|
@@ -77,6 +78,8 @@ export interface CourseRuleInput {
|
|
|
77
78
|
readonly forbid?: readonly CourseRulePatternInput[];
|
|
78
79
|
readonly unchecked?: string;
|
|
79
80
|
readonly doc?: string;
|
|
81
|
+
/** `chapter`, `deck` or `both` (the default): the edition of a lesson the rule is about. */
|
|
82
|
+
readonly editions?: string;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/** `CourseConfig.courseRules` — an array, so the settings form can add and remove one. */
|
|
@@ -100,6 +103,8 @@ export interface CourseRule {
|
|
|
100
103
|
readonly forbid: readonly CourseRulePattern[];
|
|
101
104
|
readonly unchecked?: string;
|
|
102
105
|
readonly doc?: string;
|
|
106
|
+
/** The edition of a lesson the rule is about (`docs/authoring/deck.md`). */
|
|
107
|
+
readonly editions: Editions;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
/**
|
|
@@ -225,7 +230,7 @@ export function resolveCourseRules(config: {
|
|
|
225
230
|
);
|
|
226
231
|
}
|
|
227
232
|
const raw = input as Record<string, unknown>;
|
|
228
|
-
const known = ['id', 'title', 'summary', 'elements', 'enabled', 'severity', 'forbid', 'unchecked', 'doc'];
|
|
233
|
+
const known = ['id', 'title', 'summary', 'elements', 'enabled', 'severity', 'forbid', 'unchecked', 'doc', 'editions'];
|
|
229
234
|
for (const key of Object.keys(raw)) {
|
|
230
235
|
if (!known.includes(key)) {
|
|
231
236
|
throw new Error(`${at(index)}: unknown key \`${key}\`. Takes: ${known.join(', ')}.`);
|
|
@@ -259,6 +264,11 @@ export function resolveCourseRules(config: {
|
|
|
259
264
|
`${where}.severity must be "error" or "warning"; received ${describe(raw.severity)}.`,
|
|
260
265
|
);
|
|
261
266
|
}
|
|
267
|
+
if (raw.editions !== undefined && raw.editions !== 'both' && raw.editions !== 'chapter' && raw.editions !== 'deck') {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`${where}.editions must be "both", "chapter" or "deck"; received ${describe(raw.editions)}.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
262
272
|
const forbidInput = raw.forbid;
|
|
263
273
|
if (forbidInput !== undefined && !Array.isArray(forbidInput)) {
|
|
264
274
|
throw new Error(`${where}.forbid must be an array of patterns; received ${describe(forbidInput)}.`);
|
|
@@ -283,6 +293,7 @@ export function resolveCourseRules(config: {
|
|
|
283
293
|
forbid,
|
|
284
294
|
...(raw.unchecked === undefined ? {} : { unchecked: requireString(raw.unchecked, `${where}.unchecked`) }),
|
|
285
295
|
...(raw.doc === undefined ? {} : { doc: requireString(raw.doc, `${where}.doc`) }),
|
|
296
|
+
editions: (raw.editions as Editions | undefined) ?? 'both',
|
|
286
297
|
});
|
|
287
298
|
});
|
|
288
299
|
|
|
@@ -319,6 +330,7 @@ export function courseRuleEntry(rule: CourseRule): RuleEntry {
|
|
|
319
330
|
checks,
|
|
320
331
|
...(rule.unchecked === undefined ? {} : { unchecked: rule.unchecked }),
|
|
321
332
|
source: 'course',
|
|
333
|
+
...(rule.editions === 'both' ? {} : { editions: rule.editions }),
|
|
322
334
|
};
|
|
323
335
|
}
|
|
324
336
|
|
|
@@ -377,6 +389,14 @@ export function courseRulesJsonSchema(): JsonSchema {
|
|
|
377
389
|
default: ['prose'],
|
|
378
390
|
items: { title: 'Element', type: 'string', enum: [...ELEMENTS] },
|
|
379
391
|
},
|
|
392
|
+
editions: {
|
|
393
|
+
title: 'Editions',
|
|
394
|
+
description:
|
|
395
|
+
'The edition of a lesson the rule is about: its chapter, its deck (`<lesson>.deck.mdx`), or both. A rule about one edition is checked there only, and listed on that edition’s rules-in-effect page.',
|
|
396
|
+
type: 'string',
|
|
397
|
+
enum: ['both', 'chapter', 'deck'],
|
|
398
|
+
default: 'both',
|
|
399
|
+
},
|
|
380
400
|
enabled: {
|
|
381
401
|
title: 'Enabled',
|
|
382
402
|
description:
|
package/src/rules/in-effect.ts
CHANGED
|
@@ -11,15 +11,24 @@
|
|
|
11
11
|
* `pnpm validate:content` writes it to `build/rules-in-effect.md` (git-ignored,
|
|
12
12
|
* like the manifest — regenerated every run); `pnpm content rules` prints it,
|
|
13
13
|
* optionally filtered to an element or an enforcement kind.
|
|
14
|
+
*
|
|
15
|
+
* The page is per edition of a lesson (`docs/authoring/deck.md`): the default
|
|
16
|
+
* is the chapter's, which lists no deck rule, so a course without decks reads
|
|
17
|
+
* the page it always did. A course with decks also gets
|
|
18
|
+
* `build/rules-in-effect-deck.md` (`content rules --edition deck`), where the
|
|
19
|
+
* `D` rules lead and a chapter-only rule is listed as such.
|
|
14
20
|
*/
|
|
15
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
22
|
import { join } from 'node:path';
|
|
17
23
|
import {
|
|
18
24
|
ELEMENTS,
|
|
25
|
+
appliesTo,
|
|
19
26
|
enforcementOf,
|
|
20
27
|
invariants,
|
|
21
28
|
rules as catalogRules,
|
|
22
29
|
type CatalogEntry,
|
|
30
|
+
type Edition,
|
|
31
|
+
type Editions,
|
|
23
32
|
type Element,
|
|
24
33
|
type Enforcement,
|
|
25
34
|
type RuleEntry,
|
|
@@ -36,12 +45,16 @@ export interface RulesInEffectInput {
|
|
|
36
45
|
readonly material: { readonly name: string };
|
|
37
46
|
readonly knowledge: { readonly version: string };
|
|
38
47
|
};
|
|
48
|
+
/** The course's decks (`manifest.decks`): with any, the deck page is written too. */
|
|
49
|
+
readonly decks?: readonly unknown[];
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
export interface RulesFilter {
|
|
42
53
|
/** Keep a rule whose elements (any, not just primary) include one of these. */
|
|
43
54
|
readonly elements?: readonly Element[];
|
|
44
55
|
readonly enforcement?: Enforcement | 'locked';
|
|
56
|
+
/** The edition the page is for: `chapter` (the default) or `deck`. */
|
|
57
|
+
readonly edition?: Edition;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
/** One rule as `content rules --json` reports it. */
|
|
@@ -60,6 +73,8 @@ export interface RuleInEffect {
|
|
|
60
73
|
readonly settings: Readonly<Record<string, unknown>>;
|
|
61
74
|
readonly summary: string;
|
|
62
75
|
readonly doc: string;
|
|
76
|
+
/** The edition it is about: `chapter`, `deck`, or `both`. */
|
|
77
|
+
readonly editions: Editions;
|
|
63
78
|
}
|
|
64
79
|
|
|
65
80
|
/** Where a course's copy of the rule text lives, relative to `build/`. */
|
|
@@ -84,6 +99,7 @@ function enforcementLabel(entry: CatalogEntry): Enforcement | 'locked' {
|
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
function matches(entry: CatalogEntry, filter: RulesFilter): boolean {
|
|
102
|
+
if (!appliesTo(entry, filter.edition ?? 'chapter')) return false;
|
|
87
103
|
if (filter.elements && !entry.elements.some((e) => filter.elements!.includes(e))) return false;
|
|
88
104
|
if (filter.enforcement && enforcementLabel(entry) !== filter.enforcement) return false;
|
|
89
105
|
return true;
|
|
@@ -105,6 +121,7 @@ export function rulesInEffect(input: RulesInEffectInput, filter: RulesFilter = {
|
|
|
105
121
|
settings: entry.kind === 'rule' ? (input.rules.settings[entry.id] ?? {}) : {},
|
|
106
122
|
summary: entry.summary,
|
|
107
123
|
doc: docLink(entry),
|
|
124
|
+
editions: entry.editions ?? 'both',
|
|
108
125
|
}));
|
|
109
126
|
}
|
|
110
127
|
|
|
@@ -152,17 +169,29 @@ function ruleLine(rule: RuleEntry, settings: Readonly<Record<string, unknown>>):
|
|
|
152
169
|
* the reader knows what was left out on purpose.
|
|
153
170
|
*/
|
|
154
171
|
export function renderRulesInEffect(input: RulesInEffectInput, filter: RulesFilter = {}): string {
|
|
172
|
+
const edition = filter.edition ?? 'chapter';
|
|
155
173
|
const off = new Set<string>(input.rules.disabled);
|
|
156
|
-
const
|
|
174
|
+
const live = liveRules(input).filter((r) => appliesTo(r, edition));
|
|
175
|
+
const on = live.filter((r) => !off.has(r.id));
|
|
176
|
+
const offHere = [...off].filter((id) => live.some((r) => r.id === id));
|
|
157
177
|
const ours = on.filter((r) => r.source === 'course').length;
|
|
158
178
|
const locked = on.filter((r) => r.locked !== undefined).map((r) => r.id);
|
|
159
179
|
const filtered = filter.elements || filter.enforcement;
|
|
160
180
|
const visible = on.filter((r) => matches(r, filter));
|
|
181
|
+
const editionInvariants = invariants().filter((i) => appliesTo(i, edition));
|
|
161
182
|
|
|
162
183
|
const lines: string[] = [];
|
|
163
184
|
lines.push(
|
|
164
|
-
`# Rules in effect — ${input.about.material.name} (knowledge ${input.about.knowledge.version}, ${input.about.engine.name} ${input.about.engine.version})`,
|
|
185
|
+
`# Rules in effect${edition === 'deck' ? ' for decks' : ''} — ${input.about.material.name} (knowledge ${input.about.knowledge.version}, ${input.about.engine.name} ${input.about.engine.version})`,
|
|
165
186
|
'',
|
|
187
|
+
...(edition === 'deck'
|
|
188
|
+
? [
|
|
189
|
+
'For a lesson’s deck, `<lesson>.deck.mdx` (`docs/authoring/deck.md`): the `D` rules',
|
|
190
|
+
'lead, then every rule that holds for a deck as for a chapter. A chapter-only rule is',
|
|
191
|
+
'listed under "Chapter only" at the end; on a frame, a `D` rule says it instead.',
|
|
192
|
+
'',
|
|
193
|
+
]
|
|
194
|
+
: []),
|
|
166
195
|
'Generated by `pnpm validate:content` (and `pnpm content rules`) from the rule',
|
|
167
196
|
'catalog and `rules` / `courseRules` in `course.settings.json`. Do not edit. Read this before',
|
|
168
197
|
'drafting, and open a rule page only when it is listed here and you need the',
|
|
@@ -170,7 +199,7 @@ export function renderRulesInEffect(input: RulesInEffectInput, filter: RulesFilt
|
|
|
170
199
|
'not exist, and is neither applied nor flagged in review.',
|
|
171
200
|
'',
|
|
172
201
|
`${on.length} rules on (${locked.length} locked${ours === 0 ? '' : `, ${ours} this course's own`}), ` +
|
|
173
|
-
`${
|
|
202
|
+
`${offHere.length} off, ${editionInvariants.length} invariants always on.` +
|
|
174
203
|
(filtered
|
|
175
204
|
? ` Filtered: ${[
|
|
176
205
|
filter.elements ? `element ${filter.elements.join(', ')}` : '',
|
|
@@ -200,7 +229,7 @@ export function renderRulesInEffect(input: RulesInEffectInput, filter: RulesFilt
|
|
|
200
229
|
for (const rule of here) lines.push(ruleLine(rule, input.rules.settings[rule.id] ?? {}));
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
const visibleInvariants =
|
|
232
|
+
const visibleInvariants = editionInvariants.filter((i) => matches(i, filter));
|
|
204
233
|
lines.push('', '## Always on (invariants)', '');
|
|
205
234
|
if (visibleInvariants.length === 0) {
|
|
206
235
|
lines.push('- (none match the filter)');
|
|
@@ -214,22 +243,39 @@ export function renderRulesInEffect(input: RulesInEffectInput, filter: RulesFilt
|
|
|
214
243
|
|
|
215
244
|
lines.push('', '## Off', '');
|
|
216
245
|
lines.push(
|
|
217
|
-
|
|
246
|
+
offHere.length === 0
|
|
218
247
|
? '- Nothing — every rule is on.'
|
|
219
|
-
: `- ${
|
|
248
|
+
: `- ${offHere.join(', ')} — turned off in \`course.settings.json\` (\`rules\`, or \`courseRules\` for an \`X\` rule). Do not apply them; do not flag them in review.`,
|
|
220
249
|
);
|
|
221
250
|
|
|
222
251
|
lines.push('', '## Locked', '');
|
|
223
252
|
lines.push(`- ${locked.join(', ')} — cannot be turned off; listed above under their element.`);
|
|
224
253
|
|
|
254
|
+
if (edition === 'deck') {
|
|
255
|
+
const chapterOnly = liveRules(input).filter((r) => !appliesTo(r, 'deck')).map((r) => r.id);
|
|
256
|
+
lines.push('', '## Chapter only', '');
|
|
257
|
+
lines.push(
|
|
258
|
+
chapterOnly.length === 0
|
|
259
|
+
? '- Nothing — every rule holds for a deck too.'
|
|
260
|
+
: `- ${chapterOnly.join(', ')} — about a chapter’s page, not a frame. On a deck, neither apply them nor flag them; the \`D\` rules above say the same of a frame.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
225
264
|
return `${lines.join('\n')}\n`;
|
|
226
265
|
}
|
|
227
266
|
|
|
228
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* Write `build/rules-in-effect.md` under `root` (the course package), and
|
|
269
|
+
* `build/rules-in-effect-deck.md` when the course has a deck (a stale one goes
|
|
270
|
+
* when it has none). Returns the chapter page's path.
|
|
271
|
+
*/
|
|
229
272
|
export function writeRulesInEffect(input: RulesInEffectInput, root = process.cwd()): string {
|
|
230
273
|
const dir = join(root, 'build');
|
|
231
274
|
mkdirSync(dir, { recursive: true });
|
|
232
275
|
const path = join(dir, 'rules-in-effect.md');
|
|
233
276
|
writeFileSync(path, renderRulesInEffect(input));
|
|
277
|
+
const deckPath = join(dir, 'rules-in-effect-deck.md');
|
|
278
|
+
if ((input.decks?.length ?? 0) > 0) writeFileSync(deckPath, renderRulesInEffect(input, { edition: 'deck' }));
|
|
279
|
+
else if (existsSync(deckPath)) rmSync(deckPath);
|
|
234
280
|
return path;
|
|
235
281
|
}
|
|
@@ -23,6 +23,12 @@ export interface PreferenceStore {
|
|
|
23
23
|
* fits; a caller without it writes an empty value instead.
|
|
24
24
|
*/
|
|
25
25
|
remove?(key: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Every key the store holds. Optional like `remove`; the reading-memory
|
|
28
|
+
* question (`reading-memory.ts`) uses it to forget a course's per-page
|
|
29
|
+
* entries, whose names it cannot list otherwise.
|
|
30
|
+
*/
|
|
31
|
+
keys?(): string[];
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -53,6 +59,14 @@ export function createBrowserPreferenceStore(): PreferenceStore {
|
|
|
53
59
|
// Storage unavailable — there is nothing stored to forget.
|
|
54
60
|
}
|
|
55
61
|
},
|
|
62
|
+
keys() {
|
|
63
|
+
try {
|
|
64
|
+
const storage = window.localStorage;
|
|
65
|
+
return Array.from({ length: storage.length }, (_, index) => storage.key(index) ?? '').filter(Boolean);
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
},
|
|
56
70
|
};
|
|
57
71
|
}
|
|
58
72
|
|
|
@@ -83,6 +97,14 @@ export function createSessionPreferenceStore(): PreferenceStore {
|
|
|
83
97
|
// Storage unavailable — there is nothing stored to forget.
|
|
84
98
|
}
|
|
85
99
|
},
|
|
100
|
+
keys() {
|
|
101
|
+
try {
|
|
102
|
+
const storage = window.sessionStorage;
|
|
103
|
+
return Array.from({ length: storage.length }, (_, index) => storage.key(index) ?? '').filter(Boolean);
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
},
|
|
86
108
|
};
|
|
87
109
|
}
|
|
88
110
|
|
|
@@ -118,5 +140,6 @@ export function createMemoryPreferenceStore(): PreferenceStore {
|
|
|
118
140
|
remove: (key) => {
|
|
119
141
|
map.delete(key);
|
|
120
142
|
},
|
|
143
|
+
keys: () => [...map.keys()],
|
|
121
144
|
};
|
|
122
145
|
}
|