explico 0.3.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 (151) hide show
  1. package/CHANGELOG.md +344 -1
  2. package/README.md +117 -3
  3. package/dist/compiler/astro.d.ts +38 -0
  4. package/dist/compiler/astro.js +258 -0
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +32 -12
  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 +86 -4
  10. package/dist/compiler/course-config.js +39 -0
  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/lesson-derivation.d.ts +1 -1
  14. package/dist/compiler/lesson-derivation.js +2 -2
  15. package/dist/compiler/manifest.d.ts +112 -7
  16. package/dist/compiler/manifest.js +205 -40
  17. package/dist/compiler/resolution-report.d.ts +4 -0
  18. package/dist/reference/base-abbreviations.d.mts +30 -0
  19. package/dist/reference/base-abbreviations.mjs +60 -0
  20. package/dist/reference/base-operators.d.mts +15 -0
  21. package/dist/reference/base-operators.mjs +40 -0
  22. package/dist/reference/base-roman-labels.d.mts +35 -0
  23. package/dist/reference/base-roman-labels.mjs +43 -0
  24. package/dist/reference/consistency.d.ts +8 -2
  25. package/dist/reference/consistency.js +50 -3
  26. package/dist/reference/diagrams-validate.d.ts +21 -0
  27. package/dist/reference/diagrams-validate.js +13 -0
  28. package/dist/reference/equations-validate.d.ts +8 -3
  29. package/dist/reference/equations-validate.js +1 -1
  30. package/dist/reference/equations.d.mts +66 -35
  31. package/dist/reference/equations.mjs +142 -40
  32. package/dist/reference/gate-abbreviations.d.ts +17 -0
  33. package/dist/reference/gate-abbreviations.js +64 -0
  34. package/dist/reference/gate-math.d.ts +28 -5
  35. package/dist/reference/gate-math.js +5 -2
  36. package/dist/reference/gate-prose-notation.d.ts +60 -0
  37. package/dist/reference/gate-prose-notation.js +125 -0
  38. package/dist/reference/keyed-float-validate.d.ts +14 -8
  39. package/dist/reference/keyed-float-validate.js +1 -1
  40. package/dist/reference/keyed-jsx-floats.d.mts +24 -21
  41. package/dist/reference/keyed-jsx-floats.mjs +90 -41
  42. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  43. package/dist/reference/knowledge-checks-validate.js +78 -0
  44. package/dist/reference/knowledge-checks.d.mts +25 -0
  45. package/dist/reference/knowledge-checks.mjs +45 -0
  46. package/dist/reference/lab-math-scope.d.ts +9 -1
  47. package/dist/reference/lab-math-scope.js +32 -4
  48. package/dist/reference/math-glyphs.d.mts +30 -0
  49. package/dist/reference/math-glyphs.mjs +367 -0
  50. package/dist/reference/mdx-attributes.d.mts +13 -0
  51. package/dist/reference/mdx-attributes.mjs +38 -0
  52. package/dist/reference/mixed-content.d.ts +17 -0
  53. package/dist/reference/mixed-content.js +29 -0
  54. package/dist/reference/page-notation.d.ts +52 -0
  55. package/dist/reference/page-notation.js +171 -0
  56. package/dist/reference/raw-floats-validate.d.ts +18 -0
  57. package/dist/reference/raw-floats-validate.js +53 -0
  58. package/dist/reference/raw-floats.d.mts +18 -0
  59. package/dist/reference/raw-floats.mjs +129 -0
  60. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  61. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  62. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  63. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  64. package/dist/reference/remark-abbreviations.d.mts +7 -0
  65. package/dist/reference/remark-abbreviations.mjs +174 -0
  66. package/dist/reference/remark-memo.d.mts +16 -0
  67. package/dist/reference/remark-memo.mjs +339 -0
  68. package/dist/reference/remark-notation.d.mts +102 -5
  69. package/dist/reference/remark-notation.mjs +506 -50
  70. package/dist/reference/remark-short-note.d.mts +4 -0
  71. package/dist/reference/remark-short-note.mjs +237 -0
  72. package/dist/reference/types.d.ts +16 -1
  73. package/dist/reference/units-vocab.js +4 -0
  74. package/docs/VERSIONING.md +33 -4
  75. package/docs/authoring/README.md +12 -0
  76. package/docs/authoring/agents.md +17 -2
  77. package/docs/authoring/components.md +135 -0
  78. package/docs/authoring/migrating.md +154 -7
  79. package/docs/authoring/notation.md +568 -8
  80. package/docs/authoring/structure.md +251 -0
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +332 -0
  84. package/src/compiler/cli.ts +33 -10
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +131 -3
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/lesson-derivation.ts +2 -2
  89. package/src/compiler/manifest.ts +367 -49
  90. package/src/components/HoverPanel.astro +45 -4
  91. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  92. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  93. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  94. package/src/components/assessments/AssessmentSet.astro +98 -14
  95. package/src/components/assessments/RenderedContent.tsx +55 -0
  96. package/src/components/assessments/assessment-randomization.ts +16 -0
  97. package/src/components/diagram/Diagram.astro +444 -83
  98. package/src/components/disclosure/hover-preview.ts +202 -0
  99. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  100. package/src/components/examples/CompactExamples.astro +2 -0
  101. package/src/components/figure/Figure.astro +279 -10
  102. package/src/components/memo/Memo.astro +453 -0
  103. package/src/components/memo/ShortNote.astro +223 -0
  104. package/src/components/notation/NotationFormula.astro +18 -1
  105. package/src/components/notation/NotationLayer.astro +116 -8
  106. package/src/components/notation/render-notation-math.ts +39 -2
  107. package/src/components/notation/safe-math-to-react.ts +50 -0
  108. package/src/components/reference/FloatRefLayer.astro +233 -15
  109. package/src/components/starlight/CollapsibleSections.astro +461 -0
  110. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  111. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  112. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  113. package/src/components/starlight/LayoutHeader.astro +50 -0
  114. package/src/components/starlight/LayoutSidebar.astro +62 -1
  115. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  116. package/src/components/starlight/LessonExportActions.astro +436 -0
  117. package/src/components/starlight/LessonFooter.astro +38 -2
  118. package/src/components/starlight/section-parts.ts +31 -0
  119. package/src/components/table/Table.astro +296 -9
  120. package/src/components/table/TableOverflowExpander.astro +489 -0
  121. package/src/content-config.ts +20 -0
  122. package/src/reference/base-abbreviations.mjs +60 -0
  123. package/src/reference/base-operators.mjs +40 -0
  124. package/src/reference/base-roman-labels.mjs +43 -0
  125. package/src/reference/consistency.ts +73 -2
  126. package/src/reference/diagrams-validate.ts +40 -0
  127. package/src/reference/equations-validate.ts +6 -4
  128. package/src/reference/equations.mjs +142 -40
  129. package/src/reference/gate-abbreviations.ts +128 -0
  130. package/src/reference/gate-math.ts +27 -7
  131. package/src/reference/gate-prose-notation.ts +224 -0
  132. package/src/reference/keyed-float-validate.ts +12 -9
  133. package/src/reference/keyed-jsx-floats.mjs +90 -41
  134. package/src/reference/knowledge-checks-validate.ts +130 -0
  135. package/src/reference/knowledge-checks.mjs +45 -0
  136. package/src/reference/lab-math-scope.ts +45 -3
  137. package/src/reference/math-glyphs.mjs +367 -0
  138. package/src/reference/mdx-attributes.mjs +38 -0
  139. package/src/reference/mixed-content.ts +69 -0
  140. package/src/reference/page-notation.ts +252 -0
  141. package/src/reference/raw-floats-validate.ts +103 -0
  142. package/src/reference/raw-floats.mjs +129 -0
  143. package/src/reference/rehype-heading-anchors.mjs +107 -1
  144. package/src/reference/rehype-table-header-labels.mjs +152 -0
  145. package/src/reference/remark-abbreviations.mjs +174 -0
  146. package/src/reference/remark-memo.mjs +339 -0
  147. package/src/reference/remark-notation.mjs +506 -50
  148. package/src/reference/remark-short-note.mjs +237 -0
  149. package/src/reference/types.ts +16 -0
  150. package/src/reference/units-vocab.ts +4 -0
  151. package/src/styles/global.css +1125 -50
