explico 0.1.0 → 0.2.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 (54) hide show
  1. package/dist/compiler/cli.d.ts +1 -0
  2. package/dist/compiler/cli.js +1 -1
  3. package/dist/compiler/collections.js +30 -3
  4. package/dist/compiler/manifest.d.ts +10 -1
  5. package/dist/compiler/manifest.js +28 -12
  6. package/dist/compiler/resolution-report.d.ts +1 -0
  7. package/dist/compiler/resolution-report.js +2 -2
  8. package/dist/reference/base-library.d.mts +17 -0
  9. package/dist/reference/citation-format.d.mts +55 -0
  10. package/dist/reference/consistency.d.ts +1 -1
  11. package/dist/reference/consistency.js +94 -3
  12. package/dist/reference/curriculum-alignment.js +1 -1
  13. package/dist/reference/equations.d.mts +84 -0
  14. package/dist/reference/equations.mjs +75 -39
  15. package/dist/reference/gate-math.d.ts +5 -4
  16. package/dist/reference/gate-math.js +73 -2
  17. package/dist/reference/gloss.d.ts +87 -0
  18. package/dist/reference/gloss.js +59 -0
  19. package/dist/reference/katex-options.d.mts +33 -0
  20. package/dist/reference/lab-math-scope.js +1 -1
  21. package/dist/reference/math-glyphs.d.mts +46 -0
  22. package/dist/reference/registry.js +26 -1
  23. package/dist/reference/rehype-fail-katex-errors.d.mts +6 -0
  24. package/dist/reference/rehype-heading-anchors.d.mts +1 -0
  25. package/dist/reference/rehype-heading-anchors.mjs +144 -0
  26. package/dist/reference/remark-citation.d.mts +6 -0
  27. package/dist/reference/remark-notation.d.mts +49 -0
  28. package/dist/reference/remark-notation.mjs +30 -24
  29. package/dist/reference/types.d.ts +25 -2
  30. package/dist/session/user-context.js +1 -1
  31. package/package.json +2 -2
  32. package/src/compiler/collections.ts +40 -0
  33. package/src/compiler/manifest.ts +31 -0
  34. package/src/components/HoverPanel.astro +42 -0
  35. package/src/components/citation/CitationLayer.astro +1 -0
  36. package/src/components/hover-panel.ts +71 -5
  37. package/src/components/notation/NotationFormula.astro +154 -0
  38. package/src/components/notation/NotationGlossary.astro +60 -0
  39. package/src/components/notation/NotationLayer.astro +248 -103
  40. package/src/components/notation/render-notation-math.ts +55 -0
  41. package/src/components/popup-stack.ts +345 -0
  42. package/src/components/starlight/EquationEnhancer.astro +20 -5
  43. package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
  44. package/src/components/starlight/LessonFooter.astro +4 -0
  45. package/src/content-config.ts +38 -0
  46. package/src/reference/consistency.ts +154 -1
  47. package/src/reference/equations.mjs +75 -39
  48. package/src/reference/gate-math.ts +104 -7
  49. package/src/reference/gloss.ts +153 -0
  50. package/src/reference/registry.ts +43 -0
  51. package/src/reference/rehype-heading-anchors.mjs +144 -0
  52. package/src/reference/remark-notation.mjs +30 -24
  53. package/src/reference/types.ts +26 -1
  54. package/src/styles/global.css +84 -2
@@ -7,6 +7,7 @@ import '../reference/types.js';
7
7
  import '../reference/lint-ignore.js';
8
8
  import '../reference/equations-validate.js';
9
9
  import '../reference/gate-math.js';
10
+ import '../reference/gloss.js';
10
11
  import './lesson-derivation.js';
11
12
 
