explico 0.11.1 → 0.13.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 +1292 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +11 -4
- package/dist/compiler/astro.js +48 -15
- package/dist/compiler/cli.d.ts +27 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +33 -2
- package/dist/compiler/collections.js +91 -19
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +15 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +115 -8
- package/dist/compiler/manifest.js +221 -39
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +3 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +8 -2
- package/dist/reference/base-operators.mjs +8 -10
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figure-symbols.d.ts +32 -0
- package/dist/reference/figure-symbols.js +55 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
- package/dist/reference/gate-non-chapter-pages.js +156 -0
- package/dist/reference/gate-part-pages.d.ts +4 -2
- package/dist/reference/gate-part-pages.js +2 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-katex-validate.d.ts +43 -0
- package/dist/reference/lab-katex-validate.js +96 -0
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/lint-ignore.js +1 -1
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +39 -7
- package/dist/reference/page-notation.js +62 -10
- package/dist/reference/raw-floats.mjs +37 -13
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +115 -11
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +275 -23
- package/dist/reference/render-lab-math.d.ts +13 -2
- package/dist/reference/render-lab-math.js +18 -6
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +223 -32
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +100 -27
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +33 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +115 -10
- package/docs/authoring/migrating.md +878 -0
- package/docs/authoring/notation.md +285 -30
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +84 -45
- package/docs/authoring/structure.md +67 -2
- package/docs/authoring/visualizations.md +167 -25
- package/package.json +12 -7
- package/src/compiler/astro.ts +72 -21
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +158 -27
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +387 -40
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/CourseContents.astro +6 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +18 -9
- package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/disclosure/print-as-shown.ts +42 -6
- package/src/components/figure/Figure.astro +17 -1
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/figure/figure-state.ts +26 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +300 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +66 -8
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +14 -22
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +8 -10
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figure-symbols.ts +117 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-non-chapter-pages.ts +241 -0
- package/src/reference/gate-part-pages.ts +1 -1
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-katex-validate.ts +156 -0
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/lint-ignore.ts +4 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +114 -14
- package/src/reference/raw-floats.mjs +37 -13
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +170 -9
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +275 -23
- package/src/reference/render-lab-math.ts +40 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +241 -31
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +322 -83
|
@@ -97,6 +97,13 @@ export function describeHiddenContent(
|
|
|
97
97
|
const CONTAINERS =
|
|
98
98
|
'.keyed-figure, .keyed-table, .diagram, .memo-block, .compact-examples, .compact-example, .assessment-shell, .short-note';
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* A short note's prose belongs to the paragraph it opens in, but a numbered
|
|
102
|
+
* float or equation inside it is numbered like any other, and hidden with
|
|
103
|
+
* the section: those count, and a float is named, as in a `<Fold>`.
|
|
104
|
+
*/
|
|
105
|
+
const FLOAT_CONTAINERS = CONTAINERS.replace(', .short-note', '');
|
|
106
|
+
|
|
100
107
|
/** Whether any ancestor of `element` strictly between it and `root` matches `selector`. */
|
|
101
108
|
function hasAncestorWithin(element: Element, root: Element, selector: string): boolean {
|
|
102
109
|
for (
|
|
@@ -127,25 +134,26 @@ export function collectHiddenContent(
|
|
|
127
134
|
headings: string,
|
|
128
135
|
{ nameFloats = true }: { readonly nameFloats?: boolean } = {},
|
|
129
136
|
): HiddenContent {
|
|
130
|
-
const outside = (selector: string, alsoExclude = '') =>
|
|
137
|
+
const outside = (selector: string, alsoExclude = '', containers = CONTAINERS) =>
|
|
131
138
|
Array.from(body.querySelectorAll(selector)).filter(
|
|
132
139
|
(element) =>
|
|
133
|
-
!hasAncestorWithin(element, body,
|
|
140
|
+
!hasAncestorWithin(element, body, containers) &&
|
|
134
141
|
!(alsoExclude && hasAncestorWithin(element, body, alsoExclude)),
|
|
135
142
|
);
|
|
143
|
+
const numbered = (selector: string) => outside(selector, '', FLOAT_CONTAINERS);
|
|
136
144
|
const itemsIn = (container: string, item: string) =>
|
|
137
145
|
outside(container).reduce(
|
|
138
146
|
(total, element) => total + element.querySelectorAll(item).length,
|
|
139
147
|
0,
|
|
140
148
|
);
|
|
141
149
|
const named = nameFloats
|
|
142
|
-
?
|
|
150
|
+
? numbered('.keyed-figure, .keyed-table, .diagram').flatMap((element) => {
|
|
143
151
|
const float = floatOf(element);
|
|
144
152
|
return float?.short ? [float] : [];
|
|
145
153
|
})
|
|
146
154
|
: [];
|
|
147
155
|
const unnamed = (selector: string) =>
|
|
148
|
-
|
|
156
|
+
numbered(selector).filter(
|
|
149
157
|
(element) => !named.some((float) => float.anchorId === element.id),
|
|
150
158
|
).length;
|
|
151
159
|
|
|
@@ -161,7 +169,7 @@ export function collectHiddenContent(
|
|
|
161
169
|
).length,
|
|
162
170
|
// A loose list's `<li><p>` is part of the list, not a paragraph of its own.
|
|
163
171
|
paragraphs: outside('p', 'ul, ol').length,
|
|
164
|
-
equations:
|
|
172
|
+
equations: numbered('.katex-display').length,
|
|
165
173
|
figures: unnamed('.keyed-figure'),
|
|
166
174
|
// Keyed tables, plus any raw Markdown table (warned about, but possible).
|
|
167
175
|
tables: unnamed('.keyed-table') + outside('table').length,
|
|
@@ -49,7 +49,7 @@ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
|
|
|
49
49
|
}
|
|
50
50
|
if (!number || !anchorId) {
|
|
51
51
|
throw new Error(
|
|
52
|
-
|
|
52
|
+
`Table is missing its computed \`number\`/\`anchorId\`. remark-notation numbers a <Table> only on a lesson page, at its top level or in a <Memo>, <ShortNote> or <Fold>: elsewhere (a worked example) write a plain Markdown table, and on a page that is not a chapter (rule S10) link the chapter's table. At a lesson's top level, this page is not running remark-notation with the manifest's \`tables\` (see astro.config.mjs).`,
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
---
|
|
@@ -68,19 +68,6 @@ if (!number || !anchorId) {
|
|
|
68
68
|
href={`#${anchorId}`}
|
|
69
69
|
aria-label={`Table ${number}`}>Table {number}</a
|
|
70
70
|
>
|
|
71
|
-
<a
|
|
72
|
-
class="float-caption__anchor"
|
|
73
|
-
href={`#${anchorId}`}
|
|
74
|
-
aria-label={`Copy the link to Table ${number}`}
|
|
75
|
-
title="Copy link"
|
|
76
|
-
data-copy-link={`Table ${number}`}
|
|
77
|
-
>
|
|
78
|
-
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
|
79
|
-
><path
|
|
80
|
-
d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"
|
|
81
|
-
></path></svg
|
|
82
|
-
>
|
|
83
|
-
</a>
|
|
84
71
|
{short && <b class="float-caption__title">{captionTitle(short)}</b>}{short && ' '}<span
|
|
85
72
|
class="keyed-table__caption-text float-caption__text">{caption}</span
|
|
86
73
|
>
|
|
@@ -201,6 +188,8 @@ if (!number || !anchorId) {
|
|
|
201
188
|
</style>
|
|
202
189
|
|
|
203
190
|
<script>
|
|
191
|
+
import { tableTruncation } from './table-truncation';
|
|
192
|
+
|
|
204
193
|
/**
|
|
205
194
|
* Table row show/hide pagination
|
|
206
195
|
* (docs/plans/collapsible-disclosure.md Part 5) — three states per table,
|
|
@@ -274,12 +263,15 @@ if (!number || !anchorId) {
|
|
|
274
263
|
if (totalRows === 0) return;
|
|
275
264
|
|
|
276
265
|
// Long enough to also need truncation, on top of the minimize toggle
|
|
277
|
-
// every table gets below: more rows than the
|
|
278
|
-
// `disclosure.tables.truncateAfter
|
|
279
|
-
// many rows, half from each end.
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
266
|
+
// every table gets below (`tableTruncation`): more rows than the
|
|
267
|
+
// course's `disclosure.tables.truncateAfter`, and at least two to hide.
|
|
268
|
+
// A truncated table shows up to that many rows, half from each end.
|
|
269
|
+
const truncation = tableTruncation(
|
|
270
|
+
totalRows,
|
|
271
|
+
Number(figure.dataset.truncateAfter) || 6,
|
|
272
|
+
);
|
|
273
|
+
const paginated = truncation !== undefined;
|
|
274
|
+
const keepEachEnd = truncation?.keepEachEnd ?? 0;
|
|
283
275
|
|
|
284
276
|
const caption = figure.querySelector<HTMLElement>(
|
|
285
277
|
':scope > .keyed-table__caption',
|
|
@@ -295,7 +287,7 @@ if (!number || !anchorId) {
|
|
|
295
287
|
let minimizeAll: HTMLButtonElement | null = null;
|
|
296
288
|
|
|
297
289
|
if (paginated) {
|
|
298
|
-
const hiddenCount =
|
|
290
|
+
const hiddenCount = truncation.hidden;
|
|
299
291
|
const headerCells = table.tHead?.rows[0]?.cells ?? rows[0].cells;
|
|
300
292
|
const colCount =
|
|
301
293
|
Array.from(headerCells).reduce(
|
|
@@ -321,7 +313,7 @@ if (!number || !anchorId) {
|
|
|
321
313
|
const moreLabel = document.createElement('span');
|
|
322
314
|
moreLabel.className = 'keyed-table__more-label';
|
|
323
315
|
moreLabel.textContent =
|
|
324
|
-
|
|
316
|
+
`… ${hiddenCount} more rows …`;
|
|
325
317
|
moreCell.append(moreLabel);
|
|
326
318
|
moreRow.append(moreCell);
|
|
327
319
|
tbody.insertBefore(moreRow, rows[keepEachEnd]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a long table starts (`disclosure.tables.truncateAfter`): the rows kept
|
|
3
|
+
* at each end and how many the "… N more rows …" line stands for, or
|
|
4
|
+
* `undefined` when the table shows whole — it has no more rows than the
|
|
5
|
+
* setting, or truncating it would hide a single row, which the line is as
|
|
6
|
+
* tall as and would cost the reader a click to see.
|
|
7
|
+
*/
|
|
8
|
+
export function tableTruncation(
|
|
9
|
+
totalRows: number,
|
|
10
|
+
truncateAfter: number,
|
|
11
|
+
): { readonly keepEachEnd: number; readonly hidden: number } | undefined {
|
|
12
|
+
const keepEachEnd = Math.floor(truncateAfter / 2);
|
|
13
|
+
const hidden = totalRows - keepEachEnd * 2;
|
|
14
|
+
return totalRows > truncateAfter && hidden >= 2 ? { keepEachEnd, hidden } : undefined;
|
|
15
|
+
}
|
package/src/content-config.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { defineCollection } from 'astro:content';
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
|
-
import { z } from 'astro/zod';
|
|
4
3
|
import { docsSchema } from '@astrojs/starlight/schema';
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
abbreviationSchema,
|
|
6
|
+
assessmentSchema,
|
|
7
|
+
competencySchema,
|
|
8
|
+
lessonFrontmatterSchema,
|
|
9
|
+
notationSchema,
|
|
10
|
+
sourceSchema,
|
|
11
|
+
trackSchema,
|
|
12
|
+
} from './compiler/content-schemas';
|
|
13
|
+
|
|
14
|
+
// The schemas live in `compiler/content-schemas.ts`, where the manifest walk
|
|
15
|
+
// checks every record against them too (`validate:content` rejects what this
|
|
16
|
+
// build would). This file only wires them to Astro's loaders.
|
|
7
17
|
|
|
8
18
|
// Phase F: content lives in the top-level `content/` folder, not `src/content/`.
|
|
9
19
|
// `docsLoader()` is hard-wired to `<srcDir>/content/docs`, so the `docs`
|
|
@@ -14,186 +24,9 @@ const docsGlob = glob({
|
|
|
14
24
|
pattern: '**/[^_]*.{md,mdx}',
|
|
15
25
|
});
|
|
16
26
|
|
|
17
|
-
const editorialStatus = z.enum(['draft', 'in-review', 'reviewed']);
|
|
18
|
-
const id = z.string().regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Notation `meaning` / `summary`: plain prose (no `$`, `\`, backticks), and —
|
|
22
|
-
* the C1b safeguard — substantive, not a placeholder. A short or blocklisted
|
|
23
|
-
* phrase fails the build so a local symbol cannot get a glossary-style panel
|
|
24
|
-
* for a throwaway definition.
|
|
25
|
-
*/
|
|
26
|
-
const notationProse = z
|
|
27
|
-
.string()
|
|
28
|
-
.min(1)
|
|
29
|
-
.refine((value) => !/[\\$`]/.test(value), {
|
|
30
|
-
message:
|
|
31
|
-
'Notation meaning/summary must be plain prose; render the symbol or formula in its dedicated field.',
|
|
32
|
-
})
|
|
33
|
-
.refine(isSubstantiveMeaning, {
|
|
34
|
-
message:
|
|
35
|
-
'Notation meaning must be substantive prose (at least four words, not a placeholder).',
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const notationAlignment = z.discriminatedUnion('kind', [
|
|
39
|
-
z.object({
|
|
40
|
-
kind: z.literal('competency'),
|
|
41
|
-
introducedByCompetency: id,
|
|
42
|
-
introducedInLesson: id,
|
|
43
|
-
}),
|
|
44
|
-
z.object({
|
|
45
|
-
kind: z.literal('general'),
|
|
46
|
-
rationale: z.string().min(1),
|
|
47
|
-
}),
|
|
48
|
-
]);
|
|
49
|
-
const notationAlignmentDefault = notationAlignment.default({
|
|
50
|
-
kind: 'general',
|
|
51
|
-
rationale: 'General notation.',
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* A citation on a notation definition: a bare id (locator pending, see D6/g) or
|
|
56
|
-
* `{id, locator}` — a per-claim locator verified against the cited text.
|
|
57
|
-
*/
|
|
58
|
-
const notationSource = z.union([
|
|
59
|
-
id,
|
|
60
|
-
z.object({ id, locator: z.string().min(1) }),
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* One notation entry shape (Phase C1b), shared by page-local `notation.local`
|
|
65
|
-
* and the standalone `notation` collection.
|
|
66
|
-
*
|
|
67
|
-
* Authored: `key`, `latex`, `meaning`, optional `formula`, optional `units`
|
|
68
|
-
* (or `dimensionless: true`), `seeAlso`, `sources` (`{id, locator}`),
|
|
69
|
-
* optional `alignment` (defaults to `general`), optional `label`.
|
|
70
|
-
*
|
|
71
|
-
* The display label defaults to the humanized `key` (see `reference/label.ts`);
|
|
72
|
-
* `label` is authored only where that reads wrong. The `notationProse`
|
|
73
|
-
* refinement rejects an under-specified `meaning`.
|
|
74
|
-
*/
|
|
75
|
-
/**
|
|
76
|
-
* A **gloss** on a notation card: a symbol the card's own `formula` needs in
|
|
77
|
-
* order to be written down, carrying a name and nothing else (D15).
|
|
78
|
-
*
|
|
79
|
-
* A gloss is deliberately cheaper than a card. It has no description, no
|
|
80
|
-
* sources, no curriculum alignment, no review state, and no glossary card of
|
|
81
|
-
* its own; it is not a node in the curriculum graph and never creates a
|
|
82
|
-
* reference edge. Its key is derived, not authored — `<card key>.<slug(name)>`
|
|
83
|
-
* — so a gloss is scoped to the card that declares it and two cards may each
|
|
84
|
-
* gloss `T`. Because a formula spells LaTeX and never a key, **promoting** a
|
|
85
|
-
* gloss is exactly: delete this line, add `content/notation/<key>.md`.
|
|
86
|
-
*
|
|
87
|
-
* `units` is optional and stays a bare unit string: "T — time" is ambiguous in
|
|
88
|
-
* a corpus with several `T`s, "T — time, years from valuation" is not, and it
|
|
89
|
-
* is still one line. Prose belongs on a card.
|
|
90
|
-
*/
|
|
91
|
-
const notationGloss = z.object({
|
|
92
|
-
latex: z.string().min(1),
|
|
93
|
-
name: z
|
|
94
|
-
.string()
|
|
95
|
-
.min(1)
|
|
96
|
-
.refine((value) => glossKeySegment(value) !== undefined, {
|
|
97
|
-
message:
|
|
98
|
-
'A gloss name must contain letters or digits so a key can be derived from it.',
|
|
99
|
-
})
|
|
100
|
-
// The symbol goes in `latex`, the name names it. A name carrying LaTeX is
|
|
101
|
-
// the one unambiguous way to get this field wrong, so it blocks; a name
|
|
102
|
-
// that is merely long is a Tier-3 `gloss-name-shape` warning instead,
|
|
103
|
-
// because a complicated quantity may honestly need several words.
|
|
104
|
-
.refine((value) => !/[\\$`\n]/.test(value), {
|
|
105
|
-
message:
|
|
106
|
-
'A gloss name is a plain noun phrase; put the symbol in `latex`, not in the name.',
|
|
107
|
-
}),
|
|
108
|
-
units: z.string().min(1).optional(),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const notationEntryShape = z.object({
|
|
112
|
-
key: id,
|
|
113
|
-
latex: z.string().min(1),
|
|
114
|
-
meaning: notationProse,
|
|
115
|
-
formula: z.string().min(1).optional(),
|
|
116
|
-
units: z.string().min(1).optional(),
|
|
117
|
-
dimensionless: z.literal(true).optional(),
|
|
118
|
-
seeAlso: z.array(id).default([]),
|
|
119
|
-
sources: z.array(notationSource).default([]),
|
|
120
|
-
alignment: notationAlignmentDefault,
|
|
121
|
-
label: z.string().min(1).optional(),
|
|
122
|
-
glosses: z.array(notationGloss).default([]),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const localNotationDefinition = notationEntryShape;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* One `notation.suspend` item (rule N22): a decoration (`'`, `\hat`, …) that
|
|
129
|
-
* is not an operator on this page. The loader (`collections.ts`) checks the
|
|
130
|
-
* name against the decorations the resolver knows.
|
|
131
|
-
*/
|
|
132
|
-
const notationSuspension = z.object({
|
|
133
|
-
operator: z.string().min(1),
|
|
134
|
-
reason: z.string().min(1).optional(),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const lessonNotation = z
|
|
138
|
-
.object({
|
|
139
|
-
// `notation.uses` is retired (D3): a lesson pulls a shared key into scope
|
|
140
|
-
// by referencing it in prose (`[[key]]`) or math (`\explain`).
|
|
141
|
-
local: z.array(localNotationDefinition).default([]),
|
|
142
|
-
suspend: z.array(notationSuspension).default([]),
|
|
143
|
-
})
|
|
144
|
-
.default({ local: [], suspend: [] });
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Lesson frontmatter — reduced shape (Phase C1 schema, C2 codemod applied).
|
|
148
|
-
*
|
|
149
|
-
* Authored: `title`, `description` (Starlight base), `teaches` (ordered — the
|
|
150
|
-
* curriculum contract, never derived), `assumptions`,
|
|
151
|
-
* `studyMinutes` (optional reader-facing estimate),
|
|
152
|
-
* `notation.local` (page-local symbols only).
|
|
153
|
-
* Artifact: `editorialStatus` (human-set trust flag).
|
|
154
|
-
* Derived (in `content/collections.ts` + `content/lesson-derivation.ts`, never
|
|
155
|
-
* authored): `lessonId` (doc slug), `requires` (direct competency-DAG
|
|
156
|
-
* prerequisites of `teaches`, minus `teaches`), `sources` (`[@…]`
|
|
157
|
-
* occurrences), `assessments` (colocated `<lesson>.checks.yml`),
|
|
158
|
-
* `sidebar` order (track order).
|
|
159
|
-
* Dropped: `aiAssisted`, `lastReviewed`, `riskTier` (git history +
|
|
160
|
-
* `editorialStatus` + `NEEDS_SOURCE` carry provenance);
|
|
161
|
-
* `estimatedMinutes`, whose reader-facing half returned as the
|
|
162
|
-
* optional `studyMinutes` once part pages had somewhere to total
|
|
163
|
-
* it; `notation.uses` (D3 — a shared symbol is imported by using
|
|
164
|
-
* it).
|
|
165
|
-
*/
|
|
166
|
-
/**
|
|
167
|
-
* How long this chapter takes to study: `40`, or a `[low, high]` range in
|
|
168
|
-
* minutes. It is a reader-facing estimate the author makes, aggregated by
|
|
169
|
-
* `<PartOverview>` and `<CourseContents>` into a part and course total — not
|
|
170
|
-
* the provenance field `estimatedMinutes` used to be, and nothing derives it.
|
|
171
|
-
*
|
|
172
|
-
* A part only reports a total when every one of its chapters states one, so a
|
|
173
|
-
* half-annotated course shows the chapters it knows about and no misleading
|
|
174
|
-
* sum.
|
|
175
|
-
*/
|
|
176
|
-
const studyMinutes = z
|
|
177
|
-
.union([
|
|
178
|
-
z.number().int().positive(),
|
|
179
|
-
z.tuple([z.number().int().positive(), z.number().int().positive()]),
|
|
180
|
-
])
|
|
181
|
-
.refine((value) => !Array.isArray(value) || value[0] <= value[1], {
|
|
182
|
-
message:
|
|
183
|
-
'studyMinutes as a range is [low, high]; the low end cannot exceed the high end.',
|
|
184
|
-
});
|
|
185
|
-
|
|
186
27
|
const docs = defineCollection({
|
|
187
28
|
loader: docsGlob,
|
|
188
|
-
schema: docsSchema({
|
|
189
|
-
extend: z.object({
|
|
190
|
-
editorialStatus: editorialStatus.default('draft'),
|
|
191
|
-
teaches: z.array(id).default([]),
|
|
192
|
-
assumptions: z.array(z.string().min(1)).default([]),
|
|
193
|
-
studyMinutes: studyMinutes.optional(),
|
|
194
|
-
notation: lessonNotation,
|
|
195
|
-
}),
|
|
196
|
-
}),
|
|
29
|
+
schema: docsSchema({ extend: lessonFrontmatterSchema }),
|
|
197
30
|
});
|
|
198
31
|
|
|
199
32
|
const competencies = defineCollection({
|
|
@@ -201,160 +34,35 @@ const competencies = defineCollection({
|
|
|
201
34
|
pattern: '**/*.json',
|
|
202
35
|
base: './content/competencies',
|
|
203
36
|
}),
|
|
204
|
-
schema:
|
|
205
|
-
id,
|
|
206
|
-
title: z.string().min(1),
|
|
207
|
-
domain: id,
|
|
208
|
-
facet: z.enum([
|
|
209
|
-
'knowledge',
|
|
210
|
-
'calculation',
|
|
211
|
-
'interpretation',
|
|
212
|
-
'convention',
|
|
213
|
-
'risk',
|
|
214
|
-
]),
|
|
215
|
-
outcome: z.string().min(1),
|
|
216
|
-
prerequisites: z.array(id).default([]),
|
|
217
|
-
evidence: z.object({
|
|
218
|
-
minimumIndependentItems: z.number().int().min(1),
|
|
219
|
-
requiresTransfer: z.boolean(),
|
|
220
|
-
requiresUnassistedPass: z.boolean(),
|
|
221
|
-
}),
|
|
222
|
-
misconceptions: z.array(z.string().min(1)).default([]),
|
|
223
|
-
editorialStatus,
|
|
224
|
-
}),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const assessmentItemBase = z.object({
|
|
228
|
-
id,
|
|
229
|
-
competencyId: id,
|
|
230
|
-
evidenceKind: z.enum(['direct', 'transfer']),
|
|
231
|
-
prompt: z.string().min(1),
|
|
232
|
-
explanation: z.string().min(1),
|
|
37
|
+
schema: competencySchema,
|
|
233
38
|
});
|
|
234
39
|
|
|
235
|
-
/**
|
|
236
|
-
* `AssessmentSet.astro` renders the numbered heading itself — `<h2>Knowledge
|
|
237
|
-
* check {number} — {title}</h2>` — so `title` must be just the topic. A title
|
|
238
|
-
* that repeats the "Knowledge check" label duplicates it on every page that
|
|
239
|
-
* uses `AssessmentSet` (the standalone `AssessmentRunner` heading fallback
|
|
240
|
-
* reads fine either way, so this only bites the common path).
|
|
241
|
-
*/
|
|
242
|
-
const assessmentTitle = z
|
|
243
|
-
.string()
|
|
244
|
-
.min(1)
|
|
245
|
-
.refine((value) => !/^knowledge check\b/i.test(value), {
|
|
246
|
-
message:
|
|
247
|
-
'Assessment title must not start with "Knowledge check" — AssessmentSet already renders "Knowledge check {number} — {title}", so a title with the same prefix duplicates it. Use just the topic, e.g. "AIC and KL risk".',
|
|
248
|
-
});
|
|
249
|
-
|
|
250
40
|
const assessments = defineCollection({
|
|
251
41
|
loader: glob({
|
|
252
42
|
pattern: '**/*.json',
|
|
253
43
|
base: './content/assessments',
|
|
254
44
|
}),
|
|
255
|
-
schema:
|
|
256
|
-
id,
|
|
257
|
-
title: assessmentTitle,
|
|
258
|
-
editorialStatus,
|
|
259
|
-
items: z
|
|
260
|
-
.array(
|
|
261
|
-
z.discriminatedUnion('type', [
|
|
262
|
-
assessmentItemBase.extend({
|
|
263
|
-
type: z.literal('numeric'),
|
|
264
|
-
answer: z.object({
|
|
265
|
-
value: z.number(),
|
|
266
|
-
tolerance: z.number().positive(),
|
|
267
|
-
}),
|
|
268
|
-
}),
|
|
269
|
-
assessmentItemBase.extend({
|
|
270
|
-
type: z.literal('single-choice'),
|
|
271
|
-
options: z
|
|
272
|
-
.array(
|
|
273
|
-
z.object({
|
|
274
|
-
id,
|
|
275
|
-
label: z.string().min(1),
|
|
276
|
-
}),
|
|
277
|
-
)
|
|
278
|
-
.min(2),
|
|
279
|
-
correctOptionId: id,
|
|
280
|
-
}),
|
|
281
|
-
]),
|
|
282
|
-
)
|
|
283
|
-
.min(1),
|
|
284
|
-
}),
|
|
45
|
+
schema: assessmentSchema,
|
|
285
46
|
});
|
|
286
47
|
|
|
287
48
|
const tracks = defineCollection({
|
|
288
49
|
loader: glob({ pattern: '**/*.json', base: './content/tracks' }),
|
|
289
|
-
schema:
|
|
290
|
-
id,
|
|
291
|
-
title: z.string().min(1),
|
|
292
|
-
description: z.string().min(1),
|
|
293
|
-
audience: z.string().min(1),
|
|
294
|
-
entryAssumptions: z.array(z.string().min(1)),
|
|
295
|
-
lessons: z.array(id).min(1),
|
|
296
|
-
editorialStatus,
|
|
297
|
-
}),
|
|
50
|
+
schema: trackSchema,
|
|
298
51
|
});
|
|
299
52
|
|
|
300
53
|
const sources = defineCollection({
|
|
301
54
|
loader: glob({ pattern: '**/*.json', base: './content/sources' }),
|
|
302
|
-
schema:
|
|
303
|
-
id,
|
|
304
|
-
type: z.enum([
|
|
305
|
-
'primary',
|
|
306
|
-
'official-guidance',
|
|
307
|
-
'paper',
|
|
308
|
-
'book',
|
|
309
|
-
'secondary',
|
|
310
|
-
]),
|
|
311
|
-
title: z.string().min(1),
|
|
312
|
-
authors: z.array(z.string().min(1)).optional(),
|
|
313
|
-
organization: z.string().min(1).optional(),
|
|
314
|
-
edition: z.string().optional(),
|
|
315
|
-
publisher: z.string().optional(),
|
|
316
|
-
year: z.number().int().optional(),
|
|
317
|
-
isbn: z.string().optional(),
|
|
318
|
-
url: z.url().optional(),
|
|
319
|
-
locator: z.string().optional(),
|
|
320
|
-
accessed: z.coerce.date().optional(),
|
|
321
|
-
licenseNotes: z.string().optional(),
|
|
322
|
-
editorialStatus,
|
|
323
|
-
}),
|
|
55
|
+
schema: sourceSchema,
|
|
324
56
|
});
|
|
325
57
|
|
|
326
|
-
/**
|
|
327
|
-
* Standalone notation collection — the one `notationEntry` shape plus the
|
|
328
|
-
* shared-only extras: `domain`, `aliases`, its own `editorialStatus`, and a
|
|
329
|
-
* Markdown body.
|
|
330
|
-
*/
|
|
331
58
|
const notation = defineCollection({
|
|
332
59
|
loader: glob({ pattern: '**/*.md', base: './content/notation' }),
|
|
333
|
-
schema:
|
|
334
|
-
domain: id,
|
|
335
|
-
aliases: z.array(z.string().min(1)).default([]),
|
|
336
|
-
editorialStatus,
|
|
337
|
-
aiAssisted: z.boolean().default(false),
|
|
338
|
-
}),
|
|
60
|
+
schema: notationSchema,
|
|
339
61
|
});
|
|
340
62
|
|
|
341
|
-
/**
|
|
342
|
-
* Abbreviations table (rule N14) — deliberately a lighter tier than a
|
|
343
|
-
* `notation` card: just a name resolution, no `formula`, `units`, or
|
|
344
|
-
* curriculum `alignment`. `abbr` is the canonical written form (`OLS`);
|
|
345
|
-
* `aliases` are other written forms of the same abbreviation that should
|
|
346
|
-
* resolve the same way (`w.r.t.` might alias `wrt`).
|
|
347
|
-
*/
|
|
348
63
|
const abbreviations = defineCollection({
|
|
349
64
|
loader: glob({ pattern: '**/*.md', base: './content/abbreviations' }),
|
|
350
|
-
schema:
|
|
351
|
-
key: id,
|
|
352
|
-
abbr: z.string().min(1),
|
|
353
|
-
expansion: notationProse,
|
|
354
|
-
aliases: z.array(z.string().min(1)).default([]),
|
|
355
|
-
domain: id.optional(),
|
|
356
|
-
editorialStatus: editorialStatus.default('draft'),
|
|
357
|
-
}),
|
|
65
|
+
schema: abbreviationSchema,
|
|
358
66
|
});
|
|
359
67
|
|
|
360
68
|
export const collections = {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { citationsInLinks, citedIds, withoutLinkCitations } from '../reference/citation-format.mjs';
|
|
2
|
+
|
|
1
3
|
export interface CompetencyDefinition {
|
|
2
4
|
readonly id: string;
|
|
3
5
|
readonly title: string;
|
|
@@ -11,7 +13,6 @@ export interface CompetencyDefinition {
|
|
|
11
13
|
|
|
12
14
|
export interface LessonDefinition {
|
|
13
15
|
readonly id: string;
|
|
14
|
-
readonly status: 'draft' | 'in-review' | 'reviewed';
|
|
15
16
|
readonly requires: readonly string[];
|
|
16
17
|
readonly teaches: readonly string[];
|
|
17
18
|
readonly assessments: readonly string[];
|
|
@@ -25,8 +26,17 @@ interface AssessmentItemBase {
|
|
|
25
26
|
readonly id: string;
|
|
26
27
|
readonly competencyId: string;
|
|
27
28
|
readonly evidenceKind: 'direct' | 'transfer';
|
|
29
|
+
readonly prompt?: string;
|
|
30
|
+
readonly explanation?: string;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A `[[…]]` reference or an `[@…]` citation in a check's text. A knowledge
|
|
35
|
+
* check renders its prompt, explanation and option labels as text and math
|
|
36
|
+
* only, so either would reach the reader exactly as written.
|
|
37
|
+
*/
|
|
38
|
+
const CHECK_TEXT_REFERENCE = /\[\[[^\]\n]*\]\]|\[@[^\]\n]*\]/;
|
|
39
|
+
|
|
30
40
|
export interface NumericAssessmentItem extends AssessmentItemBase {
|
|
31
41
|
readonly type: 'numeric';
|
|
32
42
|
readonly answer: { readonly value: number; readonly tolerance: number };
|
|
@@ -45,7 +55,6 @@ export interface AssessmentDefinition {
|
|
|
45
55
|
|
|
46
56
|
export interface SourceDefinition {
|
|
47
57
|
readonly id: string;
|
|
48
|
-
readonly editorialStatus: 'draft' | 'in-review' | 'reviewed';
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
export interface TrackDefinition {
|
|
@@ -73,9 +82,23 @@ export interface CurriculumIssue {
|
|
|
73
82
|
| 'track-order'
|
|
74
83
|
| 'assessment-coverage'
|
|
75
84
|
| 'invalid-assessment'
|
|
76
|
-
| '
|
|
77
|
-
| 'teaches-support'
|
|
85
|
+
| 'missing-assumptions'
|
|
86
|
+
| 'teaches-support'
|
|
87
|
+
| 'retired-key'
|
|
88
|
+
| 'content-schema'
|
|
89
|
+
| 'part-empty'
|
|
90
|
+
| 'lesson-outside-parts'
|
|
91
|
+
| 'unsupported-key'
|
|
92
|
+
| 'ignored-key'
|
|
93
|
+
| 'markup-in-prop'
|
|
94
|
+
| 'citation-in-link';
|
|
78
95
|
readonly message: string;
|
|
96
|
+
/**
|
|
97
|
+
* The content file the issue is about, when it is about one file rather
|
|
98
|
+
* than the graph (`retired-key`, `content-schema`), and the 1-based line in it.
|
|
99
|
+
*/
|
|
100
|
+
readonly file?: string;
|
|
101
|
+
readonly line?: number;
|
|
79
102
|
/**
|
|
80
103
|
* `error` (default) blocks the build; `warning` is reported but non-blocking.
|
|
81
104
|
* New checks enter as warnings and are promoted once they run clean on real
|
|
@@ -122,13 +145,8 @@ function citedSourceIds(body: string | undefined): Set<string> {
|
|
|
122
145
|
const withoutCode = body
|
|
123
146
|
.replace(/```[\s\S]*?```/g, '')
|
|
124
147
|
.replace(/`[^`\n]*`/g, '');
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/\[@([a-z0-9][a-z0-9.-]*)(?:;[^\]]*)?\]/g,
|
|
128
|
-
)) {
|
|
129
|
-
ids.add((match[1] ?? '').trim());
|
|
130
|
-
}
|
|
131
|
-
return ids;
|
|
148
|
+
// A citation in a link's text renders as typed, so it cites nothing.
|
|
149
|
+
return citedIds(withoutLinkCitations(withoutCode)) as Set<string>;
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
function findDuplicates(values: readonly string[]): readonly string[] {
|
|
@@ -314,21 +332,24 @@ export function validateCurriculum(
|
|
|
314
332
|
}
|
|
315
333
|
}
|
|
316
334
|
const cited = citedSourceIds(lesson.body);
|
|
335
|
+
if (lesson.body !== undefined) {
|
|
336
|
+
const blank = (code: string) => code.replace(/[^\n]/g, ' ');
|
|
337
|
+
const prose = lesson.body.replace(/```[\s\S]*?```/g, blank).replace(/`[^`\n]*`/g, blank);
|
|
338
|
+
for (const { text } of citationsInLinks(prose)) {
|
|
339
|
+
issues.push({
|
|
340
|
+
kind: 'citation-in-link',
|
|
341
|
+
message:
|
|
342
|
+
`${lesson.id} puts the citation ${text} inside a link's text, where it prints as typed: ` +
|
|
343
|
+
`a link cannot hold the citation's own link. Cite after the link: [the words](https://…) ${text}.`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
317
347
|
for (const sourceId of lesson.sources) {
|
|
318
|
-
|
|
319
|
-
if (!source) {
|
|
348
|
+
if (!sources.has(sourceId)) {
|
|
320
349
|
issues.push({
|
|
321
350
|
kind: 'unknown-reference',
|
|
322
351
|
message: `${lesson.id} references unknown source ${sourceId}`,
|
|
323
352
|
});
|
|
324
|
-
} else if (
|
|
325
|
-
lesson.status === 'reviewed' &&
|
|
326
|
-
source.editorialStatus !== 'reviewed'
|
|
327
|
-
) {
|
|
328
|
-
issues.push({
|
|
329
|
-
kind: 'review-state',
|
|
330
|
-
message: `${lesson.id} is reviewed but source ${sourceId} is ${source.editorialStatus}`,
|
|
331
|
-
});
|
|
332
353
|
}
|
|
333
354
|
if (lesson.body !== undefined && !cited.has(sourceId)) {
|
|
334
355
|
issues.push({
|
|
@@ -347,7 +368,7 @@ export function validateCurriculum(
|
|
|
347
368
|
}
|
|
348
369
|
if (lesson.teaches.length > 0 && lesson.assumptions.length === 0) {
|
|
349
370
|
issues.push({
|
|
350
|
-
kind: '
|
|
371
|
+
kind: 'missing-assumptions',
|
|
351
372
|
message: `${lesson.id} teaches quantitative content but declares no assumptions`,
|
|
352
373
|
});
|
|
353
374
|
}
|
|
@@ -385,14 +406,41 @@ export function validateCurriculum(
|
|
|
385
406
|
message: `${assessment.id}/${item.id} has unknown correct option ${item.correctOptionId}`,
|
|
386
407
|
});
|
|
387
408
|
}
|
|
409
|
+
} else if (!Number.isFinite(item.answer.value)) {
|
|
410
|
+
issues.push({
|
|
411
|
+
kind: 'invalid-assessment',
|
|
412
|
+
message: `${assessment.id}/${item.id} answer.value must be a number`,
|
|
413
|
+
});
|
|
388
414
|
} else if (
|
|
389
|
-
!Number.isFinite(item.answer.value) ||
|
|
390
415
|
!Number.isFinite(item.answer.tolerance) ||
|
|
391
416
|
item.answer.tolerance <= 0
|
|
392
417
|
) {
|
|
393
418
|
issues.push({
|
|
394
419
|
kind: 'invalid-assessment',
|
|
395
|
-
message:
|
|
420
|
+
message:
|
|
421
|
+
`${assessment.id}/${item.id} answer.tolerance must be a number greater than 0, ` +
|
|
422
|
+
`not ${String(item.answer.tolerance)}: an answer within it of answer.value is correct. ` +
|
|
423
|
+
'For an exact whole-number answer, use a tolerance below one half, such as 0.4.',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const texts: [string, string | undefined][] = [
|
|
428
|
+
['prompt', item.prompt],
|
|
429
|
+
['explanation', item.explanation],
|
|
430
|
+
...(item.type === 'single-choice'
|
|
431
|
+
? item.options.map((option): [string, string] => [`option ${option.id}`, option.label])
|
|
432
|
+
: []),
|
|
433
|
+
];
|
|
434
|
+
for (const [field, text] of texts) {
|
|
435
|
+
const found = text === undefined ? null : CHECK_TEXT_REFERENCE.exec(text);
|
|
436
|
+
if (!found) continue;
|
|
437
|
+
issues.push({
|
|
438
|
+
kind: 'invalid-assessment',
|
|
439
|
+
message:
|
|
440
|
+
`${assessment.id}/${item.id} ${field} contains ${found[0]}, which a knowledge ` +
|
|
441
|
+
'check shows exactly as written: its text renders math, not references or ' +
|
|
442
|
+
'citations. Name the equation, section or source in words, or cite it in the ' +
|
|
443
|
+
'lesson text the check follows.',
|
|
396
444
|
});
|
|
397
445
|
}
|
|
398
446
|
|