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
@@ -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,40 @@
1
+ /** `validateKeyedFloats` for `<Table>` — see `keyed-float-validate.ts`. */
2
+
3
+ import {
4
+ validateKeyedFloats,
5
+ type KeyedFloatLabelRecord,
6
+ type KeyedFloatLessonInput,
7
+ type KeyedFloatValidation,
8
+ } from './keyed-float-validate';
9
+
10
+ export type TableDiagnosticCode =
11
+ | 'tab-invalid-key'
12
+ | 'tab-duplicate-key'
13
+ | 'tab-ref-resolves'
14
+ | 'tab-key-unused';
15
+
16
+ export interface TableDiagnostic {
17
+ readonly code: TableDiagnosticCode;
18
+ readonly severity: 'error' | 'warning';
19
+ readonly message: string;
20
+ readonly file?: string;
21
+ readonly lessonId?: string;
22
+ readonly key?: string;
23
+ }
24
+
25
+ export type TableLabelRecord = KeyedFloatLabelRecord;
26
+ export type TableLessonInput = KeyedFloatLessonInput;
27
+
28
+ export interface TableValidation extends KeyedFloatValidation {
29
+ readonly diagnostics: readonly TableDiagnostic[];
30
+ }
31
+
32
+ export function validateTables(
33
+ lessons: readonly TableLessonInput[],
34
+ ): TableValidation {
35
+ return validateKeyedFloats(lessons, {
36
+ tagName: 'Table',
37
+ prefix: 'tab',
38
+ noun: 'table',
39
+ }) as TableValidation;
40
+ }
@@ -21,6 +21,20 @@ export type NotationAlignment =
21
21
 
22
22
  export type NotationReferenceKind = 'prose' | 'math' | 'definition';
23
23
 
24
+ /**
25
+ * A gloss resolved by the loader (D15). Declared on a card, keyed
26
+ * `<card key>.<slug(name)>`, and deliberately absent from the registry: a
27
+ * gloss is never a definition record, never a reference target, and never a
28
+ * curriculum node. See `reference/gloss.ts`.
29
+ */
30
+ export interface NotationGlossEntry {
31
+ readonly key: string;
32
+ readonly notation: string;
33
+ readonly label: string;
34
+ readonly units?: string;
35
+ readonly ownerKey: string;
36
+ }
37
+
24
38
  export interface NotationReferenceInput {
25
39
  readonly key: string;
26
40
  readonly kind: NotationReferenceKind;
@@ -44,6 +58,8 @@ export interface SharedNotationDefinitionInput {
44
58
  readonly body: string;
45
59
  /** References harvested from the Markdown body. */
46
60
  readonly references: readonly NotationReferenceInput[];
61
+ /** Symbols this entry's `formula` names but does not make cards of. */
62
+ readonly glosses: readonly NotationGlossEntry[];
47
63
  readonly source: SourceSpan;
48
64
  }
49
65
 
@@ -59,6 +75,8 @@ export interface LocalNotationDefinitionInput {
59
75
  readonly alignment: NotationAlignment;
60
76
  /** References harvested from summary content. */
61
77
  readonly references: readonly NotationReferenceInput[];
78
+ /** Symbols this entry's `formula` names but does not make cards of. */
79
+ readonly glosses: readonly NotationGlossEntry[];
62
80
  readonly source: SourceSpan;
63
81
  }
64
82
 
@@ -90,7 +108,8 @@ export type NotationDiagnosticCode =
90
108
  | 'alignment-unknown-lesson'
91
109
  | 'alignment-introduction'
92
110
  | 'alignment-unavailable'
93
- | 'alignment-review-state';
111
+ | 'alignment-review-state'
112
+ | 'gloss-collides-with-card';
94
113
 
95
114
  export type DiagnosticSeverity = 'error' | 'warning';
96
115
 
@@ -120,6 +139,12 @@ export interface ResolvedNotationReference {
120
139
  interface NotationDefinitionRecordBase {
121
140
  readonly id: string;
122
141
  readonly key: string;
142
+ /**
143
+ * Symbols this entry's `formula` names but does not make cards of (D15).
144
+ * Carried on the record so the manifest can build the entry-scoped formula
145
+ * table; deliberately **not** a definition, a reference target, or an edge.
146
+ */
147
+ readonly glosses: readonly NotationGlossEntry[];
123
148
  readonly scope: NotationDefinitionScope;
124
149
  readonly notation: string;
125
150
  readonly label: string;
@@ -118,6 +118,24 @@ export const UNITS_VOCAB: ReadonlySet<string> = new Set([
118
118
  'quoted',
119
119
  'explicitly',
120
120
  'calculations',
121
+ // motion / physical quantities (playground corpus)
122
+ 'meter',
123
+ 'meters',
124
+ 'second',
125
+ 'seconds',
126
+ 'speed',
127
+ 'velocity',
128
+ 'acceleration',
129
+ 'gravitational',
130
+ 'launch',
131
+ 'angle',
132
+ 'degrees',
133
+ 'horizontal',
134
+ 'vertical',
135
+ 'level',
136
+ 'ground',
137
+ 'squared',
138
+ 'above',
121
139
  ]);
122
140
 
123
141
  /** Lowercase word tokens of a `units` string (split on whitespace and hyphens). */