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.
- package/CHANGELOG.md +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +17 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +31 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +87 -3
- package/dist/compiler/manifest.js +133 -20
- package/dist/compiler/resolution-report.d.ts +5 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +142 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +76 -18
- package/dist/reference/equations.mjs +118 -41
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/gate-math.d.ts +5 -4
- package/dist/reference/gate-math.js +73 -2
- package/dist/reference/gloss.d.ts +87 -0
- package/dist/reference/gloss.js +59 -0
- package/dist/reference/keyed-float-validate.d.ts +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/registry.js +25 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-notation.d.mts +12 -0
- package/dist/reference/remark-notation.mjs +228 -32
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/types.d.ts +25 -2
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +52 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +270 -17
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/hover-panel.ts +71 -5
- package/src/components/notation/NotationFormula.astro +154 -0
- package/src/components/notation/NotationGlossary.astro +60 -0
- package/src/components/notation/NotationLayer.astro +248 -103
- package/src/components/notation/render-notation-math.ts +66 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +8 -2
- package/src/components/table/Table.astro +104 -0
- package/src/content-config.ts +38 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +236 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +6 -1
- package/src/reference/equations.mjs +118 -41
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +228 -32
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/types.ts +26 -1
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +193 -3
- package/src/components/starlight/EquationEnhancer.astro +0 -72
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the **`explico` engine** are recorded here. The installed
|
|
4
|
+
`package.json` `version` is the single source of truth for what you have; this
|
|
5
|
+
file says what changed between versions and [`docs/VERSIONING.md`](docs/VERSIONING.md)
|
|
6
|
+
says what a version bump means.
|
|
7
|
+
|
|
8
|
+
A course built on explico keeps its **own** knowledge version — see the course's
|
|
9
|
+
`content/CHANGELOG.md` and [`docs/authoring/versioning.md`](docs/authoring/versioning.md).
|
|
10
|
+
|
|
11
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
12
|
+
This project is pre-1.0: a MINOR bump may carry a breaking change, always listed
|
|
13
|
+
under **Breaking** below; a PATCH never breaks.
|
|
14
|
+
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
## [0.3.0] - 2026-09-05
|
|
18
|
+
|
|
19
|
+
### Breaking
|
|
20
|
+
|
|
21
|
+
- `CourseConfig` now requires an `about` block (`materialName?`, `summary?`,
|
|
22
|
+
`version`, `authors`, `disclaimer`, `changelogHref?`). It carries the course's
|
|
23
|
+
reader-facing identity and its **knowledge version**. Add one to
|
|
24
|
+
`content/course.config.ts`.
|
|
25
|
+
- Manifest gains `manifest.about` (engine identity + the course `about` block +
|
|
26
|
+
the knowledge version resolved from `content/CHANGELOG.md`) and
|
|
27
|
+
`manifest.diagnostics.knowledge`; `schemaVersion` bumped. Additive — existing
|
|
28
|
+
fields are unchanged.
|
|
29
|
+
- `EquationRefLayer` / `EquationEnhancer` are replaced by `FloatRefLayer` +
|
|
30
|
+
`KeyedFloatEnhancer`, one hover/pin/jump engine shared by equation, table, and
|
|
31
|
+
figure references. Update the wiring in your Starlight `Footer` override.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **Figures and tables as first-class keyed floats.** New `Figure` and `Table`
|
|
36
|
+
components with appearance-order numbering, `#fig-<id>` / `#tab-<id>` anchors,
|
|
37
|
+
and `[[fig-<id>]]` / `[[tab-<id>]]` cross-references that highlight the target
|
|
38
|
+
when it is on screen and preview it when it is not — the same model equations
|
|
39
|
+
already had. New build gates `reference/figures-validate.ts`,
|
|
40
|
+
`reference/tables-validate.ts`, `reference/keyed-float-validate.ts`, and the
|
|
41
|
+
`reference/keyed-jsx-floats.mjs` remark pass.
|
|
42
|
+
- **About `[?]` panel.** `components/starlight/AboutPanel.astro` +
|
|
43
|
+
`LayoutSocialIcons.astro` render a header control, on every route, showing the
|
|
44
|
+
material name and summary, the knowledge version and date, the engine version,
|
|
45
|
+
the authors and contact, the disclaimer, and a link to the content changelog.
|
|
46
|
+
Wire it with `components: { SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro' }`.
|
|
47
|
+
- **Knowledge versioning.** `compiler/knowledge-version.ts` parses
|
|
48
|
+
`content/CHANGELOG.md` and checks it against `about.version`; drift is a Tier-3
|
|
49
|
+
`knowledge-version-mismatch` / `knowledge-changelog-missing` /
|
|
50
|
+
`knowledge-version-invalid` warning (never fails the build). Governed by new
|
|
51
|
+
authoring area **C** (`docs/authoring/versioning.md`, rules C1–C4).
|
|
52
|
+
- `Diagram` component (Dagre + build-time KaTeX), with an `LR` direction and an
|
|
53
|
+
optional hand-written `description` for the text fallback.
|
|
54
|
+
- Notation engine: a base **operator** library (`reference/base-operators.mjs`)
|
|
55
|
+
alongside the base glyph library, wider automatic glyph resolution in
|
|
56
|
+
`reference/math-glyphs.mjs`, and `\explain{key}{latex}` support inside notation
|
|
57
|
+
`formula:` fields and `Diagram` node formulas.
|
|
58
|
+
- `content status` now reports the course's knowledge version.
|
|
59
|
+
- Course-author docs in the published package: `docs/authoring/agents.md`
|
|
60
|
+
(working on a course repo with an AI agent) and `docs/authoring/migrating.md`
|
|
61
|
+
(per-version upgrade steps). The published `files` are `dist`, `src`,
|
|
62
|
+
`README.md`, `CHANGELOG.md`, `docs/VERSIONING.md`, `docs/authoring/` — the
|
|
63
|
+
repo's own `AGENTS.md` is engine-development guidance and is **no longer
|
|
64
|
+
shipped** (it described `src/` internals a consumer does not have).
|
|
65
|
+
- `compileManifest` throws a named error, pointing at `migrating.md`, when a
|
|
66
|
+
course upgrading from 0.2.x has no `about` block — instead of a bare
|
|
67
|
+
`Cannot read properties of undefined`.
|
|
68
|
+
- `scripts/agent/` (`verify`, `fresh`, `shot`) and a generated, staleness-gated
|
|
69
|
+
repo map in the repo's `AGENTS.md` (`scripts/agent/gen-map.mjs`) — repo tooling,
|
|
70
|
+
not part of the package.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- Deep-linkable heading anchors (`rehype-heading-anchors` +
|
|
75
|
+
`HeadingAnchorEnhancer`) and numbered lesson equations are on by default.
|
|
76
|
+
- `remark-notation` and `consistency` gained substantial coverage for the
|
|
77
|
+
keyed-float syntax, operator resolution, and `\explain` handling.
|
|
78
|
+
|
|
79
|
+
## [0.2.0] - 2026-09-04
|
|
80
|
+
|
|
81
|
+
- Two-tier notation vocabulary: named operators (rule N8) are a Tier-3
|
|
82
|
+
`operator-explained` warning against a shipped baseline plus a per-course
|
|
83
|
+
`operators.allow` / `operators.deny` list, softer than the identifier gate.
|
|
84
|
+
- Heading deep links: `rehype-heading-anchors` + `HeadingAnchorEnhancer.astro`.
|
|
85
|
+
- `popup-stack.ts` — nested, independently positioned notation popups.
|
|
86
|
+
|
|
87
|
+
## [0.1.1] - 2026-09-03
|
|
88
|
+
|
|
89
|
+
- Make the published build load under Node ESM (`.mjs` extension fixups, `dist/`
|
|
90
|
+
resolution).
|
|
91
|
+
|
|
92
|
+
## [0.1.0] - 2026-09-03
|
|
93
|
+
|
|
94
|
+
- Initial import of the explico engine, extracted from the `equations` project:
|
|
95
|
+
the one manifest compiler, the notation / citation / equation reference
|
|
96
|
+
system with its completeness gate, the curriculum-graph validator, the
|
|
97
|
+
Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
|
|
98
|
+
|
|
99
|
+
[Unreleased]: https://github.com/danieltuzes/explico/compare/v0.2.0...HEAD
|
|
100
|
+
[0.2.0]: https://github.com/danieltuzes/explico/compare/v0.1.1...v0.2.0
|
|
101
|
+
[0.1.1]: https://github.com/danieltuzes/explico/compare/v0.1.0...v0.1.1
|
|
102
|
+
[0.1.0]: https://github.com/danieltuzes/explico/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -14,7 +14,10 @@ What it gives a course:
|
|
|
14
14
|
meaning; a **completeness gate** fails the build if any identifier in any
|
|
15
15
|
rendered-math context (lesson body, `notation.formula`, `checks.yml`,
|
|
16
16
|
assessment prompts, component slots) does not resolve to a key. One meaning
|
|
17
|
-
per glyph per page, enforced corpus-wide.
|
|
17
|
+
per glyph per page, enforced corpus-wide. Named operators get the same
|
|
18
|
+
treatment one tier softer: a lesson's `\operatorname{Var}` is a warning
|
|
19
|
+
(`operator-explained`) until it carries an entry or the course lists it in
|
|
20
|
+
`operators.allow`; a shipped baseline (`\exp`, `\int`, trig, …) is exempt.
|
|
18
21
|
- **Inline citations** — `[@source-id; locator]` → numbered marker + generated
|
|
19
22
|
reference list, resolved against a schema-checked source collection.
|
|
20
23
|
- **Equation identity** — `\label{eq:key}` gives a display equation a stable
|
|
@@ -28,6 +31,14 @@ What it gives a course:
|
|
|
28
31
|
- **Shared Astro/Starlight UI** — a hover-panel primitive shared by the
|
|
29
32
|
notation and citation layers, a glossary, a curriculum map, compact examples,
|
|
30
33
|
layout overrides.
|
|
34
|
+
- **Interactive figures, with room to grow** — a `labMathScope`/`renderLabMath`
|
|
35
|
+
seam compiles a `\slot{}`-templated formula against the page's real notation
|
|
36
|
+
at build time, so a client-side island only ever patches numbers into
|
|
37
|
+
pre-rendered KaTeX. Hand-rolled SVG is the default chart (zero dependency,
|
|
38
|
+
covers a single custom figure); reach for D3 once a figure needs real
|
|
39
|
+
tick-labeled axes, log scales, or several series sharing a scale; Observable
|
|
40
|
+
Plot pays off once a course is authoring many charts per lecture. Pick per
|
|
41
|
+
figure — nothing in the engine assumes one plotting approach.
|
|
31
42
|
- **LMS / analytics / identity seams** — framework-free interfaces shipped as
|
|
32
43
|
no-ops; a host supplies real implementations without touching a component.
|
|
33
44
|
- **A `content` CLI** — `status`, `context <lesson>`, `check <lesson>` (compiles
|
|
@@ -67,7 +78,8 @@ import { courseConfig } from './content/course.config.ts';
|
|
|
67
78
|
|
|
68
79
|
const manifest = await loadManifest(courseConfig);
|
|
69
80
|
// … starlight({ customCss: ['explico/styles/global.css'],
|
|
70
|
-
// components: { Footer: 'explico/components/starlight/LessonFooter.astro',
|
|
81
|
+
// components: { Footer: 'explico/components/starlight/LessonFooter.astro',
|
|
82
|
+
// SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro', … } })
|
|
71
83
|
// … markdown.remarkPlugins / rehypePlugins from the manifest
|
|
72
84
|
```
|
|
73
85
|
|
|
@@ -92,6 +104,19 @@ discounting → bonds → credit/CDS → derivatives and options.
|
|
|
92
104
|
| `src/content-config.ts` | the Zod collection schemas (Astro re-exports them) |
|
|
93
105
|
| `src/styles/` | `global.css` (notation panel, keyed-equation flash, KaTeX) |
|
|
94
106
|
|
|
107
|
+
## Authoring
|
|
108
|
+
|
|
109
|
+
Course content follows the rules in [`docs/authoring/`](docs/authoring/) —
|
|
110
|
+
strong recommendations for human and AI authors covering notation spelling
|
|
111
|
+
(`\mathcal{N}` for the normal, `\mathbb{P}` for probability, upright `\mathrm{d}`
|
|
112
|
+
for the differential, …), which library draws a diagram or
|
|
113
|
+
chart (`Diagram` for graphs; SVG → D3 → Observable Plot for charts), how
|
|
114
|
+
randomised interactive figures pick their axes, and how the course's knowledge
|
|
115
|
+
version and `content/CHANGELOG.md` move with a content change (area `C`,
|
|
116
|
+
surfaced to readers in the About `[?]` panel).
|
|
117
|
+
[`docs/authoring/README.md`](docs/authoring/README.md) is the governance: rule
|
|
118
|
+
IDs, enforcement levels, and how to amend.
|
|
119
|
+
|
|
95
120
|
## Build
|
|
96
121
|
|
|
97
122
|
```bash
|
|
@@ -102,7 +127,23 @@ pnpm build # tsup → dist/ (ESM + .d.ts) for the .ts core; .astro / .tsx / .c
|
|
|
102
127
|
Published `exports` resolve to `dist/` for the typed core and to `src/` for the
|
|
103
128
|
Astro components and styles (see `publishConfig` in `package.json`).
|
|
104
129
|
|
|
105
|
-
##
|
|
130
|
+
## Versioning
|
|
131
|
+
|
|
132
|
+
The engine follows [semver](https://semver.org). The **golden source** for the
|
|
133
|
+
version is the installed `package.json` (`explico`'s own `version`); the About
|
|
134
|
+
`[?]` panel reads it at build time and the docs never repeat a number.
|
|
135
|
+
|
|
136
|
+
- [`CHANGELOG.md`](CHANGELOG.md) — what changed between engine versions.
|
|
137
|
+
- [`docs/VERSIONING.md`](docs/VERSIONING.md) — what a MAJOR / MINOR / PATCH bump
|
|
138
|
+
means here, the public surface it is measured against, and the pre-1.0 clause.
|
|
139
|
+
|
|
140
|
+
A course built on explico carries an **independent knowledge version** over its
|
|
141
|
+
content — `about.version` in `content/course.config.ts`, tracked in the course's
|
|
142
|
+
`content/CHANGELOG.md`, surfaced to readers in the same `[?]` panel. See
|
|
143
|
+
[`docs/authoring/versioning.md`](docs/authoring/versioning.md).
|
|
144
|
+
|
|
145
|
+
For AI authors: [`AGENTS.md`](AGENTS.md) says which changelog and which version a
|
|
146
|
+
given change updates.
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
license is undecided (`UNLICENSED` until chosen).
|
|
148
|
+
Status: extracted from the `equations` project; API not yet stable (pre-1.0);
|
|
149
|
+
the license is undecided (`UNLICENSED` until chosen).
|
package/dist/compiler/cli.d.ts
CHANGED
|
@@ -6,7 +6,12 @@ import './collections.js';
|
|
|
6
6
|
import '../reference/types.js';
|
|
7
7
|
import '../reference/lint-ignore.js';
|
|
8
8
|
import '../reference/equations-validate.js';
|
|
9
|
+
import '../reference/tables-validate.js';
|
|
10
|
+
import '../reference/keyed-float-validate.js';
|
|
11
|
+
import '../reference/figures-validate.js';
|
|
9
12
|
import '../reference/gate-math.js';
|
|
13
|
+
import '../reference/gloss.js';
|
|
14
|
+
import './knowledge-version.js';
|
|
10
15
|
import './lesson-derivation.js';
|
|
11
16
|
|
|
12
17
|
/**
|
|
@@ -90,6 +95,8 @@ interface DomainFileSignatures {
|
|
|
90
95
|
readonly signatures: readonly string[];
|
|
91
96
|
}
|
|
92
97
|
interface StatusReport {
|
|
98
|
+
/** The course's published knowledge version (`about.version`). */
|
|
99
|
+
readonly knowledgeVersion: string;
|
|
93
100
|
readonly counts: {
|
|
94
101
|
readonly lessons: number;
|
|
95
102
|
readonly competencies: number;
|
|
@@ -98,6 +105,8 @@ interface StatusReport {
|
|
|
98
105
|
readonly tracks: number;
|
|
99
106
|
readonly notationDefinitions: number;
|
|
100
107
|
readonly keyedEquations: number;
|
|
108
|
+
readonly keyedTables: number;
|
|
109
|
+
readonly keyedFigures: number;
|
|
101
110
|
};
|
|
102
111
|
readonly draftDebt: {
|
|
103
112
|
readonly lessons: readonly string[];
|
|
@@ -144,6 +153,14 @@ interface ContextReport {
|
|
|
144
153
|
key: string;
|
|
145
154
|
number: string;
|
|
146
155
|
}[];
|
|
156
|
+
readonly tables: readonly {
|
|
157
|
+
key: string;
|
|
158
|
+
number: string;
|
|
159
|
+
}[];
|
|
160
|
+
readonly figures: readonly {
|
|
161
|
+
key: string;
|
|
162
|
+
number: string;
|
|
163
|
+
}[];
|
|
147
164
|
readonly sources: readonly {
|
|
148
165
|
id: string;
|
|
149
166
|
title: string;
|
package/dist/compiler/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@astrojs/markdown-remark";
|
|
9
9
|
import rehypeKatex from "rehype-katex";
|
|
10
10
|
import remarkMath from "remark-math";
|
|
11
|
+
import remarkMdx from "remark-mdx";
|
|
11
12
|
import { createNotationKatexOptions } from "../reference/katex-options.mjs";
|
|
12
13
|
import rehypeFailKatexErrors from "../reference/rehype-fail-katex-errors.mjs";
|
|
13
14
|
import remarkCitation from "../reference/remark-citation.mjs";
|
|
@@ -81,6 +82,20 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
81
82
|
...withFile(diagnostic.file)
|
|
82
83
|
});
|
|
83
84
|
}
|
|
85
|
+
for (const diagnostic of [
|
|
86
|
+
...manifest.diagnostics.tables,
|
|
87
|
+
...manifest.diagnostics.figures
|
|
88
|
+
]) {
|
|
89
|
+
if (diagnostic.lessonId !== id && diagnostic.file !== file && !mentions(diagnostic.message, id, file)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
out.push({
|
|
93
|
+
code: diagnostic.code,
|
|
94
|
+
severity: diagnostic.severity === "warning" ? "warning" : "error",
|
|
95
|
+
message: diagnostic.message,
|
|
96
|
+
...withFile(diagnostic.file)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
84
99
|
for (const diagnostic of manifest.diagnostics.consistency) {
|
|
85
100
|
if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
|
|
86
101
|
out.push({
|
|
@@ -100,12 +115,19 @@ function processorFor(manifest) {
|
|
|
100
115
|
syntaxHighlight: false,
|
|
101
116
|
smartypants: false,
|
|
102
117
|
remarkPlugins: [
|
|
118
|
+
// Syntax only (no JSX evaluation) — teaches the parser `<Table>` /
|
|
119
|
+
// `<Figure>` / `<CompactExample>` are `mdxJsxFlowElement` nodes, same
|
|
120
|
+
// as the real Astro/MDX build, so `remarkNotation` sees the same tree
|
|
121
|
+
// shape here that it does there (D7/F-tables-figures parity).
|
|
122
|
+
remarkMdx,
|
|
103
123
|
remarkMath,
|
|
104
124
|
[
|
|
105
125
|
remarkNotation,
|
|
106
126
|
{
|
|
107
127
|
definitions: manifest.notation.raw,
|
|
108
|
-
equations: manifest.equations.numbersBySlug
|
|
128
|
+
equations: manifest.equations.numbersBySlug,
|
|
129
|
+
tables: manifest.tables.numbersBySlug,
|
|
130
|
+
figures: manifest.figures.numbersBySlug
|
|
109
131
|
}
|
|
110
132
|
],
|
|
111
133
|
[remarkCitation, { sources: manifest.sources }]
|
|
@@ -279,16 +301,28 @@ function contentStatus(manifest) {
|
|
|
279
301
|
...manifest.diagnostics.alignment.map((d) => d.severity),
|
|
280
302
|
...manifest.diagnostics.math.map((d) => d.severity),
|
|
281
303
|
...manifest.diagnostics.equations.map((d) => d.severity),
|
|
282
|
-
...manifest.diagnostics.
|
|
304
|
+
...manifest.diagnostics.tables.map((d) => d.severity),
|
|
305
|
+
...manifest.diagnostics.figures.map((d) => d.severity),
|
|
306
|
+
...manifest.diagnostics.consistency.map(() => "warning"),
|
|
307
|
+
...manifest.diagnostics.knowledge.map((d) => d.severity)
|
|
283
308
|
];
|
|
284
309
|
const keyedEquations = manifest.lessons.reduce(
|
|
285
310
|
(count, lesson) => count + lesson.equations.length,
|
|
286
311
|
0
|
|
287
312
|
);
|
|
313
|
+
const keyedTables = manifest.lessons.reduce(
|
|
314
|
+
(count, lesson) => count + lesson.tables.length,
|
|
315
|
+
0
|
|
316
|
+
);
|
|
317
|
+
const keyedFigures = manifest.lessons.reduce(
|
|
318
|
+
(count, lesson) => count + lesson.figures.length,
|
|
319
|
+
0
|
|
320
|
+
);
|
|
288
321
|
const assessments = new Set(
|
|
289
322
|
manifest.lessons.flatMap((lesson) => lesson.assessments)
|
|
290
323
|
).size;
|
|
291
324
|
return {
|
|
325
|
+
knowledgeVersion: manifest.about.knowledge.version,
|
|
292
326
|
counts: {
|
|
293
327
|
lessons: manifest.lessons.length,
|
|
294
328
|
competencies: manifest.competencies.length,
|
|
@@ -296,7 +330,9 @@ function contentStatus(manifest) {
|
|
|
296
330
|
sources: manifest.sources.length,
|
|
297
331
|
tracks: manifest.tracks.length,
|
|
298
332
|
notationDefinitions: manifest.notation.definitions.length,
|
|
299
|
-
keyedEquations
|
|
333
|
+
keyedEquations,
|
|
334
|
+
keyedTables,
|
|
335
|
+
keyedFigures
|
|
300
336
|
},
|
|
301
337
|
draftDebt: { lessons: draftLessons, notation: draftNotation },
|
|
302
338
|
orphans,
|
|
@@ -350,6 +386,8 @@ function contentContext(manifest, rawId) {
|
|
|
350
386
|
...definition.formula === void 0 ? {} : { formula: definition.formula }
|
|
351
387
|
})),
|
|
352
388
|
equations: lesson.equations.map((equation) => ({ ...equation })),
|
|
389
|
+
tables: lesson.tables.map((table) => ({ ...table })),
|
|
390
|
+
figures: lesson.figures.map((figure) => ({ ...figure })),
|
|
353
391
|
sources: lesson.sources.map((id) => {
|
|
354
392
|
const record = sourceById.get(id);
|
|
355
393
|
return {
|
|
@@ -3,7 +3,9 @@ import { basename, join, relative } from "node:path";
|
|
|
3
3
|
import { parseFrontmatter } from "@astrojs/markdown-remark";
|
|
4
4
|
import { isSubstantiveMeaning } from "../reference/prose.js";
|
|
5
5
|
import { parseEquationRef } from "../reference/equations.mjs";
|
|
6
|
+
import { parseKeyedFloatRef } from "../reference/keyed-jsx-floats.mjs";
|
|
6
7
|
import { resolveLabel } from "../reference/label.js";
|
|
8
|
+
import { resolveGlosses } from "../reference/gloss.js";
|
|
7
9
|
import {
|
|
8
10
|
deriveRequires,
|
|
9
11
|
deriveSources,
|
|
@@ -135,7 +137,9 @@ function extractNotationReferences(text, file, kind) {
|
|
|
135
137
|
for (const [pattern, referenceKind] of patterns) {
|
|
136
138
|
for (const match of searchable.matchAll(pattern)) {
|
|
137
139
|
const key = (match[1] ?? "").trim();
|
|
138
|
-
if (parseEquationRef(key))
|
|
140
|
+
if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" })) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
139
143
|
references.push({
|
|
140
144
|
key,
|
|
141
145
|
kind: referenceKind,
|
|
@@ -206,6 +210,30 @@ async function loadCurriculumCatalog() {
|
|
|
206
210
|
tracks: jsonEntries(join(CONTENT_ROOT, "tracks"))
|
|
207
211
|
};
|
|
208
212
|
}
|
|
213
|
+
function glossEntries(raw, ownerKey, at) {
|
|
214
|
+
if (raw === void 0) return [];
|
|
215
|
+
if (!Array.isArray(raw)) throw new Error(`${at} must be an array`);
|
|
216
|
+
const authored = raw.map((value, index) => {
|
|
217
|
+
const data = record(value, `${at}[${index}]`);
|
|
218
|
+
const units = optionalString(data.units, `${at}[${index}].units`);
|
|
219
|
+
return {
|
|
220
|
+
latex: string(data.latex, `${at}[${index}].latex`),
|
|
221
|
+
name: string(data.name, `${at}[${index}].name`),
|
|
222
|
+
...units === void 0 ? {} : { units }
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
const glosses = resolveGlosses(ownerKey, authored);
|
|
226
|
+
const seen = /* @__PURE__ */ new Set();
|
|
227
|
+
for (const gloss of glosses) {
|
|
228
|
+
if (seen.has(gloss.key)) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`${at} declares two glosses named ${JSON.stringify(gloss.label)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
seen.add(gloss.key);
|
|
234
|
+
}
|
|
235
|
+
return glosses;
|
|
236
|
+
}
|
|
209
237
|
function localDefinition(raw, file, index) {
|
|
210
238
|
const at = `${file} notation.local[${index}]`;
|
|
211
239
|
const data = record(raw, at);
|
|
@@ -224,6 +252,7 @@ function localDefinition(raw, file, index) {
|
|
|
224
252
|
seeAlso: strings(data.seeAlso, `${at}.seeAlso`),
|
|
225
253
|
alignment: data.alignment === void 0 ? { kind: "general", rationale: "Lesson-local symbol." } : alignment(data.alignment, `${at}.alignment`),
|
|
226
254
|
references: extractNotationReferences(definitionText, file, "definition"),
|
|
255
|
+
glosses: glossEntries(data.glosses, key, `${at}.glosses`),
|
|
227
256
|
source: { file },
|
|
228
257
|
...formula === void 0 ? {} : { formula },
|
|
229
258
|
...units === void 0 ? {} : { units }
|
|
@@ -255,6 +284,7 @@ function sharedNotationEntries() {
|
|
|
255
284
|
aiAssisted: boolean(data.aiAssisted, `${file} aiAssisted`),
|
|
256
285
|
body,
|
|
257
286
|
references: extractNotationReferences(body, file, void 0),
|
|
287
|
+
glosses: glossEntries(data.glosses, key, `${file} glosses`),
|
|
258
288
|
source: { file },
|
|
259
289
|
...formula === void 0 ? {} : { formula },
|
|
260
290
|
...units === void 0 ? {} : { units }
|
|
@@ -18,6 +18,53 @@ interface CourseConventions {
|
|
|
18
18
|
/** Competency id that also counts as declaring the cash-flow perspective. */
|
|
19
19
|
readonly cashFlowPerspectiveCompetency: string;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A course's edits to the shipped named-operator baseline (`BASE_OPERATORS`),
|
|
23
|
+
* consumed by the `operator-explained` consistency check (rule N8). An operator
|
|
24
|
+
* a lesson uses is fine when it is on the baseline plus `allow` minus `deny`;
|
|
25
|
+
* otherwise it needs a notation entry the reader can hover.
|
|
26
|
+
*
|
|
27
|
+
* Names are the KaTeX command without its backslash (`erf`, not `\erf`) for a
|
|
28
|
+
* built-in, and the verbatim body for an `\operatorname{…}` operator (`Var`).
|
|
29
|
+
*/
|
|
30
|
+
interface CourseOperators {
|
|
31
|
+
/** Operators this course also treats as universal (`Var`, `Cov`, `sgn`, …). */
|
|
32
|
+
readonly allow?: readonly string[];
|
|
33
|
+
/** Baseline operators this course wants explained anyway. */
|
|
34
|
+
readonly deny?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
/** One person or team credited for the study material. */
|
|
37
|
+
interface CourseAuthor {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
/**
|
|
40
|
+
* Email, URL, or handle. Rendered in the About panel as a link when it looks
|
|
41
|
+
* like a `mailto:` or `https:` target, plain text otherwise.
|
|
42
|
+
*/
|
|
43
|
+
readonly contact?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The reader-facing "about this material" block, surfaced by the `[?]` panel in
|
|
47
|
+
* the header (`components/starlight/AboutPanel.astro`) on every page.
|
|
48
|
+
*
|
|
49
|
+
* `version` is the course's **knowledge version** — semver over the
|
|
50
|
+
* learning-facing content, independent of the `explico` engine's own version.
|
|
51
|
+
* It must equal the newest entry in `content/CHANGELOG.md`; a mismatch is a
|
|
52
|
+
* Tier-3 `knowledge-version-mismatch` warning (rule C3, `docs/authoring/versioning.md`).
|
|
53
|
+
*/
|
|
54
|
+
interface CourseAbout {
|
|
55
|
+
/** Display name of the material. Defaults to {@link CourseConfig.title}. */
|
|
56
|
+
readonly materialName?: string;
|
|
57
|
+
/** One or two sentences on what the material covers. Defaults to {@link CourseConfig.description}. */
|
|
58
|
+
readonly summary?: string;
|
|
59
|
+
/** Knowledge version — semver; equals the latest `content/CHANGELOG.md` heading. */
|
|
60
|
+
readonly version: string;
|
|
61
|
+
/** Who authored / maintains the material. At least one entry. */
|
|
62
|
+
readonly authors: readonly CourseAuthor[];
|
|
63
|
+
/** Shown verbatim in the About panel — scope, warranty, AI-assistance note. */
|
|
64
|
+
readonly disclaimer: string;
|
|
65
|
+
/** Link to the published content changelog (repo blob, docs page, …). */
|
|
66
|
+
readonly changelogHref?: string;
|
|
67
|
+
}
|
|
21
68
|
interface CourseConfig {
|
|
22
69
|
readonly title: string;
|
|
23
70
|
readonly description: string;
|
|
@@ -26,6 +73,10 @@ interface CourseConfig {
|
|
|
26
73
|
/** Namespaces used by competency / notation `domain` fields. */
|
|
27
74
|
readonly domains: readonly string[];
|
|
28
75
|
readonly conventions: CourseConventions;
|
|
76
|
+
/** Reader-facing identity + knowledge version (the `[?]` About panel). */
|
|
77
|
+
readonly about: CourseAbout;
|
|
78
|
+
/** Edits to the shipped named-operator baseline (rule N8). Optional. */
|
|
79
|
+
readonly operators?: CourseOperators;
|
|
29
80
|
}
|
|
30
81
|
|
|
31
|
-
export type { CourseConfig, CourseConventions, CourseSection };
|
|
82
|
+
export type { CourseAbout, CourseAuthor, CourseConfig, CourseConventions, CourseOperators, CourseSection };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The course's **knowledge version** — semver over the learning-facing content,
|
|
3
|
+
* independent of the `explico` engine's own package version.
|
|
4
|
+
*
|
|
5
|
+
* The declared value lives in `content/course.config.ts` (`about.version`); the
|
|
6
|
+
* history and the human-readable notes live in `content/CHANGELOG.md`
|
|
7
|
+
* (Keep a Changelog shape). This module parses that file and checks the two
|
|
8
|
+
* agree — rule C3 in `docs/authoring/versioning.md`. Every finding is a Tier-3
|
|
9
|
+
* warning: `pnpm validate:content` prints it, `manifest.diagnostics.knowledge`
|
|
10
|
+
* records it, and the build still passes. The resolved entry is what the `[?]`
|
|
11
|
+
* About panel publishes to readers (`manifest.about.knowledge`).
|
|
12
|
+
*
|
|
13
|
+
* Pure: `parseKnowledgeChangelog` takes text, `resolveKnowledgeVersion` takes
|
|
14
|
+
* text + the declared version. The caller (`compileManifest`) does the file IO.
|
|
15
|
+
*/
|
|
16
|
+
/** A released entry from `content/CHANGELOG.md`, newest first in file order. */
|
|
17
|
+
interface KnowledgeChangelogEntry {
|
|
18
|
+
readonly version: string;
|
|
19
|
+
/** The `- YYYY-MM-DD` part of the heading, if present. */
|
|
20
|
+
readonly date?: string;
|
|
21
|
+
/** Bullet lines under the heading, marker stripped, in file order. */
|
|
22
|
+
readonly notes: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
type KnowledgeVersionCode = 'knowledge-changelog-missing' | 'knowledge-version-invalid' | 'knowledge-version-mismatch';
|
|
25
|
+
interface KnowledgeVersionDiagnostic {
|
|
26
|
+
readonly code: KnowledgeVersionCode;
|
|
27
|
+
/** Tier 3 — a knowledge-version drift never fails the build. */
|
|
28
|
+
readonly severity: 'warning';
|
|
29
|
+
readonly message: string;
|
|
30
|
+
readonly file?: string;
|
|
31
|
+
}
|
|
32
|
+
interface KnowledgeVersion {
|
|
33
|
+
/** The declared `about.version`, echoed for consumers. */
|
|
34
|
+
readonly version: string;
|
|
35
|
+
/** The newest released `## [x.y.z]` entry, when the changelog has one. */
|
|
36
|
+
readonly latest?: KnowledgeChangelogEntry;
|
|
37
|
+
readonly diagnostics: readonly KnowledgeVersionDiagnostic[];
|
|
38
|
+
}
|
|
39
|
+
declare function isKnowledgeSemver(value: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Every released entry in a `content/CHANGELOG.md` body, in file order (newest
|
|
42
|
+
* first, by Keep a Changelog convention). `## [Unreleased]` is skipped.
|
|
43
|
+
*/
|
|
44
|
+
declare function parseKnowledgeChangelog(text: string): KnowledgeChangelogEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* Check the declared knowledge version against `content/CHANGELOG.md`.
|
|
47
|
+
*
|
|
48
|
+
* @param options.changelogText the file body, or `undefined` when it is absent
|
|
49
|
+
* @param options.declaredVersion `courseConfig.about.version`
|
|
50
|
+
* @param options.file repo-relative path, for diagnostic messages
|
|
51
|
+
*/
|
|
52
|
+
declare function resolveKnowledgeVersion(options: {
|
|
53
|
+
readonly changelogText: string | undefined;
|
|
54
|
+
readonly declaredVersion: string;
|
|
55
|
+
readonly file?: string;
|
|
56
|
+
}): KnowledgeVersion;
|
|
57
|
+
|
|
58
|
+
export { type KnowledgeChangelogEntry, type KnowledgeVersion, type KnowledgeVersionCode, type KnowledgeVersionDiagnostic, isKnowledgeSemver, parseKnowledgeChangelog, resolveKnowledgeVersion };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
2
|
+
const HEADING_RE = /^##\s+\[([^\]]+)\]\s*(?:[-–—]\s*(\S.*?))?\s*$/;
|
|
3
|
+
const BULLET_RE = /^[-*]\s+(.*\S)\s*$/;
|
|
4
|
+
function isKnowledgeSemver(value) {
|
|
5
|
+
return SEMVER_RE.test(value.trim());
|
|
6
|
+
}
|
|
7
|
+
function parseKnowledgeChangelog(text) {
|
|
8
|
+
const lines = text.split(/\r?\n/);
|
|
9
|
+
const entries = [];
|
|
10
|
+
let current = null;
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
const heading = HEADING_RE.exec(line);
|
|
13
|
+
if (heading) {
|
|
14
|
+
const token = (heading[1] ?? "").trim();
|
|
15
|
+
if (/^unreleased$/i.test(token)) {
|
|
16
|
+
current = null;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
current = { version: token, notes: [] };
|
|
20
|
+
const date = (heading[2] ?? "").trim();
|
|
21
|
+
if (date) current.date = date;
|
|
22
|
+
entries.push(current);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (/^#{1,3}\s/.test(line) && !line.startsWith("## [")) {
|
|
26
|
+
current = null;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!current) continue;
|
|
30
|
+
const bullet = BULLET_RE.exec(line);
|
|
31
|
+
if (bullet) current.notes.push(bullet[1] ?? "");
|
|
32
|
+
}
|
|
33
|
+
return entries.map((entry) => ({
|
|
34
|
+
version: entry.version,
|
|
35
|
+
...entry.date === void 0 ? {} : { date: entry.date },
|
|
36
|
+
notes: [...entry.notes]
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function resolveKnowledgeVersion(options) {
|
|
40
|
+
const { changelogText, declaredVersion } = options;
|
|
41
|
+
const file = options.file ?? "content/CHANGELOG.md";
|
|
42
|
+
const diagnostics = [];
|
|
43
|
+
const warn = (code, message) => {
|
|
44
|
+
diagnostics.push({ code, severity: "warning", message, file });
|
|
45
|
+
};
|
|
46
|
+
if (!isKnowledgeSemver(declaredVersion)) {
|
|
47
|
+
warn(
|
|
48
|
+
"knowledge-version-invalid",
|
|
49
|
+
`about.version ${JSON.stringify(declaredVersion)} in content/course.config.ts is not a semver \`x.y.z\` string.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (changelogText === void 0) {
|
|
53
|
+
warn(
|
|
54
|
+
"knowledge-changelog-missing",
|
|
55
|
+
`No ${file}. Add a Keep a Changelog file whose newest \`## [x.y.z]\` heading is ${JSON.stringify(declaredVersion)} (rule C1).`
|
|
56
|
+
);
|
|
57
|
+
return { version: declaredVersion, diagnostics };
|
|
58
|
+
}
|
|
59
|
+
const entries = parseKnowledgeChangelog(changelogText);
|
|
60
|
+
const latest = entries[0];
|
|
61
|
+
if (!latest) {
|
|
62
|
+
warn(
|
|
63
|
+
"knowledge-changelog-missing",
|
|
64
|
+
`${file} has no released \`## [x.y.z]\` entry \u2014 only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C4).`
|
|
65
|
+
);
|
|
66
|
+
return { version: declaredVersion, diagnostics };
|
|
67
|
+
}
|
|
68
|
+
if (isKnowledgeSemver(declaredVersion) && latest.version !== declaredVersion) {
|
|
69
|
+
warn(
|
|
70
|
+
"knowledge-version-mismatch",
|
|
71
|
+
`about.version is ${JSON.stringify(declaredVersion)} but the newest ${file} entry is ${JSON.stringify(latest.version)}. Bump one to match the other (rule C3).`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return { version: declaredVersion, latest, diagnostics };
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
isKnowledgeSemver,
|
|
78
|
+
parseKnowledgeChangelog,
|
|
79
|
+
resolveKnowledgeVersion
|
|
80
|
+
};
|