@@ -0,0 +1,4 @@
1
+ /** Return the parseable source form, primarily for adapter tests. */
2
+ export function preprocessShortNotes(source: any): string;
3
+ /** @this {any} */
4
+ export default function remarkShortNote(this: any): (tree: any, file: any) => void;
@@ -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
 
@@ -38,6 +38,10 @@ this folder, reviewed like any other change.
38
38
 
39
39
  ## How a rule is written
40
40
 
41
+ [`structure.md`](structure.md) is not a rule set — it is the taxonomy the rules
42
+ speak in: what a part, a chapter, a section, and a subsection are, and which of
43
+ them an equation number counts.
44
+
41
45
  Each rule has a stable ID — a letter for its area (`N` notation,
42
46
  `V` visualizations, `C` versioning) plus a number — and this shape:
43
47
 
@@ -70,6 +74,7 @@ file table.
70
74
  | [notation.md](notation.md) | `N` | how math symbols are written and bound |
71
75
  | [visualizations.md](visualizations.md) | `V` | diagrams, charts, interactive figures, randomness |
72
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 |
73
78
  | [agents.md](agents.md) | — | _guide_: working on a course repo (with an AI agent) |
74
79
  | [migrating.md](migrating.md) | — | _guide_: per-version steps when bumping the `explico` dep |
75
80
 
@@ -85,6 +90,13 @@ file table.
85
90
  | [N6](notation.md#n6) | Probability is `\mathbb{P}` (double-struck `P`) | Build-enforced (completeness gate) + review |
86
91
  | [N7](notation.md#n7) | The differential `d` is upright `\mathrm{d}` | Build-enforced (completeness gate) |
87
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`) |
88
100
  | [V1](visualizations.md#v1) | Graphs and flowcharts use the `Diagram` component | Review-only |
89
101
  | [V2](visualizations.md#v2) | Static charts: SVG → D3 → Observable Plot, chosen per figure | Review-only |
90
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.