explico 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +46 -5
  3. package/dist/compiler/cli.d.ts +17 -0
  4. package/dist/compiler/cli.js +41 -3
  5. package/dist/compiler/collections.js +31 -1
  6. package/dist/compiler/course-config.d.ts +52 -1
  7. package/dist/compiler/knowledge-version.d.ts +58 -0
  8. package/dist/compiler/knowledge-version.js +80 -0
  9. package/dist/compiler/manifest.d.ts +87 -3
  10. package/dist/compiler/manifest.js +133 -20
  11. package/dist/compiler/resolution-report.d.ts +5 -0
  12. package/dist/reference/base-operators.d.mts +38 -0
  13. package/dist/reference/base-operators.mjs +103 -0
  14. package/dist/reference/consistency.d.ts +7 -2
  15. package/dist/reference/consistency.js +142 -2
  16. package/dist/reference/diagram-layout.d.ts +63 -0
  17. package/dist/reference/diagram-layout.js +122 -0
  18. package/dist/reference/equations-validate.d.ts +5 -0
  19. package/dist/reference/equations-validate.js +1 -1
  20. package/dist/reference/equations.d.mts +76 -18
  21. package/dist/reference/equations.mjs +118 -41
  22. package/dist/reference/figures-validate.d.ts +21 -0
  23. package/dist/reference/figures-validate.js +13 -0
  24. package/dist/reference/gate-math.d.ts +5 -4
  25. package/dist/reference/gate-math.js +73 -2
  26. package/dist/reference/gloss.d.ts +87 -0
  27. package/dist/reference/gloss.js +59 -0
  28. package/dist/reference/keyed-float-validate.d.ts +67 -0
  29. package/dist/reference/keyed-float-validate.js +110 -0
  30. package/dist/reference/keyed-jsx-floats.d.mts +56 -0
  31. package/dist/reference/keyed-jsx-floats.mjs +167 -0
  32. package/dist/reference/math-glyphs.d.mts +18 -0
  33. package/dist/reference/math-glyphs.mjs +92 -0
  34. package/dist/reference/registry.js +25 -0
  35. package/dist/reference/rehype-heading-anchors.d.mts +1 -0
  36. package/dist/reference/rehype-heading-anchors.mjs +144 -0
  37. package/dist/reference/remark-notation.d.mts +12 -0
  38. package/dist/reference/remark-notation.mjs +228 -32
  39. package/dist/reference/tables-validate.d.ts +21 -0
  40. package/dist/reference/tables-validate.js +13 -0
  41. package/dist/reference/types.d.ts +25 -2
  42. package/dist/reference/units-vocab.js +19 -1
  43. package/docs/VERSIONING.md +66 -0
  44. package/docs/authoring/README.md +96 -0
  45. package/docs/authoring/agents.md +63 -0
  46. package/docs/authoring/migrating.md +107 -0
  47. package/docs/authoring/notation.md +296 -0
  48. package/docs/authoring/versioning.md +116 -0
  49. package/docs/authoring/visualizations.md +195 -0
  50. package/package.json +20 -5
  51. package/src/compiler/cli.ts +49 -0
  52. package/src/compiler/collections.ts +52 -3
  53. package/src/compiler/course-config.ts +54 -0
  54. package/src/compiler/knowledge-version.ts +157 -0
  55. package/src/compiler/manifest.ts +270 -17
  56. package/src/components/CurriculumMap.astro +27 -0
  57. package/src/components/HoverPanel.astro +42 -0
  58. package/src/components/citation/CitationLayer.astro +1 -0
  59. package/src/components/diagram/Diagram.astro +317 -0
  60. package/src/components/examples/CompactExamples.astro +1 -1
  61. package/src/components/figure/Figure.astro +104 -0
  62. package/src/components/hover-panel.ts +71 -5
  63. package/src/components/notation/NotationFormula.astro +154 -0
  64. package/src/components/notation/NotationGlossary.astro +60 -0
  65. package/src/components/notation/NotationLayer.astro +248 -103
  66. package/src/components/notation/render-notation-math.ts +66 -0
  67. package/src/components/popup-stack.ts +345 -0
  68. package/src/components/reference/FloatRefLayer.astro +671 -0
  69. package/src/components/starlight/AboutPanel.astro +255 -0
  70. package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
  71. package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
  72. package/src/components/starlight/LayoutSocialIcons.astro +12 -0
  73. package/src/components/starlight/LessonFooter.astro +8 -2
  74. package/src/components/table/Table.astro +104 -0
  75. package/src/content-config.ts +38 -0
  76. package/src/reference/base-operators.mjs +103 -0
  77. package/src/reference/consistency.ts +236 -2
  78. package/src/reference/diagram-layout.ts +246 -0
  79. package/src/reference/equations-validate.ts +6 -1
  80. package/src/reference/equations.mjs +118 -41
  81. package/src/reference/figures-validate.ts +40 -0
  82. package/src/reference/gate-math.ts +104 -7
  83. package/src/reference/gloss.ts +153 -0
  84. package/src/reference/keyed-float-validate.ts +207 -0
  85. package/src/reference/keyed-jsx-floats.mjs +167 -0
  86. package/src/reference/math-glyphs.mjs +92 -0
  87. package/src/reference/registry.ts +43 -0
  88. package/src/reference/rehype-heading-anchors.mjs +144 -0
  89. package/src/reference/remark-notation.mjs +228 -32
  90. package/src/reference/tables-validate.ts +40 -0
  91. package/src/reference/types.ts +26 -1
  92. package/src/reference/units-vocab.ts +18 -0
  93. package/src/styles/global.css +193 -3
  94. package/src/components/starlight/EquationEnhancer.astro +0 -72
