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
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Remark adapter for reveal-in-place short notes.
3
+ *
4
+ * A `<ShortNote>` may sit inside a sentence while its body contains block
5
+ * Markdown. Before mdast parsing, the adapter extracts that body and leaves an
6
+ * ordinary inline link placeholder behind. After parsing, it inserts the real
7
+ * flow component immediately after the intact paragraph. The browser enhancer
8
+ * can then split/rejoin that paragraph only while the note is open, preserving
9
+ * valid and genuinely inline collapsed markup.
10
+ */
11
+
12
+ import { stripFencedCode } from './equations.mjs';
13
+ import { KEYED_FLOAT_KEY_PATTERN } from './keyed-jsx-floats.mjs';
14
+ import {
15
+ injectMdxStringAttributes,
16
+ mdxAttributesNamed,
17
+ removeMdxAttribute,
18
+ } from './mdx-attributes.mjs';
19
+
20
+ const BLOCK_ATTRIBUTE = 'data-explico-short-note-block';
21
+ const BLOCKS_DATA = 'explicoShortNoteBlocks';
22
+ const MARKER_URL_PREFIX = '#explico-internal-short-note-marker-';
23
+ const ATTRIBUTE_SOURCE = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'])*`;
24
+ const SHORT_NOTE_PAIR = new RegExp(
25
+ String.raw`<ShortNote\b(${ATTRIBUTE_SOURCE})>([\s\S]*?)<\/ShortNote\s*>`,
26
+ 'g',
27
+ );
28
+
29
+ function maskBlock(block) {
30
+ return block.replace(/[^\n]/g, ' ');
31
+ }
32
+
33
+ function searchableSource(source) {
34
+ return stripFencedCode(source)
35
+ .replace(/<!--[\s\S]*?-->/g, maskBlock)
36
+ .replace(/\{\/\*[\s\S]*?\*\/\}/g, maskBlock);
37
+ }
38
+
39
+ function extractShortNotes(source) {
40
+ const input = String(source);
41
+ const searchable = searchableSource(input);
42
+ const matches = [...searchable.matchAll(SHORT_NOTE_PAIR)];
43
+ if (matches.length === 0) return { source: input, notes: [] };
44
+
45
+ const notes = [];
46
+ let cursor = 0;
47
+ let output = '';
48
+ for (let index = 0; index < matches.length; index++) {
49
+ const match = matches[index];
50
+ const start = match.index ?? 0;
51
+ if (start < cursor) continue;
52
+
53
+ const whole = match[0];
54
+ const maskedAttributes = match[1] ?? '';
55
+ const openLength = '<ShortNote'.length + maskedAttributes.length + 1;
56
+ const bodyStart = start + openLength;
57
+ const closeStart = start + whole.lastIndexOf('</ShortNote');
58
+ const searchableBody = searchable.slice(bodyStart, closeStart);
59
+ if (/<ShortNote\b/.test(searchableBody)) {
60
+ throw new Error('A <ShortNote> cannot be nested inside another <ShortNote>.');
61
+ }
62
+
63
+ notes.push({
64
+ token: String(index),
65
+ attributes: input.slice(start + '<ShortNote'.length, bodyStart - 1),
66
+ body: input.slice(bodyStart, closeStart),
67
+ });
68
+ output += input.slice(cursor, start);
69
+ output += `[short note](${MARKER_URL_PREFIX}${index})`;
70
+ cursor = start + whole.length;
71
+ }
72
+ output += input.slice(cursor);
73
+ return { source: output, notes };
74
+ }
75
+
76
+ /** Return the parseable source form, primarily for adapter tests. */
77
+ export function preprocessShortNotes(source) {
78
+ return extractShortNotes(source).source;
79
+ }
80
+
81
+ function fail(file, message, node) {
82
+ if (typeof file?.fail === 'function') file.fail(message, node);
83
+ const location = file?.path ? `${file.path}: ` : '';
84
+ throw new Error(`${location}${message}`);
85
+ }
86
+
87
+ function literalAttribute(node, name, file) {
88
+ const matches = mdxAttributesNamed(node, name);
89
+ if (matches.length > 1) {
90
+ fail(file, `<ShortNote> has more than one \`${name}\` prop.`, node);
91
+ }
92
+ if (matches.length === 0) {
93
+ fail(file, `<ShortNote> requires a non-empty literal \`${name}\` prop.`, node);
94
+ }
95
+ const value = matches[0]?.value;
96
+ if (typeof value !== 'string' || !value.trim()) {
97
+ fail(
98
+ file,
99
+ `<ShortNote> \`${name}\` must be a non-empty literal string; expressions are not supported for inline note triggers.`,
100
+ matches[0] ?? node,
101
+ );
102
+ }
103
+ return value;
104
+ }
105
+
106
+ function markerNode(note) {
107
+ return {
108
+ type: 'link',
109
+ url: `#${note.anchorId}`,
110
+ children: [{ type: 'text', value: note.title }],
111
+ data: {
112
+ hProperties: {
113
+ className: ['short-note-marker'],
114
+ 'data-short-note-key': note.id,
115
+ 'aria-controls': note.anchorId,
116
+ 'aria-expanded': 'false',
117
+ 'aria-label': `${note.title}: ${note.description}`,
118
+ title: note.description,
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ function visit(node, callback, parent) {
125
+ if (!node || typeof node !== 'object') return;
126
+ callback(node, parent);
127
+ if (!Array.isArray(node.children)) return;
128
+ for (const child of node.children) visit(child, callback, node);
129
+ }
130
+
131
+ /** @this {any} */
132
+ export default function remarkShortNote() {
133
+ const processor = this;
134
+ const parse = processor.parser;
135
+ if (typeof parse !== 'function') {
136
+ throw new Error(
137
+ 'remarkShortNote must be registered after the Markdown parser (and after remark-mdx when configured explicitly).',
138
+ );
139
+ }
140
+
141
+ Reflect.set(processor, 'parser', (document, file) => {
142
+ const extracted = extractShortNotes(document);
143
+ const tree = parse(extracted.source, file);
144
+ if (extracted.notes.length === 0) return tree;
145
+
146
+ const blocks = extracted.notes.map((note) => {
147
+ const source = `<ShortNote ${BLOCK_ATTRIBUTE}="${note.token}"${note.attributes}>\n\n${note.body}\n\n</ShortNote>`;
148
+ const parsed = parse(source, file);
149
+ const block = parsed.children?.find((node) => node?.name === 'ShortNote');
150
+ if (!block) {
151
+ fail(file, 'Unable to parse the extracted <ShortNote> body.', parsed);
152
+ }
153
+ return block;
154
+ });
155
+ tree.data ??= {};
156
+ tree.data[BLOCKS_DATA] = blocks;
157
+ return tree;
158
+ });
159
+
160
+ return function transform(tree, file) {
161
+ const blocks = tree.data?.[BLOCKS_DATA] ?? [];
162
+ if (blocks.length === 0) return;
163
+
164
+ const markers = new Map();
165
+ const parents = new Map();
166
+ visit(tree, (node, parent) => {
167
+ if (parent) parents.set(node, parent);
168
+ if (
169
+ node.type === 'link' &&
170
+ typeof node.url === 'string' &&
171
+ node.url.startsWith(MARKER_URL_PREFIX)
172
+ ) {
173
+ const token = node.url.slice(MARKER_URL_PREFIX.length);
174
+ if (markers.has(token)) fail(file, 'Internal short-note marker collision.', node);
175
+ markers.set(token, { node, parent });
176
+ }
177
+ });
178
+
179
+ if (blocks.length !== markers.size) {
180
+ fail(file, 'Internal short-note marker/block pairing failed.', tree);
181
+ }
182
+
183
+ const seenIds = new Set();
184
+ const insertions = new Map();
185
+ for (const block of blocks) {
186
+ const token = literalAttribute(block, BLOCK_ATTRIBUTE, file);
187
+ const marker = markers.get(token);
188
+ if (!marker?.parent || marker.parent.type !== 'paragraph') {
189
+ fail(file, '<ShortNote> must be placed in ordinary prose.', marker?.node ?? block);
190
+ }
191
+ const container = parents.get(marker.parent);
192
+ if (!container || !Array.isArray(container.children)) {
193
+ fail(file, 'A <ShortNote> paragraph has no flow parent.', marker.parent);
194
+ }
195
+ if (block.type !== 'mdxJsxFlowElement') {
196
+ fail(file, '<ShortNote> did not become a block-level MDX element.', block);
197
+ }
198
+
199
+ const id = literalAttribute(block, 'id', file);
200
+ const title = literalAttribute(block, 'title', file);
201
+ const description = literalAttribute(block, 'description', file);
202
+ if (!KEYED_FLOAT_KEY_PATTERN.test(id)) {
203
+ fail(
204
+ file,
205
+ `Invalid <ShortNote id="${id}">; use lowercase letters/numbers separated by single hyphens.`,
206
+ block,
207
+ );
208
+ }
209
+ if (seenIds.has(id)) {
210
+ fail(file, `Duplicate <ShortNote id="${id}"> on this page.`, block);
211
+ }
212
+ seenIds.add(id);
213
+
214
+ const anchorId = `short-note-${id}`;
215
+ removeMdxAttribute(block, BLOCK_ATTRIBUTE);
216
+ injectMdxStringAttributes(block, { anchorId });
217
+
218
+ const markerIndex = marker.parent.children.indexOf(marker.node);
219
+ marker.parent.children[markerIndex] = markerNode({
220
+ id,
221
+ title,
222
+ description,
223
+ anchorId,
224
+ });
225
+
226
+ const existing = insertions.get(marker.parent) ?? { container, blocks: [] };
227
+ existing.blocks.push(block);
228
+ insertions.set(marker.parent, existing);
229
+ }
230
+
231
+ for (const [paragraph, insertion] of insertions) {
232
+ const paragraphIndex = insertion.container.children.indexOf(paragraph);
233
+ insertion.container.children.splice(paragraphIndex + 1, 0, ...insertion.blocks);
234
+ }
235
+ delete tree.data[BLOCKS_DATA];
236
+ };
237
+ }
@@ -84,6 +84,21 @@ interface NotationRegistryInput {
84
84
  readonly sharedDefinitions: readonly SharedNotationDefinitionInput[];
85
85
  readonly lessons: readonly NotationLessonInput[];
86
86
  }
87
+ /**
88
+ * One entry in the abbreviations table (N14) — deliberately a lighter tier
89
+ * than {@link SharedNotationDefinitionInput}: a name resolution only, no
90
+ * `formula`, `units`, `alignment`, or `sources`. `abbr` is the canonical
91
+ * written form (`OLS`); `aliases` are other written forms of the same
92
+ * abbreviation (`w.r.t.` might alias `wrt`) that should resolve the same way.
93
+ */
94
+ interface AbbreviationDefinitionInput {
95
+ readonly key: string;
96
+ readonly abbr: string;
97
+ readonly expansion: string;
98
+ readonly aliases: readonly string[];
99
+ readonly domain?: string;
100
+ readonly source: SourceSpan;
101
+ }
87
102
  type NotationDiagnosticCode = 'invalid-key' | 'duplicate-page' | 'duplicate-definition' | 'conflicting-definition' | 'undefined-reference' | 'reference-cycle' | 'unused-definition' | 'alignment-unknown-competency' | 'alignment-unknown-lesson' | 'alignment-introduction' | 'alignment-unavailable' | 'alignment-review-state' | 'gloss-collides-with-card';
88
103
  type DiagnosticSeverity = 'error' | 'warning';
89
104
  interface NotationDiagnostic {
@@ -171,4 +186,4 @@ interface NotationRegistry {
171
186
  readonly diagnostics: readonly NotationDiagnostic[];
172
187
  }
173
188
 
174
- export type { DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
189
+ export type { AbbreviationDefinitionInput, DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
@@ -103,6 +103,10 @@ const UNITS_VOCAB = /* @__PURE__ */ new Set([
103
103
  "meters",
104
104
  "second",
105
105
  "seconds",
106
+ "kilogram",
107
+ "kilograms",
108
+ "joule",
109
+ "joules",
106
110
  "speed",
107
111
  "velocity",
108
112
  "acceleration",
@@ -48,15 +48,44 @@ every one is listed under a `### Breaking` heading in `CHANGELOG.md` for that
48
48
  version. A **PATCH** never breaks. Once the API settles this clause is dropped
49
49
  and normal semver applies.
50
50
 
51
+ ## The migration artifact
52
+
53
+ Every published engine version carries agent-readable upgrade instructions in
54
+ [`authoring/migrating.md`](authoring/migrating.md). That file is part of the npm
55
+ package, so the instructions travel with the exact implementation they describe
56
+ and remain available at
57
+ `node_modules/explico/docs/authoring/migrating.md` after installation.
58
+
59
+ A release must add a newest-first `## <previous range> → <new version>` section,
60
+ whether the bump is MAJOR, MINOR, or PATCH. The section must do one of two things:
61
+
62
+ - give concrete, ordered changes for every affected consumer-owned surface —
63
+ dependency declarations, `content/`, `content/course.config.ts`, Astro/source
64
+ wiring, course scripts, and generated artifacts — including conditional cases
65
+ and verification commands; or
66
+ - state explicitly that no course changes are required, followed by the normal
67
+ verification commands.
68
+
69
+ The changelog explains what changed and why; it is not a substitute for the
70
+ migration section. The migration section, changelog entry, version bump, and
71
+ package are one atomic release. An agent upgrading across several versions
72
+ applies every intervening section in order.
73
+
51
74
  ## Cutting a release
52
75
 
53
76
  1. Decide the next version from the rules above and the `## [Unreleased]`
54
77
  section of `CHANGELOG.md`.
55
- 2. `CHANGELOG.md`: rename `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD`, add a
78
+ 2. Add the section for `x.y.z` to `docs/authoring/migrating.md`, derived from the
79
+ complete release diff. Write explicit no-action instructions when nothing in
80
+ a downstream course needs to change.
81
+ 3. `CHANGELOG.md`: rename `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD`, add a
56
82
  fresh empty `## [Unreleased]`, and fix up the compare links at the bottom.
57
- 3. Bump `package.json` `version` (`npm version x.y.z --no-git-tag-version`).
58
- 4. `pnpm build && pnpm test`.
59
- 5. Commit, then `git tag vx.y.z`.
83
+ 4. Bump `package.json` `version` (`npm version x.y.z --no-git-tag-version`).
84
+ 5. Run `pnpm check:release-migration`. It fails when the new version has no
85
+ migration section or the shipped-file configuration excludes the guide.
86
+ 6. Run `pnpm build && pnpm test`, then inspect `pnpm pack --dry-run` and confirm
87
+ that `docs/authoring/migrating.md` and `docs/authoring/agents.md` are listed.
88
+ 7. Commit, then `git tag vx.y.z`.
60
89
 
61
90
  ## When you change the engine
62
91
 
@@ -74,6 +74,7 @@ file table.
74
74
  | [notation.md](notation.md) | `N` | how math symbols are written and bound |
75
75
  | [visualizations.md](visualizations.md) | `V` | diagrams, charts, interactive figures, randomness |
76
76
  | [versioning.md](versioning.md) | `C` | the knowledge version and `content/CHANGELOG.md` |
77
+ | [components.md](components.md) | — | _catalog_: inline/block features available in a lesson body — tooltips, tables, figures, diagrams, memos, abbreviations, citations |
77
78
  | [agents.md](agents.md) | — | _guide_: working on a course repo (with an AI agent) |
78
79
  | [migrating.md](migrating.md) | — | _guide_: per-version steps when bumping the `explico` dep |
79
80
 
@@ -89,6 +90,13 @@ file table.
89
90
  | [N6](notation.md#n6) | Probability is `\mathbb{P}` (double-struck `P`) | Build-enforced (completeness gate) + review |
90
91
  | [N7](notation.md#n7) | The differential `d` is upright `\mathrm{d}` | Build-enforced (completeness gate) |
91
92
  | [N8](notation.md#n8) | A named operator is universal (shipped/course list) or explained | Tier-3 warning (`operator-explained`) |
93
+ | [N9](notation.md#n9) | A quantity/unit/operator is typeset in `$…$`, never as plain text | Build-enforced (`gate-prose-notation.ts`) |
94
+ | [N10](notation.md#n10) | A `\mathrm{…}` multi-letter label is universal, or it is explained | Tier-3 warning (`roman-label-explained`) |
95
+ | [N11](notation.md#n11) | A worked example never drops a unit from a substituted value | Review-only |
96
+ | [N12](notation.md#n12) | Math syntax never carries the sentence's own trailing punctuation | Review-only |
97
+ | [N13](notation.md#n13) | A nonlinear function always takes a parenthesized argument | Build-enforced (`resolveMathGlyphs`) |
98
+ | [N14](notation.md#n14) | An abbreviation-shaped word in prose is universal, or explained | Build-enforced (`gate-abbreviations.ts`) |
99
+ | [N15](notation.md#n15) | A nested `(`, `[`, `{`, or `\|` must use `\left`/`\right` sizing | Build-enforced (`resolveMathGlyphs`) |
92
100
  | [V1](visualizations.md#v1) | Graphs and flowcharts use the `Diagram` component | Review-only |
93
101
  | [V2](visualizations.md#v2) | Static charts: SVG → D3 → Observable Plot, chosen per figure | Review-only |
94
102
  | [V3](visualizations.md#v3) | Interactive figures: island + `renderLabMath` seam + text alt | Review-only |
@@ -30,6 +30,9 @@ and a rule disagree, the engine wins — see [`README.md`](README.md).
30
30
  - Grep for a symbol and open a line range; don't read a whole engine module out
31
31
  of `node_modules/explico/`. The parts you actually need are
32
32
  [`notation.md`](notation.md), this file, and the `content` CLI output.
33
+ - Not sure what's available to put in a lesson body — a tooltip, a table, a
34
+ diagram, an aside? [`components.md`](components.md) is a syntax-first
35
+ catalog, not another rule file to read cover to cover.
33
36
  - If a page 500s or a change doesn't show, the dev server's content store is
34
37
  stale — a **new file in a `content/` collection needs a dev-server restart**,
35
38
  not just a save. Restart it; don't diagnose it with `ps` / `curl` scrapes.
@@ -59,5 +62,17 @@ sets `about.version` to match, in the same commit. Rules C1–C4,
59
62
 
60
63
  ## Upgrading the engine
61
64
 
62
- [`migrating.md`](migrating.md) — the per-version steps when you bump the
63
- `explico` dependency across a **Breaking** entry in `CHANGELOG.md`.
65
+ Treat every `explico` dependency bump as a migration task, including PATCH
66
+ updates. Before changing the dependency, record the installed version with
67
+ `node -p "require('explico/package.json').version"`. Install the target version,
68
+ then read the target package's
69
+ `node_modules/explico/docs/authoring/migrating.md`; the migration instructions
70
+ ship with the code they describe.
71
+
72
+ Apply every version section after the old version through the installed version,
73
+ oldest first. Follow required and conditional changes across `content/`,
74
+ `content/course.config.ts`, Astro/source wiring, course scripts, dependencies,
75
+ and generated files. Do not use `CHANGELOG.md` alone as the procedure and do not
76
+ assume that a release without a **Breaking** heading needs no work; each migration
77
+ section either gives exact work or explicitly says no course changes are
78
+ required. Finish with the validation sequence in [`migrating.md`](migrating.md).
@@ -0,0 +1,135 @@
1
+ # Inline & block features — a catalog
2
+
3
+ A fast index of what can go in a lesson body, for an AI (or a person) drafting
4
+ content without reading every rule file first. This file is not a rule area —
5
+ nothing here is build-enforced by *this* file. Where a feature has a rule
6
+ area (`N`, `V`, `C`), the enforcement and the rationale live there; this file
7
+ only says what exists, its minimal syntax, and which of several
8
+ similar-looking options to reach for.
9
+
10
+ See [README.md](README.md) for what "Build-enforced" / "Review-only" means,
11
+ [structure.md](structure.md) for the five structural levels and how floats
12
+ are numbered, and [agents.md](agents.md) for the day-to-day workflow (CLI,
13
+ validation loop).
14
+
15
+ ## Inline, zero-registration
16
+
17
+ **A native browser tooltip: `title="…"` on any element.**
18
+
19
+ ```mdx
20
+ The fit uses <span title="Ordinary least squares run on the demeaned series">OLS</span> here.
21
+ ```
22
+
23
+ This is plain HTML passed straight through MDX/JSX to the DOM — no `id`, no
24
+ content-collection entry, no build-time registration, no hover-card styling,
25
+ no click-to-pin behaviour. The browser renders its own default tooltip on
26
+ hover/focus, exactly like a native `<abbr title="…">` or an image `alt`
27
+ tooltip would. Use it for a throwaway aside that doesn't deserve a citable,
28
+ numbered, or glossary-linked definition — a wording nudge, a "why this value"
29
+ note, a translation of a variable name.
30
+
31
+ _How to apply._ Any element takes `title`; `<span>` is the usual wrapper for
32
+ bare prose text that has no other markup reason to exist. `title` is a plain
33
+ DOM string: no `$…$` math rendering happens inside it (same carve-out
34
+ [N9](notation.md#n9) documents for `<Figure caption="…">` / `<Table
35
+ caption="…">`) — write a unit as a word or plain Unicode, not LaTeX.
36
+
37
+ _What it does **not** replace:_
38
+
39
+ - **An abbreviation-shaped word in visible prose still has to satisfy
40
+ [N14](notation.md#n14).** The completeness gates blank out JSX *tags*
41
+ (`<span title="…">…</span>` → the `<span …>` and `</span>` markup) but not
42
+ the *text a reader sees between them* — so wrapping `OLS` in a tooltip span
43
+ does not exempt that visible `OLS` from needing a real
44
+ `content/abbreviations/*.md` entry, a notation binding, or a
45
+ `course.config.ts` allowlist entry. `title="…"` adds a hover hint on top of
46
+ an otherwise-explained term; it is not itself an explanation the gate
47
+ recognizes. If the word only ever needs to exist inside the tooltip string
48
+ (never as the visible text), the gate never sees it and there is nothing to
49
+ satisfy.
50
+ - **A math quantity still goes in `$…$`, not a `title`.** A hover *card* with
51
+ the definition, one per glyph, page-wide — that's `[[key]]` / notation
52
+ entries ([N4](notation.md#n4)), unrelated machinery.
53
+ - **A citable, reusable, or numbered explanation** — something another page
54
+ should be able to link to, or that belongs in the glossary — is a
55
+ [`<ShortNote>`/`<Memo>`](#short-asides-and-detours) or an
56
+ [abbreviation entry](#reference-systems), not a tooltip. A `title` string
57
+ is invisible to search, screen-reader users relying on non-hover
58
+ interaction, and print/PDF export.
59
+
60
+ _Enforcement._ None — this is unregistered, ungated HTML. The only thing that
61
+ still applies is N9/N14 against the *visible* text, as above.
62
+
63
+ ## Short asides and detours
64
+
65
+ | Component | Import | What it's for | Doc |
66
+ | --- | --- | --- | --- |
67
+ | `<ShortNote id="…" title="…" description="…">` | `explico/components/memo/ShortNote.astro` | An inline, click-to-expand aside that replaces its own trigger text in place — "the smaller, genuinely inline interaction." No number, no cross-page reference. | [structure.md](structure.md#detailed-memos-and-inline-short-notes) |
68
+ | `<Memo id="…" title="…" description="…">` | `explico/components/memo/Memo.astro` | A collapsible, independently-numbered card for optional depth (derivation, proof, edge case) that would otherwise bloat the main argument. Cited with `[[memo-id]]`. | [structure.md](structure.md#detailed-memos-and-inline-short-notes) |
69
+
70
+ Both differ from a bare `title` tooltip in the same way: they hold real,
71
+ possibly multi-paragraph Markdown content (including equations, tables,
72
+ figures), are addressable (`id`, a permalink), and are meant to be found
73
+ again — a `title` tooltip is disposable and invisible outside a hover/focus
74
+ event.
75
+
76
+ ## Numbered floats
77
+
78
+ Each of these gets a `‹part›.‹n›` number (its own counter, independent of the
79
+ others — see [structure.md § Numbering](structure.md#numbering)), a stable
80
+ `#anchor`, and same-page/cross-page reference syntax `[[prefix-id]]` /
81
+ `[[lesson/slug#prefix-id]]`.
82
+
83
+ | Component | Import | Prefix | Doc |
84
+ | --- | --- | --- | --- |
85
+ | `<Table id="…" caption="…">` | `explico/components/table/Table.astro` | `tab-` | [structure.md](structure.md#numbering), [V rules](visualizations.md) |
86
+ | `<Figure id="…" title="…" caption="…" previewable>` | `explico/components/figure/Figure.astro` | `fig-` | [structure.md](structure.md#numbering), [V2](visualizations.md#v2)/[V3](visualizations.md#v3) |
87
+ | `<Diagram id="…" label="…" nodes={…} edges={…}>` | `explico/components/diagram/Diagram.astro` | `dia-` | [V1](visualizations.md#v1) |
88
+ | A display equation with `\label{eq:key}` | `remark-math` (built in) | `eq-` | [N5](notation.md#n5), [structure.md](structure.md#numbering) |
89
+
90
+ Two naming traps worth flagging for an AI author:
91
+
92
+ - **`<Figure title="…">` is a visible short label in the collapsible title
93
+ bar, not the HTML `title` attribute's hover tooltip.** It always renders on
94
+ the page; it is a different feature from the [native tooltip](#inline-zero-registration)
95
+ above even though it reuses the same prop name.
96
+ - **A raw Markdown pipe table or a raw `<table>`, or a `<Diagram>` with no
97
+ `id`,** renders fine but never enters keyed-float validation — no number,
98
+ no `[[…]]` target. Wrap it (`<Table>`/`<Figure>`) or give the diagram an
99
+ `id` if it needs to be referenced. See [structure.md § Raw and unwrapped
100
+ floats](structure.md#raw-and-unwrapped-floats).
101
+
102
+ ## Reference systems
103
+
104
+ These bind a term to a definition that a reader can hover *and* look up
105
+ elsewhere — the machinery a bare `title` tooltip deliberately skips.
106
+
107
+ | Syntax | Registers where | Gives | Doc |
108
+ | --- | --- | --- | --- |
109
+ | `[[key]]` in prose, before first math use | `content/notation/*.md` entry | One-meaning-per-glyph hover card everywhere `$key$`-bound math appears on the page | [N4](notation.md#n4) |
110
+ | `\explain{key}{latex}` inside `$…$` | Same notation entry | Escape hatch for a compound/colliding symbol only, not the default bind | [N4](notation.md#n4) |
111
+ | A bare all-caps/dotted term (`OLS`, `w.r.t.`) in prose | `content/abbreviations/*.md`, or `course.config.ts` `abbreviations.allow` | Hover/click card on first occurrence, linked to the glossary page | [N14](notation.md#n14) |
112
+ | `[@source-id]` or `[@source-id; §1.2]` | `content/sources/*.json` | Numbered `[n]` superscript linking to a References section, with a hover/pin panel | `src/reference/remark-citation.mjs` |
113
+
114
+ ## Knowledge checks and worked examples
115
+
116
+ | Component | Import | Notes | Doc |
117
+ | --- | --- | --- | --- |
118
+ | `<AssessmentSet />` | `explico/components/assessments/AssessmentSet.astro` | Renders every check listed in the lesson's colocated `.checks.yml`; do not add a separate heading for it | [structure.md § Knowledge checks](structure.md#knowledge-checks) |
119
+ | `<CompactExample>` / `<CompactExamples>` | `explico/components/examples/CompactExample.astro` | Worked-example cards; a `<Diagram>` nested inside one is invisible to diagram numbering (see the raw/unwrapped note above) | [structure.md](structure.md#raw-and-unwrapped-floats) |
120
+
121
+ ## Choosing between the "explain a term" options
122
+
123
+ From lightest to heaviest — pick the first one that's actually enough:
124
+
125
+ 1. **Native `title="…"` tooltip** — disposable, hover-only, nothing to
126
+ register. Use when the aside doesn't need to be found again.
127
+ 2. **`<ShortNote>`** — inline, click-to-expand, can hold Markdown, no number.
128
+ Use when the aside is worth more than a one-line hover but still belongs
129
+ inline, not as its own citable unit.
130
+ 3. **Abbreviation entry / notation `[[key]]`** — page-wide hover card, build
131
+ gated (N14/N4), listed in the glossary. Use when the term recurs, is
132
+ acronym-shaped, or is genuine math notation.
133
+ 4. **`<Memo>`** — numbered, permalinked, holds real sub-content (proofs,
134
+ figures, tables). Use when the aside is long enough to be its own small
135
+ section.
@@ -1,15 +1,29 @@
1
1
  # Upgrading the explico engine
2
2
 
3
- What a course repo has to change when it bumps its `explico` dependency across a
4
- version that carries **Breaking** notes. Each section is the concrete work; the
5
- matching `CHANGELOG.md` entry (shipped in the installed package) is the full
6
- list of what moved and why.
3
+ This is the agent-readable migration artifact shipped with `explico`. It lives
4
+ at `node_modules/explico/docs/authoring/migrating.md` in an installed course, so
5
+ the instructions always accompany the engine version they describe.
6
+
7
+ There is one newest-first section for **every** published version, not only for a
8
+ version whose changelog contains **Breaking** notes. A section contains the
9
+ concrete work for consumer-owned content, configuration, source wiring, scripts,
10
+ dependencies, and generated artifacts. When a release needs none of those, its
11
+ section says **No course changes required** explicitly. The matching
12
+ `CHANGELOG.md` entry (also shipped in the package) remains the full list of what
13
+ moved and why; it is context, not a replacement for these steps.
7
14
 
8
15
  The pattern every time:
9
16
 
10
- 1. `pnpm add -D explico@<version>`
11
- 2. Do the per-version steps below.
12
- 3. `pnpm validate:content` (run it **twice** — the first run regenerates
17
+ 1. Record the currently installed version before changing it:
18
+ `node -p "require('explico/package.json').version"`.
19
+ 2. `pnpm add -D explico@<version>`.
20
+ 3. Read this file from the newly installed package and apply **every** section
21
+ after the old version through the target version, oldest first. Do not skip a
22
+ section merely because its changelog entry is not labelled **Breaking**.
23
+ 4. Make every required course-side change named by those sections, including
24
+ changes under `content/`, Astro/source configuration, and course scripts; do
25
+ not stop after changing the dependency declaration.
26
+ 5. `pnpm validate:content` (run it **twice** — the first run regenerates
13
27
  `build/resolution/*` and reports it stale; the second is clean), then
14
28
  `pnpm content check --changed`, then `astro check`, then `astro build`.
15
29
  Each stage names what it doesn't like; fix and re-run.
@@ -19,6 +33,77 @@ required config block throws with the section of this file to read.
19
33
 
20
34
  ---
21
35
 
36
+ ## 0.4.x → 0.5.0
37
+
38
+ ### 1. Rule N14: catalogue every abbreviation used in lesson prose
39
+
40
+ Every acronym (`OLS`, `MSE`) or dotted abbreviation (`w.r.t.`, `i.i.d.`) used in
41
+ plain prose must now be on the shipped baseline
42
+ (`base-abbreviations.mjs`), listed in `course.config.ts`'s new
43
+ `abbreviations.allow` / `abbreviations.deny`, given an entry in the new
44
+ `content/abbreviations/*.md` collection, or already explained as a math
45
+ quantity via N8/N10. This is build-enforced, same error-severity and
46
+ no-`lint-ignore.yml`-escape-hatch shape as N9.
47
+
48
+ ```ts
49
+ export const courseConfig = {
50
+ // …existing fields…
51
+ abbreviations: {
52
+ allow: ['GDP'], // treat as universal, skip the check
53
+ deny: ['ETA'], // demote a baseline entry back to "must be explained"
54
+ },
55
+ } satisfies CourseConfig;
56
+ ```
57
+
58
+ ```markdown
59
+ ---
60
+ key: ols
61
+ abbr: OLS
62
+ expansion: Ordinary least squares
63
+ aliases: []
64
+ ---
65
+ ```
66
+
67
+ Run `pnpm validate:content` (twice) and `pnpm content check --changed`. Fix
68
+ every `abbreviation-explained` diagnostic by adding a
69
+ `content/abbreviations/<key>.md` entry or an `allow` entry in
70
+ `course.config.ts`.
71
+
72
+ ### 2. Manifest schema is v13 (additive: v10 → v11 → v12 → v13)
73
+
74
+ `lessons[].lessonExports`-derived fields (v11), `manifest.abbreviations` (v12),
75
+ and `manifest.diagnostics.rawFloats` (v13) are new. A client asserting on
76
+ `MANIFEST_SCHEMA_VERSION` by exact equality needs the bump; nothing existing
77
+ changed shape.
78
+
79
+ ### 3. Optional: enable standalone HTML / print lesson exports
80
+
81
+ No action required to keep building. To opt in, add to
82
+ `content/course.config.ts`:
83
+
84
+ ```ts
85
+ lessonExports: {
86
+ standaloneHtml: true,
87
+ print: true,
88
+ },
89
+ canonicalBaseUrl: 'https://your-course.example.com',
90
+ ```
91
+
92
+ ### 4. Optional: turn on strict numbering cross-checks
93
+
94
+ `remarkNotation`'s new `strict: true` option — recommended via
95
+ `createRemarkNotationOptions(manifest, { strict: true })` — fails the build on
96
+ any catalogued lesson that has an equation, `<Table>`, or `<Figure>` (keyed or
97
+ not) with no placement, instead of silently falling back to legacy `##`-section
98
+ numbering. Off by default; turn it on once existing content is clean.
99
+
100
+ ### Verification
101
+
102
+ `pnpm validate:content` (run it twice), then `pnpm content check --changed`,
103
+ then `astro check`, then `astro build`.
104
+
105
+ ---
106
+
22
107
  ## 0.3.x → 0.4.0
23
108
 
24
109
  ### 1. Rename `sections` to `parts` in `content/course.config.ts`