explico 0.1.0 → 0.1.1

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.
@@ -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,16 @@
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
7
  import {
8
8
  deriveRequires,
9
9
  deriveSources,
10
10
  isLessonSlug,
11
11
  lessonIdFromSlug,
12
12
  parseCheckList
13
- } from "./lesson-derivation";
13
+ } from "./lesson-derivation.js";
14
14
  const REPOSITORY_ROOT = process.cwd();
15
15
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
16
16
  function filesBelow(directory, extensions) {
@@ -10,32 +10,32 @@ 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 { resolveReferenceId } from "../reference/references.js";
27
27
  import {
28
28
  loadCurriculumCatalog,
29
29
  loadNotationDefinitions,
30
30
  loadNotationRegistryInput,
31
31
  loadNotationSpecs,
32
32
  loadSourceRecords
33
- } from "./collections";
33
+ } from "./collections.js";
34
34
  import {
35
35
  buildSidebar,
36
36
  deriveSources,
37
37
  isLessonSlug
38
- } from "./lesson-derivation";
38
+ } from "./lesson-derivation.js";
39
39
  const MANIFEST_SCHEMA_VERSION = 3;
40
40
  const REPOSITORY_ROOT = process.cwd();
41
41
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
@@ -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;
@@ -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, " "));
@@ -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,57 @@
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
+ /**
12
+ * Every keyed display equation in a Markdown body, in appearance order, with
13
+ * its assigned number. `$$…$$` blocks only; `##` headings between them advance
14
+ * the section counter.
15
+ */
16
+ export function scanEquationLabels(markdown: any): {
17
+ key: string | undefined;
18
+ number: string;
19
+ section: number;
20
+ indexInSection: number;
21
+ }[];
22
+ /**
23
+ * The mdast equivalent of `scanEquationLabels`: walk the tree in document
24
+ * order, advancing the section counter on every depth-2 heading and emitting a
25
+ * label for every `math` node whose source carries `\label{eq:…}`.
26
+ */
27
+ export function collectEquationLabels(tree: any): any[];
28
+ /**
29
+ * Equation identity (Step D7) — shared, framework-free helpers.
30
+ *
31
+ * An author marks a display equation by putting a trailing `\label{eq:<key>}`
32
+ * inside its `$$…$$` block (inside math, so the `:` is safe and the form
33
+ * matches amsmath). The compiler assigns the *visible* number from appearance
34
+ * order, scoped to the enclosing `##` section (`(2.4)` = fourth keyed equation
35
+ * under the second section). The number is never authored; unkeyed display
36
+ * equations render clean and stay un-referenceable. Prose refers to an
37
+ * equation with the reserved `eq-` prefix (a bare `:` in `[[…]]` prose would
38
+ * be eaten by `remark-directive`):
39
+ *
40
+ * [[eq-discount-factor-def]] same page → "(2.4)" linked to #eq-…
41
+ * [[foundations/present-value#eq-pv]] cross page → number from the manifest
42
+ *
43
+ * `scanEquationLabels` (raw Markdown) and `collectEquationLabels` (mdast tree)
44
+ * MUST agree — `tests/notation/equations.test.ts` checks that on the real
45
+ * corpus. Both are here so `manifest.ts` and `remark-notation.mjs` share one
46
+ * numbering algorithm.
47
+ */
48
+ /** `eq:` key grammar — the notation-key shape without the dotted namespaces. */
49
+ export const EQ_KEY_PATTERN: RegExp;
50
+ /** A `\label{eq:<key>}` anywhere in a math source. Global; `key` in group 1. */
51
+ export const EQ_LABEL_PATTERN: RegExp;
52
+ /**
53
+ * A `[[ … ]]` payload that targets an equation: `eq-<key>` on the current page,
54
+ * or `<lesson-slug>#eq-<key>` across pages. `lesson` (group 1) is a `/`-slug.
55
+ * The `eq-` prefix is reserved — a notation key may not start with it.
56
+ */
57
+ export const EQ_REF_PATTERN: RegExp;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * KaTeX trust callback that permits exactly one inert marker attribute:
3
+ * `data-notation-key` (a registry key) or `data-lab-slot` (a lab value hole).
4
+ * Links, styles, classes, IDs, protocols, and every other data attribute stay
5
+ * untrusted. Key existence is checked by the remark adapter, the lab-math
6
+ * renderer, and the workspace validator before KaTeX runs.
7
+ */
8
+ export function trustNotationMarker(context: any): boolean;
9
+ /**
10
+ * Add non-security KaTeX settings while keeping the notation macros and trust
11
+ * boundary non-overridable.
12
+ */
13
+ export function createNotationKatexOptions(overrides?: {}): {
14
+ output: "htmlAndMathml";
15
+ throwOnError: true;
16
+ trust: typeof trustNotationMarker;
17
+ macros: any;
18
+ strict: typeof strictNotation;
19
+ };
20
+ export const EXPLAIN_MACRO: string;
21
+ export const SLOT_MACRO: string;
22
+ export const notationKatexOptions: Readonly<{
23
+ output: "htmlAndMathml";
24
+ throwOnError: true;
25
+ strict: typeof strictNotation;
26
+ trust: typeof trustNotationMarker;
27
+ macros: Readonly<{
28
+ '\\explain': string;
29
+ '\\slot': string;
30
+ }>;
31
+ }>;
32
+ declare function strictNotation(errorCode: any): "ignore" | "warn";
33
+ export {};
@@ -1,4 +1,4 @@
1
- import { loadManifest } from "../compiler/manifest";
1
+ import { loadManifest } from "../compiler/manifest.js";
2
2
  const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