12
13
  /**
@@ -12,7 +12,7 @@ import { createNotationKatexOptions } from "../reference/katex-options.mjs";
12
12
  import rehypeFailKatexErrors from "../reference/rehype-fail-katex-errors.mjs";
13
13
  import remarkCitation from "../reference/remark-citation.mjs";
14
14
  import remarkNotation from "../reference/remark-notation.mjs";
15
- import { isLessonSlug, lessonIdFromSlug } from "./lesson-derivation";
15
+ import { isLessonSlug, lessonIdFromSlug } from "./lesson-derivation.js";
16
16
  const REPOSITORY_ROOT = process.cwd();
17
17
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
18
18
  const DOCS_ROOT = join(CONTENT_ROOT, "docs");
@@ -1,16 +1,17 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { basename, join, relative } from "node:path";
3
3
  import { parseFrontmatter } from "@astrojs/markdown-remark";
4
- import { isSubstantiveMeaning } from "../reference/prose";
4
+ import { isSubstantiveMeaning } from "../reference/prose.js";
5
5
  import { parseEquationRef } from "../reference/equations.mjs";
6
- import { resolveLabel } from "../reference/label";
6
+ import { resolveLabel } from "../reference/label.js";
7
+ import { resolveGlosses } from "../reference/gloss.js";
7
8
  import {
8
9
  deriveRequires,
9
10
  deriveSources,
10
11
  isLessonSlug,
11
12
  lessonIdFromSlug,
12
13
  parseCheckList
13
- } from "./lesson-derivation";
14
+ } from "./lesson-derivation.js";
14
15
  const REPOSITORY_ROOT = process.cwd();
15
16
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
16
17
  function filesBelow(directory, extensions) {
@@ -206,6 +207,30 @@ async function loadCurriculumCatalog() {
206
207
  tracks: jsonEntries(join(CONTENT_ROOT, "tracks"))
207
208
  };
208
209
  }
210
+ function glossEntries(raw, ownerKey, at) {
211
+ if (raw === void 0) return [];
212
+ if (!Array.isArray(raw)) throw new Error(`${at} must be an array`);
213
+ const authored = raw.map((value, index) => {
214
+ const data = record(value, `${at}[${index}]`);
215
+ const units = optionalString(data.units, `${at}[${index}].units`);
216
+ return {
217
+ latex: string(data.latex, `${at}[${index}].latex`),
218
+ name: string(data.name, `${at}[${index}].name`),
219
+ ...units === void 0 ? {} : { units }
220
+ };
221
+ });
222
+ const glosses = resolveGlosses(ownerKey, authored);
223
+ const seen = /* @__PURE__ */ new Set();
224
+ for (const gloss of glosses) {
225
+ if (seen.has(gloss.key)) {
226
+ throw new Error(
227
+ `${at} declares two glosses named ${JSON.stringify(gloss.label)}`
228
+ );
229
+ }
230
+ seen.add(gloss.key);
231
+ }
232
+ return glosses;
233
+ }
209
234
  function localDefinition(raw, file, index) {
210
235
  const at = `${file} notation.local[${index}]`;
211
236
  const data = record(raw, at);
@@ -224,6 +249,7 @@ function localDefinition(raw, file, index) {
224
249
  seeAlso: strings(data.seeAlso, `${at}.seeAlso`),
225
250
  alignment: data.alignment === void 0 ? { kind: "general", rationale: "Lesson-local symbol." } : alignment(data.alignment, `${at}.alignment`),
226
251
  references: extractNotationReferences(definitionText, file, "definition"),
252
+ glosses: glossEntries(data.glosses, key, `${at}.glosses`),
227
253
  source: { file },
228
254
  ...formula === void 0 ? {} : { formula },
229
255
  ...units === void 0 ? {} : { units }
@@ -255,6 +281,7 @@ function sharedNotationEntries() {
255
281
  aiAssisted: boolean(data.aiAssisted, `${file} aiAssisted`),
256
282
  body,
257
283
  references: extractNotationReferences(body, file, void 0),
284
+ glosses: glossEntries(data.glosses, key, `${file} glosses`),
258
285
  source: { file },
259
286
  ...formula === void 0 ? {} : { formula },
260
287
  ...units === void 0 ? {} : { units }
@@ -2,7 +2,8 @@ import { CurriculumIssue } from '../curriculum/validation.js';
2
2
  import { ConsistencyDiagnostic } from '../reference/consistency.js';
3
3
  import { EquationLabelRecord, EquationDiagnostic } from '../reference/equations-validate.js';
4
4
  import { MathGateDiagnostic } from '../reference/gate-math.js';
5
- import { NotationDefinitionRecord, NotationPageBundle, NotationBacklink, NotationDiagnostic, EditorialStatus } from '../reference/types.js';
5
+ import { GlyphScopeEntry } from '../reference/gloss.js';
6
+ import { NotationDefinitionRecord, NotationPageBundle, NotationBacklink, NotationDiagnostic, EditorialStatus, NotationGlossEntry } from '../reference/types.js';
6
7
  import { CourseConfig } from './course-config.js';
7
8
  import { SidebarGroup } from './lesson-derivation.js';
8
9
  import './collections.js';
@@ -81,6 +82,14 @@ interface ManifestLessonNotation {
81
82
  readonly seeAlso: readonly string[];
82
83
  readonly units?: string;
83
84
  readonly formula?: string;
85
+ /** Symbols the formula names that have no card of their own (D15). */
86
+ readonly glosses: readonly NotationGlossEntry[];
87
+ /**
88
+ * Entry-scoped glyph table for `formula` — the entry, its glosses, and any
89
+ * card the formula names. Precomputed here because the whole card set lives
90
+ * in the compiler, not in the page component.
91
+ */
92
+ readonly formulaScope: readonly GlyphScopeEntry[];
84
93
  }
85
94
  interface ManifestLesson {
86
95
  readonly id: string;
@@ -10,32 +10,33 @@ import { dirname, join, relative } from "node:path";
10
10
  import { parseFrontmatter } from "@astrojs/markdown-remark";
11
11
  import {
12
12
  validateCurriculum
13
- } from "../curriculum/validation";
13
+ } from "../curriculum/validation.js";
14
14
  import {
15
15
  runConsistencyChecks
16
- } from "../reference/consistency";
17
- import { validateNotationAlignment } from "../reference/curriculum-alignment";
16
+ } from "../reference/consistency.js";
17
+ import { validateNotationAlignment } from "../reference/curriculum-alignment.js";
18
18
  import {
19
19
  validateEquations
20
- } from "../reference/equations-validate";
20
+ } from "../reference/equations-validate.js";
21
21
  import {
22
22
  gateContentMath
23
- } from "../reference/gate-math";
24
- import { loadLintIgnore } from "../reference/lint-ignore";
25
- import { buildNotationRegistry } from "../reference/registry";
26
- import { resolveReferenceId } from "../reference/references";
23
+ } from "../reference/gate-math.js";
24
+ import { loadLintIgnore } from "../reference/lint-ignore.js";
25
+ import { buildNotationRegistry } from "../reference/registry.js";
26
+ import { formulaGlyphScope } from "../reference/gloss.js";
27
+ import { resolveReferenceId } from "../reference/references.js";
27
28
  import {
28
29
  loadCurriculumCatalog,
29
30
  loadNotationDefinitions,
30
31
  loadNotationRegistryInput,
31
32
  loadNotationSpecs,
32
33
  loadSourceRecords
33
- } from "./collections";
34
+ } from "./collections.js";
34
35
  import {
35
36
  buildSidebar,
36
37
  deriveSources,
37
38
  isLessonSlug
38
- } from "./lesson-derivation";
39
+ } from "./lesson-derivation.js";
39
40
  const MANIFEST_SCHEMA_VERSION = 3;
