explico 0.6.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 +180 -1
- 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/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 +7 -3
- package/dist/compiler/manifest.js +1 -1
- 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-math.d.ts +2 -0
- package/dist/reference/gate-math.js +28 -6
- package/dist/reference/keyed-float-validate.d.ts +2 -2
- package/dist/reference/keyed-float-validate.js +1 -1
- 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.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 +2 -2
- package/docs/authoring/README.md +12 -2
- package/docs/authoring/agents.md +7 -3
- package/docs/authoring/migrating.md +286 -0
- package/docs/authoring/notation.md +139 -7
- package/docs/authoring/prose.md +64 -0
- package/package.json +1 -1
- package/src/compiler/astro.ts +89 -0
- package/src/compiler/cli.ts +144 -12
- package/src/compiler/exports/standalone-html.ts +15 -0
- package/src/compiler/logging.ts +248 -0
- package/src/compiler/manifest.ts +7 -3
- 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 +83 -81
- 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 +2 -0
- 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-math.ts +41 -6
- package/src/reference/keyed-float-validate.ts +3 -3
- 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 +119 -14
|
@@ -33,6 +33,282 @@ required config block throws with the section of this file to read.
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
+
## 0.6.0 → 0.7.0
|
|
37
|
+
|
|
38
|
+
### 1. Manifest schema is now v15: diagnostic severity has three levels
|
|
39
|
+
|
|
40
|
+
`MANIFEST_SCHEMA_VERSION` is 15 (v14 → v15). Public diagnostic severity unions
|
|
41
|
+
now include `info`, so a client that treated "not an error" as "a warning"
|
|
42
|
+
needs three edits:
|
|
43
|
+
|
|
44
|
+
1. A client asserting `MANIFEST_SCHEMA_VERSION` by exact equality expects 15.
|
|
45
|
+
2. Widen any hand-written `'error' | 'warning'` type over manifest diagnostics
|
|
46
|
+
to `'error' | 'warning' | 'info'`. `ConsistencyDiagnostic.severity` is now
|
|
47
|
+
`'warning' | 'info'`.
|
|
48
|
+
3. In `scripts/compile-manifest.ts` (and any other consumer), filter on
|
|
49
|
+
`severity === 'warning'` before printing or counting warnings. A script that
|
|
50
|
+
counts all of `manifest.diagnostics.consistency` as "Tier-3 findings" now
|
|
51
|
+
over-reports:
|
|
52
|
+
|
|
53
|
+
```diff
|
|
54
|
+
-const consistencyByCode = consistencyCounts(consistency);
|
|
55
|
+
-const consistencyTotal = consistency.length;
|
|
56
|
+
+const consistencyWarnings = consistency.filter(
|
|
57
|
+
+ (diagnostic) => diagnostic.severity === 'warning',
|
|
58
|
+
+);
|
|
59
|
+
+const consistencyByCode = consistencyCounts(consistencyWarnings);
|
|
60
|
+
+const consistencyTotal = consistencyWarnings.length;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
These codes moved from `warning` to `info`: `eq-key-unused`, every keyed-float
|
|
64
|
+
`*-key-unused`, `unused-definition`, `weak-local`, `gloss-wants-promoting`, and
|
|
65
|
+
`card-wants-demoting`. Their code strings and container fields did not change,
|
|
66
|
+
the `lint-ignore.yml` lines that already worked for `weak-local`,
|
|
67
|
+
`gloss-wants-promoting`, and `card-wants-demoting` still match, and nothing
|
|
68
|
+
blocks the build. They no longer appear in `pnpm content warnings`; read them
|
|
69
|
+
with `pnpm content diagnostics info`.
|
|
70
|
+
|
|
71
|
+
Run `pnpm validate:content` twice. The committed `build/resolution/*` files
|
|
72
|
+
(including `consistency-report.json`) change because those findings now carry
|
|
73
|
+
`info` severity and `numerals-tagged` findings are gone (item 2); commit them.
|
|
74
|
+
|
|
75
|
+
### 2. Retired diagnostic: `numerals-tagged`
|
|
76
|
+
|
|
77
|
+
`ConsistencyCode` no longer contains `numerals-tagged`, and the engine never
|
|
78
|
+
emits it. Delete every `numerals-tagged` line from `lint-ignore.yml` (a stale
|
|
79
|
+
line is inert but misleads the next reader) and every course script, test, or
|
|
80
|
+
type narrowing that names the code — `tsc`/`astro check` flags the latter as a
|
|
81
|
+
comparison against a non-member of the union.
|
|
82
|
+
|
|
83
|
+
### 3. `explicoContentRefresh()` fails the build on a missing remark plugin (rule N14 and its siblings)
|
|
84
|
+
|
|
85
|
+
This is the enforcement half of rule N14's Astro wiring (see item 4 of the
|
|
86
|
+
0.4.x → 0.5.0 section). Its `astro:config:setup` hook now compares the manifest
|
|
87
|
+
against the configured remark plugins and throws, naming each gap, when a
|
|
88
|
+
non-empty table has no matching plugin registered:
|
|
89
|
+
|
|
90
|
+
| Manifest table is non-empty | Plugin that must be registered |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `manifest.abbreviations` | `[remarkAbbreviations, { abbreviations: manifest.abbreviations }]` from `explico/reference/remark-abbreviations.mjs` |
|
|
93
|
+
| `manifest.sources` | `[remarkCitation, { sources: manifest.sources }]` from `explico/reference/remark-citation.mjs` |
|
|
94
|
+
| `manifest.notation.raw` | `[remarkNotation, createRemarkNotationOptions(manifest)]` from `explico/reference/remark-notation.mjs` |
|
|
95
|
+
|
|
96
|
+
Check `astro.config.mjs` against that table. The check reads both wiring
|
|
97
|
+
surfaces Astro offers — the flat `markdown.remarkPlugins` array and a
|
|
98
|
+
`markdown.processor` such as `unified({ remarkPlugins: [...] })` — and accepts
|
|
99
|
+
either the bare plugin or a `[plugin, options]` tuple. It compares by identity
|
|
100
|
+
with the module the `explico/reference/*.mjs` specifier resolves to, so register
|
|
101
|
+
the imported function itself rather than a local wrapper around it. A course
|
|
102
|
+
that was already fully wired needs no edit; one that skipped a plugin was
|
|
103
|
+
silently rendering triggers as plain text and now fails `astro build` /
|
|
104
|
+
`astro dev` until it adds the missing registration.
|
|
105
|
+
|
|
106
|
+
### 4. Removed markup: popup "Copy LaTeX", glossary `#` copy-link, formula scroll region
|
|
107
|
+
|
|
108
|
+
None of these has a replacement selector; update course-owned CSS and
|
|
109
|
+
end-to-end tests that target them:
|
|
110
|
+
|
|
111
|
+
- `.notation-popup-copy` / `[data-popup-copy]` — the button in a notation
|
|
112
|
+
popup. Copying is now a click on the equation itself (item 8).
|
|
113
|
+
- `[data-glossary-anchor]` and its "Link copied" flash — a glossary entry's `#`
|
|
114
|
+
button that wrote its URL to the clipboard. The entry title is now a link to
|
|
115
|
+
`#notation-<key>` (`.glossary-term-link`), and a hover-revealed link-icon
|
|
116
|
+
anchor (`.glossary-anchor`) simply navigates; nothing is copied.
|
|
117
|
+
- `NotationFormula`'s `tabindex="0"` scroll region moved from
|
|
118
|
+
`.notation-formula-math` to a nested `.notation-formula-scroll`. A test that
|
|
119
|
+
focuses or scrolls `.notation-formula-math` must target the inner element.
|
|
120
|
+
|
|
121
|
+
No source or config change is needed beyond selectors.
|
|
122
|
+
|
|
123
|
+
### 5. Rule N14: every occurrence of a catalogued abbreviation is now wrapped
|
|
124
|
+
|
|
125
|
+
Two changes ride under rule N14, neither of which needs a config edit:
|
|
126
|
+
|
|
127
|
+
- **Rendered output changed.** `remarkAbbreviations` used to wrap only the first
|
|
128
|
+
occurrence of each catalogued abbreviation per page; it now wraps every
|
|
129
|
+
occurrence (`a.abbreviation-term[data-abbr-key]`). A course-owned e2e test or
|
|
130
|
+
snapshot that expected exactly one such link per term per page, or CSS that
|
|
131
|
+
assumed a page's later occurrences were plain text, must be updated.
|
|
132
|
+
`AbbreviationLayer.astro` already resolved markers independently by
|
|
133
|
+
`data-abbr-key`, so no island change is needed.
|
|
134
|
+
- **The shipped baseline grew** (`base-abbreviations.mjs`): `AM`, `PM`, `A.M.`,
|
|
135
|
+
`P.M.`, and `e.t.c.` join `a.m.`, `p.m.`, `e.g.`, `i.e.`, and `etc.`. This only
|
|
136
|
+
loosens the gate. A now-redundant `abbreviations.allow` line or
|
|
137
|
+
`content/abbreviations/*.md` entry for one of them is harmless and can stay; a
|
|
138
|
+
course that wants one explained anyway lists it in `abbreviations.deny`.
|
|
139
|
+
|
|
140
|
+
### 6. Rule N16 and rule N17: delimiter conventions and sizes
|
|
141
|
+
|
|
142
|
+
Rule N16 (alternate `()`, `[]`, `\{\}` for purely stylistic nested grouping,
|
|
143
|
+
never for conventional delimiters such as `f(x)`, `\mathbb{E}[X]`, or
|
|
144
|
+
intervals) is **review-only**: nothing checks it and nothing needs to change to
|
|
145
|
+
keep building. Rule N17 (every outer scalable pair in a nest grows strictly
|
|
146
|
+
larger than the pairs inside it: `\left…\right` → `\Bigl…\Bigr` → `\biggl…\biggr`
|
|
147
|
+
→ `\Biggl…\Biggr`) is a **Tier-3 warning**, `delimiter-size-hierarchy`, reported
|
|
148
|
+
at the outer pair's line and column by the normal math gate for lesson math,
|
|
149
|
+
notation formulas, and assessment math. It never fails a build, so no course
|
|
150
|
+
needs to change to keep building. To clear the warnings, run
|
|
151
|
+
`pnpm content diagnostics warning`, and enlarge each named outer pair to the
|
|
152
|
+
next manual size without changing its delimiter shape. Rule N15's build-enforced
|
|
153
|
+
behavior is unchanged — its documentation now also accepts an N17 manual pair as
|
|
154
|
+
"sized". See [`notation.md#n16`](notation.md#n16) and
|
|
155
|
+
[`notation.md#n17`](notation.md#n17).
|
|
156
|
+
|
|
157
|
+
### 7. Rule P1: "or" never introduces a second name for the same thing
|
|
158
|
+
|
|
159
|
+
Rule P1 opens the new `P` (prose) rule area and is **review-only**: nothing is
|
|
160
|
+
checked at build time and no course change is required. When editing lessons,
|
|
161
|
+
replace "the residual or error term" with "the residual (i.e., the error
|
|
162
|
+
term)"-style wording. See [`prose.md#p1`](prose.md#p1).
|
|
163
|
+
|
|
164
|
+
### 8. Optional: click-to-copy equations, glossary equation numbers, and live lab LaTeX
|
|
165
|
+
|
|
166
|
+
Nothing to do for a course that wires `Footer` to `LessonFooter.astro`: a
|
|
167
|
+
click on any `[data-notation-formula]` box now copies its `data-latex`, and every
|
|
168
|
+
glossary formula gets a `G.<n>` number and copy button, automatically.
|
|
169
|
+
|
|
170
|
+
Only a course with an interactive lab island that renders math through
|
|
171
|
+
`renderLabMath()` can opt that equation into the gesture, and keep the copied
|
|
172
|
+
LaTeX in step with the sliders, with `plainLatexTemplate` and the new
|
|
173
|
+
`slotPlaceholder` export:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import {
|
|
177
|
+
slotPlaceholder,
|
|
178
|
+
type RenderedLabMath,
|
|
179
|
+
} from 'explico/reference/render-lab-math';
|
|
180
|
+
|
|
181
|
+
// On the math root element:
|
|
182
|
+
<div
|
|
183
|
+
className="lab-math"
|
|
184
|
+
data-notation-formula
|
|
185
|
+
data-latex={rendered.plainLatexTemplate}
|
|
186
|
+
/* … */
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
// Where the island writes each slot's live value (`values`: slot name → text):
|
|
190
|
+
let latex = rendered.plainLatexTemplate;
|
|
191
|
+
for (const [name, value] of Object.entries(values)) {
|
|
192
|
+
latex = latex.split(slotPlaceholder(name)).join(value);
|
|
193
|
+
}
|
|
194
|
+
root.dataset.latex = latex;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Rebuild `latex` from the template on every update, not from the previous result.
|
|
198
|
+
A lab that does not set these attributes is unchanged. `RenderedLabMath` gained
|
|
199
|
+
the required `plainLatexTemplate` field; a course that constructs one by hand
|
|
200
|
+
(a test double, for instance) must add it.
|
|
201
|
+
|
|
202
|
+
### 9. Only if you override `Footer` yourself: mount `AnchorScrollEnhancer`
|
|
203
|
+
|
|
204
|
+
The new smooth-scroll behavior for in-page anchor links ships in
|
|
205
|
+
`AnchorScrollEnhancer.astro`, which `LessonFooter.astro` mounts. A course whose
|
|
206
|
+
Starlight `components.Footer` is `explico/components/starlight/LessonFooter.astro`
|
|
207
|
+
gets it with no change. A course that swapped in its own footer component must
|
|
208
|
+
render it (`import AnchorScrollEnhancer from
|
|
209
|
+
'explico/components/starlight/AnchorScrollEnhancer.astro'`, then
|
|
210
|
+
`<AnchorScrollEnhancer />`) or its anchor links keep the browser's native jump.
|
|
211
|
+
|
|
212
|
+
### 10. Optional: wire `rehypeBaseLinks` for a course deployed under a subpath
|
|
213
|
+
|
|
214
|
+
Skip this for a course served from a domain root (`base` omitted or `'/'`); the
|
|
215
|
+
plugin is a no-op there. For a course under a subpath (a GitHub Pages *project*
|
|
216
|
+
site, `base: '/my-course'`), Starlight prefixes only its own navigation, so
|
|
217
|
+
every root-relative link the Markdown pipeline produces — prose links,
|
|
218
|
+
equation cross-references, citation links — 404s without this:
|
|
219
|
+
|
|
220
|
+
```diff
|
|
221
|
+
+ import rehypeBaseLinks from 'explico/reference/rehype-base-links.mjs';
|
|
222
|
+
…
|
|
223
|
+
rehypePlugins: [
|
|
224
|
+
[rehypeKatex, createNotationKatexOptions()],
|
|
225
|
+
rehypeFailKatexErrors,
|
|
226
|
+
+ [rehypeBaseLinks, base],
|
|
227
|
+
]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`base` is the same value passed to `defineConfig({ base })`. **Undo any manual
|
|
231
|
+
workaround first:** the plugin prefixes every `href` beginning with a single
|
|
232
|
+
`/`, so a lesson link already hand-written as `/my-course/other/` would become
|
|
233
|
+
`/my-course/my-course/other/`. Write lesson links without the base. A lesson's
|
|
234
|
+
Starlight frontmatter (`hero.actions[].link`) never passes through this
|
|
235
|
+
pipeline; write those as paths relative to the current page.
|
|
236
|
+
|
|
237
|
+
### 11. Optional: run logs for `pnpm validate:content`
|
|
238
|
+
|
|
239
|
+
Route the course's `scripts/compile-manifest.ts` through `createRunLog()` to
|
|
240
|
+
record every diagnostic to `build/logs/<timestamp>/{error,warning,info}.log`
|
|
241
|
+
(Python `logging` format, `build/logs/latest` symlinks the newest run):
|
|
242
|
+
|
|
243
|
+
```diff
|
|
244
|
+
+ import { createRunLog } from 'explico/compiler/logging';
|
|
245
|
+
…
|
|
246
|
+
+ const log = createRunLog();
|
|
247
|
+
const manifest = await compileManifest(courseConfig);
|
|
248
|
+
…
|
|
249
|
+
- console.warn(`[${warning.code}] ${warning.message}`);
|
|
250
|
+
+ console.warn(log.warning('notation', `[${warning.code}] ${warning.message}`));
|
|
251
|
+
…
|
|
252
|
+
+ const summary = log.close();
|
|
253
|
+
+ console.log(
|
|
254
|
+
+ `Run log: ${summary.relativeDirectory} ` +
|
|
255
|
+
+ `(${summary.counts.ERROR} error(s), ${summary.counts.WARNING} warning(s), ${summary.counts.INFO} info).`,
|
|
256
|
+
+ );
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Each `log.error/warning/info(source, message)` call returns its message, so
|
|
260
|
+
printing and recording share one expression; `log.infoDiagnostics(manifest.diagnostics)`
|
|
261
|
+
records the severity-`info` findings the console never showed. Call
|
|
262
|
+
`log.close()` before any `throw` so a failing run's log is complete
|
|
263
|
+
(`playground/scripts/compile-manifest.ts` is a full worked example). Add
|
|
264
|
+
`build/logs/` to the course's `.gitignore` — a course commits
|
|
265
|
+
`build/resolution/*`, and nothing prunes old runs.
|
|
266
|
+
|
|
267
|
+
### 12. Optional: the `content diagnostics` / `content warnings` commands and VS Code tasks
|
|
268
|
+
|
|
269
|
+
`content diagnostics [info|warning|error] [--json]` and `content warnings
|
|
270
|
+
[--json]` are new, and `content status` now reports an `info` count
|
|
271
|
+
(`report.diagnostics.infos`). The engine implements them in
|
|
272
|
+
`explico/compiler/cli`, but a course's own `scripts/content.ts` dispatches
|
|
273
|
+
commands, so it must route them:
|
|
274
|
+
|
|
275
|
+
```diff
|
|
276
|
+
import {
|
|
277
|
+
ContentCliError,
|
|
278
|
+
contentCheck,
|
|
279
|
+
contentContext,
|
|
280
|
+
+ contentDiagnostics,
|
|
281
|
+
contentStatus,
|
|
282
|
+
+ contentWarnings,
|
|
283
|
+
…
|
|
284
|
+
} from 'explico/compiler/cli';
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Add a `case 'warnings'` (`contentWarnings(manifest)`) and a `case 'diagnostics'`
|
|
288
|
+
(validate the one positional as `info`/`warning`/`error`, default `info`, then
|
|
289
|
+
`contentDiagnostics(manifest, severity)`) beside `case 'status'`, printing
|
|
290
|
+
`file:line:column: severity code: message` lines — copy
|
|
291
|
+
`playground/scripts/content.ts`. To surface findings in VS Code's Problems
|
|
292
|
+
panel, copy the three **Explico: Show content diagnostics** tasks from the
|
|
293
|
+
engine repository's `.vscode/tasks.json`, replacing the `--filter` package name
|
|
294
|
+
and the problem matcher's `fileLocation` root with the course's own.
|
|
295
|
+
|
|
296
|
+
### 13. No action: other changes in this release
|
|
297
|
+
|
|
298
|
+
- Every `<a>` back-link (`↩`) in a page's References section now also carries a
|
|
299
|
+
`title` tooltip; `remark-citation.mjs` does this itself.
|
|
300
|
+
- Standalone HTML export now inlines a `<link>` only when its `rel` is
|
|
301
|
+
`stylesheet` or an icon type, and leaves other `<link>`s (`canonical`,
|
|
302
|
+
`alternate`, `preload`, …) as authored.
|
|
303
|
+
|
|
304
|
+
### Verification
|
|
305
|
+
|
|
306
|
+
`pnpm validate:content` (run it **twice** — the first run regenerates
|
|
307
|
+
`build/resolution/*`, item 1), then `pnpm content check --changed`, then
|
|
308
|
+
`astro check`, then `astro build`. `astro build` is the step that reports item 3.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
36
312
|
## 0.5.0 → 0.6.0
|
|
37
313
|
|
|
38
314
|
### 1. Rule S1: no heading or float title runs past 65 characters
|
|
@@ -215,6 +491,16 @@ abbreviation needs its remark plugin wired in too:
|
|
|
215
491
|
]
|
|
216
492
|
```
|
|
217
493
|
|
|
494
|
+
Skipping this second half is now **build-enforced**, not just documented:
|
|
495
|
+
`explicoContentRefresh()`'s `astro:config:setup` hook checks
|
|
496
|
+
`markdown.remarkPlugins` against the manifest and throws if a catalogued
|
|
497
|
+
manifest table (`manifest.abbreviations`, `manifest.sources`, or
|
|
498
|
+
`manifest.notation.raw`) has no matching plugin (`remarkAbbreviations`,
|
|
499
|
+
`remarkCitation`, `remarkNotation` respectively) registered — so a
|
|
500
|
+
half-completed migration like this one fails `astro build`/`astro dev`
|
|
501
|
+
instead of silently rendering plain text with `pnpm validate:content` still
|
|
502
|
+
green.
|
|
503
|
+
|
|
218
504
|
### 5. Optional: rule N10 warns on an unexplained `\mathrm{…}` label
|
|
219
505
|
|
|
220
506
|
A `\mathrm{…}` multi-letter label (`\mathrm{KE}`, `\mathrm{PV}`) not on the
|
|
@@ -837,14 +837,15 @@ change to that file.
|
|
|
837
837
|
## N15
|
|
838
838
|
|
|
839
839
|
**A nested visible delimiter — `(`, `[`, `\{`, or `|` inside another one of
|
|
840
|
-
them — must use
|
|
840
|
+
them — must use a scalable sizing form, not the bare character.**
|
|
841
841
|
|
|
842
842
|
`(`, `[`, `\{` (an escaped, visible brace — bare `{`/`}` is invisible LaTeX
|
|
843
843
|
grouping and out of scope here), and `|` may be written bare only when they
|
|
844
844
|
are not nested — not inside one another, and not inside a second instance of
|
|
845
845
|
themselves. The moment one of them sits inside another, **every level of that
|
|
846
|
-
nest**
|
|
847
|
-
`\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).
|
|
848
849
|
|
|
849
850
|
```text
|
|
850
851
|
\sin(2(90\text{°}-\theta)) ✗ two bare ( nested
|
|
@@ -873,10 +874,10 @@ author on a case that can render invisibly wrong.
|
|
|
873
874
|
_How to apply._
|
|
874
875
|
|
|
875
876
|
- Reach for `\left(`/`\right)` (or the bracket/brace/pipe equivalents) the
|
|
876
|
-
moment a second one of these four glyphs would end up inside the first
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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.
|
|
880
881
|
- Mixing types counts as nesting too: `(a + [b])`, `\{a \mid (b)\}`, and
|
|
881
882
|
`(|a|)` all need every level sized, exactly like nesting the same character
|
|
882
883
|
twice.
|
|
@@ -909,3 +910,134 @@ _Exceptions._ None. A quoted source nesting bare delimiters still renders
|
|
|
909
910
|
sized in this course, the same way N13 reparenthesizes a quoted `\sin x` —
|
|
910
911
|
the sizing is a typesetting choice, not a claim about the source's own
|
|
911
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.
|
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
|
}
|