explico 0.7.0 → 0.11.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 +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +14 -1
- package/dist/compiler/logging.js +6 -2
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +69 -34
- package/dist/reference/equations.mjs +90 -44
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +3 -2
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +47 -23
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +44 -6
- package/dist/reference/gate-math.js +63 -10
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +588 -39
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +5 -5
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +24 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1324 -2
- package/docs/authoring/notation.md +616 -344
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +21 -4
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +588 -39
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +17 -32
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +24 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -0,0 +1,1298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule catalog — every authoring rule and every always-on invariant the
|
|
3
|
+
* engine enforces, as data.
|
|
4
|
+
*
|
|
5
|
+
* The prose of a rule (rationale, how to apply, examples) lives in
|
|
6
|
+
* `docs/authoring/<area>.md`; this module holds only what a machine needs:
|
|
7
|
+
* the permanent ID, the element(s) a rule affects, the diagnostic codes it
|
|
8
|
+
* owns and where they are emitted, its settings, and whether a course may
|
|
9
|
+
* turn it off. `scripts/agent/gen-rules.mjs` renders the README index from
|
|
10
|
+
* it and checks the `.md` files against it; `tests/rules/catalog.test.ts`
|
|
11
|
+
* fails when a code is emitted that no entry owns, or when two entries claim
|
|
12
|
+
* one code. See `docs/plans/rule-catalog.md` for the design.
|
|
13
|
+
*
|
|
14
|
+
* Two kinds of entry share the catalog:
|
|
15
|
+
*
|
|
16
|
+
* - A **rule** is a choice — a house style that could have gone another way.
|
|
17
|
+
* It has a lettered ID and, unless `locked`, a checkbox in the course
|
|
18
|
+
* settings (`rules.<ID>.enabled`). A rule that is retired leaves this list
|
|
19
|
+
* for the compact {@link RetiredRule} one, which costs a reader four lines
|
|
20
|
+
* instead of a whole entry.
|
|
21
|
+
* - An **invariant** is what the syntax means — a `[[key]]` must resolve, a
|
|
22
|
+
* `\label{eq:…}` key is unique. One entry per feature, always on,
|
|
23
|
+
* documented next to the syntax rather than as a rule page.
|
|
24
|
+
*
|
|
25
|
+
* Enforcement is never typed by hand: it is derived from `checks` and
|
|
26
|
+
* `unchecked` by {@link enforcementOf}.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The part of a page a rule is about. The first tag on an entry is primary. */
|
|
30
|
+
export type Element =
|
|
31
|
+
| 'prose'
|
|
32
|
+
| 'math'
|
|
33
|
+
| 'equation'
|
|
34
|
+
| 'notation'
|
|
35
|
+
| 'heading'
|
|
36
|
+
| 'part-page'
|
|
37
|
+
| 'plot'
|
|
38
|
+
| 'diagram'
|
|
39
|
+
| 'table'
|
|
40
|
+
| 'assessment'
|
|
41
|
+
| 'source'
|
|
42
|
+
| 'changelog';
|
|
43
|
+
|
|
44
|
+
/** Every element, in the order the rules-in-effect page lists them. */
|
|
45
|
+
export const ELEMENTS: readonly Element[] = [
|
|
46
|
+
'prose',
|
|
47
|
+
'math',
|
|
48
|
+
'equation',
|
|
49
|
+
'notation',
|
|
50
|
+
'heading',
|
|
51
|
+
'part-page',
|
|
52
|
+
'plot',
|
|
53
|
+
'diagram',
|
|
54
|
+
'table',
|
|
55
|
+
'assessment',
|
|
56
|
+
'source',
|
|
57
|
+
'changelog',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The five areas the engine ships, plus `X` — reserved for a course's own rules
|
|
62
|
+
* and never used by an engine rule (see {@link CourseRuleId}).
|
|
63
|
+
*/
|
|
64
|
+
export type RuleArea = 'N' | 'V' | 'C' | 'S' | 'P' | 'X';
|
|
65
|
+
export type RuleId = `${RuleArea}${number}`;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A course's own rule, defined in `courseRules` in the course settings rather
|
|
69
|
+
* than here (`src/rules/course-rules.ts`). `X` is permanently reserved for
|
|
70
|
+
* them: the engine never ships an `X` rule, so a course can number its own
|
|
71
|
+
* `X1`, `X2`, … without a future engine release colliding with them. The form
|
|
72
|
+
* is the same as an engine rule's, so one ID works everywhere — a commit
|
|
73
|
+
* message, a diagnostic, a waiver, the rules-in-effect page.
|
|
74
|
+
*/
|
|
75
|
+
export type CourseRuleId = `X${number}`;
|
|
76
|
+
|
|
77
|
+
const COURSE_RULE_ID = /^X[1-9][0-9]?$/;
|
|
78
|
+
|
|
79
|
+
/** True for a well-formed course-rule ID (`X1`…`X99`). */
|
|
80
|
+
export function isCourseRuleId(id: string): id is CourseRuleId {
|
|
81
|
+
return COURSE_RULE_ID.test(id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
85
|
+
|
|
86
|
+
/** Where a code is emitted: engine source module + the function that pushes it. */
|
|
87
|
+
export interface Emitter {
|
|
88
|
+
/** Repo-relative engine source path, e.g. `src/reference/gate-heading-length.ts`. */
|
|
89
|
+
readonly module: string;
|
|
90
|
+
/** A `function`/`const` name in that module; existence-checked by the tests. */
|
|
91
|
+
readonly fn: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RuleCheck {
|
|
95
|
+
/** The `code` the diagnostic carries. Unique across the whole catalog. */
|
|
96
|
+
readonly code: string;
|
|
97
|
+
readonly severity: Severity;
|
|
98
|
+
readonly emitters: readonly [Emitter, ...Emitter[]];
|
|
99
|
+
/**
|
|
100
|
+
* True when a lesson-body emission carries `file` + `line`, so a per-place
|
|
101
|
+
* waiver can target it. Corpus-level and per-file checks are `false`.
|
|
102
|
+
*/
|
|
103
|
+
readonly located: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A course-settable parameter of a rule — a tiny JSON-Schema subset. */
|
|
107
|
+
export type SettingSchema =
|
|
108
|
+
| { readonly type: 'integer'; readonly minimum?: number; readonly default: number }
|
|
109
|
+
| { readonly type: 'boolean'; readonly default: boolean }
|
|
110
|
+
| { readonly type: 'array'; readonly items: 'string'; readonly default: readonly string[] };
|
|
111
|
+
|
|
112
|
+
export interface SettingSpec {
|
|
113
|
+
readonly key: string;
|
|
114
|
+
readonly title: string;
|
|
115
|
+
readonly description: string;
|
|
116
|
+
readonly schema: SettingSchema;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface RuleEntry {
|
|
120
|
+
readonly kind: 'rule';
|
|
121
|
+
/** Permanent. A rule that is retired keeps its ID in {@link RetiredRule}. */
|
|
122
|
+
readonly id: RuleId;
|
|
123
|
+
/** The bold first sentence of the rule page, verbatim. */
|
|
124
|
+
readonly title: string;
|
|
125
|
+
/** Index-table phrasing, one line; the README row uses it over `title`. */
|
|
126
|
+
readonly short?: string;
|
|
127
|
+
readonly elements: readonly [Element, ...Element[]];
|
|
128
|
+
/**
|
|
129
|
+
* Path + anchor under `docs/authoring/`, e.g. `notation.md#n9`. On a course
|
|
130
|
+
* rule (`source: 'course'`) it is instead a link the course owns, used as
|
|
131
|
+
* given.
|
|
132
|
+
*/
|
|
133
|
+
readonly doc: string;
|
|
134
|
+
/** What an agent needs in order to comply — at most three sentences, no rationale. */
|
|
135
|
+
readonly summary: string;
|
|
136
|
+
readonly checks: readonly RuleCheck[];
|
|
137
|
+
/** What no script sees. Present → enforcement is `partial`. */
|
|
138
|
+
readonly unchecked?: string;
|
|
139
|
+
/** Why the rule has no checkbox. */
|
|
140
|
+
readonly locked?: string;
|
|
141
|
+
readonly settings?: readonly SettingSpec[];
|
|
142
|
+
/**
|
|
143
|
+
* Set only on the synthetic entry built from a course's own `courseRules`
|
|
144
|
+
* block (`courseRuleEntry`). The static catalog never sets it, so `source`
|
|
145
|
+
* absent means "shipped with the engine".
|
|
146
|
+
*/
|
|
147
|
+
readonly source?: 'course';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface InvariantEntry {
|
|
151
|
+
readonly kind: 'invariant';
|
|
152
|
+
/** A feature slug, e.g. `notation-references`. */
|
|
153
|
+
readonly id: string;
|
|
154
|
+
readonly title: string;
|
|
155
|
+
readonly elements: readonly [Element, ...Element[]];
|
|
156
|
+
readonly doc: string;
|
|
157
|
+
readonly summary: string;
|
|
158
|
+
readonly checks: readonly [RuleCheck, ...RuleCheck[]];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type CatalogEntry = RuleEntry | InvariantEntry;
|
|
162
|
+
|
|
163
|
+
export type Enforcement = 'script' | 'partial' | 'guide';
|
|
164
|
+
|
|
165
|
+
// --- emitters ------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
const GATE_MATH: Emitter = { module: 'src/reference/gate-math.ts', fn: 'resolveInto' };
|
|
168
|
+
const GLYPHS: Emitter = { module: 'src/reference/math-glyphs.mjs', fn: 'resolveMathGlyphs' };
|
|
169
|
+
const REGISTRY: Emitter = { module: 'src/reference/registry.ts', fn: 'buildNotationRegistry' };
|
|
170
|
+
const KEYED_FLOATS: Emitter = { module: 'src/reference/keyed-float-validate.ts', fn: 'validateKeyedFloats' };
|
|
171
|
+
|
|
172
|
+
function consistency(fn: string): Emitter {
|
|
173
|
+
return { module: 'src/reference/consistency.ts', fn };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function check(
|
|
177
|
+
code: string,
|
|
178
|
+
severity: Severity,
|
|
179
|
+
located: boolean,
|
|
180
|
+
...emitters: [Emitter, ...Emitter[]]
|
|
181
|
+
): RuleCheck {
|
|
182
|
+
return { code, severity, emitters, located };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `{eq,tab,fig,dia,check}-<suffix>` — the keyed-float family shares one
|
|
187
|
+
* validator — plus `sec-ref-resolves` for a `[[sec-…]]` to a heading, and
|
|
188
|
+
* `fold-unnamed` for a `<Fold>` with nothing to name its bar by.
|
|
189
|
+
*/
|
|
190
|
+
function keyedFloatChecks(): RuleCheck[] {
|
|
191
|
+
const out: RuleCheck[] = [];
|
|
192
|
+
for (const prefix of ['eq', 'tab', 'fig', 'dia', 'ex', 'check'] as const) {
|
|
193
|
+
const emitter: Emitter =
|
|
194
|
+
prefix === 'eq'
|
|
195
|
+
? { module: 'src/reference/equations-validate.ts', fn: 'validateEquations' }
|
|
196
|
+
: prefix === 'check'
|
|
197
|
+
? { module: 'src/reference/knowledge-checks-validate.ts', fn: 'validateKnowledgeChecks' }
|
|
198
|
+
: KEYED_FLOATS;
|
|
199
|
+
// An equation's key is validated by the `\label{eq:…}` pattern itself, so
|
|
200
|
+
// there is no `eq-invalid-key`; the JSX floats and checks report one.
|
|
201
|
+
if (prefix !== 'eq') out.push(check(`${prefix}-invalid-key`, 'error', false, emitter));
|
|
202
|
+
out.push(
|
|
203
|
+
check(`${prefix}-duplicate-key`, 'error', false, emitter),
|
|
204
|
+
check(`${prefix}-ref-resolves`, 'error', false, emitter),
|
|
205
|
+
);
|
|
206
|
+
if (prefix === 'tab' || prefix === 'fig' || prefix === 'dia' || prefix === 'ex') {
|
|
207
|
+
out.push(check(`${prefix}-key-unused`, 'info', false, emitter));
|
|
208
|
+
// A prop that is gone: a name prop the caption pair (`caption` +
|
|
209
|
+
// `short`) replaced — a figure's `title`, a diagram's `label`, an
|
|
210
|
+
// example set's `summary` — or a diagram's compaction props
|
|
211
|
+
// (`defaultOpen`, `rememberState`). The shared validator checks every
|
|
212
|
+
// JSX float; a table never had one to remove.
|
|
213
|
+
out.push(check(`${prefix}-removed-prop`, 'error', false, emitter));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// A worked-example set cannot do without its `id` (its anchor and
|
|
217
|
+
// `[[ex-…]]`) or its `short` title (its heading after the "Example" badge).
|
|
218
|
+
out.push(
|
|
219
|
+
check('ex-missing-prop', 'error', false, {
|
|
220
|
+
module: 'src/reference/examples-validate.ts',
|
|
221
|
+
fn: 'validateExamples',
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
// A section's key is its heading's anchor, which the slugger makes unique,
|
|
225
|
+
// so only the reference can go wrong.
|
|
226
|
+
out.push(
|
|
227
|
+
check('sec-ref-resolves', 'error', true, {
|
|
228
|
+
module: 'src/reference/sections-validate.ts',
|
|
229
|
+
fn: 'validateSectionRefs',
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
// A `<Fold>` is named by its `label`, or by the numbered floats it holds;
|
|
233
|
+
// one with neither has an empty bar.
|
|
234
|
+
out.push(
|
|
235
|
+
check('fold-unnamed', 'error', false, {
|
|
236
|
+
module: 'src/reference/folds-validate.ts',
|
|
237
|
+
fn: 'validateFolds',
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --- settings shared by several rules ----------------------------------------
|
|
244
|
+
|
|
245
|
+
function baselineEdits(what: string): readonly SettingSpec[] {
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
key: 'allow',
|
|
249
|
+
title: 'Also allow',
|
|
250
|
+
description: `${what} this course treats as universal, in addition to the shipped list.`,
|
|
251
|
+
schema: { type: 'array', items: 'string', default: [] },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
key: 'deny',
|
|
255
|
+
title: 'Always explain',
|
|
256
|
+
description: `Shipped ${what.toLowerCase()} this course still wants explained.`,
|
|
257
|
+
schema: { type: 'array', items: 'string', default: [] },
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- the catalog ---------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
const RULES: readonly RuleEntry[] = [
|
|
265
|
+
// ---- N — notation ------------------------------------------------------
|
|
266
|
+
{
|
|
267
|
+
kind: 'rule',
|
|
268
|
+
id: 'N2',
|
|
269
|
+
short: 'A reserved spelling is used only for its reserved meaning',
|
|
270
|
+
title:
|
|
271
|
+
'A reserved spelling means only what it is reserved for, and every other styled letter is a symbol of its own.',
|
|
272
|
+
elements: ['math'],
|
|
273
|
+
doc: 'notation.md#n2',
|
|
274
|
+
summary:
|
|
275
|
+
'The `reserved` setting is this course’s table of spellings that are fixed to one meaning — nothing else may use them, and they need no notation key. Every *other* styled letter (`\\mathbb{E}`, `\\mathcal{L}`, an italic `d` used as a variable) is an ordinary symbol that needs a notation entry of its own, even when the plain letter has one (N4).',
|
|
276
|
+
checks: [],
|
|
277
|
+
settings: [
|
|
278
|
+
{
|
|
279
|
+
key: 'reserved',
|
|
280
|
+
title: 'Reserved spellings',
|
|
281
|
+
description:
|
|
282
|
+
'One `<latex> — <meaning>` line per spelling this course fixes to a single meaning, printed on the rules-in-effect page so an author or an agent reads the table without opening the rule page. Nothing checks it — which spelling was chosen is caught in review; rule N4’s gate only asks that a glyph resolve. Replace the shipped list to change the conventions: drop the probability lines in a course with no probability, or write `\\mathcal{N}(\\mu, \\sigma)` for a course that parameterises the normal by standard deviation.',
|
|
283
|
+
schema: {
|
|
284
|
+
type: 'array',
|
|
285
|
+
items: 'string',
|
|
286
|
+
default: [
|
|
287
|
+
'\\mathbb{N} — the natural numbers',
|
|
288
|
+
'\\mathbb{Z} — the integers',
|
|
289
|
+
'\\mathbb{R} — the reals',
|
|
290
|
+
'\\mathbb{C} — the complex numbers',
|
|
291
|
+
'\\mathbb{P} — probability',
|
|
292
|
+
'\\mathcal{N}(\\mu, \\sigma^2) — the normal distribution (mean, variance)',
|
|
293
|
+
'\\mathrm{d} — the differential',
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
kind: 'rule',
|
|
301
|
+
id: 'N4',
|
|
302
|
+
short: 'One meaning per glyph; define once; `[[key]]` before first math',
|
|
303
|
+
title:
|
|
304
|
+
'One meaning per glyph per page; define once; introduce with `[[key]]` before the first math use.',
|
|
305
|
+
elements: ['math', 'notation'],
|
|
306
|
+
doc: 'notation.md#n4',
|
|
307
|
+
summary:
|
|
308
|
+
'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.',
|
|
309
|
+
checks: [
|
|
310
|
+
check('unresolved-glyph', 'error', true, GATE_MATH),
|
|
311
|
+
check('glyph-unique-in-corpus', 'warning', false, consistency('checkGlyphUniqueness')),
|
|
312
|
+
check('weak-local', 'info', false, consistency('checkWeakLocal')),
|
|
313
|
+
check('gloss-wants-promoting', 'info', false, consistency('checkGlossWantsPromoting')),
|
|
314
|
+
check('card-wants-demoting', 'info', false, consistency('checkCardWantsDemoting')),
|
|
315
|
+
],
|
|
316
|
+
locked:
|
|
317
|
+
'the notation layer is this rule — an unresolved glyph has no hover card and the render path needs every glyph bound.',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
kind: 'rule',
|
|
321
|
+
id: 'N5',
|
|
322
|
+
short: 'Referenced display equations carry `\\label{eq:key}`',
|
|
323
|
+
title: 'A display equation referenced later carries `\\label{eq:key}`.',
|
|
324
|
+
elements: ['equation'],
|
|
325
|
+
doc: 'notation.md#n5',
|
|
326
|
+
summary:
|
|
327
|
+
'Label a display equation with `\\label{eq:key}` when — and only when — prose refers back to it with `[[eq-key]]`. A label nothing references is noise.',
|
|
328
|
+
checks: [
|
|
329
|
+
check('eq-key-unused', 'info', false, {
|
|
330
|
+
module: 'src/reference/equations-validate.ts',
|
|
331
|
+
fn: 'validateEquations',
|
|
332
|
+
}),
|
|
333
|
+
],
|
|
334
|
+
unchecked: 'that a referenced-in-prose equation actually carries a label.',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
kind: 'rule',
|
|
338
|
+
id: 'N8',
|
|
339
|
+
short: 'A named operator is universal (shipped/course list) or explained',
|
|
340
|
+
title: 'A named operator is universal, or it is explained.',
|
|
341
|
+
elements: ['math', 'notation'],
|
|
342
|
+
doc: 'notation.md#n8',
|
|
343
|
+
summary:
|
|
344
|
+
'An operator such as `\\sin` or `\\operatorname{Var}` is either on the shipped universal list (plus the course’s `allow` list) or has a notation entry. The `deny` list forces an entry for a shipped one.',
|
|
345
|
+
checks: [
|
|
346
|
+
check('operator-explained', 'warning', false, consistency('checkOperatorExplained')),
|
|
347
|
+
],
|
|
348
|
+
settings: baselineEdits('Operators'),
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
kind: 'rule',
|
|
352
|
+
id: 'N9',
|
|
353
|
+
short: 'A quantity/unit/operator is typeset in `$…$`, never as plain text',
|
|
354
|
+
title:
|
|
355
|
+
'A physical quantity, unit, or math operator is typeset in `$…$`, never as plain text — in prose, a table cell, an assessment `prompt` / `explanation`, or a caption that itself lives outside math.',
|
|
356
|
+
elements: ['prose', 'assessment', 'table'],
|
|
357
|
+
doc: 'notation.md#n9',
|
|
358
|
+
summary:
|
|
359
|
+
'Never write `45 m/s`, `t (s)`, `vx` or `KE/m` as plain text, and never spell a stated quantity\'s unit out: "4 seconds", "2 meters per second squared", "30 degrees", "$x = 3$ meters" are `$4\\ \\text{s}$`, `$2\\ \\text{m/s}^2$`, `$30^{\\circ}$`, `$x = 3\\ \\text{m}$`. A unit inside math is `\\text{}`-wrapped, short form, in the same span as its number or symbol. A unit named on its own ("measured in meters") stays a word; a sentence that defines a unit is waived. JSX prop strings (`caption="…"`) and assessment choice labels render no math, so N9 does not apply there.',
|
|
360
|
+
checks: [
|
|
361
|
+
check(
|
|
362
|
+
'prose-notation',
|
|
363
|
+
'error',
|
|
364
|
+
true,
|
|
365
|
+
{ module: 'src/reference/gate-prose-notation.ts', fn: 'gateProseNotation' },
|
|
366
|
+
{ module: 'src/reference/gate-prose-notation.ts', fn: 'gateProseNotationInAssessments' },
|
|
367
|
+
),
|
|
368
|
+
check(
|
|
369
|
+
'spelled-unit',
|
|
370
|
+
'error',
|
|
371
|
+
true,
|
|
372
|
+
{ module: 'src/reference/gate-prose-notation.ts', fn: 'gateProseNotation' },
|
|
373
|
+
{ module: 'src/reference/gate-prose-notation.ts', fn: 'gateProseNotationInAssessments' },
|
|
374
|
+
),
|
|
375
|
+
],
|
|
376
|
+
settings: [
|
|
377
|
+
{
|
|
378
|
+
key: 'allow',
|
|
379
|
+
title: 'Unit words prose may spell out',
|
|
380
|
+
description:
|
|
381
|
+
'Unit words (`hour`, `minute`) this course lets prose write out next to a number or `per`, singular or plural. Every other unit in `unit-words.ts` must be a symbol in `$…$`.',
|
|
382
|
+
schema: { type: 'array', items: 'string', default: [] },
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
kind: 'rule',
|
|
388
|
+
id: 'N10',
|
|
389
|
+
short: 'A multi-letter label is `\\mathrm{…}`, and universal or explained',
|
|
390
|
+
title:
|
|
391
|
+
'A multi-letter roman label is written `\\mathrm{…}`, and it is universal or it is explained.',
|
|
392
|
+
elements: ['math', 'notation'],
|
|
393
|
+
doc: 'notation.md#n10',
|
|
394
|
+
summary:
|
|
395
|
+
'Write a word-like label inside math as `\\mathrm{KE}`, `v_{\\mathrm{max}}` — never a bare run of italic letters, which reads as a product. The label itself is on the shipped list (plus `allow`) or has a notation entry: the glyph resolver skips a `\\mathrm{}` body, so nothing else gives it a hover card.',
|
|
396
|
+
checks: [
|
|
397
|
+
check('roman-label-explained', 'warning', false, consistency('checkRomanLabelExplained')),
|
|
398
|
+
],
|
|
399
|
+
unchecked:
|
|
400
|
+
'that a bare multi-letter run was wrapped in `\\mathrm{…}` at all — rule N4’s gate reports it as unresolved one-letter symbols, which is a different message for the same mistake.',
|
|
401
|
+
settings: baselineEdits('Roman labels'),
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
kind: 'rule',
|
|
405
|
+
id: 'N11',
|
|
406
|
+
short: 'A worked example never drops a unit from a substituted value',
|
|
407
|
+
title:
|
|
408
|
+
'A worked example never drops a unit from a substituted value, and its result is dimensionally consistent.',
|
|
409
|
+
elements: ['math', 'prose'],
|
|
410
|
+
doc: 'notation.md#n11',
|
|
411
|
+
summary:
|
|
412
|
+
'When numbers replace symbols, every value keeps its unit (`$v = 3\\ \\text{m/s} \\cdot 4\\ \\text{s}$`), and the result’s unit follows from them.',
|
|
413
|
+
checks: [],
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
kind: 'rule',
|
|
417
|
+
id: 'N12',
|
|
418
|
+
short: 'Math syntax never carries the sentence\'s own trailing punctuation',
|
|
419
|
+
title: 'Math syntax never carries the sentence’s own trailing punctuation.',
|
|
420
|
+
elements: ['math', 'prose', 'assessment'],
|
|
421
|
+
doc: 'notation.md#n12',
|
|
422
|
+
summary:
|
|
423
|
+
'Math, and each row before a `\\\\`, never ends in `.`, `,`, `;` or `:` — the sentence’s own punctuation goes after the closing `$`, or is dropped after a display block. A trailing decimal point (`5.`) and a typed `...` count too. The only exception is an upright dotted abbreviation that is notation (`\\text{h.c.}`): the shipped list plus `allow`.',
|
|
424
|
+
checks: [
|
|
425
|
+
check(
|
|
426
|
+
'math-trailing-punctuation',
|
|
427
|
+
'error',
|
|
428
|
+
true,
|
|
429
|
+
{ module: 'src/reference/gate-math-punctuation.ts', fn: 'gateMathPunctuation' },
|
|
430
|
+
{ module: 'src/reference/gate-math-punctuation.ts', fn: 'gateMathPunctuationInAssessments' },
|
|
431
|
+
),
|
|
432
|
+
],
|
|
433
|
+
settings: [
|
|
434
|
+
{
|
|
435
|
+
key: 'allow',
|
|
436
|
+
title: 'Also allow',
|
|
437
|
+
description:
|
|
438
|
+
'Dotted abbreviations this course sets as notation at the end of math (`\\text{w.p.}`), in addition to the shipped `a.e.`, `a.s.`, `c.c.`, `const.`, `h.c.`, `i.i.d.`.',
|
|
439
|
+
schema: { type: 'array', items: 'string', default: [] },
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
key: 'deny',
|
|
443
|
+
title: 'Never allow',
|
|
444
|
+
description: 'Shipped abbreviations this course does not accept at the end of math.',
|
|
445
|
+
schema: { type: 'array', items: 'string', default: [] },
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
kind: 'rule',
|
|
451
|
+
id: 'N13',
|
|
452
|
+
short: 'A nonlinear function always takes a parenthesized argument',
|
|
453
|
+
title:
|
|
454
|
+
'A nonlinear function always takes a parenthesized argument, even a single symbol.',
|
|
455
|
+
elements: ['math'],
|
|
456
|
+
doc: 'notation.md#n13',
|
|
457
|
+
summary: 'Write `\\sin(x)`, `\\ln(t)`, `f(x)` — never `\\sin x`.',
|
|
458
|
+
checks: [check('unparenthesized-function', 'error', true, GLYPHS)],
|
|
459
|
+
locked:
|
|
460
|
+
'the glyph resolver cannot tell where an unparenthesized argument ends, so it cannot bind the symbols inside it.',
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
kind: 'rule',
|
|
464
|
+
id: 'N14',
|
|
465
|
+
short: 'An abbreviation-shaped word in prose is universal, or explained',
|
|
466
|
+
title: 'An abbreviation-shaped word in prose is universal, or it is explained.',
|
|
467
|
+
elements: ['prose'],
|
|
468
|
+
doc: 'notation.md#n14',
|
|
469
|
+
summary:
|
|
470
|
+
'An all-caps or dotted token in prose (`OLS`, `w.r.t.`) is on the shipped list (plus `allow`) or has an entry in `content/abbreviations/`.',
|
|
471
|
+
checks: [
|
|
472
|
+
check('abbreviation-unexplained', 'error', true, {
|
|
473
|
+
module: 'src/reference/gate-abbreviations.ts',
|
|
474
|
+
fn: 'gateAbbreviations',
|
|
475
|
+
}),
|
|
476
|
+
],
|
|
477
|
+
settings: baselineEdits('Abbreviations'),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
kind: 'rule',
|
|
481
|
+
id: 'N15',
|
|
482
|
+
short: 'A nested `(`, `[`, `{`, or `|` must use scalable sizing',
|
|
483
|
+
title:
|
|
484
|
+
'A nested visible delimiter — `(`, `[`, `\\{`, or `|` inside another one of them — must use the scalable `\\left`/`\\right` form, not the bare character.',
|
|
485
|
+
elements: ['math'],
|
|
486
|
+
doc: 'notation.md#n15',
|
|
487
|
+
summary:
|
|
488
|
+
'Inside another delimiter pair, write `\\left( … \\right)`, `\\left| … \\right|`; a bare nested `|` or `(` fails the build.',
|
|
489
|
+
checks: [check('nested-bare-delimiter', 'error', true, GLYPHS)],
|
|
490
|
+
locked:
|
|
491
|
+
'a bare `|` is both an opener and a closer; nested, the resolver cannot pair it, so the rule is a parsing precondition.',
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
kind: 'rule',
|
|
495
|
+
id: 'N17',
|
|
496
|
+
short: 'A delimiter’s shape is semantic; a nest is engine-sized, not hand-sized',
|
|
497
|
+
title:
|
|
498
|
+
'A delimiter’s shape carries its meaning and never changes to show nesting: a nest is sized by the engine — `\\left ... \\right` at every level, never `\\bigl`, `\\Bigl`, `\\biggl`, or `\\Biggl` inside it.',
|
|
499
|
+
elements: ['math'],
|
|
500
|
+
doc: 'notation.md#n17',
|
|
501
|
+
summary:
|
|
502
|
+
'Keep the shape each delimiter has for its meaning — an interval `[a, b)`, a norm `\\|·\\|`, a set `\\{…\\}`, a parenthesis for grouping — and never swap shapes (`( [ \\{ … \\} ] )`) to mark depth. Depth is size: write `\\left`/`\\right` at every level and let the engine size them, never `\\bigl…\\bigr` and friends inside a nest.',
|
|
503
|
+
checks: [
|
|
504
|
+
check('delimiter-manual-size', 'warning', true, {
|
|
505
|
+
module: 'src/reference/delimiter-size.ts',
|
|
506
|
+
fn: 'lintDelimiterSizes',
|
|
507
|
+
}),
|
|
508
|
+
],
|
|
509
|
+
unchecked:
|
|
510
|
+
'the shape half — `delimiter-manual-size` parses sizing syntax only, and never decides whether a bracket is semantic. A bare nested delimiter is rule N15’s error instead.',
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
kind: 'rule',
|
|
514
|
+
id: 'N18',
|
|
515
|
+
short: 'A multi-step derivation is one aligned chain; reasons follow `\\qquad`',
|
|
516
|
+
title:
|
|
517
|
+
'Write a multi-step derivation as one chain: one step per line, aligned at the relation sign, with any justification on the same line after `\\qquad`.',
|
|
518
|
+
elements: ['equation'],
|
|
519
|
+
doc: 'notation.md#n18',
|
|
520
|
+
summary:
|
|
521
|
+
'A derivation is one `aligned` block: each step on its own line, `&=` at the relation, the reason after `\\qquad` on the same line — not a run of separate display equations.',
|
|
522
|
+
checks: [],
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
kind: 'rule',
|
|
526
|
+
id: 'N19',
|
|
527
|
+
short: 'A notation entry states `units` from the vocabulary, or `dimensionless`',
|
|
528
|
+
title:
|
|
529
|
+
'A notation entry states its `units`, or declares itself `dimensionless`, using the units vocabulary.',
|
|
530
|
+
elements: ['notation'],
|
|
531
|
+
doc: 'notation.md#n19',
|
|
532
|
+
summary:
|
|
533
|
+
'Every notation entry has `units:` from `units-vocab.ts` (`m/s`, `s`, `kg`, …) or `dimensionless: true`. A token outside the vocabulary is a warning.',
|
|
534
|
+
checks: [
|
|
535
|
+
check('notation-units', 'warning', false, consistency('checkNotationUnits')),
|
|
536
|
+
check('units-vocab', 'warning', false, consistency('checkNotationUnits')),
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
kind: 'rule',
|
|
541
|
+
id: 'N20',
|
|
542
|
+
short: 'A notation entry\'s source citation carries a locator',
|
|
543
|
+
title: 'A notation entry’s source citation carries a locator.',
|
|
544
|
+
elements: ['notation', 'source'],
|
|
545
|
+
doc: 'notation.md#n20',
|
|
546
|
+
summary:
|
|
547
|
+
'Each item in a notation entry’s `sources` names the place in the source (`locator: "§3.2"`, `"p. 41"`), not just the source id.',
|
|
548
|
+
checks: [
|
|
549
|
+
check('notation-source-locator', 'warning', false, consistency('checkNotationSourceLocator')),
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
kind: 'rule',
|
|
554
|
+
id: 'N21',
|
|
555
|
+
short: 'A gloss is named by a short noun phrase, not a sentence',
|
|
556
|
+
title: 'A gloss is named by a short noun phrase, not described by a sentence.',
|
|
557
|
+
elements: ['notation'],
|
|
558
|
+
doc: 'notation.md#n21',
|
|
559
|
+
summary:
|
|
560
|
+
'A gloss `label` names the symbol in a few words (“launch angle”); a clause, sentence punctuation, or a definition belongs in a full notation entry instead.',
|
|
561
|
+
checks: [check('gloss-name-shape', 'warning', false, consistency('checkGlossNameShape'))],
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
kind: 'rule',
|
|
565
|
+
id: 'N22',
|
|
566
|
+
short: 'An accent or mark is an operator: universal, explained, or a whole form',
|
|
567
|
+
title:
|
|
568
|
+
'An accent or mark on a symbol is an operator: universal, explained, or part of the symbol’s own entry.',
|
|
569
|
+
elements: ['math', 'notation'],
|
|
570
|
+
doc: 'notation.md#n22',
|
|
571
|
+
summary:
|
|
572
|
+
'A decoration (`\\hat`, `\\bar`, `\\overline`, `\'`, `^*`, `^\\dagger`, …) is on the course’s `allow` list (shipped empty), explained by an operator entry (`latex: \\hat{}`), or the decorated form has its own entry (`latex: \\hat{\\beta}`), which wins for that form. A page may suspend a decoration in `notation.suspend`, with a `reason`; every decorated form of it there then needs its own entry.',
|
|
573
|
+
checks: [
|
|
574
|
+
check('accent-explained', 'warning', false, consistency('checkAccentExplained')),
|
|
575
|
+
check('suspended-operator-unresolved', 'error', true, GATE_MATH),
|
|
576
|
+
check('operator-suspension-reason', 'warning', false, consistency('checkOperatorSuspension')),
|
|
577
|
+
check('operator-suspended', 'info', false, consistency('checkOperatorSuspension')),
|
|
578
|
+
check('operator-shadowed', 'info', false, consistency('checkOperatorSuspension')),
|
|
579
|
+
],
|
|
580
|
+
settings: baselineEdits('Decorations'),
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
// ---- V — visualizations -----------------------------------------------------
|
|
584
|
+
{
|
|
585
|
+
kind: 'rule',
|
|
586
|
+
id: 'V1',
|
|
587
|
+
short: 'Graphs and flowcharts use the `Diagram` component',
|
|
588
|
+
title: 'Graphs and flowcharts use the `Diagram` component.',
|
|
589
|
+
elements: ['diagram'],
|
|
590
|
+
doc: 'visualizations.md#v1',
|
|
591
|
+
summary:
|
|
592
|
+
'A node-and-edge picture — a dependency graph, a flowchart — is a `<Diagram id="…">` with its nodes and formulas declared, never a hand-drawn SVG or an image.',
|
|
593
|
+
checks: [],
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
kind: 'rule',
|
|
597
|
+
id: 'V2',
|
|
598
|
+
short: 'Static charts: SVG → D3 → Observable Plot, chosen per figure',
|
|
599
|
+
title:
|
|
600
|
+
'Static charts: hand-rolled SVG first, then D3, then Observable Plot — chosen per figure.',
|
|
601
|
+
elements: ['plot'],
|
|
602
|
+
doc: 'visualizations.md#v2',
|
|
603
|
+
summary:
|
|
604
|
+
'For a static chart pick the lightest tool that draws it: inline SVG, then D3, then Observable Plot. State the choice in the figure’s source comment.',
|
|
605
|
+
checks: [],
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
kind: 'rule',
|
|
609
|
+
id: 'V3',
|
|
610
|
+
short: 'Interactive figures: a hydrated island + the `renderLabMath` seam',
|
|
611
|
+
title:
|
|
612
|
+
'Interactive figures: a hydrated island and the `renderLabMath` seam for equations.',
|
|
613
|
+
elements: ['plot'],
|
|
614
|
+
doc: 'visualizations.md#v3',
|
|
615
|
+
summary:
|
|
616
|
+
'An interactive figure is an island under `content/labs/` inside a `<Figure>`, renders its equations through `renderLabMath`, and labels its controls; the figure’s caption is its name, so the lab writes no accessible name or text alternative of its own.',
|
|
617
|
+
checks: [],
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
kind: 'rule',
|
|
621
|
+
id: 'V4',
|
|
622
|
+
short: 'Randomised figures expose an adjustable, deterministic seed',
|
|
623
|
+
title: 'A randomised figure exposes an adjustable, deterministic seed.',
|
|
624
|
+
elements: ['plot'],
|
|
625
|
+
doc: 'visualizations.md#v4',
|
|
626
|
+
summary:
|
|
627
|
+
'Randomness in a figure comes from a seed the reader can see and change; the same seed always draws the same picture.',
|
|
628
|
+
checks: [],
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
kind: 'rule',
|
|
632
|
+
id: 'V5',
|
|
633
|
+
short: 'Axis ranges span the first 10 seeds at the current control values',
|
|
634
|
+
title:
|
|
635
|
+
'Axis ranges span the first 10 seeds, evaluated at the current values of every other control.',
|
|
636
|
+
elements: ['plot'],
|
|
637
|
+
doc: 'visualizations.md#v5',
|
|
638
|
+
summary:
|
|
639
|
+
'Fix each axis range so the drawings for seeds 1…`seeds` all fit at the current control values; the axes do not jump when the seed changes.',
|
|
640
|
+
checks: [],
|
|
641
|
+
settings: [
|
|
642
|
+
{
|
|
643
|
+
key: 'seeds',
|
|
644
|
+
title: 'Seeds spanned',
|
|
645
|
+
description: 'How many leading seeds an axis range must accommodate.',
|
|
646
|
+
schema: { type: 'integer', minimum: 1, default: 10 },
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
kind: 'rule',
|
|
652
|
+
id: 'V6',
|
|
653
|
+
short: 'Every quantitative axis shows at least three labelled values',
|
|
654
|
+
title: 'Every quantitative axis of a chart shows at least three labelled values.',
|
|
655
|
+
elements: ['plot'],
|
|
656
|
+
doc: 'visualizations.md#v6',
|
|
657
|
+
summary:
|
|
658
|
+
'Each numeric axis carries at least `minLabels` tick labels, so a reader can read a value off the chart.',
|
|
659
|
+
checks: [],
|
|
660
|
+
settings: [
|
|
661
|
+
{
|
|
662
|
+
key: 'minLabels',
|
|
663
|
+
title: 'Minimum axis labels',
|
|
664
|
+
description: 'Labelled tick values a quantitative axis must show.',
|
|
665
|
+
schema: { type: 'integer', minimum: 2, default: 3 },
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
},
|
|
669
|
+
|
|
670
|
+
// ---- C — versioning ---------------------------------------------------------
|
|
671
|
+
{
|
|
672
|
+
kind: 'rule',
|
|
673
|
+
id: 'C1',
|
|
674
|
+
short: 'Every content change lands with a `content/CHANGELOG.md` entry',
|
|
675
|
+
title:
|
|
676
|
+
'Every content change lands with a `content/CHANGELOG.md` entry under `## [Unreleased]`.',
|
|
677
|
+
elements: ['changelog'],
|
|
678
|
+
doc: 'versioning.md#c1',
|
|
679
|
+
summary:
|
|
680
|
+
'Any edit under `content/` adds a bullet to `content/CHANGELOG.md` under `## [Unreleased]` in the same commit.',
|
|
681
|
+
checks: [],
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
kind: 'rule',
|
|
685
|
+
id: 'C2',
|
|
686
|
+
short: 'The knowledge version is semver over learning-facing content',
|
|
687
|
+
title: 'The knowledge version is semver over learning-facing content.',
|
|
688
|
+
elements: ['changelog'],
|
|
689
|
+
doc: 'versioning.md#c2',
|
|
690
|
+
summary:
|
|
691
|
+
'Bump `about.version` MAJOR when a lesson, competency, notation key or stated outcome is removed or changed; MINOR for something new; PATCH for a fix or clarification.',
|
|
692
|
+
checks: [],
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
kind: 'rule',
|
|
696
|
+
id: 'C3',
|
|
697
|
+
short: '`about.version` equals the newest changelog heading; a release moves both',
|
|
698
|
+
title:
|
|
699
|
+
'`about.version` equals the newest `## [x.y.z]` heading in `content/CHANGELOG.md`, and a release moves `## [Unreleased]` and bumps `about.version` in one commit.',
|
|
700
|
+
elements: ['changelog'],
|
|
701
|
+
doc: 'versioning.md#c3',
|
|
702
|
+
summary:
|
|
703
|
+
'The version in `course.config.ts` and the newest dated heading in `content/CHANGELOG.md` are the same string. On a release, rename `## [Unreleased]` to `## [x.y.z] - <date>`, open a fresh empty `## [Unreleased]` above it, and set `about.version` to that `x.y.z` — in one commit. Between releases `about.version` stays at the last released value.',
|
|
704
|
+
checks: [
|
|
705
|
+
check('knowledge-version-mismatch', 'warning', false, {
|
|
706
|
+
module: 'src/compiler/knowledge-version.ts',
|
|
707
|
+
fn: 'resolveKnowledgeVersion',
|
|
708
|
+
}),
|
|
709
|
+
check('knowledge-version-invalid', 'warning', false, {
|
|
710
|
+
module: 'src/compiler/knowledge-version.ts',
|
|
711
|
+
fn: 'resolveKnowledgeVersion',
|
|
712
|
+
}),
|
|
713
|
+
check('knowledge-changelog-missing', 'warning', false, {
|
|
714
|
+
module: 'src/compiler/knowledge-version.ts',
|
|
715
|
+
fn: 'resolveKnowledgeVersion',
|
|
716
|
+
}),
|
|
717
|
+
],
|
|
718
|
+
unchecked:
|
|
719
|
+
'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.',
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
// ---- S — structure ----------------------------------------------------------
|
|
723
|
+
{
|
|
724
|
+
kind: 'rule',
|
|
725
|
+
id: 'S1',
|
|
726
|
+
short: 'No heading, short caption or short-note title over 65 characters',
|
|
727
|
+
title:
|
|
728
|
+
'No heading, no short caption (`short`, an equation’s `\\caption{}`) and no short-note title runs past 65 characters.',
|
|
729
|
+
elements: ['heading'],
|
|
730
|
+
doc: 'structure-rules.md#s1',
|
|
731
|
+
summary:
|
|
732
|
+
'A Markdown heading, the `short` caption of a `Figure`, `Diagram`, `Table`, `Memo` or `CompactExamples`, an equation’s `\\caption{}` and a `ShortNote` `title` are short noun phrases of at most `maxLength` characters; the claim goes in the prose or the long `caption`, which is never measured.',
|
|
733
|
+
checks: [
|
|
734
|
+
check('title-length', 'error', true, {
|
|
735
|
+
module: 'src/reference/gate-heading-length.ts',
|
|
736
|
+
fn: 'gateHeadingLength',
|
|
737
|
+
}),
|
|
738
|
+
],
|
|
739
|
+
unchecked: 'a lesson’s `frontmatter.title` — the gate scans the body only.',
|
|
740
|
+
settings: [
|
|
741
|
+
{
|
|
742
|
+
key: 'maxLength',
|
|
743
|
+
title: 'Maximum length',
|
|
744
|
+
description: 'Characters a heading or short caption may run to.',
|
|
745
|
+
schema: { type: 'integer', minimum: 20, default: 65 },
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
kind: 'rule',
|
|
751
|
+
id: 'S2',
|
|
752
|
+
short: 'A part page carries a `<PartBridge>` (earlier parts → this one) and a `<PartScope>` paragraph',
|
|
753
|
+
title:
|
|
754
|
+
'A part’s overview page carries two authored paragraphs: a `<PartBridge>` on how the earlier parts lead into this one, then a `<PartScope>` on what this part covers.',
|
|
755
|
+
elements: ['part-page'],
|
|
756
|
+
doc: 'structure-rules.md#s2',
|
|
757
|
+
summary:
|
|
758
|
+
'A `content/docs/<dir>/index.mdx` has one `<PartBridge>` paragraph (optional on the first part) then one `<PartScope>` paragraph, each inside the word range, before `<PartOverview />`. Keep a blank line inside each tag.',
|
|
759
|
+
checks: [
|
|
760
|
+
check('part-page-paragraph', 'error', false, {
|
|
761
|
+
module: 'src/reference/gate-part-pages.ts',
|
|
762
|
+
fn: 'gatePartPages',
|
|
763
|
+
}),
|
|
764
|
+
],
|
|
765
|
+
unchecked:
|
|
766
|
+
'what each paragraph says — the gate cannot tell a bridge that links the parts from one that does not.',
|
|
767
|
+
settings: [
|
|
768
|
+
{
|
|
769
|
+
key: 'bridgeMinWords',
|
|
770
|
+
title: 'Bridge: minimum words',
|
|
771
|
+
description: 'Fewest words the `<PartBridge>` paragraph may have.',
|
|
772
|
+
schema: { type: 'integer', minimum: 1, default: 30 },
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
key: 'bridgeMaxWords',
|
|
776
|
+
title: 'Bridge: maximum words',
|
|
777
|
+
description: 'Most words the `<PartBridge>` paragraph may have.',
|
|
778
|
+
schema: { type: 'integer', minimum: 1, default: 150 },
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
key: 'scopeMinWords',
|
|
782
|
+
title: 'Scope: minimum words',
|
|
783
|
+
description: 'Fewest words the `<PartScope>` paragraph may have.',
|
|
784
|
+
schema: { type: 'integer', minimum: 1, default: 30 },
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
key: 'scopeMaxWords',
|
|
788
|
+
title: 'Scope: maximum words',
|
|
789
|
+
description: 'Most words the `<PartScope>` paragraph may have.',
|
|
790
|
+
schema: { type: 'integer', minimum: 1, default: 150 },
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
kind: 'rule',
|
|
796
|
+
id: 'S3',
|
|
797
|
+
short: 'A table is a `<Table id="…">`, never a raw Markdown/HTML table',
|
|
798
|
+
title: 'A table is a `<Table id="…">`, never a raw Markdown or HTML table.',
|
|
799
|
+
elements: ['table'],
|
|
800
|
+
doc: 'structure-rules.md#s3',
|
|
801
|
+
summary:
|
|
802
|
+
'Wrap every pipe table or `<table>` in `<Table id="…" caption="…">` so it gets a number and `[[tab-…]]` can reach it.',
|
|
803
|
+
checks: [
|
|
804
|
+
check('raw-table-markdown', 'warning', false, {
|
|
805
|
+
module: 'src/reference/raw-floats-validate.ts',
|
|
806
|
+
fn: 'validateRawFloats',
|
|
807
|
+
}),
|
|
808
|
+
check('raw-table-html', 'warning', false, {
|
|
809
|
+
module: 'src/reference/raw-floats-validate.ts',
|
|
810
|
+
fn: 'validateRawFloats',
|
|
811
|
+
}),
|
|
812
|
+
],
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
kind: 'rule',
|
|
816
|
+
id: 'S4',
|
|
817
|
+
short: 'A `<Diagram>` carries an `id` and sits where numbering can see it',
|
|
818
|
+
title: 'A `<Diagram>` carries an `id` and sits where diagram numbering can see it.',
|
|
819
|
+
elements: ['diagram'],
|
|
820
|
+
doc: 'structure-rules.md#s4',
|
|
821
|
+
summary:
|
|
822
|
+
'Give every `<Diagram>` an `id`, and place it at the top level or inside `<Memo>`/`<ShortNote>`/`<Table>`/`<Figure>` — a diagram nested elsewhere (e.g. in `<CompactExample>`) is never numbered.',
|
|
823
|
+
checks: [
|
|
824
|
+
check('diagram-unkeyed', 'warning', false, {
|
|
825
|
+
module: 'src/reference/raw-floats-validate.ts',
|
|
826
|
+
fn: 'validateRawFloats',
|
|
827
|
+
}),
|
|
828
|
+
check('diagram-nested', 'warning', false, {
|
|
829
|
+
module: 'src/reference/raw-floats-validate.ts',
|
|
830
|
+
fn: 'validateRawFloats',
|
|
831
|
+
}),
|
|
832
|
+
],
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
kind: 'rule',
|
|
836
|
+
id: 'S5',
|
|
837
|
+
short: 'A sign / perspective convention is declared once and cited elsewhere',
|
|
838
|
+
title: 'A sign or perspective convention is declared once and cited everywhere else.',
|
|
839
|
+
elements: ['prose'],
|
|
840
|
+
doc: 'structure-rules.md#s5',
|
|
841
|
+
summary:
|
|
842
|
+
'Prose that states a sign / cash-flow-perspective convention resolves it to the single declaration — the `conventions.signConventionKey` notation entry (`[[key]]`) or the `conventions.cashFlowPerspectiveCompetency` competency — rather than restating it.',
|
|
843
|
+
checks: [
|
|
844
|
+
check(
|
|
845
|
+
'convention-single-definition',
|
|
846
|
+
'warning',
|
|
847
|
+
false,
|
|
848
|
+
consistency('checkConventionSingleDefinition'),
|
|
849
|
+
),
|
|
850
|
+
],
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
kind: 'rule',
|
|
854
|
+
id: 'S6',
|
|
855
|
+
short: 'Headings descend one level at a time, from `##`',
|
|
856
|
+
title:
|
|
857
|
+
'Headings descend one level at a time: a lesson body starts at `##` and never skips a level on the way down.',
|
|
858
|
+
elements: ['heading'],
|
|
859
|
+
doc: 'structure-rules.md#s6',
|
|
860
|
+
summary:
|
|
861
|
+
'The page title is the lesson’s only `#`, so the body’s first heading is `##`, and each heading is at most one level deeper than the one before it (`##` → `###`, never `##` → `####`); going back up any number of levels is fine. A knowledge check’s and a worked-example set’s heading count as a `##`.',
|
|
862
|
+
checks: [
|
|
863
|
+
check('heading-skip', 'error', true, {
|
|
864
|
+
module: 'src/reference/gate-heading-structure.ts',
|
|
865
|
+
fn: 'gateHeadingStructure',
|
|
866
|
+
}),
|
|
867
|
+
],
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
kind: 'rule',
|
|
871
|
+
id: 'S7',
|
|
872
|
+
short: 'A heading is followed by body content, never by another heading',
|
|
873
|
+
title: 'A heading is followed by body content, never directly by another heading.',
|
|
874
|
+
elements: ['heading'],
|
|
875
|
+
doc: 'structure-rules.md#s7',
|
|
876
|
+
summary:
|
|
877
|
+
'Write body content — at least a sentence on what the section is about — between a heading and the next one, whatever their levels. `<AssessmentSet />` and `<CompactExamples>` draw an `<h2>` of their own, so no heading goes directly above either.',
|
|
878
|
+
checks: [
|
|
879
|
+
check('heading-adjacent', 'error', true, {
|
|
880
|
+
module: 'src/reference/gate-heading-structure.ts',
|
|
881
|
+
fn: 'gateHeadingStructure',
|
|
882
|
+
}),
|
|
883
|
+
],
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
kind: 'rule',
|
|
887
|
+
id: 'S8',
|
|
888
|
+
short: 'Prefer `<Memo>` over a Starlight aside when the content needs an anchor, a caption or collapsing',
|
|
889
|
+
title:
|
|
890
|
+
'Use a `<Memo>`, not a Starlight aside, for content that deserves an anchor, a caption, or collapsing.',
|
|
891
|
+
elements: ['prose'],
|
|
892
|
+
doc: 'structure-rules.md#s8',
|
|
893
|
+
summary:
|
|
894
|
+
'A `:::note`/`:::tip`/`:::caution`/`:::danger` (or `<Aside>`) is a short callout read in passing: no number, no link, never folded. Anything a reader may want to cite, skip or come back to is a `<Memo id="…">`.',
|
|
895
|
+
checks: [],
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
kind: 'rule',
|
|
899
|
+
id: 'S9',
|
|
900
|
+
short: 'A float’s short caption names it; the long caption goes on from that name',
|
|
901
|
+
title:
|
|
902
|
+
'A float’s short caption names it, and its long caption goes on from that name instead of restating it; a figure holds no running text, and a lab’s instructions go in its tools.',
|
|
903
|
+
elements: ['plot', 'table', 'diagram'],
|
|
904
|
+
doc: 'structure-rules.md#s9',
|
|
905
|
+
summary:
|
|
906
|
+
'Give each figure, table and diagram a `short` caption that names it: it opens the caption line in bold, and names the float in "Hidden here", a copied link and the "Figures and tables" page. The long `caption` goes on from that name to say what the float shows — its subject, its marks and symbols, the case shown — and at most the one thing to notice. The argument drawn from a figure is lesson text after it, and a lab’s instructions are the first line of its `tools`.',
|
|
907
|
+
checks: [],
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
// ---- P — prose --------------------------------------------------------------
|
|
911
|
+
{
|
|
912
|
+
kind: 'rule',
|
|
913
|
+
id: 'P1',
|
|
914
|
+
short: '"or" never introduces a second name for the same thing',
|
|
915
|
+
title: 'Never use "or" to give a thing a second name.',
|
|
916
|
+
elements: ['prose'],
|
|
917
|
+
doc: 'prose.md#p1',
|
|
918
|
+
summary:
|
|
919
|
+
'“The residual, or error, is …” introduces a synonym; pick one name and use it. “or” joins alternatives, not names.',
|
|
920
|
+
checks: [],
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
kind: 'rule',
|
|
924
|
+
id: 'P2',
|
|
925
|
+
short: 'One term for each concept, repeated — no synonyms for variety',
|
|
926
|
+
title: 'Use one term for each concept, and repeat it.',
|
|
927
|
+
elements: ['prose'],
|
|
928
|
+
doc: 'prose.md#p2',
|
|
929
|
+
summary: 'Say “residual” every time; never rotate synonyms for variety.',
|
|
930
|
+
checks: [],
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
kind: 'rule',
|
|
934
|
+
id: 'P3',
|
|
935
|
+
short: 'One meaning for each term; no technical term in its everyday sense',
|
|
936
|
+
title: 'Give each term one meaning, and keep it.',
|
|
937
|
+
elements: ['prose'],
|
|
938
|
+
doc: 'prose.md#p3',
|
|
939
|
+
summary:
|
|
940
|
+
'A technical term keeps its technical meaning on the page; do not also use it in its everyday sense (“significant”, “normal”).',
|
|
941
|
+
checks: [],
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
kind: 'rule',
|
|
945
|
+
id: 'P4',
|
|
946
|
+
short: 'Use the established term; define it where the argument first needs it',
|
|
947
|
+
title: 'Use the established term, and define it where the argument first needs it.',
|
|
948
|
+
elements: ['prose'],
|
|
949
|
+
doc: 'prose.md#p4',
|
|
950
|
+
summary:
|
|
951
|
+
'Prefer the field’s standard term over a coined one, and define it at first use — not earlier in a glossary paragraph, not later.',
|
|
952
|
+
checks: [],
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
kind: 'rule',
|
|
956
|
+
id: 'P5',
|
|
957
|
+
short: 'Main text is literal: no idioms, metaphors, buzzwords, entertainment',
|
|
958
|
+
title:
|
|
959
|
+
'Write the main text literally: no idioms, metaphors, colloquialisms, buzzwords, or entertainment.',
|
|
960
|
+
elements: ['prose'],
|
|
961
|
+
doc: 'prose.md#p5',
|
|
962
|
+
summary:
|
|
963
|
+
'Main text states facts and steps plainly; no “let’s dive in”, no metaphor, no joke. An aside may be lighter.',
|
|
964
|
+
checks: [],
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
kind: 'rule',
|
|
968
|
+
id: 'P6',
|
|
969
|
+
short: 'Industry jargon only in an optional "Industry jargon" aside',
|
|
970
|
+
title:
|
|
971
|
+
'Keep industry jargon out of the main text. Put jargon that the reader must recognize in an optional "Industry jargon" aside.',
|
|
972
|
+
elements: ['prose'],
|
|
973
|
+
doc: 'prose.md#p6',
|
|
974
|
+
summary:
|
|
975
|
+
'Use the textbook term in the argument; list the trade’s words for it in an “Industry jargon” aside if the reader will meet them.',
|
|
976
|
+
checks: [],
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
kind: 'rule',
|
|
980
|
+
id: 'P7',
|
|
981
|
+
short: 'One claim per sentence; every logical link stated',
|
|
982
|
+
title: 'Write one claim per sentence, and state every logical link.',
|
|
983
|
+
elements: ['prose'],
|
|
984
|
+
doc: 'prose.md#p7',
|
|
985
|
+
summary:
|
|
986
|
+
'One assertion per sentence; “because”, “so”, “therefore” are written out, never left for the reader to infer.',
|
|
987
|
+
checks: [],
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
kind: 'rule',
|
|
991
|
+
id: 'P8',
|
|
992
|
+
short: 'Words for a relation match the math exactly ("proportional to")',
|
|
993
|
+
title: 'Words that state a relation say exactly what the mathematics says.',
|
|
994
|
+
elements: ['prose', 'math'],
|
|
995
|
+
doc: 'prose.md#p8',
|
|
996
|
+
summary:
|
|
997
|
+
'“Proportional to” means `y = kx`; “grows with” means monotone; “depends on” means a function of. Match the words to the formula.',
|
|
998
|
+
checks: [],
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
kind: 'rule',
|
|
1002
|
+
id: 'P9',
|
|
1003
|
+
short: 'A display equation\'s lead-in says what it gives and where it comes from',
|
|
1004
|
+
title:
|
|
1005
|
+
'Introduce a display equation with a sentence that says what it gives and where it comes from.',
|
|
1006
|
+
elements: ['prose', 'equation'],
|
|
1007
|
+
doc: 'prose.md#p9',
|
|
1008
|
+
summary:
|
|
1009
|
+
'The sentence before a display equation names the quantity it yields and the step or law it follows from — not “we have:”.',
|
|
1010
|
+
checks: [],
|
|
1011
|
+
},
|
|
1012
|
+
];
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* A rule that is no longer in force. It keeps its ID forever — an old commit
|
|
1016
|
+
* message, review comment or waiver naming `N3` must stay unambiguous — but it
|
|
1017
|
+
* is deliberately *not* a {@link RuleEntry}: nothing iterates it as a rule, it
|
|
1018
|
+
* has no page in `docs/authoring/`, it is absent from the settings schema and
|
|
1019
|
+
* from `build/rules-in-effect.md`, and an agent never reads it while drafting.
|
|
1020
|
+
* The one place it is written out is the generated table in
|
|
1021
|
+
* `docs/authoring/retired.md`, which is where a migration guide links.
|
|
1022
|
+
*/
|
|
1023
|
+
export interface RetiredRule {
|
|
1024
|
+
readonly id: RuleId;
|
|
1025
|
+
/** The sentence the rule had, so an old reference to it still resolves. */
|
|
1026
|
+
readonly title: string;
|
|
1027
|
+
/** `YYYY-MM-DD`. */
|
|
1028
|
+
readonly date: string;
|
|
1029
|
+
/** The rule it was folded into, when it was folded into one. */
|
|
1030
|
+
readonly into?: RuleId;
|
|
1031
|
+
/** Why, and what changed for an author — one or two sentences. */
|
|
1032
|
+
readonly why: string;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const RETIRED: readonly RetiredRule[] = [
|
|
1036
|
+
{
|
|
1037
|
+
id: 'N1',
|
|
1038
|
+
title: 'The normal distribution is `\\mathcal{N}`.',
|
|
1039
|
+
date: '2026-09-26',
|
|
1040
|
+
into: 'N2',
|
|
1041
|
+
why: 'One spelling fixed to one meaning is one rule; `\\mathcal{N}(\\mu, \\sigma^2)` is a line in N2’s `reserved` table, which a course can edit.',
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
id: 'N3',
|
|
1045
|
+
title: 'Multi-letter roman labels use `\\mathrm{…}`.',
|
|
1046
|
+
date: '2026-09-26',
|
|
1047
|
+
into: 'N10',
|
|
1048
|
+
why: 'Writing a label as `\\mathrm{…}` and explaining it are two halves of one rule, and N10 is where both are checked.',
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
id: 'N6',
|
|
1052
|
+
title: 'Probability is `\\mathbb{P}` — double-struck `P`.',
|
|
1053
|
+
date: '2026-09-26',
|
|
1054
|
+
into: 'N2',
|
|
1055
|
+
why: '`\\mathbb{P}` is a line in N2’s `reserved` table.',
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
id: 'N7',
|
|
1059
|
+
title: 'The differential `d` is upright `\\mathrm{d}`.',
|
|
1060
|
+
date: '2026-09-26',
|
|
1061
|
+
into: 'N2',
|
|
1062
|
+
why: '`\\mathrm{d}` is a line in N2’s `reserved` table.',
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
id: 'N16',
|
|
1066
|
+
title:
|
|
1067
|
+
'In AI-generated mathematical content, alternate delimiter types for nested grouping when their shape is stylistic; preserve every conventional or semantic delimiter shape.',
|
|
1068
|
+
date: '2026-09-26',
|
|
1069
|
+
into: 'N17',
|
|
1070
|
+
why: 'Folded into N17 with the preference dropped: a delimiter’s shape carries its meaning, so depth is shown by size — which this engine handles automatically — not by alternating shapes. The rule also scoped itself to “AI-generated” content, which is not a property of the page.',
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
id: 'C4',
|
|
1074
|
+
title:
|
|
1075
|
+
'On a release, move `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD` and bump `about.version` in the same commit.',
|
|
1076
|
+
date: '2026-09-26',
|
|
1077
|
+
into: 'C3',
|
|
1078
|
+
why: 'The release procedure and the invariant it keeps are one rule, and `knowledge-changelog-missing` (owned by C3) was already the check that fired on a half-finished release.',
|
|
1079
|
+
},
|
|
1080
|
+
];
|
|
1081
|
+
|
|
1082
|
+
const INVARIANTS: readonly InvariantEntry[] = [
|
|
1083
|
+
{
|
|
1084
|
+
kind: 'invariant',
|
|
1085
|
+
id: 'notation-references',
|
|
1086
|
+
title: 'Notation keys are well-formed, unique, defined, and acyclic.',
|
|
1087
|
+
elements: ['notation', 'math'],
|
|
1088
|
+
doc: 'components.md#reference-systems',
|
|
1089
|
+
summary:
|
|
1090
|
+
'A `[[key]]` or `\\explain{key}{…}` names an existing entry; a key is kebab-case; a shared entry is defined once; formula references never form a cycle.',
|
|
1091
|
+
checks: [
|
|
1092
|
+
check('undefined-reference', 'error', false, REGISTRY),
|
|
1093
|
+
check('invalid-key', 'error', false, REGISTRY, GLYPHS),
|
|
1094
|
+
check('duplicate-key', 'error', false, GLYPHS),
|
|
1095
|
+
check('duplicate-page', 'error', false, REGISTRY),
|
|
1096
|
+
check('duplicate-definition', 'error', false, REGISTRY),
|
|
1097
|
+
check('conflicting-definition', 'error', false, REGISTRY),
|
|
1098
|
+
check('reference-cycle', 'error', false, REGISTRY),
|
|
1099
|
+
check('gloss-collides-with-card', 'error', false, REGISTRY),
|
|
1100
|
+
check('unknown-explicit-key', 'error', true, GLYPHS, {
|
|
1101
|
+
module: 'src/reference/gate-math.ts',
|
|
1102
|
+
fn: 'reportUnknownScopeKeys',
|
|
1103
|
+
}),
|
|
1104
|
+
check('malformed-explicit-binding', 'error', true, GLYPHS),
|
|
1105
|
+
check('unused-definition', 'info', false, REGISTRY),
|
|
1106
|
+
],
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
kind: 'invariant',
|
|
1110
|
+
id: 'math-syntax',
|
|
1111
|
+
title: 'Every math span parses and resolves unambiguously.',
|
|
1112
|
+
elements: ['math'],
|
|
1113
|
+
doc: 'notation.md',
|
|
1114
|
+
summary:
|
|
1115
|
+
'KaTeX must parse the span, the glyph table must be well-formed, and a span must not resolve to two entries at once.',
|
|
1116
|
+
checks: [
|
|
1117
|
+
check('invalid-latex', 'error', true, GLYPHS),
|
|
1118
|
+
check('invalid-latex-input', 'error', true, GLYPHS),
|
|
1119
|
+
check('invalid-notation', 'error', false, GLYPHS),
|
|
1120
|
+
check('invalid-definitions', 'error', false, GLYPHS),
|
|
1121
|
+
check('unsupported-katex-version', 'error', false, GLYPHS),
|
|
1122
|
+
check('ambiguous-span', 'error', true, GLYPHS),
|
|
1123
|
+
check('ambiguous-base-fallback', 'error', true, GLYPHS),
|
|
1124
|
+
check('ambiguous-canonical-notation', 'error', false, GLYPHS),
|
|
1125
|
+
check('math-compile-error', 'error', true, GATE_MATH),
|
|
1126
|
+
],
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
kind: 'invariant',
|
|
1130
|
+
id: 'keyed-floats',
|
|
1131
|
+
title: 'A keyed float’s `id` is well-formed and unique, it is named by its caption pair, and every `[[…]]` to it or to a section resolves.',
|
|
1132
|
+
elements: ['equation', 'table', 'plot', 'diagram', 'assessment', 'heading'],
|
|
1133
|
+
doc: 'structure.md#the-invariant-and-why-it-is-build-enforced',
|
|
1134
|
+
summary:
|
|
1135
|
+
'`\\label{eq:key}`, `<Table id>`, `<Figure id>`, `<Diagram id>`, `<CompactExamples id>` and a knowledge check’s id are kebab-case and unique on the page; `[[eq-…]]`, `[[tab-…]]`, `[[fig-…]]`, `[[dia-…]]`, `[[ex-…]]`, `[[check-…]]` point at one that exists, and `[[sec-…]]` at a top-level heading by its anchor. A float is named by `caption` and an optional `short`: `<Figure title>` and `<Diagram label>` no longer exist. A `<CompactExamples>` needs both an `id` and a `short` title. A `<Fold>` is named by its `label` or by a numbered float inside it.',
|
|
1136
|
+
checks: keyedFloatChecks() as [RuleCheck, ...RuleCheck[]],
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
kind: 'invariant',
|
|
1140
|
+
id: 'course-settings',
|
|
1141
|
+
title: 'The course settings name real rules and current keys.',
|
|
1142
|
+
elements: ['changelog'],
|
|
1143
|
+
doc: 'README.md#amending',
|
|
1144
|
+
summary:
|
|
1145
|
+
'A `rules.<ID>` block names a live rule; a deprecated top-level key (`operators`, `romanLabels`, `abbreviations`) still works but warns until it is moved under `rules`.',
|
|
1146
|
+
checks: [
|
|
1147
|
+
check('config-deprecated-key', 'warning', false, {
|
|
1148
|
+
module: 'src/rules/settings.ts',
|
|
1149
|
+
fn: 'resolveRules',
|
|
1150
|
+
}),
|
|
1151
|
+
check('config-retired-rule', 'warning', false, {
|
|
1152
|
+
module: 'src/rules/settings.ts',
|
|
1153
|
+
fn: 'resolveRules',
|
|
1154
|
+
}),
|
|
1155
|
+
],
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
kind: 'invariant',
|
|
1159
|
+
id: 'waivers',
|
|
1160
|
+
title: 'A per-place waiver names a waivable rule, gives a reason, and covers something.',
|
|
1161
|
+
elements: ['prose'],
|
|
1162
|
+
doc: 'README.md#waiving-a-rule-at-one-place',
|
|
1163
|
+
summary:
|
|
1164
|
+
'`{/* explico-allow <ID>[, <ID>]: <reason> */}` on its own line waives the named rules for the next block. The rule must be non-locked and waivable inline; a script-rule waiver that silences nothing is reported so it gets removed.',
|
|
1165
|
+
checks: [
|
|
1166
|
+
check('waiver-malformed', 'error', true, { module: 'src/rules/waivers.ts', fn: 'collectWaivers' }),
|
|
1167
|
+
check('waiver-invalid', 'error', true, { module: 'src/rules/waivers.ts', fn: 'collectWaivers' }),
|
|
1168
|
+
check('waiver-unused', 'warning', true, { module: 'src/rules/waivers.ts', fn: 'applyWaivers' }),
|
|
1169
|
+
],
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
kind: 'invariant',
|
|
1173
|
+
id: 'curriculum',
|
|
1174
|
+
title: 'The curriculum graph is well-formed.',
|
|
1175
|
+
elements: ['changelog'],
|
|
1176
|
+
doc: 'structure.md#the-names',
|
|
1177
|
+
summary:
|
|
1178
|
+
'Competency, lesson, track and assessment ids are valid and unique; prerequisites and `teaches`/`requires` name real entries with no cycles; tracks order lessons after their prerequisites; notation alignment names real competencies and lessons.',
|
|
1179
|
+
checks: [
|
|
1180
|
+
...(
|
|
1181
|
+
[
|
|
1182
|
+
'invalid-id',
|
|
1183
|
+
'duplicate-id',
|
|
1184
|
+
'unknown-reference',
|
|
1185
|
+
'duplicate-reference',
|
|
1186
|
+
'self-prerequisite',
|
|
1187
|
+
'cycle',
|
|
1188
|
+
'lesson-order',
|
|
1189
|
+
'track-order',
|
|
1190
|
+
'assessment-coverage',
|
|
1191
|
+
'invalid-assessment',
|
|
1192
|
+
'review-state',
|
|
1193
|
+
'teaches-support',
|
|
1194
|
+
] as const
|
|
1195
|
+
).map((code) =>
|
|
1196
|
+
check(code, 'error', false, {
|
|
1197
|
+
module: 'src/curriculum/validation.ts',
|
|
1198
|
+
fn: 'validateCurriculum',
|
|
1199
|
+
}),
|
|
1200
|
+
),
|
|
1201
|
+
...(
|
|
1202
|
+
[
|
|
1203
|
+
'alignment-unknown-competency',
|
|
1204
|
+
'alignment-unknown-lesson',
|
|
1205
|
+
'alignment-introduction',
|
|
1206
|
+
'alignment-unavailable',
|
|
1207
|
+
'alignment-review-state',
|
|
1208
|
+
] as const
|
|
1209
|
+
).map((code) =>
|
|
1210
|
+
check(code, 'error', false, {
|
|
1211
|
+
module: 'src/reference/curriculum-alignment.ts',
|
|
1212
|
+
fn: 'validateNotationAlignment',
|
|
1213
|
+
}),
|
|
1214
|
+
),
|
|
1215
|
+
] as [RuleCheck, ...RuleCheck[]],
|
|
1216
|
+
},
|
|
1217
|
+
];
|
|
1218
|
+
|
|
1219
|
+
export const CATALOG: readonly CatalogEntry[] = [...RULES, ...INVARIANTS];
|
|
1220
|
+
|
|
1221
|
+
// --- lookups -------------------------------------------------------------
|
|
1222
|
+
|
|
1223
|
+
const BY_ID = new Map<string, CatalogEntry>(CATALOG.map((entry) => [entry.id, entry]));
|
|
1224
|
+
|
|
1225
|
+
const OWNER_BY_CODE = (() => {
|
|
1226
|
+
const map = new Map<string, CatalogEntry>();
|
|
1227
|
+
for (const entry of CATALOG) {
|
|
1228
|
+
for (const { code } of entry.checks) {
|
|
1229
|
+
const previous = map.get(code);
|
|
1230
|
+
if (previous !== undefined) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
`rule catalog: code "${code}" is owned by both ${previous.id} and ${entry.id}`,
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
map.set(code, entry);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return map;
|
|
1239
|
+
})();
|
|
1240
|
+
|
|
1241
|
+
/** The rule or invariant that owns a diagnostic code, if any. */
|
|
1242
|
+
export function ownerOf(code: string): CatalogEntry | undefined {
|
|
1243
|
+
return OWNER_BY_CODE.get(code);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* The **rule** ID a diagnostic code belongs to — a lettered engine rule, or a
|
|
1248
|
+
* course rule, whose single check carries the rule's own ID as its code.
|
|
1249
|
+
* `undefined` for a code owned by an invariant (there is no rule page to read)
|
|
1250
|
+
* and for a code nothing owns.
|
|
1251
|
+
*/
|
|
1252
|
+
export function ruleIdOf(code: string): RuleId | undefined {
|
|
1253
|
+
const owner = OWNER_BY_CODE.get(code);
|
|
1254
|
+
if (owner !== undefined) return owner.kind === 'rule' ? owner.id : undefined;
|
|
1255
|
+
return isCourseRuleId(code) ? code : undefined;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
export function entryById(id: string): CatalogEntry | undefined {
|
|
1259
|
+
return BY_ID.get(id);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
export function ruleById(id: string): RuleEntry | undefined {
|
|
1263
|
+
const entry = BY_ID.get(id);
|
|
1264
|
+
return entry?.kind === 'rule' ? entry : undefined;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
export function rules(): readonly RuleEntry[] {
|
|
1268
|
+
return RULES;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/** Every retired rule, oldest ID first. Read by the generated table and nothing else. */
|
|
1272
|
+
export function retiredRules(): readonly RetiredRule[] {
|
|
1273
|
+
return RETIRED;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/** The retired rule with this ID, if the ID belonged to one. */
|
|
1277
|
+
export function retiredRule(id: string): RetiredRule | undefined {
|
|
1278
|
+
return RETIRED.find((rule) => rule.id === id);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
export function invariants(): readonly InvariantEntry[] {
|
|
1282
|
+
return INVARIANTS;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* `script` — every violation a check can see is reported; `partial` — checks
|
|
1287
|
+
* exist but the rule says what they miss; `guide` — nothing runs, a reviewer
|
|
1288
|
+
* (or the agent's own reading) is the enforcement.
|
|
1289
|
+
*/
|
|
1290
|
+
export function enforcementOf(entry: CatalogEntry): Enforcement {
|
|
1291
|
+
if (entry.checks.length === 0) return 'guide';
|
|
1292
|
+
return entry.kind === 'rule' && entry.unchecked !== undefined ? 'partial' : 'script';
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Every code the catalog owns, sorted. */
|
|
1296
|
+
export function ownedCodes(): readonly string[] {
|
|
1297
|
+
return [...OWNER_BY_CODE.keys()].sort();
|
|
1298
|
+
}
|