40
41
  const REPOSITORY_ROOT = process.cwd();
41
42
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
@@ -97,7 +98,7 @@ function lessonNotationResolution(lesson, definitionsById, availableDefinitionId
97
98
  };
98
99
  });
99
100
  }
100
- function lessonNotationBundle(bundle, definitionsById, resolution) {
101
+ function lessonNotationBundle(bundle, definitionsById, resolution, cardsByKey) {
101
102
  const definitions = [];
102
103
  for (const definitionId of bundle?.definitionIds ?? []) {
103
104
  const definition = definitionsById.get(definitionId);
@@ -111,6 +112,14 @@ function lessonNotationBundle(bundle, definitionsById, resolution) {
111
112
  summary: definition.summary,
112
113
  editorialStatus: definition.status,
113
114
  seeAlso: [...definition.seeAlso],
115
+ glosses: [...definition.glosses],
116
+ formulaScope: formulaGlyphScope({
117
+ key: definition.key,
118
+ notation: definition.notation,
119
+ glosses: definition.glosses,
120
+ cardsByKey,
121
+ ...definition.formula === void 0 ? {} : { formula: definition.formula }
122
+ }),
114
123
  ...definition.units === void 0 ? {} : { units: definition.units },
115
124
  ...definition.formula === void 0 ? {} : { formula: definition.formula }
116
125
  });
@@ -152,6 +161,12 @@ async function compileManifest(courseConfig) {
152
161
  loadNotationRegistryInput()
153
162
  ]);
154
163
  const registry = buildNotationRegistry(notationInput);
164
+ const cardsByKey = new Map(
165
+ notationInput.sharedDefinitions.map((definition) => [
166
+ definition.key,
167
+ { key: definition.key, notation: definition.notation }
168
+ ])
169
+ );
155
170
  const definitionsById = new Map(
156
171
  registry.definitions.map((definition) => [definition.id, definition])
157
172
  );
@@ -237,7 +252,8 @@ async function compileManifest(courseConfig) {
237
252
  notationLesson,
238
253
  definitionsById,
239
254
  availableDefinitionIds
240
- )
255
+ ),
256
+ cardsByKey
241
257
  )
