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.
- package/dist/compiler/cli.d.ts +1 -0
- package/dist/compiler/cli.js +1 -1
- package/dist/compiler/collections.js +30 -3
- package/dist/compiler/manifest.d.ts +10 -1
- package/dist/compiler/manifest.js +28 -12
- package/dist/compiler/resolution-report.d.ts +1 -0
- package/dist/compiler/resolution-report.js +2 -2
- package/dist/reference/base-library.d.mts +17 -0
- package/dist/reference/citation-format.d.mts +55 -0
- package/dist/reference/consistency.d.ts +1 -1
- package/dist/reference/consistency.js +94 -3
- package/dist/reference/curriculum-alignment.js +1 -1
- package/dist/reference/equations.d.mts +84 -0
- package/dist/reference/equations.mjs +75 -39
- 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/katex-options.d.mts +33 -0
- package/dist/reference/lab-math-scope.js +1 -1
- package/dist/reference/math-glyphs.d.mts +46 -0
- package/dist/reference/registry.js +26 -1
- package/dist/reference/rehype-fail-katex-errors.d.mts +6 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-citation.d.mts +6 -0
- package/dist/reference/remark-notation.d.mts +49 -0
- package/dist/reference/remark-notation.mjs +30 -24
- package/dist/reference/types.d.ts +25 -2
- package/dist/session/user-context.js +1 -1
- package/package.json +2 -2
- package/src/compiler/collections.ts +40 -0
- package/src/compiler/manifest.ts +31 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -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 +55 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/starlight/EquationEnhancer.astro +20 -5
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/LessonFooter.astro +4 -0
- package/src/content-config.ts +38 -0
- package/src/reference/consistency.ts +154 -1
- package/src/reference/equations.mjs +75 -39
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -0
- package/src/reference/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +30 -24
- package/src/reference/types.ts +26 -1
- package/src/styles/global.css +84 -2
package/dist/compiler/cli.d.ts
CHANGED
package/dist/compiler/cli.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
}
|
|
@@ -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;
|
|
@@ -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;
|