explico 0.5.0 → 0.7.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +295 -7
  2. package/README.md +16 -2
  3. package/dist/compiler/astro.d.ts +13 -2
  4. package/dist/compiler/astro.js +39 -0
  5. package/dist/compiler/cli.d.ts +22 -3
  6. package/dist/compiler/cli.js +71 -10
  7. package/dist/compiler/course-config.d.ts +31 -1
  8. package/dist/compiler/course-config.js +7 -0
  9. package/dist/compiler/exports/standalone-html.js +13 -0
  10. package/dist/compiler/logging.d.ts +109 -0
  11. package/dist/compiler/logging.js +107 -0
  12. package/dist/compiler/manifest.d.ts +16 -4
  13. package/dist/compiler/manifest.js +7 -3
  14. package/dist/reference/base-abbreviations.mjs +8 -2
  15. package/dist/reference/consistency.d.ts +8 -10
  16. package/dist/reference/consistency.js +3 -47
  17. package/dist/reference/delimiter-size.d.ts +39 -0
  18. package/dist/reference/delimiter-size.js +177 -0
  19. package/dist/reference/diagrams-validate.d.ts +1 -1
  20. package/dist/reference/equations-validate.d.ts +2 -2
  21. package/dist/reference/equations-validate.js +1 -1
  22. package/dist/reference/figures-validate.d.ts +1 -1
  23. package/dist/reference/gate-heading-length.d.ts +12 -0
  24. package/dist/reference/gate-heading-length.js +79 -0
  25. package/dist/reference/gate-math.d.ts +8 -2
  26. package/dist/reference/gate-math.js +30 -7
  27. package/dist/reference/keyed-float-validate.d.ts +2 -2
  28. package/dist/reference/keyed-float-validate.js +1 -1
  29. package/dist/reference/math-glyphs.d.mts +12 -0
  30. package/dist/reference/math-glyphs.mjs +183 -42
  31. package/dist/reference/references.js +2 -1
  32. package/dist/reference/registry.js +1 -1
  33. package/dist/reference/rehype-base-links.d.mts +23 -0
  34. package/dist/reference/rehype-base-links.mjs +40 -0
  35. package/dist/reference/remark-abbreviations.mjs +10 -12
  36. package/dist/reference/remark-citation.d.mts +5 -1
  37. package/dist/reference/remark-citation.mjs +0 -0
  38. package/dist/reference/render-lab-math.d.ts +18 -1
  39. package/dist/reference/render-lab-math.js +10 -3
  40. package/dist/reference/tables-validate.d.ts +1 -1
  41. package/dist/reference/types.d.ts +1 -1
  42. package/docs/VERSIONING.md +24 -4
  43. package/docs/authoring/README.md +14 -2
  44. package/docs/authoring/agents.md +7 -3
  45. package/docs/authoring/components.md +4 -1
  46. package/docs/authoring/migrating.md +477 -8
  47. package/docs/authoring/notation.md +194 -7
  48. package/docs/authoring/prose.md +64 -0
  49. package/docs/authoring/structure-rules.md +69 -0
  50. package/docs/authoring/visualizations.md +2 -2
  51. package/package.json +1 -1
  52. package/src/compiler/astro.ts +89 -0
  53. package/src/compiler/cli.ts +144 -12
  54. package/src/compiler/course-config.ts +40 -0
  55. package/src/compiler/exports/standalone-html.ts +15 -0
  56. package/src/compiler/logging.ts +248 -0
  57. package/src/compiler/manifest.ts +20 -3
  58. package/src/components/diagram/Diagram.astro +0 -55
  59. package/src/components/memo/Memo.astro +2 -5
  60. package/src/components/memo/ShortNote.astro +2 -5
  61. package/src/components/notation/NotationFormula.astro +199 -10
  62. package/src/components/notation/NotationGlossary.astro +89 -58
  63. package/src/components/notation/NotationLayer.astro +238 -156
  64. package/src/components/scroll-animation.ts +160 -0
  65. package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
  66. package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
  67. package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
  68. package/src/components/starlight/LessonFooter.astro +5 -1
  69. package/src/content-config.ts +16 -1
  70. package/src/reference/base-abbreviations.mjs +8 -2
  71. package/src/reference/consistency.ts +10 -66
  72. package/src/reference/delimiter-size.ts +301 -0
  73. package/src/reference/diagrams-validate.ts +1 -1
  74. package/src/reference/equations-validate.ts +3 -3
  75. package/src/reference/figures-validate.ts +1 -1
  76. package/src/reference/gate-heading-length.ts +131 -0
  77. package/src/reference/gate-math.ts +48 -8
  78. package/src/reference/keyed-float-validate.ts +3 -3
  79. package/src/reference/math-glyphs.mjs +183 -42
  80. package/src/reference/references.ts +2 -1
  81. package/src/reference/registry.ts +1 -1
  82. package/src/reference/rehype-base-links.mjs +40 -0
  83. package/src/reference/remark-abbreviations.mjs +10 -12
  84. package/src/reference/remark-citation.mjs +0 -0
  85. package/src/reference/render-lab-math.ts +24 -1
  86. package/src/reference/tables-validate.ts +1 -1
  87. package/src/reference/types.ts +1 -1
  88. package/src/styles/global.css +120 -15