242
258
  });
243
259
  }
@@ -7,6 +7,7 @@ import '../reference/types.js';
7
7
  import '../reference/lint-ignore.js';
8
8
  import '../reference/equations-validate.js';
9
9
  import '../reference/gate-math.js';
10
+ import '../reference/gloss.js';
10
11
  import './lesson-derivation.js';
11
12
 
12
13
  /**
@@ -9,8 +9,8 @@ import {
9
9
  import { join } from "node:path";
10
10
  import {
11
11
  consistencyCounts
12
- } from "../reference/consistency";
13
- import { MANIFEST_SCHEMA_VERSION } from "./manifest";
12
+ } from "../reference/consistency.js";
13
+ import { MANIFEST_SCHEMA_VERSION } from "./manifest.js";
14
14
  const REPOSITORY_ROOT = process.cwd();
15
15
  const RESOLUTION_DIR = join(REPOSITORY_ROOT, "build", "resolution");
16
16
  const RESOLUTION_PREFIX = "build/resolution/";
@@ -0,0 +1,17 @@
1
+ /** True when `token` is a base-library atom and needs no semantic key. */
2
+ export function isBaseLibraryAtom(token: any): boolean;
3
+ /**
4
+ * The base notation library: a small, fixed set of universal identifier atoms
5
+ * that the page-level glyph map never asks an author to declare.
6
+ *
7
+ * Rationale (cleanup plan, "Decide once"): keep a base library so the
8
+ * completeness gate does not choke on mathematical constants and the
9
+ * differential operator. It is deliberately tiny and greppable — anything a
10
+ * lesson uses with a domain meaning (rates, indices, prices) still resolves to
11
+ * a semantic key or fails at `file:line:token`.
12
+ *
13
+ * Function names (`\ln`, `\exp`, `\log`, `\sin`, `\max`, `\sum`, `\prod`, …)
14
+ * are not listed here: KaTeX parses them as operator nodes, not identifier
15
+ * atoms, so the tokenizer never treats them as bindable symbols.
16
+ */
17
+ export const BASE_LIBRARY: Readonly<Set<string>>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Short author label: one surname, "A & B" for two, "A et al." for more.
3
+ * Falls back to the publishing organization for official guidance.
4
+ *
5
+ * @param {Record<string, unknown>} record
6
+ * @returns {string}
7
+ */
8
+ export function formatAuthors(record: Record<string, unknown>): string;
9
+ /**
10
+ * The "(4th ed., 2022)" tail, omitting whichever parts are absent.
11
+ *
12
+ * @param {Record<string, unknown>} record
13
+ * @returns {string}
14
+ */
15
+ export function formatEditionYear(record: Record<string, unknown>): string;
16
+ /**
17
+ * One reference-list line: "Authors, Title (edition, year). Locator."
18
+ * `locator` is the text an author passed after the `;` in `[@id; locator]` and is
19
+ * optional. The source record's own catalog `locator` is not used here.
20
+ *
21
+ * @param {Record<string, unknown>} record
22
+ * @param {string | undefined} locator
23
+ * @returns {string}
24
+ */
25
+ export function formatReferenceText(record: Record<string, unknown>, locator: string | undefined): string;
26
+ /**
27
+ * Recover the source label (authors + title, no locator) from a rendered
28
+ * reference-list line — the inverse of the ". <locator>." tail
29
+ * `formatReferenceText` appends. Lets the citation hover panel read its
30
+ * heading off the page's own "References" list instead of a second serialized
31
+ * copy of the source database. Falls back to the whole line if the tail is
32
+ * not where it is expected.
33
+ *
34
+ * @param {string} referenceText text of the `#cite-n` list item
35
+ * @param {string | undefined} locator the marker's `data-citation-locator`
36
+ * @returns {string}
37
+ */
38
+ export function referenceLabelFromText(referenceText: string, locator: string | undefined): string;
39
+ /**
40
+ * The compact "Tuckman & Serrat, Fixed Income Securities (4th ed., 2022)"
41
+ * shown as the panel heading, without the locator.
42
+ *
43
+ * @param {Record<string, unknown>} record
44
+ * @returns {string}
45
+ */
46
+ export function formatSourceLabel(record: Record<string, unknown>): string;
47
+ /**
48
+ * Pure helpers for turning a source record plus a per-citation locator into
49
+ * the strings shown in the inline panel and the end-of-page reference list.
50
+ *
51
+ * Imported by both the remark adapter (build-time list rendering) and the
52
+ * `CitationLayer` island (hover panel), so it must not touch the DOM, the
53
+ * file system, or Astro APIs.
54
+ */
55
+ export const SOURCE_ID_PATTERN: RegExp;
@@ -30,7 +30,7 @@ import { NotationRegistry, NotationRegistryInput } from './types.js';
30
30
  * `src/content/resolution-report.ts`.
