explico 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +273 -1
  2. package/README.md +103 -8
  3. package/dist/compiler/astro.d.ts +9 -1
  4. package/dist/compiler/astro.js +182 -3
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +28 -4
  7. package/dist/compiler/collections.d.ts +3 -2
  8. package/dist/compiler/collections.js +28 -1
  9. package/dist/compiler/course-config.d.ts +58 -1
  10. package/dist/compiler/course-config.js +28 -1
  11. package/dist/compiler/exports/standalone-html.d.ts +34 -0
  12. package/dist/compiler/exports/standalone-html.js +803 -0
  13. package/dist/compiler/manifest.d.ts +90 -6
  14. package/dist/compiler/manifest.js +150 -17
  15. package/dist/compiler/resolution-report.d.ts +4 -0
  16. package/dist/reference/base-abbreviations.d.mts +30 -0
  17. package/dist/reference/base-abbreviations.mjs +60 -0
  18. package/dist/reference/base-operators.d.mts +15 -0
  19. package/dist/reference/base-operators.mjs +40 -0
  20. package/dist/reference/base-roman-labels.d.mts +35 -0
  21. package/dist/reference/base-roman-labels.mjs +43 -0
  22. package/dist/reference/consistency.d.ts +8 -2
  23. package/dist/reference/consistency.js +50 -3
  24. package/dist/reference/diagrams-validate.d.ts +21 -0
  25. package/dist/reference/diagrams-validate.js +13 -0
  26. package/dist/reference/equations.d.mts +10 -15
  27. package/dist/reference/equations.mjs +67 -14
  28. package/dist/reference/gate-abbreviations.d.ts +17 -0
  29. package/dist/reference/gate-abbreviations.js +64 -0
  30. package/dist/reference/gate-math.d.ts +28 -5
  31. package/dist/reference/gate-math.js +5 -2
  32. package/dist/reference/gate-prose-notation.d.ts +60 -0
  33. package/dist/reference/gate-prose-notation.js +125 -0
  34. package/dist/reference/keyed-float-validate.d.ts +5 -5
  35. package/dist/reference/keyed-jsx-floats.d.mts +7 -12
  36. package/dist/reference/keyed-jsx-floats.mjs +50 -25
  37. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  38. package/dist/reference/knowledge-checks-validate.js +78 -0
  39. package/dist/reference/knowledge-checks.d.mts +25 -0
  40. package/dist/reference/knowledge-checks.mjs +45 -0
  41. package/dist/reference/lab-math-scope.d.ts +9 -1
  42. package/dist/reference/lab-math-scope.js +32 -4
  43. package/dist/reference/math-glyphs.d.mts +30 -0
  44. package/dist/reference/math-glyphs.mjs +367 -0
  45. package/dist/reference/mdx-attributes.d.mts +13 -0
  46. package/dist/reference/mdx-attributes.mjs +38 -0
  47. package/dist/reference/mixed-content.d.ts +17 -0
  48. package/dist/reference/mixed-content.js +29 -0
  49. package/dist/reference/page-notation.d.ts +52 -0
  50. package/dist/reference/page-notation.js +171 -0
  51. package/dist/reference/raw-floats-validate.d.ts +18 -0
  52. package/dist/reference/raw-floats-validate.js +53 -0
  53. package/dist/reference/raw-floats.d.mts +18 -0
  54. package/dist/reference/raw-floats.mjs +129 -0
  55. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  56. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  57. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  58. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  59. package/dist/reference/remark-abbreviations.d.mts +7 -0
  60. package/dist/reference/remark-abbreviations.mjs +174 -0
  61. package/dist/reference/remark-memo.d.mts +16 -0
  62. package/dist/reference/remark-memo.mjs +339 -0
  63. package/dist/reference/remark-notation.d.mts +57 -9
  64. package/dist/reference/remark-notation.mjs +354 -42
  65. package/dist/reference/remark-short-note.d.mts +4 -0
  66. package/dist/reference/remark-short-note.mjs +237 -0
  67. package/dist/reference/types.d.ts +16 -1
  68. package/dist/reference/units-vocab.js +4 -0
  69. package/docs/VERSIONING.md +33 -4
  70. package/docs/authoring/README.md +8 -0
  71. package/docs/authoring/agents.md +17 -2
  72. package/docs/authoring/components.md +135 -0
  73. package/docs/authoring/migrating.md +92 -7
  74. package/docs/authoring/notation.md +560 -1
  75. package/docs/authoring/structure.md +120 -8
  76. package/docs/authoring/visualizations.md +36 -3
  77. package/package.json +5 -2
  78. package/src/compiler/astro.ts +208 -3
  79. package/src/compiler/cli.ts +29 -2
  80. package/src/compiler/collections.ts +45 -4
  81. package/src/compiler/course-config.ts +92 -0
  82. package/src/compiler/exports/standalone-html.ts +963 -0
  83. package/src/compiler/manifest.ts +268 -22
  84. package/src/components/HoverPanel.astro +45 -4
  85. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  86. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  87. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  88. package/src/components/assessments/AssessmentSet.astro +98 -14
  89. package/src/components/assessments/RenderedContent.tsx +55 -0
  90. package/src/components/assessments/assessment-randomization.ts +16 -0
  91. package/src/components/diagram/Diagram.astro +444 -83
  92. package/src/components/disclosure/hover-preview.ts +202 -0
  93. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  94. package/src/components/examples/CompactExamples.astro +2 -0
  95. package/src/components/figure/Figure.astro +279 -10
  96. package/src/components/memo/Memo.astro +453 -0
  97. package/src/components/memo/ShortNote.astro +223 -0
  98. package/src/components/notation/NotationFormula.astro +18 -1
  99. package/src/components/notation/NotationLayer.astro +105 -8
  100. package/src/components/notation/render-notation-math.ts +39 -2
  101. package/src/components/notation/safe-math-to-react.ts +50 -0
  102. package/src/components/reference/FloatRefLayer.astro +233 -15
  103. package/src/components/starlight/CollapsibleSections.astro +461 -0
  104. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  105. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  106. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  107. package/src/components/starlight/LayoutHeader.astro +50 -0
  108. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  109. package/src/components/starlight/LessonExportActions.astro +436 -0
  110. package/src/components/starlight/LessonFooter.astro +27 -2
  111. package/src/components/starlight/section-parts.ts +31 -0
  112. package/src/components/table/Table.astro +293 -7
  113. package/src/components/table/TableOverflowExpander.astro +489 -0
  114. package/src/content-config.ts +20 -0
  115. package/src/reference/base-abbreviations.mjs +60 -0
  116. package/src/reference/base-operators.mjs +40 -0
  117. package/src/reference/base-roman-labels.mjs +43 -0
  118. package/src/reference/consistency.ts +73 -2
  119. package/src/reference/diagrams-validate.ts +40 -0
  120. package/src/reference/equations.mjs +67 -14
  121. package/src/reference/gate-abbreviations.ts +128 -0
  122. package/src/reference/gate-math.ts +27 -7
  123. package/src/reference/gate-prose-notation.ts +224 -0
  124. package/src/reference/keyed-float-validate.ts +5 -5
  125. package/src/reference/keyed-jsx-floats.mjs +50 -25
  126. package/src/reference/knowledge-checks-validate.ts +130 -0
  127. package/src/reference/knowledge-checks.mjs +45 -0
  128. package/src/reference/lab-math-scope.ts +45 -3
  129. package/src/reference/math-glyphs.mjs +367 -0
  130. package/src/reference/mdx-attributes.mjs +38 -0
  131. package/src/reference/mixed-content.ts +69 -0
  132. package/src/reference/page-notation.ts +252 -0
  133. package/src/reference/raw-floats-validate.ts +103 -0
  134. package/src/reference/raw-floats.mjs +129 -0
  135. package/src/reference/rehype-heading-anchors.mjs +107 -1
  136. package/src/reference/rehype-table-header-labels.mjs +152 -0
  137. package/src/reference/remark-abbreviations.mjs +174 -0
  138. package/src/reference/remark-memo.mjs +339 -0
  139. package/src/reference/remark-notation.mjs +354 -42
  140. package/src/reference/remark-short-note.mjs +237 -0
  141. package/src/reference/types.ts +16 -0
  142. package/src/reference/units-vocab.ts +4 -0
  143. package/src/styles/global.css +996 -50