@@ -131,6 +131,61 @@ _How to apply._ See the engine README's notation section and the `notation`
131
131
  collection schema. A letter that exists only so one entry's own `formula` can
132
132
  be stated is a **gloss**, not a card — it creates no curriculum edge.
133
133
 
134
+ Do not satisfy this rule by dumping every `[[key]]` a page uses into one
135
+ trailing "Notation used on this page" (or similarly named) section near the
136
+ end of the lesson. `LessonFooter.astro` already injects an interactive
137
+ "Page notation" panel — built from the page's actual notation manifest, with
138
+ hover/click cards — into every lesson's footer automatically; a hand-written
139
+ list repeats the same term set as flat, non-interactive prose immediately
140
+ above it. Introduce each shared symbol inline, in the sentence where it is
141
+ first defined or used, the way `[[eq-key]]` and other cross-references are
142
+ already used inline elsewhere in these lessons. The completeness gate only
143
+ checks that a `[[key]]`/`\explain{key}` appears *somewhere* in the page body
144
+ (document order does not matter to it), but inline-at-first-use is what keeps
145
+ the rendered page free of a redundant list — and matches what a reader
146
+ actually wants: the symbol explained where it shows up, not gathered into an
147
+ appendix.
148
+
149
+ A page's own `notation.local` entries (declared in frontmatter) are already in
150
+ scope everywhere on that page for the *math completeness gate* — a bare
151
+ occurrence of a local symbol never trips "Unresolved notation" even with zero
152
+ `[[key]]`/`\explain` references. But a **different** check,
153
+ `assertLocalNotationParity` (run at `astro build`, not `pnpm validate:content`),
154
+ still requires every `notation.local` entry to be *referenced* somewhere in
155
+ the body — otherwise the build fails with "Stale Explico manifest for
156
+ ‹lesson›: lesson notation.local has […], but the page notation bundle has
157
+ […]." So every local entry still needs one `\explain{key}{…}` (or `[[key]]`)
158
+ mention, even though the math gate alone wouldn't ask for it.
159
+
160
+ The auto-annotator (`resolveMathGlyphs` in `math-glyphs.mjs`) re-derives every
161
+ `\explain{…}` marker's insertion point from KaTeX's own parsed offsets, then
162
+ re-applies those offsets to the raw source. Three shapes used to corrupt that
163
+ offset math badly enough to either silently drop a symbol (no tooltip, no
164
+ error) or emit unparseable output: a bare roman-label symbol (`\mathrm{SSR}`)
165
+ could never auto-resolve at all regardless of where it sat on the page; a bare
166
+ symbol immediately adjacent to an explicit `\explain{…}{…}` call could have
167
+ its span swallow the whole following command; and a canonical whole-form
168
+ match could be found _through_ an explicit call's own wrapping and extend
169
+ past its span. All three are fixed at the resolver, not just detected — a
170
+ bare `\mathrm{SST}`, `\epsilon\explain{feature-function}{h}`, and similar
171
+ shapes now resolve correctly with no special authoring care required, and
172
+ `gate-math.ts` exercises the same resolver at `pnpm validate:content` time
173
+ (it reuses `resolveMathGlyphs` directly), so a regression here fails the
174
+ build immediately rather than passing silently through to a render-time
175
+ surprise.
176
+
177
+ One related shape is still worth knowing, because it is a genuine authoring
178
+ mistake rather than a resolver bug: a partial `\explain{key}{…}` wrap that
179
+ covers only part of the key's declared `latex`. If `latex: Z_N` and you write
180
+ `\explain{z-statistic}{Z}_N` (base wrapped, subscript left outside), the
181
+ resolver no longer corrupts the output — but the uncovered remainder (`N`)
182
+ has no binding of its own and correctly fails loudly as "Unresolved
183
+ notation," at `pnpm validate:content` time. Wrap the whole declared form —
184
+ `\explain{z-statistic}{Z_N}` — instead; bare `Z_N` elsewhere on the page then
185
+ resolves automatically. The same applies to accent-wrapped forms
186
+ (`latex: \widehat{\theta}`): wrap the accent and its argument together, not
187
+ just the inner letter.
188
+
134
189
  Math authored inside a nested Astro component is outside the lesson MDX scan,
