explico 0.5.0 → 0.7.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 +295 -7
- package/README.md +16 -2
- package/dist/compiler/astro.d.ts +13 -2
- package/dist/compiler/astro.js +39 -0
- package/dist/compiler/cli.d.ts +22 -3
- package/dist/compiler/cli.js +71 -10
- package/dist/compiler/course-config.d.ts +31 -1
- package/dist/compiler/course-config.js +7 -0
- package/dist/compiler/exports/standalone-html.js +13 -0
- package/dist/compiler/logging.d.ts +109 -0
- package/dist/compiler/logging.js +107 -0
- package/dist/compiler/manifest.d.ts +16 -4
- package/dist/compiler/manifest.js +7 -3
- package/dist/reference/base-abbreviations.mjs +8 -2
- package/dist/reference/consistency.d.ts +8 -10
- package/dist/reference/consistency.js +3 -47
- package/dist/reference/delimiter-size.d.ts +39 -0
- package/dist/reference/delimiter-size.js +177 -0
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +2 -2
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-heading-length.d.ts +12 -0
- package/dist/reference/gate-heading-length.js +79 -0
- package/dist/reference/gate-math.d.ts +8 -2
- package/dist/reference/gate-math.js +30 -7
- package/dist/reference/keyed-float-validate.d.ts +2 -2
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/math-glyphs.d.mts +12 -0
- package/dist/reference/math-glyphs.mjs +183 -42
- package/dist/reference/references.js +2 -1
- package/dist/reference/registry.js +1 -1
- package/dist/reference/rehype-base-links.d.mts +23 -0
- package/dist/reference/rehype-base-links.mjs +40 -0
- package/dist/reference/remark-abbreviations.mjs +10 -12
- package/dist/reference/remark-citation.d.mts +5 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/render-lab-math.d.ts +18 -1
- package/dist/reference/render-lab-math.js +10 -3
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +1 -1
- package/docs/VERSIONING.md +24 -4
- package/docs/authoring/README.md +14 -2
- package/docs/authoring/agents.md +7 -3
- package/docs/authoring/components.md +4 -1
- package/docs/authoring/migrating.md +477 -8
- package/docs/authoring/notation.md +194 -7
- package/docs/authoring/prose.md +64 -0
- package/docs/authoring/structure-rules.md +69 -0
- package/docs/authoring/visualizations.md +2 -2
- package/package.json +1 -1
- package/src/compiler/astro.ts +89 -0
- package/src/compiler/cli.ts +144 -12
- package/src/compiler/course-config.ts +40 -0
- package/src/compiler/exports/standalone-html.ts +15 -0
- package/src/compiler/logging.ts +248 -0
- package/src/compiler/manifest.ts +20 -3
- package/src/components/diagram/Diagram.astro +0 -55
- package/src/components/memo/Memo.astro +2 -5
- package/src/components/memo/ShortNote.astro +2 -5
- package/src/components/notation/NotationFormula.astro +199 -10
- package/src/components/notation/NotationGlossary.astro +89 -58
- package/src/components/notation/NotationLayer.astro +238 -156
- package/src/components/scroll-animation.ts +160 -0
- package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
- package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
- package/src/components/starlight/LessonFooter.astro +5 -1
- package/src/content-config.ts +16 -1
- package/src/reference/base-abbreviations.mjs +8 -2
- package/src/reference/consistency.ts +10 -66
- package/src/reference/delimiter-size.ts +301 -0
- package/src/reference/diagrams-validate.ts +1 -1
- package/src/reference/equations-validate.ts +3 -3
- package/src/reference/figures-validate.ts +1 -1
- package/src/reference/gate-heading-length.ts +131 -0
- package/src/reference/gate-math.ts +48 -8
- package/src/reference/keyed-float-validate.ts +3 -3
- package/src/reference/math-glyphs.mjs +183 -42
- package/src/reference/references.ts +2 -1
- package/src/reference/registry.ts +1 -1
- package/src/reference/rehype-base-links.mjs +40 -0
- package/src/reference/remark-abbreviations.mjs +10 -12
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/render-lab-math.ts +24 -1
- package/src/reference/tables-validate.ts +1 -1
- package/src/reference/types.ts +1 -1
- package/src/styles/global.css +120 -15
package/dist/compiler/cli.js
CHANGED
|
@@ -30,6 +30,9 @@ class ContentCliError extends Error {
|
|
|
30
30
|
this.code = code;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
function diagnosticSeverity(value) {
|
|
34
|
+
return value === "info" || value === "warning" ? value : "error";
|
|
35
|
+
}
|
|
33
36
|
function mentions(message, lessonId, file) {
|
|
34
37
|
return message.includes(lessonId) || message.includes(file) || message.includes(file.replace(/^content\/docs\//, ""));
|
|
35
38
|
}
|
|
@@ -42,7 +45,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
42
45
|
if (!mentions(issue.message, id, file)) continue;
|
|
43
46
|
out.push({
|
|
44
47
|
code: `curriculum:${issue.kind}`,
|
|
45
|
-
severity: issue.severity
|
|
48
|
+
severity: diagnosticSeverity(issue.severity),
|
|
46
49
|
message: issue.message
|
|
47
50
|
});
|
|
48
51
|
}
|
|
@@ -57,7 +60,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
57
60
|
}
|
|
58
61
|
out.push({
|
|
59
62
|
code: `${group}:${entry.code}`,
|
|
60
|
-
severity: entry.severity
|
|
63
|
+
severity: diagnosticSeverity(entry.severity),
|
|
61
64
|
message: entry.message,
|
|
62
65
|
...withFile(entryFile)
|
|
63
66
|
});
|
|
@@ -68,11 +71,12 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
68
71
|
);
|
|
69
72
|
if (!forLesson) continue;
|
|
70
73
|
out.push({
|
|
71
|
-
code: `math:${diagnostic.context}`,
|
|
74
|
+
code: `math:${diagnostic.code ?? diagnostic.context}`,
|
|
72
75
|
severity: diagnostic.severity,
|
|
73
76
|
message: diagnostic.message,
|
|
74
77
|
file: diagnostic.file,
|
|
75
|
-
...diagnostic.line === void 0 ? {} : { line: diagnostic.line }
|
|
78
|
+
...diagnostic.line === void 0 ? {} : { line: diagnostic.line },
|
|
79
|
+
...diagnostic.column === void 0 ? {} : { column: diagnostic.column }
|
|
76
80
|
});
|
|
77
81
|
}
|
|
78
82
|
for (const diagnostic of manifest.diagnostics.equations) {
|
|
@@ -81,7 +85,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
81
85
|
}
|
|
82
86
|
out.push({
|
|
83
87
|
code: `equations:${diagnostic.code}`,
|
|
84
|
-
severity: diagnostic.severity
|
|
88
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
85
89
|
message: diagnostic.message,
|
|
86
90
|
...withFile(diagnostic.file)
|
|
87
91
|
});
|
|
@@ -98,7 +102,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
98
102
|
}
|
|
99
103
|
out.push({
|
|
100
104
|
code: diagnostic.code,
|
|
101
|
-
severity: diagnostic.severity
|
|
105
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
102
106
|
message: diagnostic.message,
|
|
103
107
|
...withFile(diagnostic.file)
|
|
104
108
|
});
|
|
@@ -107,7 +111,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
107
111
|
if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
|
|
108
112
|
out.push({
|
|
109
113
|
code: `consistency:${diagnostic.code}`,
|
|
110
|
-
severity:
|
|
114
|
+
severity: diagnostic.severity,
|
|
111
115
|
message: diagnostic.message,
|
|
112
116
|
...withFile(diagnostic.file)
|
|
113
117
|
});
|
|
@@ -301,7 +305,7 @@ function contentStatus(manifest) {
|
|
|
301
305
|
const draftNotation = manifest.notation.definitions.filter((definition) => definition.status === "draft").map((definition) => definition.id).sort();
|
|
302
306
|
const orphans = manifest.notation.diagnostics.filter((diagnostic) => diagnostic.code === "unused-definition").map((diagnostic) => ({
|
|
303
307
|
code: `notation:${diagnostic.code}`,
|
|
304
|
-
severity: diagnostic.severity
|
|
308
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
305
309
|
message: diagnostic.message,
|
|
306
310
|
...diagnostic.source?.file === void 0 ? {} : { file: diagnostic.source.file }
|
|
307
311
|
})).sort((a, b) => a.message.localeCompare(b.message));
|
|
@@ -316,7 +320,7 @@ function contentStatus(manifest) {
|
|
|
316
320
|
...manifest.diagnostics.diagrams.map((d) => d.severity),
|
|
317
321
|
...manifest.diagnostics.checks.map((d) => d.severity),
|
|
318
322
|
...manifest.diagnostics.rawFloats.map((d) => d.severity),
|
|
319
|
-
...manifest.diagnostics.consistency.map(() =>
|
|
323
|
+
...manifest.diagnostics.consistency.map((d) => d.severity),
|
|
320
324
|
...manifest.diagnostics.knowledge.map((d) => d.severity)
|
|
321
325
|
];
|
|
322
326
|
const keyedEquations = manifest.lessons.reduce(
|
|
@@ -356,10 +360,65 @@ function contentStatus(manifest) {
|
|
|
356
360
|
orphans,
|
|
357
361
|
diagnostics: {
|
|
358
362
|
errors: allDiagnostics.filter((severity) => severity === "error").length,
|
|
359
|
-
warnings: allDiagnostics.filter((severity) => severity === "warning").length
|
|
363
|
+
warnings: allDiagnostics.filter((severity) => severity === "warning").length,
|
|
364
|
+
infos: allDiagnostics.filter((severity) => severity === "info").length
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function numericLocation(value) {
|
|
369
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
370
|
+
}
|
|
371
|
+
const SEVERITY_RANK = {
|
|
372
|
+
info: 0,
|
|
373
|
+
warning: 1,
|
|
374
|
+
error: 2
|
|
375
|
+
};
|
|
376
|
+
function contentDiagnostics(manifest, minimumSeverity = "info") {
|
|
377
|
+
const lessonFileByHint = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const lesson of manifest.lessons) {
|
|
379
|
+
lessonFileByHint.set(lesson.id, lesson.file);
|
|
380
|
+
lessonFileByHint.set(lesson.slug, lesson.file);
|
|
381
|
+
lessonFileByHint.set(lesson.file, lesson.file);
|
|
382
|
+
}
|
|
383
|
+
const groups = Object.entries(manifest.diagnostics);
|
|
384
|
+
const diagnostics = [];
|
|
385
|
+
for (const [group, values] of groups) {
|
|
386
|
+
for (const value of values) {
|
|
387
|
+
const diagnostic = value;
|
|
388
|
+
const severity = diagnosticSeverity(diagnostic.severity);
|
|
389
|
+
if (SEVERITY_RANK[severity] < SEVERITY_RANK[minimumSeverity]) continue;
|
|
390
|
+
const source = diagnostic.source !== null && typeof diagnostic.source === "object" ? diagnostic.source : void 0;
|
|
391
|
+
const rawFile = typeof diagnostic.file === "string" ? diagnostic.file : typeof source?.file === "string" ? source.file : void 0;
|
|
392
|
+
const lessonId = typeof diagnostic.lessonId === "string" ? diagnostic.lessonId : void 0;
|
|
393
|
+
const file = (rawFile === void 0 ? void 0 : lessonFileByHint.get(rawFile)) ?? (lessonId === void 0 ? void 0 : lessonFileByHint.get(lessonId)) ?? rawFile;
|
|
394
|
+
const line = numericLocation(diagnostic.line) ?? numericLocation(source?.line);
|
|
395
|
+
const column = numericLocation(diagnostic.column) ?? numericLocation(source?.column);
|
|
396
|
+
const detail = typeof diagnostic.code === "string" ? diagnostic.code : typeof diagnostic.kind === "string" ? diagnostic.kind : "diagnostic";
|
|
397
|
+
diagnostics.push({
|
|
398
|
+
code: `${group}:${detail}`,
|
|
399
|
+
severity,
|
|
400
|
+
message: String(diagnostic.message ?? detail),
|
|
401
|
+
...file === void 0 ? {} : { file },
|
|
402
|
+
...line === void 0 ? {} : { line },
|
|
403
|
+
...column === void 0 ? {} : { column }
|
|
404
|
+
});
|
|
360
405
|
}
|
|
406
|
+
}
|
|
407
|
+
diagnostics.sort(
|
|
408
|
+
(left, right) => (left.file ?? "").localeCompare(right.file ?? "") || (left.line ?? 0) - (right.line ?? 0) || (left.column ?? 0) - (right.column ?? 0) || left.code.localeCompare(right.code) || left.message.localeCompare(right.message)
|
|
409
|
+
);
|
|
410
|
+
return {
|
|
411
|
+
minimumSeverity,
|
|
412
|
+
count: diagnostics.length,
|
|
413
|
+
diagnostics
|
|
361
414
|
};
|
|
362
415
|
}
|
|
416
|
+
function contentWarnings(manifest) {
|
|
417
|
+
const warnings = contentDiagnostics(manifest, "warning").diagnostics.filter(
|
|
418
|
+
(diagnostic) => diagnostic.severity === "warning"
|
|
419
|
+
);
|
|
420
|
+
return { count: warnings.length, warnings };
|
|
421
|
+
}
|
|
363
422
|
function contentContext(manifest, rawId) {
|
|
364
423
|
const lesson = findLesson(manifest, rawId);
|
|
365
424
|
const { frontmatter, body } = lessonRaw(lesson);
|
|
@@ -599,7 +658,9 @@ export {
|
|
|
599
658
|
changedLessonIds,
|
|
600
659
|
contentCheck,
|
|
601
660
|
contentContext,
|
|
661
|
+
contentDiagnostics,
|
|
602
662
|
contentStatus,
|
|
663
|
+
contentWarnings,
|
|
603
664
|
estimateTokens,
|
|
604
665
|
findLesson,
|
|
605
666
|
lessonDiagnostics,
|
|
@@ -107,6 +107,33 @@ interface CourseAbout {
|
|
|
107
107
|
/** Link to the published content changelog (repo blob, docs page, …). */
|
|
108
108
|
readonly changelogHref?: string;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* A course's chosen initial open/closed state for the engine's own built-in
|
|
112
|
+
* collapsible sections — the "Page notation" panel (`NotationLayer.astro`)
|
|
113
|
+
* and the generated "References" section (`remark-citation.mjs`). Each is
|
|
114
|
+
* independent of `defaultCollapsible.sections`/`.subsections` (an author's
|
|
115
|
+
* own `##` headings): both of these are engine-authored, not written by a
|
|
116
|
+
* lesson, so they need their own default rather than inheriting the site's
|
|
117
|
+
* general heading default.
|
|
118
|
+
*
|
|
119
|
+
* `true` starts open, `false` (or omitted) starts closed — the engine's own
|
|
120
|
+
* default is closed either way, matching a long panel a reader usually
|
|
121
|
+
* doesn't need open on arrival.
|
|
122
|
+
*/
|
|
123
|
+
interface CourseDisclosureDefaults {
|
|
124
|
+
/** Initial state of the "Page notation" panel. Default: closed. */
|
|
125
|
+
readonly notation?: boolean;
|
|
126
|
+
/** Initial state of the generated "References" section. Default: closed. */
|
|
127
|
+
readonly references?: boolean;
|
|
128
|
+
}
|
|
129
|
+
/** The stable, non-optional form `NormalizedLessonExports` mirrors. */
|
|
130
|
+
interface NormalizedDisclosureDefaults {
|
|
131
|
+
readonly notation: boolean;
|
|
132
|
+
readonly references: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** Validate and canonicalize `CourseConfig.disclosureDefaults`. Absent (or
|
|
135
|
+
* an absent sub-field) resolves to closed, the engine's own default. */
|
|
136
|
+
declare function normalizeDisclosureDefaults(config: CourseConfig): NormalizedDisclosureDefaults;
|
|
110
137
|
/** Optional, build-time lesson artifacts exposed to readers. */
|
|
111
138
|
interface LessonExportsConfig {
|
|
112
139
|
/** Emit one self-contained, offline-capable HTML file per lesson. */
|
|
@@ -151,6 +178,9 @@ interface CourseConfig {
|
|
|
151
178
|
readonly abbreviations?: CourseAbbreviations;
|
|
152
179
|
/** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
|
|
153
180
|
readonly lessonExports?: LessonExportsConfig;
|
|
181
|
+
/** Initial open/closed state for the engine's own built-in collapsible
|
|
182
|
+
* sections ("Page notation", "References"). Closed when omitted. */
|
|
183
|
+
readonly disclosureDefaults?: CourseDisclosureDefaults;
|
|
154
184
|
}
|
|
155
185
|
/** Validate and canonicalize the build-facing lesson-export configuration. */
|
|
156
186
|
declare function normalizeLessonExports(config: CourseConfig): NormalizedLessonExports;
|
|
@@ -161,4 +191,4 @@ declare function normalizeLessonExports(config: CourseConfig): NormalizedLessonE
|
|
|
161
191
|
*/
|
|
162
192
|
declare function coursePartsOf(config: CourseConfig): readonly CoursePart[];
|
|
163
193
|
|
|
164
|
-
export { type CourseAbbreviations, type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseOperators, type CoursePart, type CourseRomanLabels, type CourseSection, type LessonExportsConfig, type NormalizedLessonExports, coursePartsOf, normalizeLessonExports };
|
|
194
|
+
export { type CourseAbbreviations, type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseDisclosureDefaults, type CourseOperators, type CoursePart, type CourseRomanLabels, type CourseSection, type LessonExportsConfig, type NormalizedDisclosureDefaults, type NormalizedLessonExports, coursePartsOf, normalizeDisclosureDefaults, normalizeLessonExports };
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
function normalizeDisclosureDefaults(config) {
|
|
2
|
+
return {
|
|
3
|
+
notation: config.disclosureDefaults?.notation === true,
|
|
4
|
+
references: config.disclosureDefaults?.references === true
|
|
5
|
+
};
|
|
6
|
+
}
|
|
1
7
|
function normalizeLessonExports(config) {
|
|
2
8
|
const standaloneHtml = config.lessonExports?.standaloneHtml === true;
|
|
3
9
|
const print = config.lessonExports?.print === true;
|
|
@@ -35,5 +41,6 @@ function coursePartsOf(config) {
|
|
|
35
41
|
}
|
|
36
42
|
export {
|
|
37
43
|
coursePartsOf,
|
|
44
|
+
normalizeDisclosureDefaults,
|
|
38
45
|
normalizeLessonExports
|
|
39
46
|
};
|
|
@@ -584,6 +584,18 @@ async function inlineResourceAttribute(node, name, context) {
|
|
|
584
584
|
const path = await resolveLocalFile(reference, context.sourceFile, context.outputRoot);
|
|
585
585
|
setAttribute(node, name, await assetDataUrl(path, context));
|
|
586
586
|
}
|
|
587
|
+
const INLINEABLE_LINK_RELS = /* @__PURE__ */ new Set([
|
|
588
|
+
"stylesheet",
|
|
589
|
+
"icon",
|
|
590
|
+
"shortcut icon",
|
|
591
|
+
"apple-touch-icon",
|
|
592
|
+
"apple-touch-icon-precomposed",
|
|
593
|
+
"mask-icon"
|
|
594
|
+
]);
|
|
595
|
+
function isInlineableLinkRel(node) {
|
|
596
|
+
const rel = attribute(node, "rel");
|
|
597
|
+
return rel !== void 0 && INLINEABLE_LINK_RELS.has(rel.trim().toLowerCase());
|
|
598
|
+
}
|
|
587
599
|
function parseSourceSet(value) {
|
|
588
600
|
const candidates = value.match(/(?:[^\s,]+)(?:\s+(?:\d+w|\d+(?:\.\d+)?x))?/gu) ?? [];
|
|
589
601
|
return candidates.map((candidate) => {
|
|
@@ -609,6 +621,7 @@ async function inlineMedia(document, context) {
|
|
|
609
621
|
"Standalone HTML does not support an iframe src; use self-contained semantic DOM instead."
|
|
610
622
|
);
|
|
611
623
|
}
|
|
624
|
+
if (node.tagName === "link" && !isInlineableLinkRel(node)) continue;
|
|
612
625
|
const names = resources[node.tagName] ?? [];
|
|
613
626
|
for (const name of names) await inlineResourceAttribute(node, name, context);
|
|
614
627
|
if (node.tagName === "use" || node.tagName === "image") {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run logs for `pnpm validate:content`, in Python's `logging` format.
|
|
3
|
+
*
|
|
4
|
+
* The terminal keeps the compact `[code] message` lines it has always shown.
|
|
5
|
+
* This module is the second handler over the same records: it writes them to
|
|
6
|
+
* `build/logs/<timestamp>/` in the format Python's `logging` emits, so a run
|
|
7
|
+
* can be grepped by level and by subsystem long after the terminal scrolled
|
|
8
|
+
* away.
|
|
9
|
+
*
|
|
10
|
+
* %(asctime)s %(levelname)s %(name)s: %(message)s
|
|
11
|
+
*
|
|
12
|
+
* 2026-09-18 03:22:47,482 WARNING explico.notation: [undefined-reference] …
|
|
13
|
+
*
|
|
14
|
+
* Three files per run, each a superset of the one above it, so you open the
|
|
15
|
+
* narrowest one that still answers the question:
|
|
16
|
+
*
|
|
17
|
+
* build/logs/2026-09-18T03-22-47/
|
|
18
|
+
* error.log errors only
|
|
19
|
+
* warning.log errors + warnings
|
|
20
|
+
* info.log errors + warnings + info
|
|
21
|
+
*
|
|
22
|
+
* Nothing is pruned — `build/` is git-ignored, so old runs are yours to clean
|
|
23
|
+
* up — and `build/logs/latest` points at the newest run.
|
|
24
|
+
*
|
|
25
|
+
* Levels carry their Python meaning. `error` is a blocking diagnostic (the
|
|
26
|
+
* caller still throws; this module only records), `warning` is a non-blocking
|
|
27
|
+
* finding, and `info` is the run summary plus the severity-`info` diagnostics
|
|
28
|
+
* the engine has always produced but never printed.
|
|
29
|
+
*
|
|
30
|
+
* Each level method **returns its message**, so a call site can log and print
|
|
31
|
+
* in one expression without the two drifting apart:
|
|
32
|
+
*
|
|
33
|
+
* console.warn(log.warning('notation', `[${d.code}] ${d.message}`));
|
|
34
|
+
*/
|
|
35
|
+
/** Sits beside `build/manifest.json` and `build/resolution/` (Phase F). */
|
|
36
|
+
declare const LOGS_DIR: string;
|
|
37
|
+
/** Python's levels, narrowed to the three the engine actually emits. */
|
|
38
|
+
type LogLevel = 'ERROR' | 'WARNING' | 'INFO';
|
|
39
|
+
/**
|
|
40
|
+
* Python's `%(asctime)s` — local time, with the comma before the milliseconds
|
|
41
|
+
* that `logging.Formatter` writes by default.
|
|
42
|
+
*/
|
|
43
|
+
declare function formatTimestamp(date: Date): string;
|
|
44
|
+
/** A filesystem-safe local-time stamp for the run directory's name. */
|
|
45
|
+
declare function formatRunStamp(date: Date): string;
|
|
46
|
+
/**
|
|
47
|
+
* `explico.<source>` — the logger name is the diagnostic's subsystem, so
|
|
48
|
+
* filtering by name is filtering by subsystem. `rawFloats` becomes
|
|
49
|
+
* `explico.raw-floats`; anything already lowercase passes through.
|
|
50
|
+
*/
|
|
51
|
+
declare function loggerName(source: string): string;
|
|
52
|
+
/** One formatted line, without its trailing newline. */
|
|
53
|
+
declare function formatRecord(level: LogLevel, source: string, message: string, date: Date): string;
|
|
54
|
+
interface RunLogSummary {
|
|
55
|
+
/** Absolute path of this run's directory. */
|
|
56
|
+
readonly directory: string;
|
|
57
|
+
/** Repo-relative form, for printing. */
|
|
58
|
+
readonly relativeDirectory: string;
|
|
59
|
+
readonly counts: Readonly<Record<LogLevel, number>>;
|
|
60
|
+
}
|
|
61
|
+
/** One diagnostic, across every category's slightly different shape. */
|
|
62
|
+
interface AnyDiagnostic {
|
|
63
|
+
readonly severity?: string;
|
|
64
|
+
readonly message: string;
|
|
65
|
+
readonly code?: string;
|
|
66
|
+
readonly kind?: string;
|
|
67
|
+
readonly file?: string;
|
|
68
|
+
readonly line?: number;
|
|
69
|
+
readonly column?: number;
|
|
70
|
+
}
|
|
71
|
+
/** `manifest.diagnostics`, structurally — one array per subsystem. */
|
|
72
|
+
type DiagnosticGroups = Readonly<Record<string, readonly AnyDiagnostic[]>>;
|
|
73
|
+
interface RunLog {
|
|
74
|
+
/** Absolute path of this run's directory. */
|
|
75
|
+
readonly directory: string;
|
|
76
|
+
/** Records at `ERROR` and returns `message`. */
|
|
77
|
+
error(source: string, message: string): string;
|
|
78
|
+
/** Records at `WARNING` and returns `message`. */
|
|
79
|
+
warning(source: string, message: string): string;
|
|
80
|
+
/** Records at `INFO` and returns `message`. */
|
|
81
|
+
info(source: string, message: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Sweeps every severity-`info` diagnostic in a manifest's `diagnostics` into
|
|
84
|
+
* `info.log`. Errors and warnings are deliberately **not** taken from here:
|
|
85
|
+
* they are recorded at the call site that also prints the console line, so
|
|
86
|
+
* the two can never drift.
|
|
87
|
+
*/
|
|
88
|
+
infoDiagnostics(diagnostics: DiagnosticGroups): number;
|
|
89
|
+
/** Refreshes `latest` and reports what the run recorded. */
|
|
90
|
+
close(): RunLogSummary;
|
|
91
|
+
}
|
|
92
|
+
/** `[code] message`, matching what the console has always printed. */
|
|
93
|
+
declare function diagnosticLine(diagnostic: AnyDiagnostic): string;
|
|
94
|
+
interface RunLogOptions {
|
|
95
|
+
/** Overridden in tests; defaults to `build/logs` under the repository root. */
|
|
96
|
+
readonly root?: string;
|
|
97
|
+
/** Overridden in tests so the directory name is deterministic. */
|
|
98
|
+
readonly now?: Date;
|
|
99
|
+
/** Set `false` to skip the `latest` symlink. */
|
|
100
|
+
readonly latest?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Opens a run directory and returns its log. The directory and its three files
|
|
104
|
+
* are created up front, so a run that crashes before {@link RunLog.close} still
|
|
105
|
+
* leaves everything recorded up to that point on disk.
|
|
106
|
+
*/
|
|
107
|
+
declare function createRunLog(options?: RunLogOptions): RunLog;
|
|
108
|
+
|
|
109
|
+
export { type AnyDiagnostic, type DiagnosticGroups, LOGS_DIR, type LogLevel, type RunLog, type RunLogOptions, type RunLogSummary, createRunLog, diagnosticLine, formatRecord, formatRunStamp, formatTimestamp, loggerName };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
symlinkSync,
|
|
7
|
+
writeFileSync
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
const REPOSITORY_ROOT = process.cwd();
|
|
11
|
+
const LOGS_DIR = join(REPOSITORY_ROOT, "build", "logs");
|
|
12
|
+
const LEVELS = ["ERROR", "WARNING", "INFO"];
|
|
13
|
+
const FILES = [
|
|
14
|
+
{ name: "error.log", level: "ERROR" },
|
|
15
|
+
{ name: "warning.log", level: "WARNING" },
|
|
16
|
+
{ name: "info.log", level: "INFO" }
|
|
17
|
+
];
|
|
18
|
+
function includes(file, record) {
|
|
19
|
+
return LEVELS.indexOf(record) <= LEVELS.indexOf(file);
|
|
20
|
+
}
|
|
21
|
+
function pad(value, width = 2) {
|
|
22
|
+
return String(value).padStart(width, "0");
|
|
23
|
+
}
|
|
24
|
+
function formatTimestamp(date) {
|
|
25
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())},${pad(date.getMilliseconds(), 3)}`;
|
|
26
|
+
}
|
|
27
|
+
function formatRunStamp(date) {
|
|
28
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}-${pad(date.getMinutes())}-${pad(date.getSeconds())}`;
|
|
29
|
+
}
|
|
30
|
+
function loggerName(source) {
|
|
31
|
+
const normalized = source.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
32
|
+
return normalized.startsWith("explico.") ? normalized : `explico.${normalized}`;
|
|
33
|
+
}
|
|
34
|
+
function formatRecord(level, source, message, date) {
|
|
35
|
+
return `${formatTimestamp(date)} ${level} ${loggerName(source)}: ${message}`;
|
|
36
|
+
}
|
|
37
|
+
function diagnosticLine(diagnostic) {
|
|
38
|
+
const code = diagnostic.code ?? diagnostic.kind;
|
|
39
|
+
const at = [diagnostic.file, diagnostic.line, diagnostic.column].filter((part) => part !== void 0).join(":");
|
|
40
|
+
const prefix = code === void 0 ? "" : `[${code}] `;
|
|
41
|
+
return at === "" ? `${prefix}${diagnostic.message}` : `${prefix}${at}: ${diagnostic.message}`;
|
|
42
|
+
}
|
|
43
|
+
function createRunLog(options = {}) {
|
|
44
|
+
const root = options.root ?? LOGS_DIR;
|
|
45
|
+
const started = options.now ?? /* @__PURE__ */ new Date();
|
|
46
|
+
const stamp = formatRunStamp(started);
|
|
47
|
+
let directory = join(root, stamp);
|
|
48
|
+
for (let suffix = 2; existsSync(directory); suffix += 1) {
|
|
49
|
+
directory = join(root, `${stamp}-${suffix}`);
|
|
50
|
+
}
|
|
51
|
+
mkdirSync(directory, { recursive: true });
|
|
52
|
+
for (const file of FILES) writeFileSync(join(directory, file.name), "");
|
|
53
|
+
const counts = { ERROR: 0, WARNING: 0, INFO: 0 };
|
|
54
|
+
const record = (level, source, message) => {
|
|
55
|
+
counts[level] += 1;
|
|
56
|
+
const line = `${formatRecord(level, source, message, /* @__PURE__ */ new Date())}
|
|
57
|
+
`;
|
|
58
|
+
for (const file of FILES) {
|
|
59
|
+
if (includes(file.level, level)) {
|
|
60
|
+
appendFileSync(join(directory, file.name), line);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return message;
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
directory,
|
|
67
|
+
error: (source, message) => record("ERROR", source, message),
|
|
68
|
+
warning: (source, message) => record("WARNING", source, message),
|
|
69
|
+
info: (source, message) => record("INFO", source, message),
|
|
70
|
+
infoDiagnostics(diagnostics) {
|
|
71
|
+
let written = 0;
|
|
72
|
+
for (const [source, group] of Object.entries(diagnostics)) {
|
|
73
|
+
if (!Array.isArray(group)) continue;
|
|
74
|
+
for (const diagnostic of group) {
|
|
75
|
+
if (diagnostic.severity !== "info") continue;
|
|
76
|
+
record("INFO", source, diagnosticLine(diagnostic));
|
|
77
|
+
written += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return written;
|
|
81
|
+
},
|
|
82
|
+
close() {
|
|
83
|
+
if (options.latest !== false) {
|
|
84
|
+
const link = join(root, "latest");
|
|
85
|
+
try {
|
|
86
|
+
rmSync(link, { force: true });
|
|
87
|
+
symlinkSync(directory, link, "dir");
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
directory,
|
|
93
|
+
relativeDirectory: relative(REPOSITORY_ROOT, directory),
|
|
94
|
+
counts: { ...counts }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
LOGS_DIR,
|
|
101
|
+
createRunLog,
|
|
102
|
+
diagnosticLine,
|
|
103
|
+
formatRecord,
|
|
104
|
+
formatRunStamp,
|
|
105
|
+
formatTimestamp,
|
|
106
|
+
loggerName
|
|
107
|
+
};
|
|
@@ -10,7 +10,7 @@ import { MathGateDiagnostic } from '../reference/gate-math.js';
|
|
|
10
10
|
import { ContentSegment } from '../reference/mixed-content.js';
|
|
11
11
|
import { GlyphScopeEntry } from '../reference/gloss.js';
|
|
12
12
|
import { NotationDefinitionRecord, NotationPageBundle, NotationBacklink, NotationDiagnostic, EditorialStatus, NotationGlossEntry } from '../reference/types.js';
|
|
13
|
-
import { NormalizedLessonExports, CourseConfig } from './course-config.js';
|
|
13
|
+
import { NormalizedLessonExports, NormalizedDisclosureDefaults, CourseConfig } from './course-config.js';
|
|
14
14
|
import { KnowledgeVersionDiagnostic } from './knowledge-version.js';
|
|
15
15
|
import { SidebarGroup } from './lesson-derivation.js';
|
|
16
16
|
import './collections.js';
|
|
@@ -90,8 +90,18 @@ import '../reference/keyed-float-validate.js';
|
|
|
90
90
|
* validation and can never get a stable number or `[[…]]` reference; this
|
|
91
91
|
* flags it (warning-only — the manifest shape a client already reads is
|
|
92
92
|
* unchanged).
|
|
93
|
+
* v14 (disclosure defaults, additive): `disclosureDefaults` — the resolved
|
|
94
|
+
* initial open/closed state for the "Page notation" panel and the generated
|
|
95
|
+
* "References" section, from `CourseConfig.disclosureDefaults`. Both are
|
|
96
|
+
* engine-authored sections, not written by a lesson, so they get their own
|
|
97
|
+
* default independent of a course's `##` heading default; closed unless the
|
|
98
|
+
* course sets one.
|
|
99
|
+
* v15 (diagnostic severity): public diagnostic severity unions add `info` for
|
|
100
|
+
* optional advice. Unused keys/definitions and restructuring candidates move
|
|
101
|
+
* from `warning` to `info`; their diagnostic codes and container fields stay
|
|
102
|
+
* unchanged.
|
|
93
103
|
*/
|
|
94
|
-
declare const MANIFEST_SCHEMA_VERSION =
|
|
104
|
+
declare const MANIFEST_SCHEMA_VERSION = 15;
|
|
95
105
|
declare const MANIFEST_PATH: string;
|
|
96
106
|
interface ManifestCitation {
|
|
97
107
|
readonly id: string;
|
|
@@ -256,6 +266,8 @@ interface Manifest {
|
|
|
256
266
|
readonly schemaVersion: number;
|
|
257
267
|
/** Normalized build/UI settings for opt-in per-lesson exports. */
|
|
258
268
|
readonly lessonExports: NormalizedLessonExports;
|
|
269
|
+
/** Initial open/closed state for "Page notation" and "References". */
|
|
270
|
+
readonly disclosureDefaults: NormalizedDisclosureDefaults;
|
|
259
271
|
/** Reader-facing identity + versions (the `[?]` About panel). */
|
|
260
272
|
readonly about: ManifestAbout;
|
|
261
273
|
readonly notation: {
|
|
@@ -316,11 +328,11 @@ interface Manifest {
|
|
|
316
328
|
readonly notation: readonly NotationDiagnostic[];
|
|
317
329
|
readonly alignment: readonly NotationDiagnostic[];
|
|
318
330
|
readonly math: readonly MathGateDiagnostic[];
|
|
319
|
-
/** Tier-3 corpus consistency checks (D6). Warnings
|
|
331
|
+
/** Tier-3 corpus consistency checks (D6). Warnings and optional info. */
|
|
320
332
|
readonly consistency: readonly ConsistencyDiagnostic[];
|
|
321
333
|
/** Knowledge-version checks (C3): `about.version` vs `content/CHANGELOG.md`. Warnings only. */
|
|
322
334
|
readonly knowledge: readonly KnowledgeVersionDiagnostic[];
|
|
323
|
-
/** Equation
|
|
335
|
+
/** Equation identity: broken identities are errors; unused keys are info. */
|
|
324
336
|
readonly equations: readonly EquationDiagnostic[];
|
|
325
337
|
/** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
|
|
326
338
|
readonly tables: readonly TableDiagnostic[];
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
gateProseNotationInAssessments
|
|
46
46
|
} from "../reference/gate-prose-notation.js";
|
|
47
47
|
import { gateAbbreviations } from "../reference/gate-abbreviations.js";
|
|
48
|
+
import { gateHeadingLength } from "../reference/gate-heading-length.js";
|
|
48
49
|
import {
|
|
49
50
|
splitMixedContent
|
|
50
51
|
} from "../reference/mixed-content.js";
|
|
@@ -54,7 +55,8 @@ import { formulaGlyphScope } from "../reference/gloss.js";
|
|
|
54
55
|
import { resolveReferenceId } from "../reference/references.js";
|
|
55
56
|
import {
|
|
56
57
|
coursePartsOf,
|
|
57
|
-
normalizeLessonExports
|
|
58
|
+
normalizeLessonExports,
|
|
59
|
+
normalizeDisclosureDefaults
|
|
58
60
|
} from "./course-config.js";
|
|
59
61
|
import {
|
|
60
62
|
resolveKnowledgeVersion
|
|
@@ -73,7 +75,7 @@ import {
|
|
|
73
75
|
isLessonSlug,
|
|
74
76
|
lessonIdFromSlug
|
|
75
77
|
} from "./lesson-derivation.js";
|
|
76
|
-
const MANIFEST_SCHEMA_VERSION =
|
|
78
|
+
const MANIFEST_SCHEMA_VERSION = 15;
|
|
77
79
|
const REPOSITORY_ROOT = process.cwd();
|
|
78
80
|
const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
|
|
79
81
|
const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
|
|
@@ -243,7 +245,8 @@ async function compileManifest(courseConfig) {
|
|
|
243
245
|
notationInput,
|
|
244
246
|
abbreviations: abbreviationsInput,
|
|
245
247
|
courseAbbreviations: courseConfig.abbreviations
|
|
246
|
-
})
|
|
248
|
+
}),
|
|
249
|
+
...gateHeadingLength(notationInput.lessons)
|
|
247
250
|
];
|
|
248
251
|
const notationLessonById = new Map(
|
|
249
252
|
notationInput.lessons.map((lesson) => [lesson.lessonId, lesson])
|
|
@@ -517,6 +520,7 @@ async function compileManifest(courseConfig) {
|
|
|
517
520
|
return {
|
|
518
521
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
519
522
|
lessonExports: normalizeLessonExports(courseConfig),
|
|
523
|
+
disclosureDefaults: normalizeDisclosureDefaults(courseConfig),
|
|
520
524
|
about,
|
|
521
525
|
notation: {
|
|
522
526
|
definitions: registry.definitions,
|
|
@@ -20,12 +20,18 @@ export const BASE_ABBREVIATIONS = Object.freeze(
|
|
|
20
20
|
'e.g.',
|
|
21
21
|
'i.e.',
|
|
22
22
|
'etc.',
|
|
23
|
+
'e.t.c.',
|
|
23
24
|
'cf.',
|
|
24
25
|
'vs.',
|
|
25
|
-
'a.m.',
|
|
26
|
-
'p.m.',
|
|
27
26
|
'a.k.a.',
|
|
28
27
|
'et al.',
|
|
28
|
+
// time of day, in each casing/punctuation style a lesson might use
|
|
29
|
+
'a.m.',
|
|
30
|
+
'p.m.',
|
|
31
|
+
'A.M.',
|
|
32
|
+
'P.M.',
|
|
33
|
+
'AM',
|
|
34
|
+
'PM',
|
|
29
35
|
// generic acronyms with no domain-specific meaning in this engine
|
|
30
36
|
'AI',
|
|
31
37
|
'ID',
|
|
@@ -8,11 +8,11 @@ import { NotationRegistry, NotationRegistryInput } from './types.js';
|
|
|
8
8
|
* Corpus consistency checks (Step D6) — the Tier 1–2 checks from the cleanup
|
|
9
9
|
* plan's "Enforced checks" section that no earlier step covers.
|
|
10
10
|
*
|
|
11
|
-
* Every check here
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Every check here is non-blocking Tier 3: repairable violations are warnings
|
|
12
|
+
* and optional restructuring advice is info. `pnpm validate:content` surfaces
|
|
13
|
+
* warning counts, `manifest.diagnostics.consistency` records every detail, and
|
|
14
|
+
* `resolution/consistency-report.json` is the committed, diff-checkable
|
|
15
|
+
* snapshot. Nothing here changes content. A finding is silenced only by a
|
|
16
16
|
* reviewed line in `lint-ignore.yml` (see `./lint-ignore`).
|
|
17
17
|
*
|
|
18
18
|
* (a) glyph-unique-in-corpus one rendered glyph → one meaning corpus-wide
|
|
@@ -20,8 +20,6 @@ import { NotationRegistry, NotationRegistryInput } from './types.js';
|
|
|
20
20
|
* unit strings drawn from a controlled vocab
|
|
21
21
|
* (c) convention-single-definition a sign / cash-flow-perspective convention a
|
|
22
22
|
* lesson uses resolves to one declaration
|
|
23
|
-
* (e) numerals-tagged numerals in `$…$` / result tables are tagged
|
|
24
|
-
* to a `src/domain/` call or a source locator
|
|
25
23
|
* (f) weak-local a `notation.local` symbol used only in its
|
|
26
24
|
* own defining equation and prose
|
|
27
25
|
* (g) notation-source-locator every notation `sources` entry has a locator
|
|
@@ -37,11 +35,11 @@ import { NotationRegistry, NotationRegistryInput } from './types.js';
|
|
|
37
35
|
* `src/content/resolution-report.ts`.
|
|
38
36
|
*/
|
|
39
37
|
|
|
40
|
-
type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | '
|
|
38
|
+
type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape' | 'operator-explained' | 'roman-label-explained';
|
|
41
39
|
interface ConsistencyDiagnostic {
|
|
42
40
|
readonly code: ConsistencyCode;
|
|
43
|
-
/** Tier 3 —
|
|
44
|
-
readonly severity: 'warning';
|
|
41
|
+
/** Tier 3 — repairable violations are warnings; optional advice is info. */
|
|
42
|
+
readonly severity: 'warning' | 'info';
|
|
45
43
|
readonly message: string;
|
|
46
44
|
readonly file?: string;
|
|
47
45
|
readonly lessonId?: string;
|