explico 0.2.0 → 0.3.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 +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +16 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +4 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +77 -2
- package/dist/compiler/manifest.js +115 -18
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +51 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +42 -11
- package/dist/reference/equations.mjs +58 -17
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/keyed-float-validate.d.ts +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/remark-notation.d.mts +12 -0
- package/dist/reference/remark-notation.mjs +199 -9
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +12 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +239 -17
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/notation/render-notation-math.ts +13 -2
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +4 -2
- package/src/components/table/Table.astro +104 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +83 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +6 -1
- package/src/reference/equations.mjs +58 -17
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/remark-notation.mjs +199 -9
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +115 -7
- package/src/components/starlight/EquationEnhancer.astro +0 -87
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Versioning rules
|
|
2
|
+
|
|
3
|
+
`C` rules govern the course's **knowledge version** — semver over the
|
|
4
|
+
learning-facing content, independent of the `explico` engine's own version. The
|
|
5
|
+
declared value is `about.version` in `content/course.config.ts`; the history and
|
|
6
|
+
the reader-facing notes are in `content/CHANGELOG.md`. Both are surfaced to
|
|
7
|
+
readers by the About `[?]` panel.
|
|
8
|
+
|
|
9
|
+
See [README.md](README.md) for what "Build-enforced" and "Review-only" mean here.
|
|
10
|
+
The engine's own versioning is a separate policy —
|
|
11
|
+
[`docs/VERSIONING.md`](../VERSIONING.md).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## C1
|
|
16
|
+
|
|
17
|
+
**Every content change lands with a `content/CHANGELOG.md` entry under
|
|
18
|
+
`## [Unreleased]`.**
|
|
19
|
+
|
|
20
|
+
A change to any lesson `.mdx`, notation entry, assessment, competency, source,
|
|
21
|
+
`content/domain/` file, or `course.config.ts` gets a bullet under
|
|
22
|
+
`## [Unreleased]` in the same commit.
|
|
23
|
+
|
|
24
|
+
_Rationale._ The knowledge version is meaningless without a record of what moved
|
|
25
|
+
between two values of it. A reader who sees "knowledge v1.4.0" in the `[?]` panel
|
|
26
|
+
can follow `changelogHref` to find out what 1.4.0 means.
|
|
27
|
+
|
|
28
|
+
_How to apply._ Keep a Changelog shape. One bullet per user-visible change,
|
|
29
|
+
written for a reader, not a commit log:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
## [Unreleased]
|
|
33
|
+
|
|
34
|
+
- Reworked the "Optimal Launch Angle" derivation to use the identity
|
|
35
|
+
`sin(2θ) = 2 sinθ cosθ` explicitly.
|
|
36
|
+
- New lesson: "Projectile Range with Drag" (motion.range-drag).
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
_Enforcement._ Review-only. Rule [C3](#c3) partially enforces it: a content
|
|
40
|
+
change with no matching version bump eventually trips the mismatch warning.
|
|
41
|
+
|
|
42
|
+
_Exceptions._ A pure typo fix in prose that changes no rendered symbol, no
|
|
43
|
+
number, and no outcome. When in doubt, add the bullet.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## C2
|
|
48
|
+
|
|
49
|
+
**The knowledge version is semver over learning-facing content.**
|
|
50
|
+
|
|
51
|
+
Pick the bump by the largest change in the release:
|
|
52
|
+
|
|
53
|
+
- **MAJOR** — a lesson, competency, or notation `key` is removed or renamed; a
|
|
54
|
+
stated learning outcome (`teaches`, a competency `outcome`) changes; a result
|
|
55
|
+
or convention a reader may have memorised changes.
|
|
56
|
+
- **MINOR** — something new and backward-compatible: a new lesson, section,
|
|
57
|
+
notation entry, assessment, or worked example.
|
|
58
|
+
- **PATCH** — a fix or clarification: corrected arithmetic, better prose, a added
|
|
59
|
+
or fixed `[@source]`, a non-semantic notation tweak.
|
|
60
|
+
|
|
61
|
+
_Rationale._ A downstream consumer of the content (an LMS import, a printed
|
|
62
|
+
reader, a course that forks this one) needs the same "did this break me?" signal
|
|
63
|
+
from the knowledge version that a library gets from a package version.
|
|
64
|
+
|
|
65
|
+
_How to apply._ Decide the bump when you move `## [Unreleased]` to a dated
|
|
66
|
+
heading ([C4](#c4)); set `about.version` to match.
|
|
67
|
+
|
|
68
|
+
_Enforcement._ Review-only.
|
|
69
|
+
|
|
70
|
+
_Exceptions._ None. If a change does not fit a tier, it is PATCH.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## C3
|
|
75
|
+
|
|
76
|
+
**`about.version` equals the newest `## [x.y.z]` heading in
|
|
77
|
+
`content/CHANGELOG.md`.**
|
|
78
|
+
|
|
79
|
+
_Rationale._ Two sources of the same fact must not drift. The manifest — and so
|
|
80
|
+
the `[?]` panel — reads `about.version`; a reader chasing "what changed" reads
|
|
81
|
+
the changelog. If they disagree, one is lying.
|
|
82
|
+
|
|
83
|
+
_How to apply._ On a release, edit both in the same commit ([C4](#c4)). Between
|
|
84
|
+
releases, `about.version` stays at the last released value while
|
|
85
|
+
`## [Unreleased]` accumulates bullets.
|
|
86
|
+
|
|
87
|
+
_Enforcement._ Build-enforced (Tier-3). `pnpm validate:content` emits
|
|
88
|
+
`knowledge-version-mismatch` when they differ, `knowledge-changelog-missing`
|
|
89
|
+
when there is no released heading, and `knowledge-version-invalid` when
|
|
90
|
+
`about.version` is not `x.y.z`. All are warnings — they never fail the build, but
|
|
91
|
+
CI may choose to.
|
|
92
|
+
|
|
93
|
+
_Exceptions._ None.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## C4
|
|
98
|
+
|
|
99
|
+
**On a release, move `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD` and bump
|
|
100
|
+
`about.version` in the same commit.**
|
|
101
|
+
|
|
102
|
+
_Rationale._ A release is the atomic moment the two records agree again
|
|
103
|
+
([C3](#c3)). Splitting it across commits leaves a window where the build warns
|
|
104
|
+
and the panel is wrong.
|
|
105
|
+
|
|
106
|
+
_How to apply._
|
|
107
|
+
|
|
108
|
+
1. Choose `x.y.z` from the accumulated `## [Unreleased]` bullets and [C2](#c2).
|
|
109
|
+
2. Rename the heading to `## [x.y.z] - <today>`; add a fresh empty
|
|
110
|
+
`## [Unreleased]` above it.
|
|
111
|
+
3. Set `about.version` in `content/course.config.ts` to `x.y.z`.
|
|
112
|
+
4. `pnpm validate:content` — no `knowledge-*` warning.
|
|
113
|
+
|
|
114
|
+
_Enforcement._ Review-only (the end state is checked by [C3](#c3)).
|
|
115
|
+
|
|
116
|
+
_Exceptions._ None.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Visualization rules
|
|
2
|
+
|
|
3
|
+
`V` rules govern anything a lesson draws: diagrams, static charts, and hydrated
|
|
4
|
+
interactive figures. The engine assumes no single plotting approach — these
|
|
5
|
+
rules make the choice per figure predictable and keep every figure accessible
|
|
6
|
+
and deterministic.
|
|
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
|
+
## V1
|
|
14
|
+
|
|
15
|
+
**Graphs and flowcharts use the `Diagram` component.**
|
|
16
|
+
|
|
17
|
+
Any node-and-edge picture — a derivation pipeline, a dependency graph, a state
|
|
18
|
+
machine, a decision tree — is built with
|
|
19
|
+
`explico/components/diagram/Diagram.astro`. It does build-time layout with
|
|
20
|
+
`@dagrejs/dagre` and renders KaTeX-labelled nodes as static SVG through the same
|
|
21
|
+
trust boundary as lesson math.
|
|
22
|
+
|
|
23
|
+
Do not add Mermaid, cytoscape, react-flow, elkjs, or any other runtime graph or
|
|
24
|
+
layout library. Nothing client-side lays out a graph.
|
|
25
|
+
|
|
26
|
+
_Rationale._ One diagram primitive means node math is gate-checked and
|
|
27
|
+
hover-explained exactly like prose math (an unresolved glyph fails the build
|
|
28
|
+
with the node id), zero runtime layout code ships, and diagrams render
|
|
29
|
+
identically in print and with JS off.
|
|
30
|
+
|
|
31
|
+
_How to apply._ Pass `nodes` / `edges` and, if any node has a `formula`, a
|
|
32
|
+
`scope` from `labMathScope` — see
|
|
33
|
+
`playground/content/labs/DerivationDiagram.astro`. A `formula` node's symbols
|
|
34
|
+
must resolve against the page glyph table, same as `NotationFormula`. If
|
|
35
|
+
`Diagram` genuinely cannot express the picture, raise it as an engine gap
|
|
36
|
+
rather than reaching for another library.
|
|
37
|
+
|
|
38
|
+
_Enforcement._ Review-only.
|
|
39
|
+
|
|
40
|
+
_Exceptions._ A one-off illustrative schematic that is not a graph (a labelled
|
|
41
|
+
photo, a geometric figure) is a [V2](#v2) figure, not a `Diagram`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## V2
|
|
46
|
+
|
|
47
|
+
**Static charts: hand-rolled SVG first, then D3, then Observable Plot — chosen
|
|
48
|
+
per figure.**
|
|
49
|
+
|
|
50
|
+
- **Hand-rolled inline SVG** is the default: zero dependency, covers a single
|
|
51
|
+
bespoke figure.
|
|
52
|
+
- **D3** once a figure needs real tick-labelled axes, log scales, or several
|
|
53
|
+
series sharing a scale.
|
|
54
|
+
- **Observable Plot** (`@observablehq/plot`) once a course is authoring many
|
|
55
|
+
charts per lecture and the per-chart boilerplate of D3 is the bottleneck.
|
|
56
|
+
|
|
57
|
+
Decide per figure. A lesson may hold an SVG sketch and a Plot chart side by
|
|
58
|
+
side. Whatever the choice, the chart is produced at build time or inside a
|
|
59
|
+
`client:*` island — never a CDN `<script>`.
|
|
60
|
+
|
|
61
|
+
_Rationale._ The cost curve is real: SVG is free but unscalable, D3 is powerful
|
|
62
|
+
but verbose, Plot is concise but a dependency and a bundle. Matching the tool to
|
|
63
|
+
the figure keeps both the bundle and the authoring effort down.
|
|
64
|
+
|
|
65
|
+
_How to apply._ For a static (non-interactive) chart, prefer rendering in an
|
|
66
|
+
`.astro` component so nothing hydrates. Reach for an island only when the figure
|
|
67
|
+
responds to input — then it is a [V3](#v3) figure.
|
|
68
|
+
|
|
69
|
+
_Enforcement._ Review-only.
|
|
70
|
+
|
|
71
|
+
_Exceptions._ None — this rule _is_ the exception process.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## V3
|
|
76
|
+
|
|
77
|
+
**Interactive figures: a hydrated island, the `renderLabMath` seam for
|
|
78
|
+
equations, and a text alternative.**
|
|
79
|
+
|
|
80
|
+
An interactive figure is a React island, hydrated `client:visible` (or
|
|
81
|
+
`client:idle`). It carries:
|
|
82
|
+
|
|
83
|
+
- **Gate-checked math.** Any equation shown in the figure goes through the
|
|
84
|
+
`labMathScope` / `renderLabMath` / `\slot{}` seam so its symbols are resolved
|
|
85
|
+
against the page's real notation at build time and the island only patches
|
|
86
|
+
numbers into pre-rendered KaTeX. See
|
|
87
|
+
`playground/content/labs/ProjectileRangeExplorer.tsx` +
|
|
88
|
+
`ProjectileRangeMath.astro`.
|
|
89
|
+
- **An accessible name and description** — `ariaLabel` / `ariaDescription` on
|
|
90
|
+
the plot, marks that are decorative set `ariaHidden`.
|
|
91
|
+
- **A text alternative.** A `<details>` block with the model's assumptions and a
|
|
92
|
+
small table of representative values, so the figure is usable with no JS and
|
|
93
|
+
by a screen reader.
|
|
94
|
+
- **Labelled controls.** Every control is a labelled `<input>`; where precision
|
|
95
|
+
matters, pair a `range` slider with a `number` field (see `RangeField` in
|
|
96
|
+
`ProjectileRangeExplorer.tsx`).
|
|
97
|
+
|
|
98
|
+
_Rationale._ The seam is the only reason interactive math is not a silent
|
|
99
|
+
exception to the completeness gate. The text alternative and aria wiring are the
|
|
100
|
+
difference between a figure and an inaccessible one.
|
|
101
|
+
|
|
102
|
+
_How to apply._ Keep the island's job to "patch numbers and redraw". Domain
|
|
103
|
+
math lives in a plain module (`playground/content/domain/motion.ts`), not in the
|
|
104
|
+
component.
|
|
105
|
+
|
|
106
|
+
_Enforcement._ Review-only.
|
|
107
|
+
|
|
108
|
+
_Exceptions._ None.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## V4
|
|
113
|
+
|
|
114
|
+
**A randomised figure exposes an adjustable, deterministic seed.**
|
|
115
|
+
|
|
116
|
+
Any figure that draws random numbers has a visible **seed** control — an
|
|
117
|
+
integer, default `0` — shown near the figure. The same seed always produces the
|
|
118
|
+
same figure.
|
|
119
|
+
|
|
120
|
+
_Rationale._ A figure that reshuffles on every render can't be pointed at ("look
|
|
121
|
+
at the outlier near x = 3"), can't be reproduced in a question, and can't be
|
|
122
|
+
regression-tested. A seed makes randomness a parameter the reader controls.
|
|
123
|
+
|
|
124
|
+
_How to apply._
|
|
125
|
+
|
|
126
|
+
- Thread the seed through a real PRNG (e.g. a small `mulberry32` /
|
|
127
|
+
`splitmix32`), never `Math.random()`.
|
|
128
|
+
- The seed is a control like any other — a `number` field, or a slider over a
|
|
129
|
+
small range plus a field.
|
|
130
|
+
- Label it plainly ("Random seed") and show its current value.
|
|
131
|
+
|
|
132
|
+
_Enforcement._ Review-only.
|
|
133
|
+
|
|
134
|
+
_Exceptions._ None. If the figure is deterministic, there is no seed to show.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## V5
|
|
139
|
+
|
|
140
|
+
**Axis ranges span the first 10 seeds, evaluated at the current values of every
|
|
141
|
+
other control.**
|
|
142
|
+
|
|
143
|
+
Fix each axis `domain` explicitly. Compute it as the min and max of the plotted
|
|
144
|
+
quantity over **seeds 0–9**, with every non-seed control at its current value.
|
|
145
|
+
Then:
|
|
146
|
+
|
|
147
|
+
- **Some points, for some seeds, will fall outside the drawn range.** That is
|
|
148
|
+
expected and fine — clip them. Do **not** autoscale the axis per seed: a
|
|
149
|
+
domain that jumps as the seed changes defeats the comparison the seed control
|
|
150
|
+
exists for.
|
|
151
|
+
- **Recompute the domain when any non-seed control changes** (a slider, a
|
|
152
|
+
toggle). Its old range no longer describes the new family of curves.
|
|
153
|
+
- **Do not recompute the domain when only the seed changes.** The range is a
|
|
154
|
+
property of the control setting, not of the current draw.
|
|
155
|
+
- Pad the computed range by a small fixed fraction (≈ 5%) so extremes don't sit
|
|
156
|
+
on the frame.
|
|
157
|
+
- Keep the sample count (`10`) a named constant so it can be tuned in one place.
|
|
158
|
+
|
|
159
|
+
_Rationale._ A reader steps through seeds to see how much the outcome varies. A
|
|
160
|
+
fixed frame makes that variation legible — points move, the axes don't. Ten
|
|
161
|
+
seeds is enough to size the frame for the typical spread without chasing every
|
|
162
|
+
tail.
|
|
163
|
+
|
|
164
|
+
_How to apply._ A `useMemo` keyed on all non-seed inputs (not the seed) builds
|
|
165
|
+
the domain:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const SEED_SAMPLE_COUNT = 10;
|
|
169
|
+
const RANGE_PAD = 0.05;
|
|
170
|
+
|
|
171
|
+
const yDomain = useMemo(() => {
|
|
172
|
+
let lo = Infinity;
|
|
173
|
+
let hi = -Infinity;
|
|
174
|
+
for (let seed = 0; seed < SEED_SAMPLE_COUNT; seed++) {
|
|
175
|
+
for (const point of series({ ...controls, seed })) {
|
|
176
|
+
if (point.y < lo) lo = point.y;
|
|
177
|
+
if (point.y > hi) hi = point.y;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const pad = (hi - lo) * RANGE_PAD || 1;
|
|
181
|
+
return [lo - pad, hi + pad] as const;
|
|
182
|
+
}, [controls.a, controls.b /* every non-seed control, not seed */]);
|
|
183
|
+
|
|
184
|
+
// Plot.plot({ y: { domain: yDomain, ... }, ... })
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Both axes get this treatment if both carry random spread. If one axis is a
|
|
188
|
+
deterministic sweep (angle 0–90°), give it its natural domain and apply the rule
|
|
189
|
+
only to the random axis.
|
|
190
|
+
|
|
191
|
+
_Enforcement._ Review-only.
|
|
192
|
+
|
|
193
|
+
_Exceptions._ A figure where the meaningful range is a known physical bound
|
|
194
|
+
(a probability on `[0, 1]`, an angle on `[0°, 360°]`) uses that bound instead —
|
|
195
|
+
state it in a comment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explico",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -27,8 +27,14 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"src",
|
|
30
|
-
"README.md"
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"docs/VERSIONING.md",
|
|
33
|
+
"docs/authoring"
|
|
31
34
|
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@dagrejs/dagre": "3.1.1"
|
|
37
|
+
},
|
|
32
38
|
"peerDependencies": {
|
|
33
39
|
"@astrojs/markdown-remark": "7.2.4",
|
|
34
40
|
"@astrojs/react": "6.0.4",
|
|
@@ -38,16 +44,25 @@
|
|
|
38
44
|
"react": "19.2.8",
|
|
39
45
|
"react-dom": "19.2.8",
|
|
40
46
|
"rehype-katex": "7.0.1",
|
|
41
|
-
"remark-math": "6.0.0"
|
|
47
|
+
"remark-math": "6.0.0",
|
|
48
|
+
"remark-mdx": "3.1.1"
|
|
42
49
|
},
|
|
43
50
|
"devDependencies": {
|
|
51
|
+
"@astrojs/markdown-remark": "7.2.4",
|
|
44
52
|
"@types/node": "24.13.3",
|
|
45
53
|
"@types/react": "19.2.18",
|
|
46
54
|
"@types/react-dom": "19.2.5",
|
|
55
|
+
"katex": "0.16.47",
|
|
56
|
+
"rehype-katex": "7.0.1",
|
|
57
|
+
"remark-math": "6.0.0",
|
|
58
|
+
"remark-mdx": "3.1.1",
|
|
47
59
|
"tsup": "8.5.1",
|
|
48
|
-
"typescript": "6.0.2"
|
|
60
|
+
"typescript": "6.0.2",
|
|
61
|
+
"vitest": "4.1.11"
|
|
49
62
|
},
|
|
50
63
|
"scripts": {
|
|
51
|
-
"build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json"
|
|
64
|
+
"build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest"
|
|
52
67
|
}
|
|
53
68
|
}
|
package/src/compiler/cli.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from '@astrojs/markdown-remark';
|
|
36
36
|
import rehypeKatex from 'rehype-katex';
|
|
37
37
|
import remarkMath from 'remark-math';
|
|
38
|
+
import remarkMdx from 'remark-mdx';
|
|
38
39
|
|
|
39
40
|
import { createNotationKatexOptions } from '../reference/katex-options.mjs';
|
|
40
41
|
import rehypeFailKatexErrors from '../reference/rehype-fail-katex-errors.mjs';
|
|
@@ -169,6 +170,25 @@ export function lessonDiagnostics(
|
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
for (const diagnostic of [
|
|
174
|
+
...manifest.diagnostics.tables,
|
|
175
|
+
...manifest.diagnostics.figures,
|
|
176
|
+
]) {
|
|
177
|
+
if (
|
|
178
|
+
diagnostic.lessonId !== id &&
|
|
179
|
+
diagnostic.file !== file &&
|
|
180
|
+
!mentions(diagnostic.message, id, file)
|
|
181
|
+
) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
out.push({
|
|
185
|
+
code: diagnostic.code,
|
|
186
|
+
severity: diagnostic.severity === 'warning' ? 'warning' : 'error',
|
|
187
|
+
message: diagnostic.message,
|
|
188
|
+
...withFile(diagnostic.file),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
for (const diagnostic of manifest.diagnostics.consistency) {
|
|
173
193
|
if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
|
|
174
194
|
out.push({
|
|
@@ -195,12 +215,19 @@ function processorFor(manifest: Manifest) {
|
|
|
195
215
|
syntaxHighlight: false,
|
|
196
216
|
smartypants: false,
|
|
197
217
|
remarkPlugins: [
|
|
218
|
+
// Syntax only (no JSX evaluation) — teaches the parser `<Table>` /
|
|
219
|
+
// `<Figure>` / `<CompactExample>` are `mdxJsxFlowElement` nodes, same
|
|
220
|
+
// as the real Astro/MDX build, so `remarkNotation` sees the same tree
|
|
221
|
+
// shape here that it does there (D7/F-tables-figures parity).
|
|
222
|
+
remarkMdx,
|
|
198
223
|
remarkMath,
|
|
199
224
|
[
|
|
200
225
|
remarkNotation,
|
|
201
226
|
{
|
|
202
227
|
definitions: manifest.notation.raw,
|
|
203
228
|
equations: manifest.equations.numbersBySlug,
|
|
229
|
+
tables: manifest.tables.numbersBySlug,
|
|
230
|
+
figures: manifest.figures.numbersBySlug,
|
|
204
231
|
},
|
|
205
232
|
],
|
|
206
233
|
[remarkCitation, { sources: manifest.sources }],
|
|
@@ -441,6 +468,8 @@ function domainSignaturesForSection(section: string): DomainFileSignatures[] {
|
|
|
441
468
|
// --- content status ------------------------------------------------------
|
|
442
469
|
|
|
443
470
|
export interface StatusReport {
|
|
471
|
+
/** The course's published knowledge version (`about.version`). */
|
|
472
|
+
readonly knowledgeVersion: string;
|
|
444
473
|
readonly counts: {
|
|
445
474
|
readonly lessons: number;
|
|
446
475
|
readonly competencies: number;
|
|
@@ -449,6 +478,8 @@ export interface StatusReport {
|
|
|
449
478
|
readonly tracks: number;
|
|
450
479
|
readonly notationDefinitions: number;
|
|
451
480
|
readonly keyedEquations: number;
|
|
481
|
+
readonly keyedTables: number;
|
|
482
|
+
readonly keyedFigures: number;
|
|
452
483
|
};
|
|
453
484
|
readonly draftDebt: {
|
|
454
485
|
readonly lessons: readonly string[];
|
|
@@ -492,18 +523,30 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
492
523
|
...manifest.diagnostics.alignment.map((d) => d.severity),
|
|
493
524
|
...manifest.diagnostics.math.map((d) => d.severity),
|
|
494
525
|
...manifest.diagnostics.equations.map((d) => d.severity),
|
|
526
|
+
...manifest.diagnostics.tables.map((d) => d.severity),
|
|
527
|
+
...manifest.diagnostics.figures.map((d) => d.severity),
|
|
495
528
|
...manifest.diagnostics.consistency.map(() => 'warning' as const),
|
|
529
|
+
...manifest.diagnostics.knowledge.map((d) => d.severity),
|
|
496
530
|
];
|
|
497
531
|
|
|
498
532
|
const keyedEquations = manifest.lessons.reduce(
|
|
499
533
|
(count, lesson) => count + lesson.equations.length,
|
|
500
534
|
0,
|
|
501
535
|
);
|
|
536
|
+
const keyedTables = manifest.lessons.reduce(
|
|
537
|
+
(count, lesson) => count + lesson.tables.length,
|
|
538
|
+
0,
|
|
539
|
+
);
|
|
540
|
+
const keyedFigures = manifest.lessons.reduce(
|
|
541
|
+
(count, lesson) => count + lesson.figures.length,
|
|
542
|
+
0,
|
|
543
|
+
);
|
|
502
544
|
const assessments = new Set(
|
|
503
545
|
manifest.lessons.flatMap((lesson) => lesson.assessments),
|
|
504
546
|
).size;
|
|
505
547
|
|
|
506
548
|
return {
|
|
549
|
+
knowledgeVersion: manifest.about.knowledge.version,
|
|
507
550
|
counts: {
|
|
508
551
|
lessons: manifest.lessons.length,
|
|
509
552
|
competencies: manifest.competencies.length,
|
|
@@ -512,6 +555,8 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
512
555
|
tracks: manifest.tracks.length,
|
|
513
556
|
notationDefinitions: manifest.notation.definitions.length,
|
|
514
557
|
keyedEquations,
|
|
558
|
+
keyedTables,
|
|
559
|
+
keyedFigures,
|
|
515
560
|
},
|
|
516
561
|
draftDebt: { lessons: draftLessons, notation: draftNotation },
|
|
517
562
|
orphans,
|
|
@@ -550,6 +595,8 @@ export interface ContextReport {
|
|
|
550
595
|
formula?: string;
|
|
551
596
|
}[];
|
|
552
597
|
readonly equations: readonly { key: string; number: string }[];
|
|
598
|
+
readonly tables: readonly { key: string; number: string }[];
|
|
599
|
+
readonly figures: readonly { key: string; number: string }[];
|
|
553
600
|
readonly sources: readonly {
|
|
554
601
|
id: string;
|
|
555
602
|
title: string;
|
|
@@ -618,6 +665,8 @@ export function contentContext(
|
|
|
618
665
|
: { formula: definition.formula }),
|
|
619
666
|
})),
|
|
620
667
|
equations: lesson.equations.map((equation) => ({ ...equation })),
|
|
668
|
+
tables: lesson.tables.map((table) => ({ ...table })),
|
|
669
|
+
figures: lesson.figures.map((figure) => ({ ...figure })),
|
|
621
670
|
sources: lesson.sources.map((id) => {
|
|
622
671
|
const record = sourceById.get(id) as Record<string, unknown> | undefined;
|
|
623
672
|
return {
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
} from '../reference/types';
|
|
37
37
|
import { isSubstantiveMeaning } from '../reference/prose';
|
|
38
38
|
import { parseEquationRef } from '../reference/equations.mjs';
|
|
39
|
+
import { parseKeyedFloatRef } from '../reference/keyed-jsx-floats.mjs';
|
|
39
40
|
import { resolveLabel } from '../reference/label';
|
|
40
41
|
import { resolveGlosses } from '../reference/gloss';
|
|
41
42
|
import {
|
|
@@ -229,9 +230,17 @@ export function extractNotationReferences(
|
|
|
229
230
|
for (const [pattern, referenceKind] of patterns) {
|
|
230
231
|
for (const match of searchable.matchAll(pattern)) {
|
|
231
232
|
const key = (match[1] ?? '').trim();
|
|
232
|
-
// `[[eq
|
|
233
|
-
//
|
|
234
|
-
|
|
233
|
+
// `[[eq-key]]` / `[[slug#eq-key]]` are equation references (D7), and
|
|
234
|
+
// `[[tab-key]]`/`[[fig-key]]` (and their cross-page forms) are
|
|
235
|
+
// `<Table>`/`<Figure>` references (F-tables-figures) — all resolved by
|
|
236
|
+
// the remark pass and the manifest, not notation keys.
|
|
237
|
+
if (
|
|
238
|
+
parseEquationRef(key) ||
|
|
239
|
+
parseKeyedFloatRef(key, { prefix: 'tab' }) ||
|
|
240
|
+
parseKeyedFloatRef(key, { prefix: 'fig' })
|
|
241
|
+
) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
235
244
|
references.push({
|
|
236
245
|
key,
|
|
237
246
|
kind: referenceKind,
|
|
@@ -21,6 +21,56 @@ export interface CourseConventions {
|
|
|
21
21
|
readonly cashFlowPerspectiveCompetency: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A course's edits to the shipped named-operator baseline (`BASE_OPERATORS`),
|
|
26
|
+
* consumed by the `operator-explained` consistency check (rule N8). An operator
|
|
27
|
+
* a lesson uses is fine when it is on the baseline plus `allow` minus `deny`;
|
|
28
|
+
* otherwise it needs a notation entry the reader can hover.
|
|
29
|
+
*
|
|
30
|
+
* Names are the KaTeX command without its backslash (`erf`, not `\erf`) for a
|
|
31
|
+
* built-in, and the verbatim body for an `\operatorname{…}` operator (`Var`).
|
|
32
|
+
*/
|
|
33
|
+
export interface CourseOperators {
|
|
34
|
+
/** Operators this course also treats as universal (`Var`, `Cov`, `sgn`, …). */
|
|
35
|
+
readonly allow?: readonly string[];
|
|
36
|
+
/** Baseline operators this course wants explained anyway. */
|
|
37
|
+
readonly deny?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One person or team credited for the study material. */
|
|
41
|
+
export interface CourseAuthor {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/**
|
|
44
|
+
* Email, URL, or handle. Rendered in the About panel as a link when it looks
|
|
45
|
+
* like a `mailto:` or `https:` target, plain text otherwise.
|
|
46
|
+
*/
|
|
47
|
+
readonly contact?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The reader-facing "about this material" block, surfaced by the `[?]` panel in
|
|
52
|
+
* the header (`components/starlight/AboutPanel.astro`) on every page.
|
|
53
|
+
*
|
|
54
|
+
* `version` is the course's **knowledge version** — semver over the
|
|
55
|
+
* learning-facing content, independent of the `explico` engine's own version.
|
|
56
|
+
* It must equal the newest entry in `content/CHANGELOG.md`; a mismatch is a
|
|
57
|
+
* Tier-3 `knowledge-version-mismatch` warning (rule C3, `docs/authoring/versioning.md`).
|
|
58
|
+
*/
|
|
59
|
+
export interface CourseAbout {
|
|
60
|
+
/** Display name of the material. Defaults to {@link CourseConfig.title}. */
|
|
61
|
+
readonly materialName?: string;
|
|
62
|
+
/** One or two sentences on what the material covers. Defaults to {@link CourseConfig.description}. */
|
|
63
|
+
readonly summary?: string;
|
|
64
|
+
/** Knowledge version — semver; equals the latest `content/CHANGELOG.md` heading. */
|
|
65
|
+
readonly version: string;
|
|
66
|
+
/** Who authored / maintains the material. At least one entry. */
|
|
67
|
+
readonly authors: readonly CourseAuthor[];
|
|
68
|
+
/** Shown verbatim in the About panel — scope, warranty, AI-assistance note. */
|
|
69
|
+
readonly disclaimer: string;
|
|
70
|
+
/** Link to the published content changelog (repo blob, docs page, …). */
|
|
71
|
+
readonly changelogHref?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
24
74
|
export interface CourseConfig {
|
|
25
75
|
readonly title: string;
|
|
26
76
|
readonly description: string;
|
|
@@ -29,4 +79,8 @@ export interface CourseConfig {
|
|
|
29
79
|
/** Namespaces used by competency / notation `domain` fields. */
|
|
30
80
|
readonly domains: readonly string[];
|
|
31
81
|
readonly conventions: CourseConventions;
|
|
82
|
+
/** Reader-facing identity + knowledge version (the `[?]` About panel). */
|
|
83
|
+
readonly about: CourseAbout;
|
|
84
|
+
/** Edits to the shipped named-operator baseline (rule N8). Optional. */
|
|
85
|
+
readonly operators?: CourseOperators;
|
|
32
86
|
}
|