135
190
  so the component must declare the formula text at render time. `Diagram` and
136
191
  `NotationFormula` do this automatically from the notation keys they actually
@@ -782,14 +837,15 @@ change to that file.
782
837
  ## N15
783
838
 
784
839
  **A nested visible delimiter — `(`, `[`, `\{`, or `|` inside another one of
785
- them — must use its `\left`/`\right` sizing form, not the bare character.**
840
+ them — must use a scalable sizing form, not the bare character.**
786
841
 
787
842
  `(`, `[`, `\{` (an escaped, visible brace — bare `{`/`}` is invisible LaTeX
788
843
  grouping and out of scope here), and `|` may be written bare only when they
789
844
  are not nested — not inside one another, and not inside a second instance of
790
845
  themselves. The moment one of them sits inside another, **every level of that
791
- nest** switches to the auto-sizing form: `\left(`/`\right)`, `\left[`/`\right]`,
792
- `\left\{`/`\right\}`, `\left|`/`\right|`.
846
+ nest** uses either the auto-sizing form (`\left(`/`\right)`,
847
+ `\left[`/`\right]`, `\left\{`/`\right\}`, `\left|`/`\right|`) or an explicit
848
+ manual pair from [N17](#n17).
793
849
 
794
850
  ```text
795
851
  \sin(2(90\text{°}-\theta)) ✗ two bare ( nested
@@ -818,10 +874,10 @@ author on a case that can render invisibly wrong.
818
874
  _How to apply._
819
875
 
820
876
  - Reach for `\left(`/`\right)` (or the bracket/brace/pipe equivalents) the
821
- moment a second one of these four glyphs would end up inside the first —
822
- regardless of which of the four it is, and regardless of whether the outer
823
- one is already sized. Sizing the outer pair does not exempt an inner bare
824
- one; both levels of a two-level nest need their own `\left`/`\right`.
877
+ moment a second one of these four glyphs would end up inside the first, then
878
+ apply [N17](#n17)'s manual sizes where necessary. Sizing the outer pair does
879
+ not exempt an inner bare one; both levels of a two-level nest need a sizing
880
+ command.
825
881
  - Mixing types counts as nesting too: `(a + [b])`, `\{a \mid (b)\}`, and
826
882
  `(|a|)` all need every level sized, exactly like nesting the same character
827
883
  twice.
@@ -854,3 +910,134 @@ _Exceptions._ None. A quoted source nesting bare delimiters still renders
854
910
  sized in this course, the same way N13 reparenthesizes a quoted `\sin x` —
855
911
  the sizing is a typesetting choice, not a claim about the source's own
856
912
  notation.
913
+
914
+ ---
915
+
916
+ ## N16
917
+
918
+ **In AI-generated mathematical content, alternate delimiter types for nested
919
+ grouping when their shape is stylistic; preserve every conventional or
920
+ semantic delimiter shape.**
921
+
922
+ For a nest whose delimiters do nothing except group an expression, prefer this
923
+ cycle from the inside out:
924
+
925
+ 1. `( ... )`
926
+ 2. `[ ... ]`
927
+ 3. `\{ ... \}`
928
+ 4. repeat the cycle if another grouping level is genuinely necessary.
929
+
930
+ This is a preference for generated notation, not a mechanical rewrite. Do not
931
+ change or alternate a delimiter whose type is part of the mathematical
932
+ notation. Preserve, among other conventional forms:
933
+
934
+ - function application such as `f(x)`;
935
+ - expectation notation such as `\mathbb{E}[X]`;
936
+ - intervals such as `[a,b]` and `(a,b]`;
937
+ - sets and set-builder notation;
938
+ - tuples and coordinates;
939
+ - commutators and probability notation;
940
+ - matrices, vectors, indexing, norms, and absolute values.
941
+
942
+ When there is doubt about whether the delimiter is semantic, keep the
943
+ conventional notation. Do not alternate merely to complete the cycle.
944
+
945
+ _Rationale._ Repeating the same shape at every purely stylistic grouping level
946
+ makes the boundaries harder to scan. Alternation can expose the grouping
947
+ structure, but only mathematical context can distinguish a disposable grouping
948
+ mark from notation whose shape carries meaning. That judgment belongs to the
949
+ authoring agent or reviewer, not a syntax linter.
950
+
951
+ _How to apply._ Decide whether each pair is stylistic before choosing its
952
+ shape. Alternate only the pairs that pass that test. Apply [N17](#n17)
953
+ separately so the visual size still increases from the inside out; shape and
954
+ size are independent decisions.
955
+
956
+ ```text
957
+ \Biggl[
958
+ a+
959
+ \biggl(
960
+ b+
961
+ \Bigl[
962
+ c+
963
+ \left(d+e\right)
964
+ \Bigr]
965
+ \biggr)
966
+ \Biggr] ✓
967
+ ```
968
+
969
+ Semantic notation stays intact even when stylistic grouping surrounds it:
970
+
971
+ ```text
972
+ \Biggl[
973
+ \mathbb{E}\!\Bigl[
974
+ f\!\left(X\right)
975
+ \Bigr]
976
+ +
977
+ \biggl(
978
+ a+b
979
+ \biggr)
980
+ \Biggr] ✓
981
+ ```
982
+
983
+ In particular, never turn `\mathbb{E}[X]` into another delimiter type merely
984
+ to alternate shapes.
985
+
986
+ _Enforcement._ Review-only. Delimiter semantics cannot be inferred reliably
987
+ from LaTeX syntax, so no automated check enforces alternation. The N17 linter
988
+ checks sizes only and never recommends changing a delimiter type.
989
+
990
+ _Exceptions._ Every conventional or semantically meaningful delimiter is
991
+ outside the alternation preference. Preserving established mathematical
992
+ notation takes precedence over the stylistic cycle.
993
+
994
+ ---
995
+
996
+ ## N17
997
+
998
+ **Every outer pair in a nest of scalable `()`, `[]`, or `\{\}` delimiters must
999
+ be visibly larger than every pair inside it.**
1000
+
1001
+ Use progressively larger standard manual sizes to make grouping depth visible
1002
+ without producing unnecessarily oversized marks:
1003
+
1004
+ 1. innermost baseline: `\left ... \right` (or `\bigl ... \bigr`);
1005
+ 2. next: `\Bigl ... \Bigr`;
1006
+ 3. next: `\biggl ... \biggr`;
1007
+ 4. outermost: `\Biggl ... \Biggr`.
1008
+
1009
+ The opening and closing commands change size only. Preserve each delimiter's
1010
+ shape: `[` remains `[`, `(` remains `(`, and `\{` remains `\{`.
1011
+
1012
+ _Rationale._ Two nested `\left...\right` pairs around similarly short content
1013
+ can render at the same apparent size, hiding the nesting that scalable
1014
+ delimiters were meant to clarify. A strict inside-to-outside hierarchy makes
1015
+ the depth legible and has an objective syntax-level repair through the four
1016
+ standard manual size levels.
1017
+
1018
+ _How to apply._ Start at the innermost scalable pair and assign each enclosing
1019
+ pair the next larger level. Four or fewer levels must have a strict increase at
1020
+ every level. If a valid expression needs more levels than the standard sizes
1021
+ can distinguish, restructure the expression rather than inventing a fifth
1022
+ size. This rule is independent of N16: semantic shapes stay semantic, but their
1023
+ sizes still increase when their scalable pairs are nested.
1024
+
1025
+ _Enforcement._ Tier-3 warning `delimiter-size-hierarchy`, emitted by
1026
+ `lintDelimiterSizes` (`src/reference/delimiter-size.ts`) through the normal
1027
+ math gate for lesson math, notation formulas, and assessment math. The linter
1028
+ uses a stack to parse arbitrarily deep `\left...\right`,
1029
+ `\bigl...\bigr`, `\Bigl...\Bigr`, `\biggl...\biggr`, and
1030
+ `\Biggl...\Biggr` pairs for `()`, `[]`, and `\{\}`. It reports the outer
1031
+ pair's source line and column and suggests the required larger manual size when
1032
+ one exists. Malformed pairs remain KaTeX syntax errors; the size linter does not
1033
+ guess at their structure.
1034
+
1035
+ The automated check deliberately does **not** infer mathematical semantics or
1036
+ lint delimiter-type alternation. Its suggestion repeats the exact opening and
1037
+ closing delimiter shapes it found.
1038
+
1039
+ _Exceptions._ None for a valid nest of scalable pairs with four or fewer
1040
+ levels. Beyond four levels, the warning remains but has no automatic size
1041
+ replacement; restructure the expression. Bare, non-nested semantic notation
1042
+ such as `f(x)` or `\mathbb{E}[X]` is outside this linter and remains governed by
1043
+ the mathematical convention and [N15](#n15)'s separate bare-nesting rule.
@@ -0,0 +1,64 @@
1
+ # Prose rules
2
+
3
+ `P` rules govern how a lesson's sentences are worded — choices a reader
4
+ meets in running text rather than in a symbol, a figure, or a heading.
5
+
6
+ See [README.md](README.md) for what "strong recommendation", "Build-enforced",
7
+ and "Review-only" mean here, and how to amend a rule.
8
+
9
+ ---
10
+
11
+ ## P1
12
+
13
+ **Never use "or" to give a thing a second name.**
14
+
15
+ When a sentence introduces an alternative name, synonym, or equivalent
16
+ expression for something, do not join the two with "or". Use "i.e.,", "also
17
+ called", "also known as", or parentheses, whichever fits the sentence.
18
+
19
+ ```md
20
+ <!-- "or" reads as a choice between two different things. -->
21
+
22
+ The residual or error term is what the model leaves unexplained.
23
+ Fit the model by ordinary least squares or OLS.
24
+
25
+ <!-- Each names one thing, and says so. -->
26
+
27
+ The residual (also called the error term) is what the model leaves unexplained.
28
+ Fit the model by ordinary least squares (OLS).
29
+ ```
30
+
31
+ _Rationale._ "Or" has two jobs in English: it separates alternatives ("a
32
+ sample or a population"), and it introduces a second name for the same thing.
33
+ Nothing in the sentence says which job it is doing, so a reader can take "the
34
+ residual or error term" as two different quantities to choose between. In a
35
+ statistics course that misreading is expensive, because many pairs that look
36
+ like synonyms are not ("variance or standard deviation", "estimate or
37
+ estimator"). A reader who takes a real distinction for a synonym, or the
38
+ reverse, learns the wrong thing without noticing.
39
+
40
+ _How to apply._ Decide whether the two expressions name the same thing or
41
+ different things.
42
+
43
+ - **Same thing** — rewrite with one of:
44
+ - _i.e.,_ when the second expression restates or defines the first: "the
45
+ estimator is unbiased, i.e., its expected value equals the parameter".
46
+ - _also called_ / _also known as_ when it is a second name in use: "the
47
+ coefficient of determination, also known as $R^2$".
48
+ - _parentheses_ when the second name is short and the sentence carries
49
+ on: "the mean squared error (MSE)".
50
+ - **Different things** — "or" is correct, and stays. If the sentence could
51
+ still be read as a synonym, name the choice: "either a sample or a
52
+ population", "whether the variance or the standard deviation is reported".
53
+
54
+ An abbreviation introduced in parentheses is still subject to
55
+ [N14](notation.md#n14): it has to be catalogued or explained.
56
+
57
+ _Enforcement._ Review-only. Whether "or" is a logical alternative or a
58
+ second name depends on what the sentence means, so no gate can decide it; a
59
+ reviewer does. An automated "or" scan would flag every legitimate
60
+ alternative.
61
+
62
+ _Exceptions._ A quotation, a source title, or a code identifier that contains
63
+ "or" is left as written. So is "or" inside an assessment answer choice that
64
+ really offers alternatives.
@@ -0,0 +1,69 @@
1
+ # Structure rules
2
+
3
+ `S` rules govern the shape of a lesson's headings and the short titles on its
4
+ keyed floats — the parts of a page a reader scans before reading prose. This
5
+ is not [structure.md](structure.md), which is the taxonomy these rules (and
6
+ the rest of the engine) speak in, not a rule set itself.
7
+
8
+ See [README.md](README.md) for what "strong recommendation", "Build-enforced",
9
+ and "Review-only" mean here, and how to amend a rule.
10
+
11
+ ---
12
+
13
+ ## S1
14
+
15
+ **No heading, and no figure/diagram/memo/short-note title, runs past 65
16
+ characters.**
17
+
18
+ This caps every Markdown heading (`##` through `######`), a lesson's own
19
+ `frontmatter.title` (rendered as the page's `<h1>`), and the short `title` /
20
+ `label` prop on `Figure`, `Diagram`, `Memo`, and `ShortNote` — the text shown
21
+ on a collapsed title bar, a diagram's caption line, or a chapter's sidebar
22
+ entry. `Table` is exempt: it has only a `caption`, no separate `title`, and
23
+ that caption is the LaTeX-convention full sentence shown above the table.
24
+
25
+ A heading or float title that runs long is usually doing a caption's job. Move
26
+ the rest of the sentence into the paragraph right after a heading, or into the
27
+ float's own `caption`.
28
+
29
+ ```mdx
30
+ <!-- Over the limit: the title carries the whole explanation. -->
31
+ <Figure
32
+ id="ar1-forecast-fan"
33
+ caption="Multi-step AR(1) forecast: the conditional mean (teal) decays geometrically to the unconditional mean while the Gaussian 95% band (shaded) widens toward the stationary dispersion Γ₀; the coral line is one seeded future path."
34
+ >
35
+
36
+ <!-- At the limit: a short label, the rest moved into `caption`. -->
37
+ <Figure
38
+ id="ar1-forecast-fan"
39
+ title="Multi-step AR(1) forecast"
40
+ caption="The conditional mean (teal) decays geometrically to the unconditional mean while the Gaussian 95% band (shaded) widens toward the stationary dispersion Γ₀; the coral line is one seeded future path."
41
+ >
42
+ ```
43
+
44
+ _Rationale._ A `Figure`'s `title` falls back to `caption` when omitted (see
45
+ `Figure.astro`), so an unset `title` on a long-captioned figure silently
46
+ becomes the text on the always-visible collapsible title bar — the reader
47
+ sees the entire caption twice, once as the "heading" and once again in
48
+ `<figcaption>` once they expand it. The same abuse happens with a heading
49
+ authored as a full sentence instead of a label: the sidebar, the table of
50
+ contents, and every anchor link now carry a paragraph instead of a title.
51
+ Sixty-five characters is roughly one sidebar line at normal zoom before
52
+ Starlight starts wrapping or truncating it.
53
+
54
+ _How to apply._ Write the heading or `title` as a short noun phrase (a topic,
55
+ not a claim about it); put the claim in the prose or the `caption`. If a
56
+ `Figure` needs no distinct title beyond its caption's own first clause,
57
+ extract that clause into `title` explicitly rather than omitting the prop and
58
+ relying on the fallback — an omitted `title` on a long caption is exactly the
59
+ failure this rule exists to catch.
60
+
61
+ _Enforcement._ Build-enforced (`gate-heading-length.ts`, `pnpm
62
+ validate:content`) for every Markdown heading in a lesson body and every
63
+ `Figure`/`Diagram`/`Memo`/`ShortNote` `title`/`label` prop. Review-only for a
64
+ lesson's `frontmatter.title` — the gate scans lesson body text and cannot see
65
+ frontmatter, so a long page title (`<h1>`) is caught by a reviewer, not the
66
+ build.
67
+
68
+ _Exceptions._ None — a heading or float title that must exceed the limit
69
+ should not be a heading or a float title; find the shorter label first.
@@ -53,8 +53,8 @@ the original diagram toward the full in-page region and shrink it back; it is
53
53
  not modal, follows the diagram's document position while the page scrolls, and
54
54
  adds internal vertical scrolling when the viewport is too short. The shrink
55
55
  control or Escape restores its original width. Clicking the title minimizes/
56
- restores the diagram and its text description; this state is remembered per
57
- page by default (`defaultOpen` / `rememberState` configure it).
56
+ restores the diagram; this state is remembered per page by default
57
+ (`defaultOpen` / `rememberState` configure it).
58
58
  The title also carries the same chain-link permalink treatment as headings.
59
59
 
60
60
  By default a laid-out width above `800px` emits a compile-time warning. Treat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explico",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -13,6 +13,9 @@ import {
13
13
  } from './manifest';
14
14
  import { missingNotationPayloadKeys } from '../reference/page-notation';
15
15
  import { writeStandaloneLessonExports } from './exports/standalone-html';
16
+ import remarkAbbreviations from '../reference/remark-abbreviations.mjs';
17
+ import remarkCitation from '../reference/remark-citation.mjs';
18
+ import remarkNotation from '../reference/remark-notation.mjs';
16
19
 
17
20
  // Vite keeps its server object across a restart. Do not accumulate watcher
18
21
  // callbacks each time Astro reloads its configuration.
@@ -132,6 +135,91 @@ export function assertNotationPayloadHtml(page: string, html: string): void {
132
135
  );
133
136
  }
134
137
 
138
+ type MarkdownConfig = AstroConfig['markdown'];
139
+
140
+ /**
141
+ * Every configured remark plugin, regardless of which of Astro's two
142
+ * mutually-exclusive wiring surfaces a course uses: the deprecated flat
143
+ * `markdown.remarkPlugins` array, or a `markdown.processor` (e.g.
144
+ * `unified({ remarkPlugins })`) whose own `options.remarkPlugins` a
145
+ * non-`unified` processor (e.g. `satteri()`) may not have at all.
146
+ */
147
+ function configuredRemarkPlugins(markdown: MarkdownConfig): readonly unknown[] {
148
+ const direct = markdown?.remarkPlugins ?? [];
149
+ const processorOptions = markdown?.processor?.options as
150
+ | { remarkPlugins?: unknown[] }
151
+ | undefined;
152
+ return [...direct, ...(processorOptions?.remarkPlugins ?? [])];
153
+ }
154
+
155
+ function remarkPluginIsWired(remarkPlugins: readonly unknown[], plugin: unknown): boolean {
156
+ return remarkPlugins.some(
157
+ (entry) => entry === plugin || (Array.isArray(entry) && entry[0] === plugin),
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Fail the build when a manifest-driven remark plugin has catalogued content
163
+ * but was never registered in Astro's markdown config — the "half-migrated"
164
+ * failure mode `docs/authoring/migrating.md` documents but, until now, never
165
+ * checked: `pnpm validate:content` stays green because the content-side gate
166
+ * (e.g. `gate-abbreviations.ts`'s N14 check) only scans prose, not Astro
167
+ * config, so a skipped plugin registration renders every trigger as plain
168
+ * text with no signal anywhere.
169
+ */
170
+ export function assertRemarkPluginsWired(
171
+ manifest: Pick<Manifest, 'abbreviations' | 'sources' | 'notation'>,
172
+ markdown: MarkdownConfig,
173
+ ): void {
174
+ const remarkPlugins = configuredRemarkPlugins(markdown);
175
+ const missing: string[] = [];
176
+
177
+ if (
178
+ manifest.abbreviations.length > 0 &&
179
+ !remarkPluginIsWired(remarkPlugins, remarkAbbreviations)
180
+ ) {
181
+ missing.push(
182
+ `remarkAbbreviations is missing from markdown.remarkPlugins, but ` +
183
+ `manifest.abbreviations has ${manifest.abbreviations.length} ` +
184
+ `entr${manifest.abbreviations.length === 1 ? 'y' : 'ies'}. Import it from ` +
185
+ `'explico/reference/remark-abbreviations.mjs' and add ` +
186
+ `[remarkAbbreviations, { abbreviations: manifest.abbreviations }] to ` +
187
+ `markdown.remarkPlugins — see docs/authoring/migrating.md#4-rule-n14.`,
188
+ );
189
+ }
190
+
191
+ if (manifest.sources.length > 0 && !remarkPluginIsWired(remarkPlugins, remarkCitation)) {
192
+ missing.push(
193
+ `remarkCitation is missing from markdown.remarkPlugins, but ` +
194
+ `manifest.sources has ${manifest.sources.length} ` +
195
+ `entr${manifest.sources.length === 1 ? 'y' : 'ies'}. Import it from ` +
196
+ `'explico/reference/remark-citation.mjs' and add ` +
197
+ `[remarkCitation, { sources: manifest.sources }] to markdown.remarkPlugins.`,
198
+ );
199
+ }
200
+
201
+ if (
202
+ manifest.notation.raw.length > 0 &&
203
+ !remarkPluginIsWired(remarkPlugins, remarkNotation)
204
+ ) {
205
+ missing.push(
206
+ `remarkNotation is missing from markdown.remarkPlugins, but ` +
207
+ `manifest.notation.raw has ${manifest.notation.raw.length} ` +
208
+ `entr${manifest.notation.raw.length === 1 ? 'y' : 'ies'}. Import it from ` +
209
+ `'explico/reference/remark-notation.mjs' and add ` +
210
+ `[remarkNotation, createRemarkNotationOptions(manifest)] to markdown.remarkPlugins.`,
211
+ );
212
+ }
213
+
214
+ if (missing.length === 0) return;
215
+ throw new Error(
216
+ `Explico manifest-driven remark plugin${missing.length === 1 ? ' is' : 's are'} ` +
217
+ `not wired into Astro's markdown config:\n${missing
218
+ .map((line) => ` - ${line}`)
219
+ .join('\n')}`,
220
+ );
221
+ }
222
+
135
223
  async function validateGeneratedNotationPayloads(directory: URL): Promise<void> {
136
224
  const root = fileURLToPath(directory);
137
225
  const walk = async (path: string): Promise<void> => {
@@ -200,6 +288,7 @@ export function explicoContentRefresh(
200
288
  name: 'explico-content-refresh',
201
289
  hooks: {
202
290
  'astro:config:setup': ({ command, config }) => {
291
+ assertRemarkPluginsWired(manifest, config.markdown);
203
292
  if (command === 'dev') {
204
293
  developmentConfig = { root: config.root, base: config.base };
205
294
  }