31
31
  */
32
32
 
33
- type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator';
33
+ type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape';
34
34
  interface ConsistencyDiagnostic {
35
35
  readonly code: ConsistencyCode;
36
36
  /** Tier 3 — every finding here is a warning until content is brought clean. */
@@ -1,4 +1,4 @@
1
- import { offVocabularyTokens } from "./units-vocab";
1
+ import { offVocabularyTokens } from "./units-vocab.js";
2
2
  const CONVENTION_RE = /\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
3
3
  function stripCode(text) {
4
4
  return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " ")).replace(/(^|[^`])`[^`\n]*`/g, (match) => match.replace(/[^\n]/g, " "));
@@ -200,13 +200,101 @@ function checkNotationSourceLocator(input, out) {
200
200
  }
201
201
  }
202
202
  }
203
+ const GLOSS_PROMOTION_LIMIT = 3;
204
+ function checkGlossWantsPromoting(input, out) {
205
+ const owners = /* @__PURE__ */ new Map();
206
+ const record = (ownerKey, gloss) => {
207
+ const id = `${gloss.notation}\0${gloss.label.toLowerCase()}`;
208
+ const current = owners.get(id) ?? { label: gloss.label, keys: /* @__PURE__ */ new Set() };
209
+ current.keys.add(ownerKey);
210
+ owners.set(id, current);
211
+ };
212
+ for (const definition of input.notationInput.sharedDefinitions) {
213
+ for (const gloss of definition.glosses) record(definition.key, gloss);
214
+ }
215
+ for (const lesson of input.notationInput.lessons) {
216
+ for (const definition of lesson.localDefinitions) {
217
+ for (const gloss of definition.glosses) record(definition.key, gloss);
218
+ }
219
+ }
220
+ for (const [id, { label, keys }] of [...owners].sort(
221
+ ([a], [b]) => a.localeCompare(b)
222
+ )) {
223
+ if (keys.size < GLOSS_PROMOTION_LIMIT) continue;
224
+ const glyph = id.split("\0")[0] ?? "";
225
+ if (input.lintIgnore.matches("gloss-wants-promoting", glyph)) continue;
226
+ out.push({
227
+ code: "gloss-wants-promoting",
228
+ severity: "warning",
229
+ glyph,
230
+ message: `gloss ${JSON.stringify(glyph)} (${label}) is declared on ${keys.size} entries (${[...keys].sort().join(", ")}); a meaning reused this widely wants promoting to a card`
231
+ });
232
+ }
233
+ }
234
+ const CARD_BODY_MINIMUM_WORDS = 25;
235
+ function checkCardWantsDemoting(input, out) {
236
+ const used = new Set(
237
+ input.registry.backlinks.map((backlink) => backlink.key)
238
+ );
239
+ for (const definition of input.notationInput.sharedDefinitions) {
240
+ if (definition.formula !== void 0) continue;
241
+ if (definition.sources.length > 0) continue;
242
+ if (used.has(definition.key)) continue;
243
+ const words = stripCode(definition.body).split(/\s+/).filter((word) => word.length > 0).length;
244
+ if (words >= CARD_BODY_MINIMUM_WORDS) continue;
245
+ if (input.lintIgnore.matches("card-wants-demoting", definition.key)) {
246
+ continue;
247
+ }
248
+ out.push({
249
+ code: "card-wants-demoting",
250
+ severity: "warning",
251
+ key: definition.key,
252
+ file: definition.source.file,
253
+ count: words,
254
+ message: `notation card ${definition.key} has no formula, no sources, a ${words}-word body, and no lesson uses it; it carries no more than a name \u2014 a gloss on the entry that needs it may be the right home`
255
+ });
256
+ }
257
+ }
258
+ const GLOSS_NAME_WORD_LIMIT = 6;
259
+ const GLOSS_NAME_PROSE = /[.;]|\b(?:which|whose|that is|such that)\b/i;
260
+ function checkGlossNameShape(input, out) {
261
+ const check = (gloss, file, lessonId) => {
262
+ const words = gloss.label.trim().split(/\s+/).filter(Boolean);
263
+ const reason = words.length > GLOSS_NAME_WORD_LIMIT ? `${words.length} words` : GLOSS_NAME_PROSE.test(gloss.label) ? "sentence punctuation or a relative clause" : void 0;
264
+ if (reason === void 0) return;
265
+ if (input.lintIgnore.matches("gloss-name-shape", gloss.key)) return;
266
+ out.push({
267
+ code: "gloss-name-shape",
268
+ severity: "warning",
269
+ key: gloss.key,
270
+ file,
271
+ ...lessonId ? { lessonId } : {},
272
+ message: `gloss ${gloss.key} is named ${JSON.stringify(gloss.label)} (${reason}); a gloss name is a short noun phrase naming the symbol, not a description of it`
273
+ });
274
+ };
275
+ for (const definition of input.notationInput.sharedDefinitions) {
276
+ for (const gloss of definition.glosses) {
277
+ check(gloss, definition.source.file);
278
+ }
279
+ }
280
+ for (const lesson of input.notationInput.lessons) {
281
+ for (const definition of lesson.localDefinitions) {
282
+ for (const gloss of definition.glosses) {
283
+ check(gloss, definition.source.file, lesson.lessonId);
284
+ }
285
+ }
286
+ }
287
+ }
203
288
  const CHECKS = [
204
289
  checkGlyphUniqueness,
205
290
  checkNotationUnits,
206
291
  checkConventionSingleDefinition,
207
292
  checkNumeralsTagged,
208
293
  checkWeakLocal,
209
- checkNotationSourceLocator
294
+ checkNotationSourceLocator,
295
+ checkGlossWantsPromoting,
296
+ checkCardWantsDemoting,
297
+ checkGlossNameShape
210
298
  ];
211
299
  function runConsistencyChecks(input) {
212
300
  const diagnostics = [];
@@ -223,7 +311,10 @@ function consistencyCounts(diagnostics) {
223
311
  "convention-single-definition": 0,
224
312
  "numerals-tagged": 0,
225
313
  "weak-local": 0,
226
- "notation-source-locator": 0
314
+ "notation-source-locator": 0,
315
+ "gloss-wants-promoting": 0,
316
+ "card-wants-demoting": 0,
317
+ "gloss-name-shape": 0
227
318
  };
228
319
  for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
229
320
  return counts;
@@ -1,4 +1,4 @@
1
- import { compareDiagnostics } from "./references";
1
+ import { compareDiagnostics } from "./references.js";
2
2
  function validateNotationAlignment(registry, catalog) {
3
3
  const diagnostics = [];
4
4
  const competencies = new Map(
@@ -0,0 +1,84 @@
1
+ /** `{ lesson?, key }` when `raw` is an equation reference, else `undefined`. */
2
+ export function parseEquationRef(raw: any): {
3
+ lesson: string;
4
+ key: string;
5
+ } | {
6
+ key: string;
7
+ lesson?: undefined;
8
+ } | undefined;
9
+ /** Remove every `\label{eq:…}` (and the whitespace it leaves) from a math source. */
10
+ export function stripEquationLabels(latex: any): string;
11
+ /** The positional anchor id for an unkeyed equation, e.g. `2.4` → `eq-2-4`. */
12
+ export function equationAnchorId({ key, number }: {
13
+ key: any;
14
+ number: any;
15
+ }): string;
16
+ /**
17
+ * Every keyed display equation in a Markdown body, in appearance order, with
18
+ * its assigned number. `$$…$$` blocks only; `##` headings between them advance
19
+ * the section counter. Every display block advances the in-section index, so a
20
+ * keyed equation's number reflects its position among *all* display equations,
21
+ * not only the keyed ones.
22
+ */
23
+ export function scanEquationLabels(markdown: any): {
24
+ key: string;
25
+ number: string;
26
+ section: number;
27
+ indexInSection: number;
28
+ }[];
29
+ /**
30
+ * The mdast equivalent of `scanEquationLabels` for *every* display equation:
31
+ * walk the tree in document order, advancing the section counter on every
32
+ * depth-2 heading and emitting a record for every `math` node. `key` is the
33
+ * `\label{eq:…}` payload when present, else `null`.
34
+ */
35
+ export function collectDisplayEquations(tree: any): {
36
+ key: string | null;
37
+ number: string;
38
+ section: number;
39
+ indexInSection: number;
40
+ node: any;
41
+ }[];
42
+ /**
43
+ * The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
44
+ * `scanEquationLabels`. Numbers reflect position among all display equations.
45
+ */
46
+ export function collectEquationLabels(tree: any): {
47
+ key: string | null;
48
+ number: string;
49
+ section: number;
50
+ indexInSection: number;
51
+ node: any;
52
+ }[];
53
+ /**
54
+ * Equation identity (Step D7, extended F3) — shared, framework-free helpers.
55
+ *
56
+ * Every display equation (`$$…$$`) in a lesson is numbered and given an
57
+ * `id`, from appearance order scoped to the enclosing `##` section (`(2.4)` =
58
+ * fourth display equation under the second section). An author additionally
59
+ * marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
60
+ * block (inside math, so the `:` is safe and the form matches amsmath) when
61
+ * they want a *stable* anchor that survives edits and cross-page references:
62
+ *
63
+ * [[eq-discount-factor-def]] same page → "(2.4)" linked to #eq-…
64
+ * [[foundations/present-value#eq-pv]] cross page → number from the manifest
65
+ *
66
+ * An unkeyed equation still gets a positional anchor (`#eq-2-4`) so it can be
67
+ * deep-linked, but only a keyed equation is referenceable by name.
68
+ *
69
+ * `scanEquationLabels` (raw Markdown) and `collectEquationLabels` (mdast tree)
70
+ * MUST agree — `tests/notation/equations.test.ts` checks that on the real
71
+ * corpus. Both are here so `manifest.ts` and `remark-notation.mjs` share one
72
+ * numbering algorithm. `collectDisplayEquations` is the superset the render
73
+ * path uses to number and anchor every display equation, keyed or not.
74
+ */
75
+ /** `eq:` key grammar — the notation-key shape without the dotted namespaces. */
76
+ export const EQ_KEY_PATTERN: RegExp;
77
+ /** A `\label{eq:<key>}` anywhere in a math source. Global; `key` in group 1. */
78
+ export const EQ_LABEL_PATTERN: RegExp;
79
+ /**
80
+ * A `[[ … ]]` payload that targets an equation: `eq-<key>` on the current page,
81
+ * or `<lesson-slug>#eq-<key>` across pages. `lesson` (group 1) is a `/`-slug.
82
+ * The `eq-` prefix is reserved — a notation key may not start with it.
83
+ */
84
+ export const EQ_REF_PATTERN: RegExp;