@@ -131,8 +131,32 @@ _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
+ Math authored inside a nested Astro component is outside the lesson MDX scan,
135
+ so the component must declare the formula text at render time. `Diagram` and
136
+ `NotationFormula` do this automatically from the notation keys they actually
137
+ emit. A lab wrapper must pass its formula/template source and request context to
138
+ `labMathScope`:
139
+
140
+ ```astro
141
+ ---
142
+ const formula = String.raw`\explain{motion.launch-speed}{v_0}`;
143
+ const scope = await labMathScope(undefined, formula, {
144
+ locals: Astro.locals,
145
+ context: 'LaunchSummary fixed-speed formula',
146
+ });
147
+ const math = renderLabMath(formula, scope);
148
+ ---
149
+ ```
150
+
151
+ The registration is request-local: it adds only keys used by that component
152
+ (and their normal definition dependencies) to that lesson's popup payload. It
153
+ does not make the global registry page-wide. An unknown key fails with the
154
+ lesson and supplied component/formula context; a final Astro build check also
155
+ rejects any `data-notation-key` in generated HTML that has no matching
156
+ `data-notation-entry` source.
157
+
134
158
  _Enforcement._ Build-enforced (completeness gate + one-meaning-per-glyph
135
- consistency check).
159
+ consistency check + final rendered-trigger/payload parity check).
136
160
 
137
161
  _Exceptions._ None. Genuinely different meanings need different glyphs or
138
162
  different pages.
@@ -295,3 +319,538 @@ reviewed line in `lint-ignore.yml` (`operator-explained:Var — <reason>`). Like
295
319
 
296
320
  _Exceptions._ Quoting a source that uses an operator glyph **and** showing what
297
321
  it means in the same passage.