3
3
  const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
4
4
  async function labMathScope(notation, body = "") {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Resolve and annotate one KaTeX expression against a page's flat glyph table.
3
+ * Source offsets in the result always refer to the original, unmodified LaTeX.
4
+ *
5
+ * @param {string} latex
6
+ * @param {Array<{key: string, notation: string}>} definitions
7
+ */
8
+ export function resolveMathGlyphs(latex: string, definitions: Array<{
9
+ key: string;
10
+ notation: string;
11
+ }>): {
12
+ latex: string;
13
+ bindings: {
14
+ key: any;
15
+ level: any;
16
+ match: any;
17
+ token: any;
18
+ }[];
19
+ unresolved: any;
20
+ };
21
+ /**
22
+ * Page-level glyph-map resolver (cleanup plan, D1 decision 2026-09-02).
23
+ *
24
+ * A page declares its glyphs once — `notation.local` plus the shared entries it
25
+ * introduces in prose with `[[key]]` — so the glyph table handed in here is a
26
+ * flat list with **one meaning per glyph**. This module parses one `$…$` /
27
+ * `$$…$$` expression, matches each declared symbol against it (whole canonical
28
+ * form first, then a unique base glyph for a shorter or instantiated form),
29
+ * marks every match with a validated `\explain` marker, and reports any
30
+ * remaining identifier atom so the caller can fail at `file:line:token`.
31
+ * `\explain{key}{latex}` in the source stays as the escape hatch for a
32
+ * compound or glyph-colliding symbol.
33
+ *
34
+ * Because the table is unambiguous, matching is a lookup, not a search: no
35
+ * cross-definition candidate solving, no ambiguity permutation. What remains is
36
+ * the tokenizer the page-glyph-map syntax requires — a KaTeX parse plus the
37
+ * structural signatures that let `CF_k` be found as a three-node run and `D` be
38
+ * found as the head of `D(0,2)`. `katex.__parse` is that tokenizer; the version
39
+ * guard below keeps a dependency bump from silently changing its tree.
40
+ */
41
+ export const SUPPORTED_KATEX_PARSE_VERSION: "0.16.47";
42
+ export class GlyphResolutionError extends Error {
43
+ constructor(code: any, message: any, details?: undefined);
44
+ code: any;
45
+ details: any;
46
+ }
@@ -5,7 +5,7 @@ import {
5
5
  localDefinitionId,
6
6
  resolveReferenceId,
7
7
  sharedDefinitionId
8
- } from "./references";
8
+ } from "./references.js";
9
9
  function buildNotationRegistry(rawInput) {
10
10
  const diagnostics = [];
11
11
  const lessons = buildLessonIndex(rawInput.lessons, diagnostics);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * rehype-katex reports parse failures as non-fatal VFile messages and renders
3
+ * a `.katex-error` fallback. Turn those recovered failures into compiler
4
+ * errors so Astro cannot successfully build a lesson containing broken math.
5
+ */
6
+ export default function rehypeFailKatexErrors(): (tree: any, file: any) => void;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @param {{ sources?: Map<string, object> | readonly object[] | (() => Map<string, object> | readonly object[]) }} [options]
3
+ */
4
+ export default function remarkCitation(options?: {
5
+ sources?: Map<string, object> | readonly object[] | (() => Map<string, object> | readonly object[]);
6
+ }): (tree: any, file: any) => void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @param {{
3
+ * definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>),
4
+ * resolve?: (key: string, context: object) => object | undefined,
5
+ * unknown?: 'error' | 'warn' | 'ignore',
6
+ * base?: string,
7
+ * equations?: Record<string, Record<string, string>>,
8
+ * }} [options]
9
+ */
10
+ export default function remarkNotation(options?: {
11
+ definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>);
12
+ resolve?: (key: string, context: object) => object | undefined;
13
+ unknown?: "error" | "warn" | "ignore";
14
+ base?: string;
15
+ equations?: Record<string, Record<string, string>>;
16
+ }): (tree: any, file: any) => void;
17
+ /**
18
+ * Remark adapter for semantic notation references.
19
+ *
20
+ * Author syntax:
21
+ * - Prose introduces a meaning: `[[rates.discount-factor]]`
22
+ * - Later math uses ordinary LaTeX: `D(0,t)`
23
+ * - `\\explain{rates.discount-factor}{D(0,t)}` remains available inside math
24
+ * when lexical scope cannot select one meaning unambiguously.
25
+ *
26
+ * Prose references become ordinary links. Lesson math is resolved against the
27
+ * page glyph table — `notation.local` plus the shared entries the page names
28
+ * with `[[key]]` (or `\\explain{key}{…}`) — and the base library; the
29
+ * adapter then injects the narrowly trusted `\\explain` marker before
30
+ * build-time KaTeX rendering. `notation.uses` is retired: the use is the
31
+ * declaration (D1 decision, 2026-09-02).
32
+ *
33
+ * Definitions can be supplied as a Map, an array, an object keyed by the
34
+ * semantic key, or a loader returning one of those forms. Loaders are
35
+ * evaluated for every document transform so a long-running development
36
+ * server can discover definitions created after startup. A resolver can
37
+ * provide page-local lexical scoping:
38
+ *
39
+ * remarkNotation({
40
+ * definitions: sharedDefinitions,
41
+ * resolve(key, { file, kind, localDefinitions, sharedDefinitions }) {
42
+ * return localDefinitions.get(key) ?? sharedDefinitions.get(key);
43
+ * },
44
+ * })
45
+ *
46
+ * A definition may be either its data object or an Astro collection entry
47
+ * with the data object under `.data`.
48
+ */
49
+ export const NOTATION_KEY_PATTERN: RegExp;
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext } from "react";
2
- import { anonymousUser } from "./user";
2
+ import { anonymousUser } from "./user.js";
3
3
  const SessionUserContext = createContext(anonymousUser);
4
4
  function useSessionUser() {
5
5
  return useContext(SessionUserContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explico",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -48,6 +48,6 @@
48
48
  "typescript": "6.0.2"
49
49
  },
50
50
  "scripts": {
51
- "build": "tsup && node scripts/copy-assets.mjs"
51
+ "build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json"
52
52
  }
53
53
  }