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.
- package/CHANGELOG.md +295 -7
- package/README.md +16 -2
- package/dist/compiler/astro.d.ts +13 -2
- package/dist/compiler/astro.js +39 -0
- package/dist/compiler/cli.d.ts +22 -3
- package/dist/compiler/cli.js +71 -10
- package/dist/compiler/course-config.d.ts +31 -1
- package/dist/compiler/course-config.js +7 -0
- package/dist/compiler/exports/standalone-html.js +13 -0
- package/dist/compiler/logging.d.ts +109 -0
- package/dist/compiler/logging.js +107 -0
- package/dist/compiler/manifest.d.ts +16 -4
- package/dist/compiler/manifest.js +7 -3
- package/dist/reference/base-abbreviations.mjs +8 -2
- package/dist/reference/consistency.d.ts +8 -10
- package/dist/reference/consistency.js +3 -47
- package/dist/reference/delimiter-size.d.ts +39 -0
- package/dist/reference/delimiter-size.js +177 -0
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +2 -2
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-heading-length.d.ts +12 -0
- package/dist/reference/gate-heading-length.js +79 -0
- package/dist/reference/gate-math.d.ts +8 -2
- package/dist/reference/gate-math.js +30 -7
- package/dist/reference/keyed-float-validate.d.ts +2 -2
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/math-glyphs.d.mts +12 -0
- package/dist/reference/math-glyphs.mjs +183 -42
- package/dist/reference/references.js +2 -1
- package/dist/reference/registry.js +1 -1
- package/dist/reference/rehype-base-links.d.mts +23 -0
- package/dist/reference/rehype-base-links.mjs +40 -0
- package/dist/reference/remark-abbreviations.mjs +10 -12
- package/dist/reference/remark-citation.d.mts +5 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/render-lab-math.d.ts +18 -1
- package/dist/reference/render-lab-math.js +10 -3
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +1 -1
- package/docs/VERSIONING.md +24 -4
- package/docs/authoring/README.md +14 -2
- package/docs/authoring/agents.md +7 -3
- package/docs/authoring/components.md +4 -1
- package/docs/authoring/migrating.md +477 -8
- package/docs/authoring/notation.md +194 -7
- package/docs/authoring/prose.md +64 -0
- package/docs/authoring/structure-rules.md +69 -0
- package/docs/authoring/visualizations.md +2 -2
- package/package.json +1 -1
- package/src/compiler/astro.ts +89 -0
- package/src/compiler/cli.ts +144 -12
- package/src/compiler/course-config.ts +40 -0
- package/src/compiler/exports/standalone-html.ts +15 -0
- package/src/compiler/logging.ts +248 -0
- package/src/compiler/manifest.ts +20 -3
- package/src/components/diagram/Diagram.astro +0 -55
- package/src/components/memo/Memo.astro +2 -5
- package/src/components/memo/ShortNote.astro +2 -5
- package/src/components/notation/NotationFormula.astro +199 -10
- package/src/components/notation/NotationGlossary.astro +89 -58
- package/src/components/notation/NotationLayer.astro +238 -156
- package/src/components/scroll-animation.ts +160 -0
- package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
- package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
- package/src/components/starlight/LessonFooter.astro +5 -1
- package/src/content-config.ts +16 -1
- package/src/reference/base-abbreviations.mjs +8 -2
- package/src/reference/consistency.ts +10 -66
- package/src/reference/delimiter-size.ts +301 -0
- package/src/reference/diagrams-validate.ts +1 -1
- package/src/reference/equations-validate.ts +3 -3
- package/src/reference/figures-validate.ts +1 -1
- package/src/reference/gate-heading-length.ts +131 -0
- package/src/reference/gate-math.ts +48 -8
- package/src/reference/keyed-float-validate.ts +3 -3
- package/src/reference/math-glyphs.mjs +183 -42
- package/src/reference/references.ts +2 -1
- package/src/reference/registry.ts +1 -1
- package/src/reference/rehype-base-links.mjs +40 -0
- package/src/reference/remark-abbreviations.mjs +10 -12
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/render-lab-math.ts +24 -1
- package/src/reference/tables-validate.ts +1 -1
- package/src/reference/types.ts +1 -1
- 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
|
|
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**
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
|
57
|
-
|
|
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.
|
|
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",
|
package/src/compiler/astro.ts
CHANGED
|
@@ -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
|
}
|