322
+
323
+ ---
324
+
325
+ ## N9
326
+
327
+ **A physical quantity, unit, or math operator is typeset in `$…$`, never as
328
+ plain text — in prose, a table cell, an assessment `prompt` / `explanation`,
329
+ or a caption that itself lives outside math.**
330
+
331
+ If it would be written in italics or a special symbol in a printed textbook —
332
+ a variable, a subscript or superscript, a unit abbreviation, an absolute-value
333
+ bar, a division of two symbols — it goes in `$…$` (or `$$…$$`), full stop, no
334
+ matter where on the page it sits. `45 m/s`, `t (s)`, `vx`, `\|v\|`, `KE/m` are
335
+ all the same mistake: notation typed as if the reader's browser were a plain
336
+ text file instead of the KaTeX pipeline this engine already ships.
337
+
338
+ A unit inside math is still upright roman, never italic — SI symbols are
339
+ upright in every printed textbook too (`\text{m/s}`, not a bare italic `m/s`,
340
+ which KaTeX would render as the product of two variables `m` and `s`). Wrap
341
+ the unit portion in `\text{}` and fold it into the _same_ math span as the
342
+ symbol it modifies (`$t\ \text{(s)}$`, not two adjacent spans `$t$
343
+ $\text{(s)}$`) — and prefer the short symbol over the spelled-out word even
344
+ inside `\text{}` (`\text{(s)}`, not `\text{(seconds)}`), the same way a
345
+ variable is `v_0` and not "initial velocity" once it's introduced. This does
346
+ **not** cover a fully spelled-out unit used as an ordinary English noun in a
347
+ sentence — "the ball leaves at 10 m/s, 30 **degrees** above the horizontal"
348
+ keeps "degrees" as a word; that carve-out is about flowing prose, not a
349
+ compact data label like a table header or a computed answer.
350
+
351
+ This also does not apply inside a JSX component prop (`<Figure caption="…">`,
352
+ `<Table caption="…">`, `title="…"`) — a prop string is handed to the DOM as
353
+ plain text with no math rendering at all, so `$…$` there would show up as
354
+ broken literal dollar signs instead of rendering; write the unit as a word or
355
+ the symbol as plain Unicode there instead ("peaks at 45°" in a caption is
356
+ fine).
357
+
358
+ _Rationale._ A reader who can hover `$v_0$` for its notation card gets nothing
359
+ from the same quantity spelled `v0` in a table header — the two look
360
+ interchangeable to a drafter but only one is inside the notation system this
361
+ course is built on — hover cards, one meaning per glyph, the completeness
362
+ gate. A table of sampled values is exactly where this slips in, because the
363
+ completeness gate ([N4](#n4)) only ever looks _inside_ `$…$` — text sitting
364
+ next to it was invisible to every check until this rule.
365
+
366
+ _How to apply._
367
+
368
+ - A table header cell that names a quantity uses the same bound symbol the
369
+ lesson already introduced, `\explain{key}{…}`-wrapped if the column needs a
370
+ component or a different style than the card's own notation (`v_x` for
371
+ `[[motion.velocity]]`'s `v(t)`, exactly as `motion.acceleration`'s own
372
+ `formula:` already does for `\explain{motion.velocity}{\mathbf{v}}`), with
373
+ its unit folded into the same span as a `\text{}` suffix:
374
+
375
+ ```text
376
+ | $t\ \text{(s)}$ | $\explain{motion.velocity}{v_x}\ \text{(m/s)}$ |
377
+ ```
378
+
379
+ - An absolute value or norm is `\lvert…\rvert`, never a literal `|` — a raw
380
+ `|` inside a Markdown table cell also collides with the table's own column
381
+ delimiter, which is why it shows up escaped (`\|v\|`) instead of rendering
382
+ as anything.
383
+
384
+ - A specific (per-unit) quantity like kinetic energy per unit mass is
385
+ `\mathrm{KE}/m`, not `KE/m` — multi-letter roman labels still take
386
+ `\mathrm{}` ([N3](#n3)) inside the math span.
387
+
388
+ - A unit exponent is a real LaTeX superscript (`\text{m/s}^2`), never the
389
+ Unicode "²"/"³" character — KaTeX's strict mode rejects those as unknown
390
+ symbols (`src/reference/math-glyphs.mjs`'s `parseMath` calls
391
+ `katex.__parse(latex, { strict: 'error' })` on every math span the
392
+ completeness gate collects, table headers included), so this is already
393
+ build-enforced via [N4](#n4)'s existing gate, not a separate check N9 needs
394
+ of its own. This is exactly why a unit has to be _inside_ `$…$` in the
395
+ first place: outside math, nothing parses the LaTeX at all, so a stray "²"
396
+ typed as plain Unicode text would sail through unchecked.
397
+
398
+ - A header cell with **no plain-language word next to its symbol** — a bare
399
+ `$t$`, `$x$`, `$v_x$` column, not `Launch speed $\text{(m/s)}$` — ends up
400
+ 100% KaTeX output, which axe-core's `empty-table-header` check flags as
401
+ having no text "visible to screen readers" (it does not read into `<math>`
402
+ for the accessible-name computation, so a header that is entirely `$…$`
403
+ reads as empty to it even though a sighted or MathML-supporting reader sees
404
+ the symbol fine). This is handled automatically, not by writing anything
405
+ differently: `rehype-table-header-labels.mjs` (wired into
406
+ `playground/astro.config.mjs`'s `rehypePlugins`, right after
407
+ `rehypeKatex`) finds any `<th>` with no visible text, reads the
408
+ `\explain{key}{…}` key(s) its LaTeX names, and inserts a visually-hidden
409
+ (`sr-only`) text node from that key's own `label` / `units` notation
410
+ metadata — a fuller, more descriptive accessible name than the visible
411
+ abbreviation ever showed, generated for free from data already authored.
412
+ It has to be real (`sr-only`-clipped, not `display:none`) content, not
413
+ just an `aria-label` attribute: axe-core's `empty-table-header` check
414
+ specifically runs `has-visible-text` (real text content), with no
415
+ `aria-label`/`aria-labelledby` alternative the way most of its other
416
+ name-checking rules allow — confirmed by reading the rule itself in
417
+ `node_modules/axe-core/axe.js`. The one thing this means for an author: a
418
+ header's symbol has to actually resolve to a key for this to have anything
419
+ to work with. A bare `$t$`/`$x$` already does, via the ordinary `[[key]]`
420
+ intro + auto-resolution every other bound glyph on the page uses; the one
421
+ case that doesn't is a `\mathrm{…}` compound like `\mathrm{KE}/m`, because
422
+ the glyph resolver never looks inside a bare `\mathrm{}` body ([N3](#n3),
423
+ [N10](#n10)) — explicitly `\explain{}`-wrap each side there
424
+ (`\explain{kinetic-energy}{\mathrm{KE}}/\explain{mass}{m}`) so the plugin
425
+ has something to read; a header with more than one bound key composes them
426
+ joined by `" / "`.
427
+
428
+ _Enforcement._ Build-enforced (`gate-prose-notation.ts`, wired into the same
429
+ `pnpm validate:content` / `astro build` completeness gate as [N4](#n4), context
430
+ `prose`). `gateProseNotation` scans lesson prose and table cells — with code
431
+ spans, `$…$` math, `[[key]]` references, `import` lines, and JSX tags already
432
+ blanked out; `gateProseNotationInAssessments` runs the same checks over every
433
+ assessment item's `prompt` / `explanation` string, since `AssessmentRunner`
434
+ renders both through the same KaTeX pipeline as lesson body math
435
+ (`manifest.assessmentContent`, schema v8) — a plain-text quantity there is
436
+ just as invisible to a hover card as one in a lesson. Both look for: an
437
+ escaped `\|`; a numeral glued to an abbreviated unit (`45 m/s`); a bare unit
438
+ in parens (`(s)`, `(m/s²)`); a hand-typed `_`/`^` between alphanumerics; and a
439
+ `/` between two short alphabetic tokens that isn't on a small reviewed idiom
440
+ allowlist (`and/or`, `his/her`, …) in the check's own source. Unlike every
441
+ other build-enforced rule here, N9 ships as a hard error
442
+ with no Tier-3 warming-up period and no `lint-ignore.yml` entry, because it
443
+ landed in the same change that brought the whole corpus to zero — see the
444
+ check's file for the exact patterns and the allowlist.
445
+
446
+ _Exceptions._ None acknowledged through `lint-ignore.yml` — that registry is
447
+ for corpus-level warnings a reviewer accepts, and N9 has no warning tier to
448
+ accept into. A genuinely non-mathematical slash idiom (`input/output`, …) is
449
+ added to the allowlist in `gate-prose-notation.ts` itself, reviewed like any
450
+ other change to this file.
451
+
452
+ ---
453
+
454
+ ## N10
455
+
456
+ **A `\mathrm{…}` multi-letter label is universal, or it is explained.**
457
+
458
+ Every multi-letter roman label a lesson uses in math — `\mathrm{KE}`,
459
+ `\mathrm{PV}`, `\mathrm{opt}` ([N3](#n3)) — is either on the shipped
460
+ roman-label baseline (`src/reference/base-roman-labels.mjs` plus this
461
+ course's `romanLabels.allow` in `course.config.ts`) or it carries a notation
462
+ entry the reader can hover.
463
+
464
+ _Rationale._ N3 fixes the _format_ (`\mathrm{}`, never bare letters) but is
465
+ silent on whether the label is ever explained, because — same as an
466
+ `\operatorname{}` body ([N8](#n8)) — the glyph resolver skips a `\mathrm{}`
467
+ body whole; nothing about satisfying N3 requires a definition to exist. N9's
468
+ enforcement fixed one direction of this exact page (`KE`/`PE` typed as plain
469
+ text); this rule closes the other: `KE`/`PE` typed correctly as `\mathrm{}`
470
+ but with no card at all, which N9 cannot see because the notation is already
471
+ inside `$…$`.
472
+
473
+ _How to apply._
474
+
475
+ - A generic qualifier that functions like an operator's extremum subscript —
476
+ `x_{\mathrm{opt}}`, an "optimal" reading with no quantity of its own — just
477
+ works. The full baseline is the greppable set in `base-roman-labels.mjs`,
478
+ kept deliberately small: unlike a trig function, an upright label is almost
479
+ always a named quantity specific to one course, so it defaults to needing a
480
+ card rather than defaulting to universal.
481
+
482
+ - A named quantity gets a notation entry, exactly like a glyph ([N4](#n4)) —
483
+ a shared card if the course teaches it broadly, otherwise `notation.local`,
484
+ same shape as [N9](#n9)'s `mass` example:
485
+
486
+ ```yaml
487
+ key: kinetic-energy
488
+ latex: \mathrm{KE}
489
+ label: Kinetic energy
490
+ meaning: The energy an object has from its motion.
491
+ ```
492
+
493
+ Then introduce it in prose with `[[kinetic-energy]]` before its first
494
+ appearance in math.
495
+
496
+ - A course whose domain treats a label as common lists it once in
497
+ `course.config.ts` instead:
498
+
499
+ ```ts
500
+ romanLabels: { allow: ['PV', 'NPV'] }
501
+ ```
502
+
503
+ Names are the `\mathrm{…}` body. `romanLabels.deny` forces a baseline label
504
+ to be explained anyway.
505
+
506
+ _Enforcement._ Tier-3 warning (`roman-label-explained`), surfaced by
507
+ `pnpm validate:content`, recorded in the manifest, and silenced only by a
508
+ reviewed line in `lint-ignore.yml` (`roman-label-explained:KE — <reason>`).
509
+ Same rollout shape as [N8](#n8): the tier can be promoted once a corpus is
510
+ clean.
511
+
512
+ _Exceptions._ Quoting a source that uses a roman-label glyph **and** showing
513
+ what it means in the same passage. A single-letter `\mathrm{…}` body (`d`,
514
+ [N7](#n7)) is out of scope entirely — that is a fixed typographic operator,
515
+ not a named quantity.
516
+
517
+ ---
518
+
519
+ ## N11
520
+
521
+ **A worked example never drops a unit from a substituted value, and its
522
+ result is dimensionally consistent.**
523
+
524
+ The moment a symbol in an equation is replaced by a number — in a
525
+ `CompactExample`, an assessment `explanation`, a derivation step, anywhere —
526
+ that number keeps the unit its symbol carried. Never substitute bare numbers
527
+ and attach a unit only to the final answer:
528
+
529
+ ```text
530
+ v(4) = 5 + 2 \times 4 = 13\ \text{m/s} ✗ 5 and 2 have silently lost their units
531
+ v(4) = 5\ \text{m/s} + 2\ \text{m/s}^2 \times 4\ \text{s} = 13\ \text{m/s} ✓
532
+ ```
533
+
534
+ The mirror failure is a result whose unit does not actually follow from the
535
+ arithmetic — copy-pasting the previous line's unit onto a different quantity.
536
+ `v = s/t = 10/5 = 2\ \text{s}` is wrong twice over: the substitution dropped
537
+ $s$'s and $t$'s units, which is what let `2` pick up `\text{s}` (a time unit)
538
+ for what is actually a velocity — the arithmetic $10/5$ alone is just the
539
+ number 2, and nothing about it says "seconds". Carried through, the units
540
+ themselves catch the mistake: $10\ \text{m} / 5\ \text{s} = 2\ \text{m/s}$
541
+ leaves no room to mislabel the answer, which is the entire point of the rule.
542
+
543
+ _Rationale._ A worked example is the one place a reader checks their own
544
+ understanding by redoing the arithmetic. If the displayed steps drop units,
545
+ the example teaches "plug in the numbers" instead of "the units are part of
546
+ the equation" — and it stops being able to catch the author's own dimensional
547
+ mistakes, since a number has no unit to contradict. N9 already forces every
548
+ quantity into `$…$` with its unit properly typeset; this rule is the
549
+ complementary content requirement that the _substitution steps themselves_
550
+ keep carrying those units all the way to the answer, not just the input
551
+ values and the final line.
552
+
553
+ _How to apply._
554
+
555
+ - Attach each value's unit with `\text{}` at the moment it replaces its
556
+ symbol, exactly as [N9](#n9) already typesets a bound quantity — folded
557
+ into the same math term as its number, not a separate span
558
+ (`2\ \text{m/s}^2`, not `2` and `\text{m/s}^2` as two adjacent `$…$`).
559
+ - Carry the units through every intermediate line a reader can see, not just
560
+ the first substitution and the last answer. If the full chain would be
561
+ unreasonably long, it is fine to skip straight from substitution to answer
562
+ for a single-step calculation — the rule is about not _losing_ units
563
+ mid-derivation, not about spelling out unit algebra for its own sake.
564
+ - A ratio of two same-unit quantities (`\arctan(y/x)` for two lengths) is
565
+ dimensionless by cancellation; leaving it unitless is correct, not an
566
+ omission — write the units only where they actually survive to the result.
567
+ - Squaring or otherwise combining a substituted value carries its unit
568
+ through the operation: `(3\ \text{m})^{2} = 9\ \text{m}^2`, not `3^2 =
569
+ 9\ \text{m}^2` with the unit reattached after the fact.
570
+
571
+ _Enforcement._ Review-only. Checking that a substitution step's units are
572
+ _present_ is straightforward text matching, but checking that they are
573
+ _dimensionally correct_ — that `m/s²` times `s` really does collapse to
574
+ `m/s` — is a small computer-algebra problem, not a pattern match; no gate in
575
+ this repo attempts it. A reviewer reads the worked example the way a student
576
+ would: redo the substitution and confirm every number's unit, then confirm
577
+ the arithmetic on the units themselves lands on the stated result's unit.
578
+
579
+ _Exceptions._ A dimensionless ratio or angle carries no unit to begin with
580
+ (see "how to apply" above) — that is not an exception, just correctly having
581
+ nothing to attach.
582
+
583
+ ---
584
+
585
+ ## N12
586
+
587
+ **Math syntax never carries the sentence's own trailing punctuation.**
588
+
589
+ A period that ends a sentence, or a comma that joins it to the next clause,
590
+ stays in the prose — outside `$…$` / `$$…$$` — even when the math is the last
591
+ thing before it. Never let it drift inside the span, and never bury it inside
592
+ a trailing `\text{}` unit:
593
+
594
+ ```text
595
+ $$
596
+ \rho = \sqrt{3^2 + 4^2} = 5\ \text{m,} ✗ the comma is now part of the LaTeX
597
+ $$
598
+
599
+ $$
600
+ \rho = \sqrt{3^2 + 4^2} = 5\ \text{m} ✓ drop it — the next sentence needs no connector here
601
+ $$
602
+ ```
603
+
604
+ This is about the sentence's _own_ punctuation riding along with the math by
605
+ accident, not about punctuation that is itself part of the mathematical
606
+ notation — a comma separating two side-by-side equations in one display block
607
+ (`x = \ldots, \qquad y = \ldots`) is a list separator inside the math, not
608
+ prose leaking in, and is unaffected by this rule.
609
+
610
+ _Rationale._ A trailing period or comma inside `$…$` becomes part of the
611
+ LaTeX source: KaTeX renders it in math italic/spacing rather than as
612
+ punctuation, it is included in whatever `\text{}` it landed inside (turning a
613
+ unit label `\text{m}` into the unrelated string `\text{m,}`), and a reader
614
+ who copies the equation out of the rendered page copies the stray mark with
615
+ it. A display equation followed by a `\label{eq:…}` and then a fresh
616
+ paragraph — the pattern most of this corpus already uses — never has this
617
+ problem in the first place, which is the simplest fix in practice.
618
+
619
+ _How to apply._ Omit the connector. A display equation does not need a
620
+ trailing comma to link it to the sentence that continues below it — the
621
+ paragraph break already does that job, and every rule-following equation in
622
+ this corpus (see [N5](#n5)'s `\label{eq:…}` pattern) already ends bare. For
623
+ inline math (`$x = 5$` mid-sentence), put the period or comma after the
624
+ closing `$`, where it has always belonged.
625
+
626
+ _Enforcement._ Review-only. A gate could, in principle, flag `.` or `,` as
627
+ the last character before a closing `$`/`$$`, but it cannot tell a stray
628
+ sentence-ending mark from a legitimate mathematical use of the same
629
+ character (a decimal point, a `\ldots`, a matrix row separator) without
630
+ false positives outweighing the catches — left to review, the same as
631
+ [N5](#n5).
632
+
633
+ _Exceptions._ A comma or period that is itself notation — a decimal point in
634
+ a number, `\ldots`, a matrix/vector component separator — is not this rule's
635
+ target and stays exactly where the math needs it.
636
+
637
+ ---
638
+
639
+ ## N13
640
+
641
+ **A nonlinear function always takes a parenthesized argument, even a single
642
+ symbol.**
643
+
644
+ Every use of a trigonometric, inverse-trigonometric, or hyperbolic function,
645
+ and every logarithm or exponential, wraps its argument in a visible
646
+ parenthesis (or bracket) — never a bare symbol or number sitting directly
647
+ after the function name:
648
+
649
+ ```text
650
+ \sin x = \sin y ✗
651
+ \sin(x) = \sin(y) ✓
652
+
653
+ log x ✗
654
+ \log(x) ✓
655
+
656
+ \sin^2 x ✗ — the exponent doesn't excuse the missing parens
657
+ \sin^2(x) ✓
658
+ ```
659
+
660
+ _Rationale._ `\sin x` leaves how far the argument extends ambiguous the
661
+ instant anything follows it — `\sin x + 1` visually reads as "sine of x,
662
+ plus one" but a reader has to already know the convention to be sure it
663
+ isn't "sine of (x + 1)". `\sin(x)` is unambiguous regardless of what
664
+ surrounds it, at the cost of nothing when the argument really is a single
665
+ symbol. Textbooks vary on this; this course picks the form that never
666
+ requires the reader to know the convention.
667
+
668
+ _How to apply._
669
+
670
+ - Wrap the argument in `(...)` — or `\left( ... \right)` when it needs to
671
+ grow with its contents, or `[...]`/`\left[ ... \right]` when an outer
672
+ pair of parentheses is already in use for something else on the same
673
+ line. A bare `{...}` does not count: LaTeX braces are invisible grouping,
674
+ so `\sin{x}` renders identically to the forbidden `\sin x`.
675
+ - An exponent between the function and its argument does not exempt it —
676
+ `\sin^{2}(x)`, `\log_{2}(x)`, `\sin^{-1}(x)` all still need the
677
+ parenthesis after the modifier.
678
+ - The function covered by this rule is exactly
679
+ [`PARENTHESIZED_FUNCTIONS`](../../src/reference/base-operators.mjs): the
680
+ six trigonometric functions, `arcsin`/`arccos`/`arctan`, the four
681
+ hyperbolic functions in the base operator library, and `exp`/`log`/`ln`/
682
+ `lg`. A different named operator ([N8](#n8)'s `\operatorname{Var}` and
683
+ friends) is unaffected — this rule is scoped to the functions whose
684
+ argument's extent is genuinely ambiguous without a mark, not every named
685
+ operator.
686
+
687
+ _Enforcement._ Build-enforced, with no partial-corpus warm-up tier — like
688
+ [N9](#n9), it ships as a hard error. `resolveMathGlyphs`
689
+ (`src/reference/math-glyphs.mjs`) walks the parsed KaTeX tree for every
690
+ rendered-math context this engine has — the same single resolver [N1](#n1)
691
+ through [N10](#n10) already rely on — and fails the moment a
692
+ `PARENTHESIZED_FUNCTIONS` name is not immediately followed by a visible
693
+ opening delimiter. Because every context (lesson body, `notation.formula`,
694
+ assessment `prompt`/`explanation`, a `Diagram` node's `formula`, interactive
695
+ lab math) calls this one function, the check needs no separate wiring per
696
+ content type: `pnpm validate:content` catches it in lesson body, formula, and
697
+ assessment math; `astro build` catches a `Diagram` or lab-math violation
698
+ (these are authored outside any manifest-scanned MDX body, in a component's
699
+ own `nodes={[...]}` or lab template) as a hard render failure, since neither
700
+ `Diagram` nor lab math has a code path that tolerates degrading a formula
701
+ silently for this rule the way an ambiguous-glyph error can.
702
+
703
+ _Exceptions._ None. A quoted source that uses `\sin x` still gets
704
+ parenthesized in this course's rendering — the parenthesis is a typesetting
705
+ choice, not a claim about the source's own notation, so quoting accurately
706
+ does not require reproducing it.
707
+
708
+ ---
709
+
710
+ ## N14
711
+
712
+ **An abbreviation-shaped word in prose is universal, or it is explained.**
713
+
714
+ Every all-caps acronym (`OLS`, `MSE`, `SSR`) or dotted abbreviation (`w.r.t.`,
715
+ `i.i.d.`) a lesson uses in ordinary prose — outside any `$…$` — is either on
716
+ the shipped abbreviation baseline (`src/reference/base-abbreviations.mjs`
717
+ plus this course's `abbreviations.allow` in `course.config.ts`), carries an
718
+ entry in the `abbreviations` content collection
719
+ (`content/abbreviations/*.md`), or is already explained as a math quantity on
720
+ the same page ([N8](#n8) / [N10](#n10)). A term resolved by the table is not
721
+ just build-checked: `remarkAbbreviations` wraps its first occurrence on the
722
+ page in the same hover/click card the math-notation and citation systems
723
+ already use, linking to its entry on the glossary page.
724
+
725
+ _Rationale._ [N9](#n9) already forces a _math_ quantity or unit into `$…$` so
726
+ it gets a hover card; this rule closes the parallel gap for a term that reads
727
+ as English, not math — a reader who knows `\mathrm{MSE}` has a card two
728
+ sections later gets nothing from "the MSE is minimized" if that sentence
729
+ never binds to it. An abbreviation is exactly the kind of thing an
730
+ experienced writer stops noticing they haven't defined.
731
+
732
+ _How to apply._
733
+
734
+ - An abbreviation that names no quantity of its own — `OLS`, `w.r.t.`,
735
+ `i.i.d.` — gets an entry in `content/abbreviations/*.md`:
736
+
737
+ ```yaml
738
+ key: ols
739
+ abbr: OLS
740
+ expansion: Ordinary least squares — the estimator that minimizes the sum of squared residuals.
741
+ ```
742
+
743
+ This is a lighter tier than a notation card: a name resolution only, no
744
+ `formula`, `units`, or curriculum `alignment`. It has no authoring syntax
745
+ of its own in prose (no `[[key]]` needed) — the gate below recognizes the
746
+ literal written form (`abbr`, or one of its `aliases`) anywhere on the
747
+ page.
748
+
749
+ - An abbreviation that _is_ a math quantity — `MSE`, `SSR`, kinetic energy's
750
+ `KE` — is introduced the normal [N8](#n8)/[N10](#n10) way instead: a
751
+ notation entry whose `latex` is `\mathrm{MSE}` (or an `\operatorname{…}`),
752
+ bound with `[[key]]` or `\explain{key}{…}`. Once that entry is in scope on
753
+ a page, this rule already treats the bare word `MSE` in that page's prose
754
+ as explained — it does **not** also need an `abbreviations` table entry.
755
+
756
+ - A course whose readers already know a term — `AI`, `GDP`, whatever the
757
+ domain treats as common — lists it once in `course.config.ts` instead:
758
+
759
+ ```ts
760
+ abbreviations: { allow: ['GDP', 'CPU'] }
761
+ ```
762
+
763
+ `abbreviations.deny` forces a shipped baseline entry (`e.g.`, `i.e.`, …) to
764
+ be explained anyway.
765
+
766
+ _Enforcement._ Build-enforced (`gate-abbreviations.ts`), wired into the same
767
+ `pnpm validate:content` pass as [N9](#n9), context `abbreviation`. It scans
768
+ the exact same "prose-only" surface N9 does (code, math, `[[key]]`
769
+ references, and JSX tags already blanked out), looking for an all-caps run of
770
+ 2+ letters or a run of 2+ single-letter-then-period groups. Like N9, it ships
771
+ as a hard error with no Tier-3 warm-up period and no `lint-ignore.yml` entry
772
+ — the corpus had no prior instances of either shape in prose when the check
773
+ was added.
774
+
775
+ _Exceptions._ None acknowledged through `lint-ignore.yml`. A genuinely
776
+ universal abbreviation this course's readers cannot be expected to look up is
777
+ added to `abbreviations.allow` in `course.config.ts`, reviewed like any other
778
+ change to that file.
779
+
780
+ ---
781
+
782
+ ## N15
783
+
784
+ **A nested visible delimiter — `(`, `[`, `\{`, or `|` inside another one of
785
+ them — must use its `\left`/`\right` sizing form, not the bare character.**
786
+
787
+ `(`, `[`, `\{` (an escaped, visible brace — bare `{`/`}` is invisible LaTeX
788
+ grouping and out of scope here), and `|` may be written bare only when they
789
+ are not nested — not inside one another, and not inside a second instance of
790
+ themselves. The moment one of them sits inside another, **every level of that
791
+ nest** switches to the auto-sizing form: `\left(`/`\right)`, `\left[`/`\right]`,
792
+ `\left\{`/`\right\}`, `\left|`/`\right|`.
793
+
794
+ ```text
795
+ \sin(2(90\text{°}-\theta)) ✗ two bare ( nested
796
+ \sin\left(2\left(90\text{°}-\theta\right)\right) ✓
797
+
798
+ \left(1 - \sin^{2}(\theta)\right) ✗ bare ( one level inside \left(
799
+ \left(1 - \sin^{2}\left(\theta\right)\right) ✓ — sizing the outer pair
800
+ does not size the inner one
801
+
802
+ (a + [b - c]) ✗ nested across types too
803
+ \left(a + \left[b - c\right]\right) ✓
804
+
805
+ (x) ✓ — not nested, bare is fine
806
+ ```
807
+
808
+ _Rationale._ A bare delimiter is a fixed-height glyph; `\left`/`\right` grows
809
+ to match whatever it wraps. Two bare pairs nested inside each other render at
810
+ the _same_ fixed size regardless of which one's content is actually taller,
811
+ so the inner and outer marks visually collide or look like a typo the moment
812
+ either side holds anything but a single short symbol. Requiring the sizing
813
+ form at every level of a nest — rather than trying to judge, pair by pair,
814
+ whether _this particular_ nesting happens to look fine — is the simplest rule
815
+ that is actually checkable by machine and never leaves a judgment call to the
816
+ author on a case that can render invisibly wrong.
817
+
818
+ _How to apply._
819
+
820
+ - Reach for `\left(`/`\right)` (or the bracket/brace/pipe equivalents) the
821
+ moment a second one of these four glyphs would end up inside the first —
822
+ regardless of which of the four it is, and regardless of whether the outer
823
+ one is already sized. Sizing the outer pair does not exempt an inner bare
824
+ one; both levels of a two-level nest need their own `\left`/`\right`.
825
+ - Mixing types counts as nesting too: `(a + [b])`, `\{a \mid (b)\}`, and
826
+ `(|a|)` all need every level sized, exactly like nesting the same character
827
+ twice.
828
+ - A `|` has no distinct open/close glyph, so a lone pair — `\left|x\right|`,
829
+ or bare `|x|` when it is not nested in anything — reads as one absolute
830
+ value/norm, matched left to right. Conditional probability still prefers
831
+ `\mid` over a bare `|` regardless of nesting ([N6](#n6)).
832
+ - A delimiter inside `\text{…}` is display text, not a sizing mark — a unit
833
+ written `\text{(s)}` ([N9](#n9)) is untouched by this rule even when the
834
+ math around it also uses `(`.
835
+ - This is unrelated to [N13](#n13)'s requirement that a nonlinear function's
836
+ argument always be visibly parenthesized: N13 is satisfied by a bare
837
+ `\sin(x)` just as well as `\sin\left(x\right)`; N15 only fires once a
838
+ second one of these four glyphs nests inside the first.
839
+
840
+ _Enforcement._ Build-enforced, with no partial-corpus warm-up tier — like
841
+ [N9](#n9)/[N13](#n13), it ships as a hard error, and the corpus's two prior
842
+ instances (`playground/content/docs/projectiles/optimal-launch-angle.mdx`)
843
+ were fixed in the same change that added the check.
844
+ `findNestedBareDelimiters` (`src/reference/math-glyphs.mjs`) walks the same
845
+ parsed KaTeX tree `resolveMathGlyphs` and [N13](#n13) already build, carrying
846
+ one stack through the whole walk: pushing a tracked bare or `\left`-sized
847
+ delimiter, and flagging the enclosing entry (if bare) and the new one (if
848
+ bare) the moment a second one opens while the first is still open. Because
849
+ every rendered-math context calls `resolveMathGlyphs` — lesson body,
850
+ `notation.formula`, assessment `prompt`/`explanation`, `Diagram`/lab math —
851
+ the check needs no separate wiring per content type, the same story as N13.
852
+
853
+ _Exceptions._ None. A quoted source nesting bare delimiters still renders
854
+ sized in this course, the same way N13 reparenthesizes a quoted `\sin x` —
855
+ the sizing is a typesetting choice, not a claim about the source's own
856
+ notation.