explico 0.8.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 +1239 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- 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 +4 -1
- package/dist/compiler/logging.js +4 -1
- 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 +3 -2
- 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 +60 -31
- package/dist/reference/equations.mjs +83 -43
- 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 +1 -0
- 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 +45 -21
- 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 +22 -6
- package/dist/reference/gate-math.js +42 -8
- 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 +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- 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 +572 -37
- 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 +4 -4
- 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 +17 -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 +1096 -2
- package/docs/authoring/notation.md +576 -322
- 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/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- 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 +8 -2
- 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 +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- 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 +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- 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 +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- 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 +572 -37
- 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 +16 -31
- 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 +17 -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,983 @@
|
|
|
1
|
+
const ELEMENTS = [
|
|
2
|
+
"prose",
|
|
3
|
+
"math",
|
|
4
|
+
"equation",
|
|
5
|
+
"notation",
|
|
6
|
+
"heading",
|
|
7
|
+
"part-page",
|
|
8
|
+
"plot",
|
|
9
|
+
"diagram",
|
|
10
|
+
"table",
|
|
11
|
+
"assessment",
|
|
12
|
+
"source",
|
|
13
|
+
"changelog"
|
|
14
|
+
];
|
|
15
|
+
const COURSE_RULE_ID = /^X[1-9][0-9]?$/;
|
|
16
|
+
function isCourseRuleId(id) {
|
|
17
|
+
return COURSE_RULE_ID.test(id);
|
|
18
|
+
}
|
|
19
|
+
const GATE_MATH = { module: "src/reference/gate-math.ts", fn: "resolveInto" };
|
|
20
|
+
const GLYPHS = { module: "src/reference/math-glyphs.mjs", fn: "resolveMathGlyphs" };
|
|
21
|
+
const REGISTRY = { module: "src/reference/registry.ts", fn: "buildNotationRegistry" };
|
|
22
|
+
const KEYED_FLOATS = { module: "src/reference/keyed-float-validate.ts", fn: "validateKeyedFloats" };
|
|
23
|
+
function consistency(fn) {
|
|
24
|
+
return { module: "src/reference/consistency.ts", fn };
|
|
25
|
+
}
|
|
26
|
+
function check(code, severity, located, ...emitters) {
|
|
27
|
+
return { code, severity, emitters, located };
|
|
28
|
+
}
|
|
29
|
+
function keyedFloatChecks() {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const prefix of ["eq", "tab", "fig", "dia", "ex", "check"]) {
|
|
32
|
+
const emitter = prefix === "eq" ? { module: "src/reference/equations-validate.ts", fn: "validateEquations" } : prefix === "check" ? { module: "src/reference/knowledge-checks-validate.ts", fn: "validateKnowledgeChecks" } : KEYED_FLOATS;
|
|
33
|
+
if (prefix !== "eq") out.push(check(`${prefix}-invalid-key`, "error", false, emitter));
|
|
34
|
+
out.push(
|
|
35
|
+
check(`${prefix}-duplicate-key`, "error", false, emitter),
|
|
36
|
+
check(`${prefix}-ref-resolves`, "error", false, emitter)
|
|
37
|
+
);
|
|
38
|
+
if (prefix === "tab" || prefix === "fig" || prefix === "dia" || prefix === "ex") {
|
|
39
|
+
out.push(check(`${prefix}-key-unused`, "info", false, emitter));
|
|
40
|
+
out.push(check(`${prefix}-removed-prop`, "error", false, emitter));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
out.push(
|
|
44
|
+
check("ex-missing-prop", "error", false, {
|
|
45
|
+
module: "src/reference/examples-validate.ts",
|
|
46
|
+
fn: "validateExamples"
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
out.push(
|
|
50
|
+
check("sec-ref-resolves", "error", true, {
|
|
51
|
+
module: "src/reference/sections-validate.ts",
|
|
52
|
+
fn: "validateSectionRefs"
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
out.push(
|
|
56
|
+
check("fold-unnamed", "error", false, {
|
|
57
|
+
module: "src/reference/folds-validate.ts",
|
|
58
|
+
fn: "validateFolds"
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function baselineEdits(what) {
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
key: "allow",
|
|
67
|
+
title: "Also allow",
|
|
68
|
+
description: `${what} this course treats as universal, in addition to the shipped list.`,
|
|
69
|
+
schema: { type: "array", items: "string", default: [] }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "deny",
|
|
73
|
+
title: "Always explain",
|
|
74
|
+
description: `Shipped ${what.toLowerCase()} this course still wants explained.`,
|
|
75
|
+
schema: { type: "array", items: "string", default: [] }
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
const RULES = [
|
|
80
|
+
// ---- N — notation ------------------------------------------------------
|
|
81
|
+
{
|
|
82
|
+
kind: "rule",
|
|
83
|
+
id: "N2",
|
|
84
|
+
short: "A reserved spelling is used only for its reserved meaning",
|
|
85
|
+
title: "A reserved spelling means only what it is reserved for, and every other styled letter is a symbol of its own.",
|
|
86
|
+
elements: ["math"],
|
|
87
|
+
doc: "notation.md#n2",
|
|
88
|
+
summary: "The `reserved` setting is this course\u2019s table of spellings that are fixed to one meaning \u2014 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).",
|
|
89
|
+
checks: [],
|
|
90
|
+
settings: [
|
|
91
|
+
{
|
|
92
|
+
key: "reserved",
|
|
93
|
+
title: "Reserved spellings",
|
|
94
|
+
description: "One `<latex> \u2014 <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 \u2014 which spelling was chosen is caught in review; rule N4\u2019s 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.",
|
|
95
|
+
schema: {
|
|
96
|
+
type: "array",
|
|
97
|
+
items: "string",
|
|
98
|
+
default: [
|
|
99
|
+
"\\mathbb{N} \u2014 the natural numbers",
|
|
100
|
+
"\\mathbb{Z} \u2014 the integers",
|
|
101
|
+
"\\mathbb{R} \u2014 the reals",
|
|
102
|
+
"\\mathbb{C} \u2014 the complex numbers",
|
|
103
|
+
"\\mathbb{P} \u2014 probability",
|
|
104
|
+
"\\mathcal{N}(\\mu, \\sigma^2) \u2014 the normal distribution (mean, variance)",
|
|
105
|
+
"\\mathrm{d} \u2014 the differential"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
kind: "rule",
|
|
113
|
+
id: "N4",
|
|
114
|
+
short: "One meaning per glyph; define once; `[[key]]` before first math",
|
|
115
|
+
title: "One meaning per glyph per page; define once; introduce with `[[key]]` before the first math use.",
|
|
116
|
+
elements: ["math", "notation"],
|
|
117
|
+
doc: "notation.md#n4",
|
|
118
|
+
summary: "Every glyph in rendered math resolves to exactly one notation entry on the page: introduce it with `[[key]]` in prose before its first `$\u2026$`, 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.",
|
|
119
|
+
checks: [
|
|
120
|
+
check("unresolved-glyph", "error", true, GATE_MATH),
|
|
121
|
+
check("glyph-unique-in-corpus", "warning", false, consistency("checkGlyphUniqueness")),
|
|
122
|
+
check("weak-local", "info", false, consistency("checkWeakLocal")),
|
|
123
|
+
check("gloss-wants-promoting", "info", false, consistency("checkGlossWantsPromoting")),
|
|
124
|
+
check("card-wants-demoting", "info", false, consistency("checkCardWantsDemoting"))
|
|
125
|
+
],
|
|
126
|
+
locked: "the notation layer is this rule \u2014 an unresolved glyph has no hover card and the render path needs every glyph bound."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
kind: "rule",
|
|
130
|
+
id: "N5",
|
|
131
|
+
short: "Referenced display equations carry `\\label{eq:key}`",
|
|
132
|
+
title: "A display equation referenced later carries `\\label{eq:key}`.",
|
|
133
|
+
elements: ["equation"],
|
|
134
|
+
doc: "notation.md#n5",
|
|
135
|
+
summary: "Label a display equation with `\\label{eq:key}` when \u2014 and only when \u2014 prose refers back to it with `[[eq-key]]`. A label nothing references is noise.",
|
|
136
|
+
checks: [
|
|
137
|
+
check("eq-key-unused", "info", false, {
|
|
138
|
+
module: "src/reference/equations-validate.ts",
|
|
139
|
+
fn: "validateEquations"
|
|
140
|
+
})
|
|
141
|
+
],
|
|
142
|
+
unchecked: "that a referenced-in-prose equation actually carries a label."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
kind: "rule",
|
|
146
|
+
id: "N8",
|
|
147
|
+
short: "A named operator is universal (shipped/course list) or explained",
|
|
148
|
+
title: "A named operator is universal, or it is explained.",
|
|
149
|
+
elements: ["math", "notation"],
|
|
150
|
+
doc: "notation.md#n8",
|
|
151
|
+
summary: "An operator such as `\\sin` or `\\operatorname{Var}` is either on the shipped universal list (plus the course\u2019s `allow` list) or has a notation entry. The `deny` list forces an entry for a shipped one.",
|
|
152
|
+
checks: [
|
|
153
|
+
check("operator-explained", "warning", false, consistency("checkOperatorExplained"))
|
|
154
|
+
],
|
|
155
|
+
settings: baselineEdits("Operators")
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
kind: "rule",
|
|
159
|
+
id: "N9",
|
|
160
|
+
short: "A quantity/unit/operator is typeset in `$\u2026$`, never as plain text",
|
|
161
|
+
title: "A physical quantity, unit, or math operator is typeset in `$\u2026$`, never as plain text \u2014 in prose, a table cell, an assessment `prompt` / `explanation`, or a caption that itself lives outside math.",
|
|
162
|
+
elements: ["prose", "assessment", "table"],
|
|
163
|
+
doc: "notation.md#n9",
|
|
164
|
+
summary: '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="\u2026"`) and assessment choice labels render no math, so N9 does not apply there.',
|
|
165
|
+
checks: [
|
|
166
|
+
check(
|
|
167
|
+
"prose-notation",
|
|
168
|
+
"error",
|
|
169
|
+
true,
|
|
170
|
+
{ module: "src/reference/gate-prose-notation.ts", fn: "gateProseNotation" },
|
|
171
|
+
{ module: "src/reference/gate-prose-notation.ts", fn: "gateProseNotationInAssessments" }
|
|
172
|
+
),
|
|
173
|
+
check(
|
|
174
|
+
"spelled-unit",
|
|
175
|
+
"error",
|
|
176
|
+
true,
|
|
177
|
+
{ module: "src/reference/gate-prose-notation.ts", fn: "gateProseNotation" },
|
|
178
|
+
{ module: "src/reference/gate-prose-notation.ts", fn: "gateProseNotationInAssessments" }
|
|
179
|
+
)
|
|
180
|
+
],
|
|
181
|
+
settings: [
|
|
182
|
+
{
|
|
183
|
+
key: "allow",
|
|
184
|
+
title: "Unit words prose may spell out",
|
|
185
|
+
description: "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 `$\u2026$`.",
|
|
186
|
+
schema: { type: "array", items: "string", default: [] }
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
kind: "rule",
|
|
192
|
+
id: "N10",
|
|
193
|
+
short: "A multi-letter label is `\\mathrm{\u2026}`, and universal or explained",
|
|
194
|
+
title: "A multi-letter roman label is written `\\mathrm{\u2026}`, and it is universal or it is explained.",
|
|
195
|
+
elements: ["math", "notation"],
|
|
196
|
+
doc: "notation.md#n10",
|
|
197
|
+
summary: "Write a word-like label inside math as `\\mathrm{KE}`, `v_{\\mathrm{max}}` \u2014 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.",
|
|
198
|
+
checks: [
|
|
199
|
+
check("roman-label-explained", "warning", false, consistency("checkRomanLabelExplained"))
|
|
200
|
+
],
|
|
201
|
+
unchecked: "that a bare multi-letter run was wrapped in `\\mathrm{\u2026}` at all \u2014 rule N4\u2019s gate reports it as unresolved one-letter symbols, which is a different message for the same mistake.",
|
|
202
|
+
settings: baselineEdits("Roman labels")
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
kind: "rule",
|
|
206
|
+
id: "N11",
|
|
207
|
+
short: "A worked example never drops a unit from a substituted value",
|
|
208
|
+
title: "A worked example never drops a unit from a substituted value, and its result is dimensionally consistent.",
|
|
209
|
+
elements: ["math", "prose"],
|
|
210
|
+
doc: "notation.md#n11",
|
|
211
|
+
summary: "When numbers replace symbols, every value keeps its unit (`$v = 3\\ \\text{m/s} \\cdot 4\\ \\text{s}$`), and the result\u2019s unit follows from them.",
|
|
212
|
+
checks: []
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
kind: "rule",
|
|
216
|
+
id: "N12",
|
|
217
|
+
short: "Math syntax never carries the sentence's own trailing punctuation",
|
|
218
|
+
title: "Math syntax never carries the sentence\u2019s own trailing punctuation.",
|
|
219
|
+
elements: ["math", "prose", "assessment"],
|
|
220
|
+
doc: "notation.md#n12",
|
|
221
|
+
summary: "Math, and each row before a `\\\\`, never ends in `.`, `,`, `;` or `:` \u2014 the sentence\u2019s 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`.",
|
|
222
|
+
checks: [
|
|
223
|
+
check(
|
|
224
|
+
"math-trailing-punctuation",
|
|
225
|
+
"error",
|
|
226
|
+
true,
|
|
227
|
+
{ module: "src/reference/gate-math-punctuation.ts", fn: "gateMathPunctuation" },
|
|
228
|
+
{ module: "src/reference/gate-math-punctuation.ts", fn: "gateMathPunctuationInAssessments" }
|
|
229
|
+
)
|
|
230
|
+
],
|
|
231
|
+
settings: [
|
|
232
|
+
{
|
|
233
|
+
key: "allow",
|
|
234
|
+
title: "Also allow",
|
|
235
|
+
description: "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.`.",
|
|
236
|
+
schema: { type: "array", items: "string", default: [] }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: "deny",
|
|
240
|
+
title: "Never allow",
|
|
241
|
+
description: "Shipped abbreviations this course does not accept at the end of math.",
|
|
242
|
+
schema: { type: "array", items: "string", default: [] }
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
kind: "rule",
|
|
248
|
+
id: "N13",
|
|
249
|
+
short: "A nonlinear function always takes a parenthesized argument",
|
|
250
|
+
title: "A nonlinear function always takes a parenthesized argument, even a single symbol.",
|
|
251
|
+
elements: ["math"],
|
|
252
|
+
doc: "notation.md#n13",
|
|
253
|
+
summary: "Write `\\sin(x)`, `\\ln(t)`, `f(x)` \u2014 never `\\sin x`.",
|
|
254
|
+
checks: [check("unparenthesized-function", "error", true, GLYPHS)],
|
|
255
|
+
locked: "the glyph resolver cannot tell where an unparenthesized argument ends, so it cannot bind the symbols inside it."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
kind: "rule",
|
|
259
|
+
id: "N14",
|
|
260
|
+
short: "An abbreviation-shaped word in prose is universal, or explained",
|
|
261
|
+
title: "An abbreviation-shaped word in prose is universal, or it is explained.",
|
|
262
|
+
elements: ["prose"],
|
|
263
|
+
doc: "notation.md#n14",
|
|
264
|
+
summary: "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/`.",
|
|
265
|
+
checks: [
|
|
266
|
+
check("abbreviation-unexplained", "error", true, {
|
|
267
|
+
module: "src/reference/gate-abbreviations.ts",
|
|
268
|
+
fn: "gateAbbreviations"
|
|
269
|
+
})
|
|
270
|
+
],
|
|
271
|
+
settings: baselineEdits("Abbreviations")
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
kind: "rule",
|
|
275
|
+
id: "N15",
|
|
276
|
+
short: "A nested `(`, `[`, `{`, or `|` must use scalable sizing",
|
|
277
|
+
title: "A nested visible delimiter \u2014 `(`, `[`, `\\{`, or `|` inside another one of them \u2014 must use the scalable `\\left`/`\\right` form, not the bare character.",
|
|
278
|
+
elements: ["math"],
|
|
279
|
+
doc: "notation.md#n15",
|
|
280
|
+
summary: "Inside another delimiter pair, write `\\left( \u2026 \\right)`, `\\left| \u2026 \\right|`; a bare nested `|` or `(` fails the build.",
|
|
281
|
+
checks: [check("nested-bare-delimiter", "error", true, GLYPHS)],
|
|
282
|
+
locked: "a bare `|` is both an opener and a closer; nested, the resolver cannot pair it, so the rule is a parsing precondition."
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
kind: "rule",
|
|
286
|
+
id: "N17",
|
|
287
|
+
short: "A delimiter\u2019s shape is semantic; a nest is engine-sized, not hand-sized",
|
|
288
|
+
title: "A delimiter\u2019s shape carries its meaning and never changes to show nesting: a nest is sized by the engine \u2014 `\\left ... \\right` at every level, never `\\bigl`, `\\Bigl`, `\\biggl`, or `\\Biggl` inside it.",
|
|
289
|
+
elements: ["math"],
|
|
290
|
+
doc: "notation.md#n17",
|
|
291
|
+
summary: "Keep the shape each delimiter has for its meaning \u2014 an interval `[a, b)`, a norm `\\|\xB7\\|`, a set `\\{\u2026\\}`, a parenthesis for grouping \u2014 and never swap shapes (`( [ \\{ \u2026 \\} ] )`) to mark depth. Depth is size: write `\\left`/`\\right` at every level and let the engine size them, never `\\bigl\u2026\\bigr` and friends inside a nest.",
|
|
292
|
+
checks: [
|
|
293
|
+
check("delimiter-manual-size", "warning", true, {
|
|
294
|
+
module: "src/reference/delimiter-size.ts",
|
|
295
|
+
fn: "lintDelimiterSizes"
|
|
296
|
+
})
|
|
297
|
+
],
|
|
298
|
+
unchecked: "the shape half \u2014 `delimiter-manual-size` parses sizing syntax only, and never decides whether a bracket is semantic. A bare nested delimiter is rule N15\u2019s error instead."
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
kind: "rule",
|
|
302
|
+
id: "N18",
|
|
303
|
+
short: "A multi-step derivation is one aligned chain; reasons follow `\\qquad`",
|
|
304
|
+
title: "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`.",
|
|
305
|
+
elements: ["equation"],
|
|
306
|
+
doc: "notation.md#n18",
|
|
307
|
+
summary: "A derivation is one `aligned` block: each step on its own line, `&=` at the relation, the reason after `\\qquad` on the same line \u2014 not a run of separate display equations.",
|
|
308
|
+
checks: []
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
kind: "rule",
|
|
312
|
+
id: "N19",
|
|
313
|
+
short: "A notation entry states `units` from the vocabulary, or `dimensionless`",
|
|
314
|
+
title: "A notation entry states its `units`, or declares itself `dimensionless`, using the units vocabulary.",
|
|
315
|
+
elements: ["notation"],
|
|
316
|
+
doc: "notation.md#n19",
|
|
317
|
+
summary: "Every notation entry has `units:` from `units-vocab.ts` (`m/s`, `s`, `kg`, \u2026) or `dimensionless: true`. A token outside the vocabulary is a warning.",
|
|
318
|
+
checks: [
|
|
319
|
+
check("notation-units", "warning", false, consistency("checkNotationUnits")),
|
|
320
|
+
check("units-vocab", "warning", false, consistency("checkNotationUnits"))
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
kind: "rule",
|
|
325
|
+
id: "N20",
|
|
326
|
+
short: "A notation entry's source citation carries a locator",
|
|
327
|
+
title: "A notation entry\u2019s source citation carries a locator.",
|
|
328
|
+
elements: ["notation", "source"],
|
|
329
|
+
doc: "notation.md#n20",
|
|
330
|
+
summary: 'Each item in a notation entry\u2019s `sources` names the place in the source (`locator: "\xA73.2"`, `"p. 41"`), not just the source id.',
|
|
331
|
+
checks: [
|
|
332
|
+
check("notation-source-locator", "warning", false, consistency("checkNotationSourceLocator"))
|
|
333
|
+
]
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
kind: "rule",
|
|
337
|
+
id: "N21",
|
|
338
|
+
short: "A gloss is named by a short noun phrase, not a sentence",
|
|
339
|
+
title: "A gloss is named by a short noun phrase, not described by a sentence.",
|
|
340
|
+
elements: ["notation"],
|
|
341
|
+
doc: "notation.md#n21",
|
|
342
|
+
summary: "A gloss `label` names the symbol in a few words (\u201Claunch angle\u201D); a clause, sentence punctuation, or a definition belongs in a full notation entry instead.",
|
|
343
|
+
checks: [check("gloss-name-shape", "warning", false, consistency("checkGlossNameShape"))]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
kind: "rule",
|
|
347
|
+
id: "N22",
|
|
348
|
+
short: "An accent or mark is an operator: universal, explained, or a whole form",
|
|
349
|
+
title: "An accent or mark on a symbol is an operator: universal, explained, or part of the symbol\u2019s own entry.",
|
|
350
|
+
elements: ["math", "notation"],
|
|
351
|
+
doc: "notation.md#n22",
|
|
352
|
+
summary: "A decoration (`\\hat`, `\\bar`, `\\overline`, `'`, `^*`, `^\\dagger`, \u2026) is on the course\u2019s `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.",
|
|
353
|
+
checks: [
|
|
354
|
+
check("accent-explained", "warning", false, consistency("checkAccentExplained")),
|
|
355
|
+
check("suspended-operator-unresolved", "error", true, GATE_MATH),
|
|
356
|
+
check("operator-suspension-reason", "warning", false, consistency("checkOperatorSuspension")),
|
|
357
|
+
check("operator-suspended", "info", false, consistency("checkOperatorSuspension")),
|
|
358
|
+
check("operator-shadowed", "info", false, consistency("checkOperatorSuspension"))
|
|
359
|
+
],
|
|
360
|
+
settings: baselineEdits("Decorations")
|
|
361
|
+
},
|
|
362
|
+
// ---- V — visualizations -----------------------------------------------------
|
|
363
|
+
{
|
|
364
|
+
kind: "rule",
|
|
365
|
+
id: "V1",
|
|
366
|
+
short: "Graphs and flowcharts use the `Diagram` component",
|
|
367
|
+
title: "Graphs and flowcharts use the `Diagram` component.",
|
|
368
|
+
elements: ["diagram"],
|
|
369
|
+
doc: "visualizations.md#v1",
|
|
370
|
+
summary: 'A node-and-edge picture \u2014 a dependency graph, a flowchart \u2014 is a `<Diagram id="\u2026">` with its nodes and formulas declared, never a hand-drawn SVG or an image.',
|
|
371
|
+
checks: []
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
kind: "rule",
|
|
375
|
+
id: "V2",
|
|
376
|
+
short: "Static charts: SVG \u2192 D3 \u2192 Observable Plot, chosen per figure",
|
|
377
|
+
title: "Static charts: hand-rolled SVG first, then D3, then Observable Plot \u2014 chosen per figure.",
|
|
378
|
+
elements: ["plot"],
|
|
379
|
+
doc: "visualizations.md#v2",
|
|
380
|
+
summary: "For a static chart pick the lightest tool that draws it: inline SVG, then D3, then Observable Plot. State the choice in the figure\u2019s source comment.",
|
|
381
|
+
checks: []
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
kind: "rule",
|
|
385
|
+
id: "V3",
|
|
386
|
+
short: "Interactive figures: a hydrated island + the `renderLabMath` seam",
|
|
387
|
+
title: "Interactive figures: a hydrated island and the `renderLabMath` seam for equations.",
|
|
388
|
+
elements: ["plot"],
|
|
389
|
+
doc: "visualizations.md#v3",
|
|
390
|
+
summary: "An interactive figure is an island under `content/labs/` inside a `<Figure>`, renders its equations through `renderLabMath`, and labels its controls; the figure\u2019s caption is its name, so the lab writes no accessible name or text alternative of its own.",
|
|
391
|
+
checks: []
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
kind: "rule",
|
|
395
|
+
id: "V4",
|
|
396
|
+
short: "Randomised figures expose an adjustable, deterministic seed",
|
|
397
|
+
title: "A randomised figure exposes an adjustable, deterministic seed.",
|
|
398
|
+
elements: ["plot"],
|
|
399
|
+
doc: "visualizations.md#v4",
|
|
400
|
+
summary: "Randomness in a figure comes from a seed the reader can see and change; the same seed always draws the same picture.",
|
|
401
|
+
checks: []
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
kind: "rule",
|
|
405
|
+
id: "V5",
|
|
406
|
+
short: "Axis ranges span the first 10 seeds at the current control values",
|
|
407
|
+
title: "Axis ranges span the first 10 seeds, evaluated at the current values of every other control.",
|
|
408
|
+
elements: ["plot"],
|
|
409
|
+
doc: "visualizations.md#v5",
|
|
410
|
+
summary: "Fix each axis range so the drawings for seeds 1\u2026`seeds` all fit at the current control values; the axes do not jump when the seed changes.",
|
|
411
|
+
checks: [],
|
|
412
|
+
settings: [
|
|
413
|
+
{
|
|
414
|
+
key: "seeds",
|
|
415
|
+
title: "Seeds spanned",
|
|
416
|
+
description: "How many leading seeds an axis range must accommodate.",
|
|
417
|
+
schema: { type: "integer", minimum: 1, default: 10 }
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
kind: "rule",
|
|
423
|
+
id: "V6",
|
|
424
|
+
short: "Every quantitative axis shows at least three labelled values",
|
|
425
|
+
title: "Every quantitative axis of a chart shows at least three labelled values.",
|
|
426
|
+
elements: ["plot"],
|
|
427
|
+
doc: "visualizations.md#v6",
|
|
428
|
+
summary: "Each numeric axis carries at least `minLabels` tick labels, so a reader can read a value off the chart.",
|
|
429
|
+
checks: [],
|
|
430
|
+
settings: [
|
|
431
|
+
{
|
|
432
|
+
key: "minLabels",
|
|
433
|
+
title: "Minimum axis labels",
|
|
434
|
+
description: "Labelled tick values a quantitative axis must show.",
|
|
435
|
+
schema: { type: "integer", minimum: 2, default: 3 }
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
},
|
|
439
|
+
// ---- C — versioning ---------------------------------------------------------
|
|
440
|
+
{
|
|
441
|
+
kind: "rule",
|
|
442
|
+
id: "C1",
|
|
443
|
+
short: "Every content change lands with a `content/CHANGELOG.md` entry",
|
|
444
|
+
title: "Every content change lands with a `content/CHANGELOG.md` entry under `## [Unreleased]`.",
|
|
445
|
+
elements: ["changelog"],
|
|
446
|
+
doc: "versioning.md#c1",
|
|
447
|
+
summary: "Any edit under `content/` adds a bullet to `content/CHANGELOG.md` under `## [Unreleased]` in the same commit.",
|
|
448
|
+
checks: []
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
kind: "rule",
|
|
452
|
+
id: "C2",
|
|
453
|
+
short: "The knowledge version is semver over learning-facing content",
|
|
454
|
+
title: "The knowledge version is semver over learning-facing content.",
|
|
455
|
+
elements: ["changelog"],
|
|
456
|
+
doc: "versioning.md#c2",
|
|
457
|
+
summary: "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.",
|
|
458
|
+
checks: []
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
kind: "rule",
|
|
462
|
+
id: "C3",
|
|
463
|
+
short: "`about.version` equals the newest changelog heading; a release moves both",
|
|
464
|
+
title: "`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.",
|
|
465
|
+
elements: ["changelog"],
|
|
466
|
+
doc: "versioning.md#c3",
|
|
467
|
+
summary: "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` \u2014 in one commit. Between releases `about.version` stays at the last released value.",
|
|
468
|
+
checks: [
|
|
469
|
+
check("knowledge-version-mismatch", "warning", false, {
|
|
470
|
+
module: "src/compiler/knowledge-version.ts",
|
|
471
|
+
fn: "resolveKnowledgeVersion"
|
|
472
|
+
}),
|
|
473
|
+
check("knowledge-version-invalid", "warning", false, {
|
|
474
|
+
module: "src/compiler/knowledge-version.ts",
|
|
475
|
+
fn: "resolveKnowledgeVersion"
|
|
476
|
+
}),
|
|
477
|
+
check("knowledge-changelog-missing", "warning", false, {
|
|
478
|
+
module: "src/compiler/knowledge-version.ts",
|
|
479
|
+
fn: "resolveKnowledgeVersion"
|
|
480
|
+
})
|
|
481
|
+
],
|
|
482
|
+
unchecked: "that the two edits landed in one commit \u2014 the build only ever sees the end state, so a release split across commits warns until the second half lands."
|
|
483
|
+
},
|
|
484
|
+
// ---- S — structure ----------------------------------------------------------
|
|
485
|
+
{
|
|
486
|
+
kind: "rule",
|
|
487
|
+
id: "S1",
|
|
488
|
+
short: "No heading, short caption or short-note title over 65 characters",
|
|
489
|
+
title: "No heading, no short caption (`short`, an equation\u2019s `\\caption{}`) and no short-note title runs past 65 characters.",
|
|
490
|
+
elements: ["heading"],
|
|
491
|
+
doc: "structure-rules.md#s1",
|
|
492
|
+
summary: "A Markdown heading, the `short` caption of a `Figure`, `Diagram`, `Table`, `Memo` or `CompactExamples`, an equation\u2019s `\\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.",
|
|
493
|
+
checks: [
|
|
494
|
+
check("title-length", "error", true, {
|
|
495
|
+
module: "src/reference/gate-heading-length.ts",
|
|
496
|
+
fn: "gateHeadingLength"
|
|
497
|
+
})
|
|
498
|
+
],
|
|
499
|
+
unchecked: "a lesson\u2019s `frontmatter.title` \u2014 the gate scans the body only.",
|
|
500
|
+
settings: [
|
|
501
|
+
{
|
|
502
|
+
key: "maxLength",
|
|
503
|
+
title: "Maximum length",
|
|
504
|
+
description: "Characters a heading or short caption may run to.",
|
|
505
|
+
schema: { type: "integer", minimum: 20, default: 65 }
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
kind: "rule",
|
|
511
|
+
id: "S2",
|
|
512
|
+
short: "A part page carries a `<PartBridge>` (earlier parts \u2192 this one) and a `<PartScope>` paragraph",
|
|
513
|
+
title: "A part\u2019s 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.",
|
|
514
|
+
elements: ["part-page"],
|
|
515
|
+
doc: "structure-rules.md#s2",
|
|
516
|
+
summary: "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.",
|
|
517
|
+
checks: [
|
|
518
|
+
check("part-page-paragraph", "error", false, {
|
|
519
|
+
module: "src/reference/gate-part-pages.ts",
|
|
520
|
+
fn: "gatePartPages"
|
|
521
|
+
})
|
|
522
|
+
],
|
|
523
|
+
unchecked: "what each paragraph says \u2014 the gate cannot tell a bridge that links the parts from one that does not.",
|
|
524
|
+
settings: [
|
|
525
|
+
{
|
|
526
|
+
key: "bridgeMinWords",
|
|
527
|
+
title: "Bridge: minimum words",
|
|
528
|
+
description: "Fewest words the `<PartBridge>` paragraph may have.",
|
|
529
|
+
schema: { type: "integer", minimum: 1, default: 30 }
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
key: "bridgeMaxWords",
|
|
533
|
+
title: "Bridge: maximum words",
|
|
534
|
+
description: "Most words the `<PartBridge>` paragraph may have.",
|
|
535
|
+
schema: { type: "integer", minimum: 1, default: 150 }
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
key: "scopeMinWords",
|
|
539
|
+
title: "Scope: minimum words",
|
|
540
|
+
description: "Fewest words the `<PartScope>` paragraph may have.",
|
|
541
|
+
schema: { type: "integer", minimum: 1, default: 30 }
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
key: "scopeMaxWords",
|
|
545
|
+
title: "Scope: maximum words",
|
|
546
|
+
description: "Most words the `<PartScope>` paragraph may have.",
|
|
547
|
+
schema: { type: "integer", minimum: 1, default: 150 }
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
kind: "rule",
|
|
553
|
+
id: "S3",
|
|
554
|
+
short: 'A table is a `<Table id="\u2026">`, never a raw Markdown/HTML table',
|
|
555
|
+
title: 'A table is a `<Table id="\u2026">`, never a raw Markdown or HTML table.',
|
|
556
|
+
elements: ["table"],
|
|
557
|
+
doc: "structure-rules.md#s3",
|
|
558
|
+
summary: 'Wrap every pipe table or `<table>` in `<Table id="\u2026" caption="\u2026">` so it gets a number and `[[tab-\u2026]]` can reach it.',
|
|
559
|
+
checks: [
|
|
560
|
+
check("raw-table-markdown", "warning", false, {
|
|
561
|
+
module: "src/reference/raw-floats-validate.ts",
|
|
562
|
+
fn: "validateRawFloats"
|
|
563
|
+
}),
|
|
564
|
+
check("raw-table-html", "warning", false, {
|
|
565
|
+
module: "src/reference/raw-floats-validate.ts",
|
|
566
|
+
fn: "validateRawFloats"
|
|
567
|
+
})
|
|
568
|
+
]
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
kind: "rule",
|
|
572
|
+
id: "S4",
|
|
573
|
+
short: "A `<Diagram>` carries an `id` and sits where numbering can see it",
|
|
574
|
+
title: "A `<Diagram>` carries an `id` and sits where diagram numbering can see it.",
|
|
575
|
+
elements: ["diagram"],
|
|
576
|
+
doc: "structure-rules.md#s4",
|
|
577
|
+
summary: "Give every `<Diagram>` an `id`, and place it at the top level or inside `<Memo>`/`<ShortNote>`/`<Table>`/`<Figure>` \u2014 a diagram nested elsewhere (e.g. in `<CompactExample>`) is never numbered.",
|
|
578
|
+
checks: [
|
|
579
|
+
check("diagram-unkeyed", "warning", false, {
|
|
580
|
+
module: "src/reference/raw-floats-validate.ts",
|
|
581
|
+
fn: "validateRawFloats"
|
|
582
|
+
}),
|
|
583
|
+
check("diagram-nested", "warning", false, {
|
|
584
|
+
module: "src/reference/raw-floats-validate.ts",
|
|
585
|
+
fn: "validateRawFloats"
|
|
586
|
+
})
|
|
587
|
+
]
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
kind: "rule",
|
|
591
|
+
id: "S5",
|
|
592
|
+
short: "A sign / perspective convention is declared once and cited elsewhere",
|
|
593
|
+
title: "A sign or perspective convention is declared once and cited everywhere else.",
|
|
594
|
+
elements: ["prose"],
|
|
595
|
+
doc: "structure-rules.md#s5",
|
|
596
|
+
summary: "Prose that states a sign / cash-flow-perspective convention resolves it to the single declaration \u2014 the `conventions.signConventionKey` notation entry (`[[key]]`) or the `conventions.cashFlowPerspectiveCompetency` competency \u2014 rather than restating it.",
|
|
597
|
+
checks: [
|
|
598
|
+
check(
|
|
599
|
+
"convention-single-definition",
|
|
600
|
+
"warning",
|
|
601
|
+
false,
|
|
602
|
+
consistency("checkConventionSingleDefinition")
|
|
603
|
+
)
|
|
604
|
+
]
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
kind: "rule",
|
|
608
|
+
id: "S6",
|
|
609
|
+
short: "Headings descend one level at a time, from `##`",
|
|
610
|
+
title: "Headings descend one level at a time: a lesson body starts at `##` and never skips a level on the way down.",
|
|
611
|
+
elements: ["heading"],
|
|
612
|
+
doc: "structure-rules.md#s6",
|
|
613
|
+
summary: "The page title is the lesson\u2019s only `#`, so the body\u2019s first heading is `##`, and each heading is at most one level deeper than the one before it (`##` \u2192 `###`, never `##` \u2192 `####`); going back up any number of levels is fine. A knowledge check\u2019s and a worked-example set\u2019s heading count as a `##`.",
|
|
614
|
+
checks: [
|
|
615
|
+
check("heading-skip", "error", true, {
|
|
616
|
+
module: "src/reference/gate-heading-structure.ts",
|
|
617
|
+
fn: "gateHeadingStructure"
|
|
618
|
+
})
|
|
619
|
+
]
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
kind: "rule",
|
|
623
|
+
id: "S7",
|
|
624
|
+
short: "A heading is followed by body content, never by another heading",
|
|
625
|
+
title: "A heading is followed by body content, never directly by another heading.",
|
|
626
|
+
elements: ["heading"],
|
|
627
|
+
doc: "structure-rules.md#s7",
|
|
628
|
+
summary: "Write body content \u2014 at least a sentence on what the section is about \u2014 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.",
|
|
629
|
+
checks: [
|
|
630
|
+
check("heading-adjacent", "error", true, {
|
|
631
|
+
module: "src/reference/gate-heading-structure.ts",
|
|
632
|
+
fn: "gateHeadingStructure"
|
|
633
|
+
})
|
|
634
|
+
]
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
kind: "rule",
|
|
638
|
+
id: "S8",
|
|
639
|
+
short: "Prefer `<Memo>` over a Starlight aside when the content needs an anchor, a caption or collapsing",
|
|
640
|
+
title: "Use a `<Memo>`, not a Starlight aside, for content that deserves an anchor, a caption, or collapsing.",
|
|
641
|
+
elements: ["prose"],
|
|
642
|
+
doc: "structure-rules.md#s8",
|
|
643
|
+
summary: '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="\u2026">`.',
|
|
644
|
+
checks: []
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
kind: "rule",
|
|
648
|
+
id: "S9",
|
|
649
|
+
short: "A float\u2019s short caption names it; the long caption goes on from that name",
|
|
650
|
+
title: "A float\u2019s 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\u2019s instructions go in its tools.",
|
|
651
|
+
elements: ["plot", "table", "diagram"],
|
|
652
|
+
doc: "structure-rules.md#s9",
|
|
653
|
+
summary: '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 \u2014 its subject, its marks and symbols, the case shown \u2014 and at most the one thing to notice. The argument drawn from a figure is lesson text after it, and a lab\u2019s instructions are the first line of its `tools`.',
|
|
654
|
+
checks: []
|
|
655
|
+
},
|
|
656
|
+
// ---- P — prose --------------------------------------------------------------
|
|
657
|
+
{
|
|
658
|
+
kind: "rule",
|
|
659
|
+
id: "P1",
|
|
660
|
+
short: '"or" never introduces a second name for the same thing',
|
|
661
|
+
title: 'Never use "or" to give a thing a second name.',
|
|
662
|
+
elements: ["prose"],
|
|
663
|
+
doc: "prose.md#p1",
|
|
664
|
+
summary: "\u201CThe residual, or error, is \u2026\u201D introduces a synonym; pick one name and use it. \u201Cor\u201D joins alternatives, not names.",
|
|
665
|
+
checks: []
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
kind: "rule",
|
|
669
|
+
id: "P2",
|
|
670
|
+
short: "One term for each concept, repeated \u2014 no synonyms for variety",
|
|
671
|
+
title: "Use one term for each concept, and repeat it.",
|
|
672
|
+
elements: ["prose"],
|
|
673
|
+
doc: "prose.md#p2",
|
|
674
|
+
summary: "Say \u201Cresidual\u201D every time; never rotate synonyms for variety.",
|
|
675
|
+
checks: []
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
kind: "rule",
|
|
679
|
+
id: "P3",
|
|
680
|
+
short: "One meaning for each term; no technical term in its everyday sense",
|
|
681
|
+
title: "Give each term one meaning, and keep it.",
|
|
682
|
+
elements: ["prose"],
|
|
683
|
+
doc: "prose.md#p3",
|
|
684
|
+
summary: "A technical term keeps its technical meaning on the page; do not also use it in its everyday sense (\u201Csignificant\u201D, \u201Cnormal\u201D).",
|
|
685
|
+
checks: []
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
kind: "rule",
|
|
689
|
+
id: "P4",
|
|
690
|
+
short: "Use the established term; define it where the argument first needs it",
|
|
691
|
+
title: "Use the established term, and define it where the argument first needs it.",
|
|
692
|
+
elements: ["prose"],
|
|
693
|
+
doc: "prose.md#p4",
|
|
694
|
+
summary: "Prefer the field\u2019s standard term over a coined one, and define it at first use \u2014 not earlier in a glossary paragraph, not later.",
|
|
695
|
+
checks: []
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
kind: "rule",
|
|
699
|
+
id: "P5",
|
|
700
|
+
short: "Main text is literal: no idioms, metaphors, buzzwords, entertainment",
|
|
701
|
+
title: "Write the main text literally: no idioms, metaphors, colloquialisms, buzzwords, or entertainment.",
|
|
702
|
+
elements: ["prose"],
|
|
703
|
+
doc: "prose.md#p5",
|
|
704
|
+
summary: "Main text states facts and steps plainly; no \u201Clet\u2019s dive in\u201D, no metaphor, no joke. An aside may be lighter.",
|
|
705
|
+
checks: []
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
kind: "rule",
|
|
709
|
+
id: "P6",
|
|
710
|
+
short: 'Industry jargon only in an optional "Industry jargon" aside',
|
|
711
|
+
title: 'Keep industry jargon out of the main text. Put jargon that the reader must recognize in an optional "Industry jargon" aside.',
|
|
712
|
+
elements: ["prose"],
|
|
713
|
+
doc: "prose.md#p6",
|
|
714
|
+
summary: "Use the textbook term in the argument; list the trade\u2019s words for it in an \u201CIndustry jargon\u201D aside if the reader will meet them.",
|
|
715
|
+
checks: []
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
kind: "rule",
|
|
719
|
+
id: "P7",
|
|
720
|
+
short: "One claim per sentence; every logical link stated",
|
|
721
|
+
title: "Write one claim per sentence, and state every logical link.",
|
|
722
|
+
elements: ["prose"],
|
|
723
|
+
doc: "prose.md#p7",
|
|
724
|
+
summary: "One assertion per sentence; \u201Cbecause\u201D, \u201Cso\u201D, \u201Ctherefore\u201D are written out, never left for the reader to infer.",
|
|
725
|
+
checks: []
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
kind: "rule",
|
|
729
|
+
id: "P8",
|
|
730
|
+
short: 'Words for a relation match the math exactly ("proportional to")',
|
|
731
|
+
title: "Words that state a relation say exactly what the mathematics says.",
|
|
732
|
+
elements: ["prose", "math"],
|
|
733
|
+
doc: "prose.md#p8",
|
|
734
|
+
summary: "\u201CProportional to\u201D means `y = kx`; \u201Cgrows with\u201D means monotone; \u201Cdepends on\u201D means a function of. Match the words to the formula.",
|
|
735
|
+
checks: []
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
kind: "rule",
|
|
739
|
+
id: "P9",
|
|
740
|
+
short: "A display equation's lead-in says what it gives and where it comes from",
|
|
741
|
+
title: "Introduce a display equation with a sentence that says what it gives and where it comes from.",
|
|
742
|
+
elements: ["prose", "equation"],
|
|
743
|
+
doc: "prose.md#p9",
|
|
744
|
+
summary: "The sentence before a display equation names the quantity it yields and the step or law it follows from \u2014 not \u201Cwe have:\u201D.",
|
|
745
|
+
checks: []
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
const RETIRED = [
|
|
749
|
+
{
|
|
750
|
+
id: "N1",
|
|
751
|
+
title: "The normal distribution is `\\mathcal{N}`.",
|
|
752
|
+
date: "2026-09-26",
|
|
753
|
+
into: "N2",
|
|
754
|
+
why: "One spelling fixed to one meaning is one rule; `\\mathcal{N}(\\mu, \\sigma^2)` is a line in N2\u2019s `reserved` table, which a course can edit."
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
id: "N3",
|
|
758
|
+
title: "Multi-letter roman labels use `\\mathrm{\u2026}`.",
|
|
759
|
+
date: "2026-09-26",
|
|
760
|
+
into: "N10",
|
|
761
|
+
why: "Writing a label as `\\mathrm{\u2026}` and explaining it are two halves of one rule, and N10 is where both are checked."
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
id: "N6",
|
|
765
|
+
title: "Probability is `\\mathbb{P}` \u2014 double-struck `P`.",
|
|
766
|
+
date: "2026-09-26",
|
|
767
|
+
into: "N2",
|
|
768
|
+
why: "`\\mathbb{P}` is a line in N2\u2019s `reserved` table."
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
id: "N7",
|
|
772
|
+
title: "The differential `d` is upright `\\mathrm{d}`.",
|
|
773
|
+
date: "2026-09-26",
|
|
774
|
+
into: "N2",
|
|
775
|
+
why: "`\\mathrm{d}` is a line in N2\u2019s `reserved` table."
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
id: "N16",
|
|
779
|
+
title: "In AI-generated mathematical content, alternate delimiter types for nested grouping when their shape is stylistic; preserve every conventional or semantic delimiter shape.",
|
|
780
|
+
date: "2026-09-26",
|
|
781
|
+
into: "N17",
|
|
782
|
+
why: "Folded into N17 with the preference dropped: a delimiter\u2019s shape carries its meaning, so depth is shown by size \u2014 which this engine handles automatically \u2014 not by alternating shapes. The rule also scoped itself to \u201CAI-generated\u201D content, which is not a property of the page."
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
id: "C4",
|
|
786
|
+
title: "On a release, move `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD` and bump `about.version` in the same commit.",
|
|
787
|
+
date: "2026-09-26",
|
|
788
|
+
into: "C3",
|
|
789
|
+
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."
|
|
790
|
+
}
|
|
791
|
+
];
|
|
792
|
+
const INVARIANTS = [
|
|
793
|
+
{
|
|
794
|
+
kind: "invariant",
|
|
795
|
+
id: "notation-references",
|
|
796
|
+
title: "Notation keys are well-formed, unique, defined, and acyclic.",
|
|
797
|
+
elements: ["notation", "math"],
|
|
798
|
+
doc: "components.md#reference-systems",
|
|
799
|
+
summary: "A `[[key]]` or `\\explain{key}{\u2026}` names an existing entry; a key is kebab-case; a shared entry is defined once; formula references never form a cycle.",
|
|
800
|
+
checks: [
|
|
801
|
+
check("undefined-reference", "error", false, REGISTRY),
|
|
802
|
+
check("invalid-key", "error", false, REGISTRY, GLYPHS),
|
|
803
|
+
check("duplicate-key", "error", false, GLYPHS),
|
|
804
|
+
check("duplicate-page", "error", false, REGISTRY),
|
|
805
|
+
check("duplicate-definition", "error", false, REGISTRY),
|
|
806
|
+
check("conflicting-definition", "error", false, REGISTRY),
|
|
807
|
+
check("reference-cycle", "error", false, REGISTRY),
|
|
808
|
+
check("gloss-collides-with-card", "error", false, REGISTRY),
|
|
809
|
+
check("unknown-explicit-key", "error", true, GLYPHS, {
|
|
810
|
+
module: "src/reference/gate-math.ts",
|
|
811
|
+
fn: "reportUnknownScopeKeys"
|
|
812
|
+
}),
|
|
813
|
+
check("malformed-explicit-binding", "error", true, GLYPHS),
|
|
814
|
+
check("unused-definition", "info", false, REGISTRY)
|
|
815
|
+
]
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
kind: "invariant",
|
|
819
|
+
id: "math-syntax",
|
|
820
|
+
title: "Every math span parses and resolves unambiguously.",
|
|
821
|
+
elements: ["math"],
|
|
822
|
+
doc: "notation.md",
|
|
823
|
+
summary: "KaTeX must parse the span, the glyph table must be well-formed, and a span must not resolve to two entries at once.",
|
|
824
|
+
checks: [
|
|
825
|
+
check("invalid-latex", "error", true, GLYPHS),
|
|
826
|
+
check("invalid-latex-input", "error", true, GLYPHS),
|
|
827
|
+
check("invalid-notation", "error", false, GLYPHS),
|
|
828
|
+
check("invalid-definitions", "error", false, GLYPHS),
|
|
829
|
+
check("unsupported-katex-version", "error", false, GLYPHS),
|
|
830
|
+
check("ambiguous-span", "error", true, GLYPHS),
|
|
831
|
+
check("ambiguous-base-fallback", "error", true, GLYPHS),
|
|
832
|
+
check("ambiguous-canonical-notation", "error", false, GLYPHS),
|
|
833
|
+
check("math-compile-error", "error", true, GATE_MATH)
|
|
834
|
+
]
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
kind: "invariant",
|
|
838
|
+
id: "keyed-floats",
|
|
839
|
+
title: "A keyed float\u2019s `id` is well-formed and unique, it is named by its caption pair, and every `[[\u2026]]` to it or to a section resolves.",
|
|
840
|
+
elements: ["equation", "table", "plot", "diagram", "assessment", "heading"],
|
|
841
|
+
doc: "structure.md#the-invariant-and-why-it-is-build-enforced",
|
|
842
|
+
summary: "`\\label{eq:key}`, `<Table id>`, `<Figure id>`, `<Diagram id>`, `<CompactExamples id>` and a knowledge check\u2019s id are kebab-case and unique on the page; `[[eq-\u2026]]`, `[[tab-\u2026]]`, `[[fig-\u2026]]`, `[[dia-\u2026]]`, `[[ex-\u2026]]`, `[[check-\u2026]]` point at one that exists, and `[[sec-\u2026]]` 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.",
|
|
843
|
+
checks: keyedFloatChecks()
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
kind: "invariant",
|
|
847
|
+
id: "course-settings",
|
|
848
|
+
title: "The course settings name real rules and current keys.",
|
|
849
|
+
elements: ["changelog"],
|
|
850
|
+
doc: "README.md#amending",
|
|
851
|
+
summary: "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`.",
|
|
852
|
+
checks: [
|
|
853
|
+
check("config-deprecated-key", "warning", false, {
|
|
854
|
+
module: "src/rules/settings.ts",
|
|
855
|
+
fn: "resolveRules"
|
|
856
|
+
}),
|
|
857
|
+
check("config-retired-rule", "warning", false, {
|
|
858
|
+
module: "src/rules/settings.ts",
|
|
859
|
+
fn: "resolveRules"
|
|
860
|
+
})
|
|
861
|
+
]
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
kind: "invariant",
|
|
865
|
+
id: "waivers",
|
|
866
|
+
title: "A per-place waiver names a waivable rule, gives a reason, and covers something.",
|
|
867
|
+
elements: ["prose"],
|
|
868
|
+
doc: "README.md#waiving-a-rule-at-one-place",
|
|
869
|
+
summary: "`{/* 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.",
|
|
870
|
+
checks: [
|
|
871
|
+
check("waiver-malformed", "error", true, { module: "src/rules/waivers.ts", fn: "collectWaivers" }),
|
|
872
|
+
check("waiver-invalid", "error", true, { module: "src/rules/waivers.ts", fn: "collectWaivers" }),
|
|
873
|
+
check("waiver-unused", "warning", true, { module: "src/rules/waivers.ts", fn: "applyWaivers" })
|
|
874
|
+
]
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
kind: "invariant",
|
|
878
|
+
id: "curriculum",
|
|
879
|
+
title: "The curriculum graph is well-formed.",
|
|
880
|
+
elements: ["changelog"],
|
|
881
|
+
doc: "structure.md#the-names",
|
|
882
|
+
summary: "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.",
|
|
883
|
+
checks: [
|
|
884
|
+
...[
|
|
885
|
+
"invalid-id",
|
|
886
|
+
"duplicate-id",
|
|
887
|
+
"unknown-reference",
|
|
888
|
+
"duplicate-reference",
|
|
889
|
+
"self-prerequisite",
|
|
890
|
+
"cycle",
|
|
891
|
+
"lesson-order",
|
|
892
|
+
"track-order",
|
|
893
|
+
"assessment-coverage",
|
|
894
|
+
"invalid-assessment",
|
|
895
|
+
"review-state",
|
|
896
|
+
"teaches-support"
|
|
897
|
+
].map(
|
|
898
|
+
(code) => check(code, "error", false, {
|
|
899
|
+
module: "src/curriculum/validation.ts",
|
|
900
|
+
fn: "validateCurriculum"
|
|
901
|
+
})
|
|
902
|
+
),
|
|
903
|
+
...[
|
|
904
|
+
"alignment-unknown-competency",
|
|
905
|
+
"alignment-unknown-lesson",
|
|
906
|
+
"alignment-introduction",
|
|
907
|
+
"alignment-unavailable",
|
|
908
|
+
"alignment-review-state"
|
|
909
|
+
].map(
|
|
910
|
+
(code) => check(code, "error", false, {
|
|
911
|
+
module: "src/reference/curriculum-alignment.ts",
|
|
912
|
+
fn: "validateNotationAlignment"
|
|
913
|
+
})
|
|
914
|
+
)
|
|
915
|
+
]
|
|
916
|
+
}
|
|
917
|
+
];
|
|
918
|
+
const CATALOG = [...RULES, ...INVARIANTS];
|
|
919
|
+
const BY_ID = new Map(CATALOG.map((entry) => [entry.id, entry]));
|
|
920
|
+
const OWNER_BY_CODE = (() => {
|
|
921
|
+
const map = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const entry of CATALOG) {
|
|
923
|
+
for (const { code } of entry.checks) {
|
|
924
|
+
const previous = map.get(code);
|
|
925
|
+
if (previous !== void 0) {
|
|
926
|
+
throw new Error(
|
|
927
|
+
`rule catalog: code "${code}" is owned by both ${previous.id} and ${entry.id}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
map.set(code, entry);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return map;
|
|
934
|
+
})();
|
|
935
|
+
function ownerOf(code) {
|
|
936
|
+
return OWNER_BY_CODE.get(code);
|
|
937
|
+
}
|
|
938
|
+
function ruleIdOf(code) {
|
|
939
|
+
const owner = OWNER_BY_CODE.get(code);
|
|
940
|
+
if (owner !== void 0) return owner.kind === "rule" ? owner.id : void 0;
|
|
941
|
+
return isCourseRuleId(code) ? code : void 0;
|
|
942
|
+
}
|
|
943
|
+
function entryById(id) {
|
|
944
|
+
return BY_ID.get(id);
|
|
945
|
+
}
|
|
946
|
+
function ruleById(id) {
|
|
947
|
+
const entry = BY_ID.get(id);
|
|
948
|
+
return entry?.kind === "rule" ? entry : void 0;
|
|
949
|
+
}
|
|
950
|
+
function rules() {
|
|
951
|
+
return RULES;
|
|
952
|
+
}
|
|
953
|
+
function retiredRules() {
|
|
954
|
+
return RETIRED;
|
|
955
|
+
}
|
|
956
|
+
function retiredRule(id) {
|
|
957
|
+
return RETIRED.find((rule) => rule.id === id);
|
|
958
|
+
}
|
|
959
|
+
function invariants() {
|
|
960
|
+
return INVARIANTS;
|
|
961
|
+
}
|
|
962
|
+
function enforcementOf(entry) {
|
|
963
|
+
if (entry.checks.length === 0) return "guide";
|
|
964
|
+
return entry.kind === "rule" && entry.unchecked !== void 0 ? "partial" : "script";
|
|
965
|
+
}
|
|
966
|
+
function ownedCodes() {
|
|
967
|
+
return [...OWNER_BY_CODE.keys()].sort();
|
|
968
|
+
}
|
|
969
|
+
export {
|
|
970
|
+
CATALOG,
|
|
971
|
+
ELEMENTS,
|
|
972
|
+
enforcementOf,
|
|
973
|
+
entryById,
|
|
974
|
+
invariants,
|
|
975
|
+
isCourseRuleId,
|
|
976
|
+
ownedCodes,
|
|
977
|
+
ownerOf,
|
|
978
|
+
retiredRule,
|
|
979
|
+
retiredRules,
|
|
980
|
+
ruleById,
|
|
981
|
+
ruleIdOf,
|
|
982
|
+
rules
|
|
983
|
+
};
|