@@ -5,7 +5,16 @@
5
5
  * unknown?: 'error' | 'warn' | 'ignore',
6
6
  * base?: string,
7
7
  * equations?: Record<string, Record<string, string>>,
8
+ * tables?: Record<string, Record<string, string>>,
9
+ * figures?: Record<string, Record<string, string>>,
10
+ * chapters?: Record<string, number>,
8
11
  * }} [options]
12
+ *
13
+ * `chapters` maps a lesson slug to its 1-based course ordinal. When the
14
+ * current page has one, every display equation / `<Table>` / `<Figure>` is
15
+ * numbered `‹chapter›.‹n›` (`##` headings ignored); with no entry the legacy
16
+ * `##`-section scheme applies. It must agree with `manifest.lessons[].chapter`,
17
+ * which is what feeds `equations`/`tables`/`figures` above.
9
18
  */
10
19
  export default function remarkNotation(options?: {
11
20
  definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>);
@@ -13,6 +22,9 @@ export default function remarkNotation(options?: {
13
22
  unknown?: "error" | "warn" | "ignore";
14
23
  base?: string;
15
24
  equations?: Record<string, Record<string, string>>;
25
+ tables?: Record<string, Record<string, string>>;
26
+ figures?: Record<string, Record<string, string>>;
27
+ chapters?: Record<string, number>;
16
28
  }): (tree: any, file: any) => void;
17
29
  /**
18
30
  * Remark adapter for semantic notation references.
@@ -36,10 +36,17 @@ export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
36
36
  import { GlyphResolutionError, resolveMathGlyphs } from './math-glyphs.mjs';
37
37
  import {
38
38
  EQ_LABEL_PATTERN,
39
+ collectDisplayEquations,
39
40
  collectEquationLabels,
41
+ equationAnchorId,
40
42
  parseEquationRef,
41
43
  stripEquationLabels,
42
44
  } from './equations.mjs';
45
+ import {
46
+ collectKeyedJsxFloats,
47
+ keyedFloatAnchorId,
48
+ parseKeyedFloatRef,
49
+ } from './keyed-jsx-floats.mjs';
43
50
 
44
51
  const PROSE_SKIP_TYPES = new Set([
45
52
  'code',
@@ -307,34 +314,39 @@ function defaultHref(definition, key, base = '') {
307
314
  }
308
315
 
309
316
  /**
310
- * Wrap a keyed `$$…$$` `math` node so the container survives `rehype-katex`
311
- * (which splices the inner `math-display` element out). The visible number is
312
- * a focusable `<a>` sibling — never authored, never inside KaTeX — so it
313
- * survives print and screen readers name it "Equation 2.4".
317
+ * Wrap a display `$$…$$` `math` node so the container survives `rehype-katex`
318
+ * (which splices the inner `math-display` element out). Every display equation
319
+ * in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
320
+ * at its positional `#eq-<section>-<index>`. The visible number is a focusable
321
+ * `<a>` sibling — never authored, never inside KaTeX — so it survives print and
322
+ * screen readers name it "Equation 2.4".
314
323
  */
315
- function keyedEquationNode(mathNode, key, number) {
324
+ function equationNode(mathNode, { key, number }) {
325
+ const id = equationAnchorId({ key, number });
326
+ const hProperties = {
327
+ id,
328
+ className: ['keyed-equation'],
329
+ 'data-eq-number': number,
330
+ };
331
+ if (key) hProperties['data-eq-key'] = key;
332
+ else hProperties['data-eq-auto'] = '';
333
+
316
334
  return {
317
335
  type: 'paragraph',
318
- data: {
319
- hName: 'div',
320
- hProperties: {
321
- id: `eq-${key}`,
322
- className: ['keyed-equation'],
323
- 'data-eq-key': key,
324
- 'data-eq-number': number,
325
- },
326
- },
336
+ data: { hName: 'div', hProperties },
327
337
  children: [
328
338
  mathNode,
329
339
  {
330
340
  type: 'link',
331
- url: `#eq-${key}`,
341
+ url: `#${id}`,
332
342
  children: [{ type: 'text', value: `(${number})` }],
333
343
  data: {
334
344
  hProperties: {
335
345
  className: ['keyed-equation__number'],
336
346
  'data-eq-number': number,
337
347
  'aria-label': `Equation ${number}`,
348
+ // Plain hover tooltip — the number is a permalink to this equation.
349
+ title: 'Anchor to this equation',
338
350
  },
339
351
  },
340
352
  },
@@ -342,6 +354,32 @@ function keyedEquationNode(mathNode, key, number) {
342
354
  };
343
355
  }
344
356
 
357
+ /**
358
+ * Give a `<Table>`/`<Figure>` `mdxJsxFlowElement` its computed `number` and
359
+ * `anchorId` as real JSX attributes, so the actual Astro component (which
360
+ * renders the numbered anchor, unlike `equationNode`'s synthesized hast div)
361
+ * receives them as props. The compiler is the single source of truth for
362
+ * both — the component only renders what it's given.
363
+ */
364
+ function injectFloatNumberAttributes(node, float, prefix) {
365
+ const anchorId = keyedFloatAnchorId({
366
+ key: float.key,
367
+ number: float.number,
368
+ prefix,
369
+ });
370
+ node.attributes = (node.attributes ?? []).filter(
371
+ (attribute) =>
372
+ !(
373
+ attribute?.type === 'mdxJsxAttribute' &&
374
+ (attribute.name === 'number' || attribute.name === 'anchorId')
375
+ ),
376
+ );
377
+ node.attributes.push(
378
+ { type: 'mdxJsxAttribute', name: 'number', value: float.number },
379
+ { type: 'mdxJsxAttribute', name: 'anchorId', value: anchorId },
380
+ );
381
+ }
382
+
345
383
  /** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
346
384
  function resolveEquationRef(eqRef, eqCtx, file, node) {
347
385
  if (eqRef.lesson) {
@@ -369,7 +407,47 @@ function resolveEquationRef(eqRef, eqCtx, file, node) {
369
407
  return { number, href: `#eq-${eqRef.key}` };
370
408
  }
371
409
 
372
- function proseNodes(value, resolve, file, node, references, eqCtx) {
410
+ /**
411
+ * Resolve a `[[<prefix>-key]]` / `[[slug#<prefix>-key]]` reference (a
412
+ * `<Table>`/`<Figure>` float) to `{ number, href }` — the table/figure
413
+ * equivalent of `resolveEquationRef`.
414
+ */
415
+ function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
416
+ if (floatRef.lesson) {
417
+ const number = ctx.crossPage?.[floatRef.lesson]?.[floatRef.key];
418
+ if (number === undefined) {
419
+ fail(
420
+ file,
421
+ `Unknown ${tagName} reference [[${floatRef.lesson}#${prefix}-${floatRef.key}]]: lesson ${floatRef.lesson} has no <${tagName}> with id="${floatRef.key}".`,
422
+ node,
423
+ );
424
+ }
425
+ return {
426
+ number,
427
+ href: `${ctx.base.replace(/\/$/, '')}/${floatRef.lesson}/#${prefix}-${floatRef.key}`,
428
+ };
429
+ }
430
+ const number = ctx.pageLabels.get(floatRef.key);
431
+ if (number === undefined) {
432
+ fail(
433
+ file,
434
+ `Unknown ${tagName} reference [[${prefix}-${floatRef.key}]]: no <${tagName}> on this page carries id="${floatRef.key}".`,
435
+ node,
436
+ );
437
+ }
438
+ return { number, href: `#${prefix}-${floatRef.key}` };
439
+ }
440
+
441
+ function proseNodes(
442
+ value,
443
+ resolve,
444
+ file,
445
+ node,
446
+ references,
447
+ eqCtx,
448
+ tableCtx,
449
+ figureCtx,
450
+ ) {
373
451
  const base = eqCtx?.base ?? '';
374
452
  const strayExplain = firstUnescaped(value, EXPLAIN);
375
453
  if (strayExplain !== -1) {
@@ -390,7 +468,18 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
390
468
 
391
469
  const key = match[1].trim();
392
470
  const eqRef = eqCtx ? parseEquationRef(key) : undefined;
393
- if (!eqRef && !NOTATION_KEY_PATTERN.test(key)) {
471
+ const tableRef =
472
+ !eqRef && tableCtx ? parseKeyedFloatRef(key, { prefix: 'tab' }) : undefined;
473
+ const figureRef =
474
+ !eqRef && !tableRef && figureCtx
475
+ ? parseKeyedFloatRef(key, { prefix: 'fig' })
476
+ : undefined;
477
+ if (
478
+ !eqRef &&
479
+ !tableRef &&
480
+ !figureRef &&
481
+ !NOTATION_KEY_PATTERN.test(key)
482
+ ) {
394
483
  fail(file, `Invalid notation key "${key}" in [[...]].`, node);
395
484
  }
396
485
 
@@ -421,9 +510,46 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
421
510
  continue;
422
511
  }
423
512
 
513
+ const floatRef = tableRef ?? figureRef;
514
+ if (floatRef) {
515
+ const kind = tableRef ? 'table' : 'figure';
516
+ const prefix = tableRef ? 'tab' : 'fig';
517
+ const tagName = tableRef ? 'Table' : 'Figure';
518
+ const { number, href } = resolveKeyedFloatRef(
519
+ floatRef,
520
+ tableRef ? tableCtx : figureCtx,
521
+ { prefix, tagName },
522
+ file,
523
+ node,
524
+ );
525
+ output.push({
526
+ type: 'link',
527
+ url: href,
528
+ children: [{ type: 'text', value: `${tagName} ${number}` }],
529
+ data: {
530
+ hProperties: {
531
+ className: [`${kind}-ref`],
532
+ [`data-${prefix}-ref`]: floatRef.key,
533
+ ...(floatRef.lesson ? { [`data-${prefix}-lesson`]: floatRef.lesson } : {}),
534
+ },
535
+ },
536
+ });
537
+ references.push({
538
+ key: floatRef.key,
539
+ kind,
540
+ ...(floatRef.lesson ? { lesson: floatRef.lesson } : {}),
541
+ });
542
+ cursor = match.index + match[0].length;
543
+ continue;
544
+ }
545
+
424
546
  const definition = resolve(key, 'prose', node);
425
- const title = definition?.title ?? key;
426
- const summary = definition?.summary;
547
+ // `title`/`summary` are the pre-C1b field names, kept as the first
548
+ // fallback for a caller still supplying them; real content authors
549
+ // `label`/`meaning` (see `content-config.ts`), which raw frontmatter
550
+ // (`manifest.notation.raw`, a page's own `notation.local`) carries as-is.
551
+ const title = definition?.title ?? definition?.label ?? key;
552
+ const summary = definition?.summary ?? definition?.meaning;
427
553
  const href = defaultHref(definition, key, base);
428
554
 
429
555
  const properties = {
@@ -552,7 +678,16 @@ function inspectExplainCalls(value, resolve, file, node, references) {
552
678
  * unknown?: 'error' | 'warn' | 'ignore',
553
679
  * base?: string,
554
680
  * equations?: Record<string, Record<string, string>>,
681
+ * tables?: Record<string, Record<string, string>>,
682
+ * figures?: Record<string, Record<string, string>>,
683
+ * chapters?: Record<string, number>,
555
684
  * }} [options]
685
+ *
686
+ * `chapters` maps a lesson slug to its 1-based course ordinal. When the
687
+ * current page has one, every display equation / `<Table>` / `<Figure>` is
688
+ * numbered `‹chapter›.‹n›` (`##` headings ignored); with no entry the legacy
689
+ * `##`-section scheme applies. It must agree with `manifest.lessons[].chapter`,
690
+ * which is what feeds `equations`/`tables`/`figures` above.
556
691
  */
557
692
  export default function remarkNotation(options = {}) {
558
693
  const unknown = options.unknown ?? 'error';
@@ -568,6 +703,10 @@ export default function remarkNotation(options = {}) {
568
703
  );
569
704
  const localDefinitions = localDefinitionsFromFile(file);
570
705
  const lessonId = lessonIdFrom(file);
706
+ // The lesson's course ordinal (see the `chapters` option). Falsy / missing
707
+ // ⇒ the legacy `##`-section numbering, so `0` and `undefined` behave alike.
708
+ const chapter =
709
+ (lessonId && options.chapters?.[lessonId.replace(/\./g, '/')]) || undefined;
571
710
  const registryFile = isNotationRegistryFile(file);
572
711
  const mathScope = pageGlyphScope(
573
712
  localDefinitions,
@@ -577,10 +716,10 @@ export default function remarkNotation(options = {}) {
577
716
  const references = [];
578
717
  const resolved = new Map();
579
718
 
580
- // Equation identity (D7): assign every `\label{eq:…}` its section-scoped
581
- // number before any math node is rewritten, so a `[[eq-key]]` earlier in
582
- // the page still resolves.
583
- const equationLabels = collectEquationLabels(tree);
719
+ // Equation identity (D7): assign every `\label{eq:…}` its number before any
720
+ // math node is rewritten, so a `[[eq-key]]` earlier in the page still
721
+ // resolves. `chapter` picks the `‹chapter›.‹n›` vs `##`-section scheme.
722
+ const equationLabels = collectEquationLabels(tree, chapter);
584
723
  const pageLabels = new Map();
585
724
  for (const label of equationLabels) {
586
725
  if (pageLabels.has(label.key)) {
@@ -598,6 +737,53 @@ export default function remarkNotation(options = {}) {
598
737
  crossPage: options.equations ?? {},
599
738
  };
600
739
 
740
+ // Number and anchor every display equation on a lesson page, keyed or
741
+ // not (F3). Non-lesson pages (notation registry, stray docs) keep the
742
+ // keyed-only behaviour so their body math is left untouched.
743
+ const displayEquations = lessonId
744
+ ? collectDisplayEquations(tree, chapter)
745
+ : [];
746
+ const equationByNode = new Map(
747
+ displayEquations.map((equation) => [equation.node, equation]),
748
+ );
749
+
750
+ // Keyed floats (F-tables-figures): every `<Table>`/`<Figure>` on a lesson
751
+ // page is numbered and anchored, keyed or not — same rule as equations,
752
+ // via the shared engine in `keyed-jsx-floats.mjs`.
753
+ const makeFloatCtx = (tagName, crossPage) => {
754
+ const floats = lessonId
755
+ ? collectKeyedJsxFloats(tree, { tagName, chapter })
756
+ : [];
757
+ const pageFloatLabels = new Map();
758
+ for (const float of floats) {
759
+ if (float.key == null) continue;
760
+ if (pageFloatLabels.has(float.key)) {
761
+ fail(
762
+ file,
763
+ `Duplicate <${tagName} id="${float.key}"> on this page; an id is unique per lesson.`,
764
+ float.node,
765
+ );
766
+ }
767
+ pageFloatLabels.set(float.key, float.number);
768
+ }
769
+ return {
770
+ ctx: {
771
+ base: options.base ?? '',
772
+ pageLabels: pageFloatLabels,
773
+ crossPage: crossPage ?? {},
774
+ },
775
+ byNode: new Map(floats.map((float) => [float.node, float])),
776
+ };
777
+ };
778
+ const { ctx: tableCtx, byNode: tableByNode } = makeFloatCtx(
779
+ 'Table',
780
+ options.tables,
781
+ );
782
+ const { ctx: figureCtx, byNode: figureByNode } = makeFloatCtx(
783
+ 'Figure',
784
+ options.figures,
785
+ );
786
+
601
787
  const resolve = (key, kind, node) => {
602
788
  const context = {
603
789
  file,
@@ -647,7 +833,6 @@ export default function remarkNotation(options = {}) {
647
833
  node,
648
834
  );
649
835
  }
650
- node.__eqKey = labelMatch[1];
651
836
  value = stripEquationLabels(value);
652
837
  replaceMathSource(node, value);
653
838
  }
@@ -717,6 +902,8 @@ export default function remarkNotation(options = {}) {
717
902
  child,
718
903
  references,
719
904
  eqCtx,
905
+ tableCtx,
906
+ figureCtx,
720
907
  );
721
908
  if (replacement) {
722
909
  node.children.splice(index, 1, ...replacement);
@@ -725,14 +912,12 @@ export default function remarkNotation(options = {}) {
725
912
  continue;
726
913
  }
727
914
  visit(child, false);
728
- if (child && child.__eqKey) {
729
- const key = child.__eqKey;
730
- delete child.__eqKey;
731
- node.children[index] = keyedEquationNode(
732
- child,
733
- key,
734
- pageLabels.get(key),
735
- );
915
+ if (child && child.type === 'math' && equationByNode.has(child)) {
916
+ node.children[index] = equationNode(child, equationByNode.get(child));
917
+ } else if (child?.type === 'mdxJsxFlowElement' && tableByNode.has(child)) {
918
+ injectFloatNumberAttributes(child, tableByNode.get(child), 'tab');
919
+ } else if (child?.type === 'mdxJsxFlowElement' && figureByNode.has(child)) {
920
+ injectFloatNumberAttributes(child, figureByNode.get(child), 'fig');
736
921
  }
737
922
  }
738
923
  };
@@ -749,5 +934,16 @@ export default function remarkNotation(options = {}) {
749
934
  indexInSection,
750
935
  }),
751
936
  );
937
+ const floatLabelData = (byNode) =>
938
+ [...byNode.values()]
939
+ .filter((float) => float.key != null)
940
+ .map(({ key, number, section, indexInSection }) => ({
941
+ key,
942
+ number,
943
+ section,
944
+ indexInSection,
945
+ }));
946
+ file.data.tableLabels = floatLabelData(tableByNode);
947
+ file.data.figureLabels = floatLabelData(figureByNode);
752
948
  };
753
949
  }
@@ -0,0 +1,21 @@
1
+ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } from './keyed-float-validate.js';
2
+
3
+ /** `validateKeyedFloats` for `<Table>` — see `keyed-float-validate.ts`. */
4
+
5
+ type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused';
6
+ interface TableDiagnostic {
7
+ readonly code: TableDiagnosticCode;
8
+ readonly severity: 'error' | 'warning';
9
+ readonly message: string;
10
+ readonly file?: string;
11
+ readonly lessonId?: string;
12
+ readonly key?: string;
13
+ }
14
+ type TableLabelRecord = KeyedFloatLabelRecord;
15
+ type TableLessonInput = KeyedFloatLessonInput;
16
+ interface TableValidation extends KeyedFloatValidation {
17
+ readonly diagnostics: readonly TableDiagnostic[];
18
+ }
19
+ declare function validateTables(lessons: readonly TableLessonInput[]): TableValidation;
20
+
21
+ export { type TableDiagnostic, type TableDiagnosticCode, type TableLabelRecord, type TableLessonInput, type TableValidation, validateTables };
@@ -0,0 +1,13 @@
1
+ import {
2
+ validateKeyedFloats
3
+ } from "./keyed-float-validate.js";
4
+ function validateTables(lessons) {
5
+ return validateKeyedFloats(lessons, {
6
+ tagName: "Table",
7
+ prefix: "tab",
8
+ noun: "table"
9
+ });
10
+ }
11
+ export {
12
+ validateTables
13
+ };
@@ -15,6 +15,19 @@ type NotationAlignment = {
15
15
  readonly rationale: string;
16
16
  };
17
17
  type NotationReferenceKind = 'prose' | 'math' | 'definition';
18
+ /**
19
+ * A gloss resolved by the loader (D15). Declared on a card, keyed
20
+ * `<card key>.<slug(name)>`, and deliberately absent from the registry: a
21
+ * gloss is never a definition record, never a reference target, and never a
22
+ * curriculum node. See `reference/gloss.ts`.
23
+ */
24
+ interface NotationGlossEntry {
25
+ readonly key: string;
26
+ readonly notation: string;
27
+ readonly label: string;
28
+ readonly units?: string;
29
+ readonly ownerKey: string;
30
+ }
18
31
  interface NotationReferenceInput {
19
32
  readonly key: string;
20
33
  readonly kind: NotationReferenceKind;
@@ -37,6 +50,8 @@ interface SharedNotationDefinitionInput {
37
50
  readonly body: string;
38
51
  /** References harvested from the Markdown body. */
39
52
  readonly references: readonly NotationReferenceInput[];
53
+ /** Symbols this entry's `formula` names but does not make cards of. */
54
+ readonly glosses: readonly NotationGlossEntry[];
40
55
  readonly source: SourceSpan;
41
56
  }
42
57
  interface LocalNotationDefinitionInput {
@@ -51,6 +66,8 @@ interface LocalNotationDefinitionInput {
51
66
  readonly alignment: NotationAlignment;
52
67
  /** References harvested from summary content. */
53
68
  readonly references: readonly NotationReferenceInput[];
69
+ /** Symbols this entry's `formula` names but does not make cards of. */
70
+ readonly glosses: readonly NotationGlossEntry[];
54
71
  readonly source: SourceSpan;
55
72
  }
56
73
  interface NotationLessonInput {
@@ -67,7 +84,7 @@ interface NotationRegistryInput {
67
84
  readonly sharedDefinitions: readonly SharedNotationDefinitionInput[];
68
85
  readonly lessons: readonly NotationLessonInput[];
69
86
  }
70
- 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';
87
+ 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';
71
88
  type DiagnosticSeverity = 'error' | 'warning';
72
89
  interface NotationDiagnostic {
73
90
  readonly code: NotationDiagnosticCode;
@@ -95,6 +112,12 @@ interface ResolvedNotationReference {
95
112
  interface NotationDefinitionRecordBase {
96
113
  readonly id: string;
97
114
  readonly key: string;
115
+ /**
116
+ * Symbols this entry's `formula` names but does not make cards of (D15).
117
+ * Carried on the record so the manifest can build the entry-scoped formula
118
+ * table; deliberately **not** a definition, a reference target, or an edge.
119
+ */
120
+ readonly glosses: readonly NotationGlossEntry[];
98
121
  readonly scope: NotationDefinitionScope;
99
122
  readonly notation: string;
100
123
  readonly label: string;
@@ -148,4 +171,4 @@ interface NotationRegistry {
148
171
  readonly diagnostics: readonly NotationDiagnostic[];
149
172
  }
150
173
 
151
- export type { DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
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 };
@@ -97,7 +97,25 @@ const UNITS_VOCAB = /* @__PURE__ */ new Set([
97
97
  "quantity",
98
98
  "quoted",
99
99
  "explicitly",
100
- "calculations"
100
+ "calculations",
101
+ // motion / physical quantities (playground corpus)
102
+ "meter",
103
+ "meters",
104
+ "second",
105
+ "seconds",
106
+ "speed",
107
+ "velocity",
108
+ "acceleration",
109
+ "gravitational",
110
+ "launch",
111
+ "angle",
112
+ "degrees",
113
+ "horizontal",
114
+ "vertical",
115
+ "level",
116
+ "ground",
117
+ "squared",
118
+ "above"
101
119
  ]);
102
120
  function unitsTokens(units) {
103
121
  return units.toLowerCase().split(/[\s]+/).flatMap((word) => word.split(/(?<=[a-z0-9])-(?=[a-z])/)).map((word) => word.replace(/^[^a-z0-9_]+|[^a-z0-9_]+$/g, "")).filter((word) => word.length > 0 && !/^\d+$/.test(word));
@@ -0,0 +1,66 @@
1
+ # Versioning
2
+
3
+ `explico` follows [Semantic Versioning](https://semver.org/). The installed
4
+ `package.json` `version` is authoritative for what a consumer has;
5
+ [`CHANGELOG.md`](../CHANGELOG.md) records what changed; this file defines what a
6
+ bump *means*.
7
+
8
+ There are two independent version streams. This document is about the **engine**.
9
+ A course built on explico carries its own **knowledge version** over its
10
+ content — see [`authoring/versioning.md`](authoring/versioning.md).
11
+
12
+ ## The public surface
13
+
14
+ A change is "breaking" when it can break a consumer who followed the docs. The
15
+ consumer-visible surface is:
16
+
17
+ | Surface | Where |
18
+ | --- | --- |
19
+ | Published module entry points | `package.json` `publishConfig.exports` — the `explico/*` specifiers and their `.d.ts` |
20
+ | Astro / React components | names + props of everything under `components/` that the README wires |
21
+ | remark / rehype adapter options | the option objects of `reference/*.mjs` (`remarkNotation`, `remarkCitation`, `createNotationKatexOptions`, …) |
22
+ | The compiler manifest | `MANIFEST_SCHEMA_VERSION` and the shape of `Manifest` |
23
+ | The `content` CLI | the commands, their flags, and every diagnostic `code` string |
24
+ | `CourseConfig` | the interface in `compiler/course-config.ts` |
25
+ | Build-enforced authoring rules | the `N*` / `V*` / `C*` rules marked *Build-enforced* in `docs/authoring/` |
26
+
27
+ ## What each bump means
28
+
29
+ **MAJOR** — an incompatible change to any surface above: removing or renaming an
30
+ export, a component, a prop, or a CLI command; adding a required `CourseConfig`
31
+ field; a breaking change to the manifest shape (which also bumps
32
+ `MANIFEST_SCHEMA_VERSION`); tightening a build-enforced rule so content that
33
+ passed now fails.
34
+
35
+ **MINOR** — a backward-compatible addition: a new export, component, or optional
36
+ `CourseConfig` field; a new opt-in authoring rule; a new manifest field
37
+ (additive — still bump `MANIFEST_SCHEMA_VERSION`, note it additive in
38
+ `CHANGELOG.md`).
39
+
40
+ **PATCH** — a bug fix, a docs change, an internal refactor with no surface
41
+ change, or a new **Tier-3 warning** (warnings never fail a build, so surfacing
42
+ one does not break a consumer).
43
+
44
+ ### Pre-1.0
45
+
46
+ While the version is `0.x`, a **MINOR** bump *may* include a breaking change —
47
+ every one is listed under a `### Breaking` heading in `CHANGELOG.md` for that
48
+ version. A **PATCH** never breaks. Once the API settles this clause is dropped
49
+ and normal semver applies.
50
+
51
+ ## Cutting a release
52
+
53
+ 1. Decide the next version from the rules above and the `## [Unreleased]`
54
+ section of `CHANGELOG.md`.
55
+ 2. `CHANGELOG.md`: rename `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD`, add a
56
+ 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`.
60
+
61
+ ## When you change the engine
62
+
63
+ Every PR that touches the public surface adds a bullet under `## [Unreleased]`
64
+ in `CHANGELOG.md`, in the right group (`Added` / `Changed` / `Fixed` /
65
+ `Removed`, plus `Breaking` when it is). Pick the group by the rules above; the
66
+ version number itself is set only at release time. See [`AGENTS.md`](../AGENTS.md).