explico 0.6.0 → 0.8.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 +350 -1
- 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 +30 -4
- package/dist/compiler/cli.js +79 -10
- package/dist/compiler/collections.js +16 -3
- package/dist/compiler/exports/standalone-html.js +13 -0
- package/dist/compiler/logging.d.ts +119 -0
- package/dist/compiler/logging.js +108 -0
- package/dist/compiler/manifest.d.ts +7 -3
- package/dist/compiler/manifest.js +1 -1
- 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 +48 -0
- package/dist/reference/delimiter-size.js +153 -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/equations.d.mts +9 -3
- package/dist/reference/equations.mjs +7 -1
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +2 -2
- package/dist/reference/gate-heading-length.js +2 -2
- package/dist/reference/gate-math.d.ts +25 -1
- package/dist/reference/gate-math.js +50 -9
- package/dist/reference/gate-prose-notation.d.ts +12 -1
- package/dist/reference/gate-prose-notation.js +5 -1
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +125 -0
- package/dist/reference/katex-options.mjs +4 -0
- package/dist/reference/keyed-float-validate.d.ts +2 -2
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/math-glyphs.mjs +16 -2
- 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.mjs +0 -0
- package/dist/reference/render-lab-math.d.ts +18 -1
- package/dist/reference/render-lab-math.js +11 -4
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +8 -1
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +12 -2
- package/docs/authoring/agents.md +7 -3
- package/docs/authoring/migrating.md +514 -0
- package/docs/authoring/notation.md +157 -7
- package/docs/authoring/prose.md +64 -0
- package/docs/katex-bracket-scaling.md +191 -0
- package/package.json +2 -1
- package/src/compiler/astro.ts +89 -0
- package/src/compiler/cli.ts +167 -12
- package/src/compiler/collections.ts +33 -3
- package/src/compiler/exports/standalone-html.ts +15 -0
- package/src/compiler/logging.ts +259 -0
- package/src/compiler/manifest.ts +7 -3
- 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 +100 -58
- package/src/components/notation/NotationLayer.astro +83 -81
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/scroll-animation.ts +160 -0
- package/src/components/starlight/AboutPanel.astro +17 -2
- 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 +2 -0
- package/src/reference/base-abbreviations.mjs +8 -2
- package/src/reference/consistency.ts +10 -66
- package/src/reference/delimiter-size.ts +279 -0
- package/src/reference/diagrams-validate.ts +1 -1
- package/src/reference/equations-validate.ts +3 -3
- package/src/reference/equations.mjs +7 -1
- package/src/reference/figures-validate.ts +1 -1
- package/src/reference/gate-abbreviations.ts +2 -2
- package/src/reference/gate-heading-length.ts +2 -2
- package/src/reference/gate-math.ts +91 -8
- package/src/reference/gate-prose-notation.ts +15 -1
- package/src/reference/katex-delimiters.mjs +125 -0
- package/src/reference/katex-options.mjs +4 -0
- package/src/reference/keyed-float-validate.ts +3 -3
- package/src/reference/math-glyphs.mjs +16 -2
- 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 +25 -2
- package/src/reference/tables-validate.ts +1 -1
- package/src/reference/types.ts +8 -1
- package/src/styles/global.css +119 -14
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,13 @@ 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 },
|
|
80
|
+
...diagnostic.equation === void 0 ? {} : { equation: diagnostic.equation }
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
for (const diagnostic of manifest.diagnostics.equations) {
|
|
@@ -81,7 +86,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
81
86
|
}
|
|
82
87
|
out.push({
|
|
83
88
|
code: `equations:${diagnostic.code}`,
|
|
84
|
-
severity: diagnostic.severity
|
|
89
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
85
90
|
message: diagnostic.message,
|
|
86
91
|
...withFile(diagnostic.file)
|
|
87
92
|
});
|
|
@@ -98,7 +103,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
98
103
|
}
|
|
99
104
|
out.push({
|
|
100
105
|
code: diagnostic.code,
|
|
101
|
-
severity: diagnostic.severity
|
|
106
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
102
107
|
message: diagnostic.message,
|
|
103
108
|
...withFile(diagnostic.file)
|
|
104
109
|
});
|
|
@@ -107,7 +112,7 @@ function lessonDiagnostics(manifest, lesson) {
|
|
|
107
112
|
if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
|
|
108
113
|
out.push({
|
|
109
114
|
code: `consistency:${diagnostic.code}`,
|
|
110
|
-
severity:
|
|
115
|
+
severity: diagnostic.severity,
|
|
111
116
|
message: diagnostic.message,
|
|
112
117
|
...withFile(diagnostic.file)
|
|
113
118
|
});
|
|
@@ -301,7 +306,7 @@ function contentStatus(manifest) {
|
|
|
301
306
|
const draftNotation = manifest.notation.definitions.filter((definition) => definition.status === "draft").map((definition) => definition.id).sort();
|
|
302
307
|
const orphans = manifest.notation.diagnostics.filter((diagnostic) => diagnostic.code === "unused-definition").map((diagnostic) => ({
|
|
303
308
|
code: `notation:${diagnostic.code}`,
|
|
304
|
-
severity: diagnostic.severity
|
|
309
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
305
310
|
message: diagnostic.message,
|
|
306
311
|
...diagnostic.source?.file === void 0 ? {} : { file: diagnostic.source.file }
|
|
307
312
|
})).sort((a, b) => a.message.localeCompare(b.message));
|
|
@@ -316,7 +321,7 @@ function contentStatus(manifest) {
|
|
|
316
321
|
...manifest.diagnostics.diagrams.map((d) => d.severity),
|
|
317
322
|
...manifest.diagnostics.checks.map((d) => d.severity),
|
|
318
323
|
...manifest.diagnostics.rawFloats.map((d) => d.severity),
|
|
319
|
-
...manifest.diagnostics.consistency.map(() =>
|
|
324
|
+
...manifest.diagnostics.consistency.map((d) => d.severity),
|
|
320
325
|
...manifest.diagnostics.knowledge.map((d) => d.severity)
|
|
321
326
|
];
|
|
322
327
|
const keyedEquations = manifest.lessons.reduce(
|
|
@@ -356,10 +361,72 @@ function contentStatus(manifest) {
|
|
|
356
361
|
orphans,
|
|
357
362
|
diagnostics: {
|
|
358
363
|
errors: allDiagnostics.filter((severity) => severity === "error").length,
|
|
359
|
-
warnings: allDiagnostics.filter((severity) => severity === "warning").length
|
|
364
|
+
warnings: allDiagnostics.filter((severity) => severity === "warning").length,
|
|
365
|
+
infos: allDiagnostics.filter((severity) => severity === "info").length
|
|
360
366
|
}
|
|
361
367
|
};
|
|
362
368
|
}
|
|
369
|
+
function equationLink(value) {
|
|
370
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
371
|
+
const { key, href } = value;
|
|
372
|
+
return typeof key === "string" && typeof href === "string" ? { key, href } : void 0;
|
|
373
|
+
}
|
|
374
|
+
function numericLocation(value) {
|
|
375
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
376
|
+
}
|
|
377
|
+
const SEVERITY_RANK = {
|
|
378
|
+
info: 0,
|
|
379
|
+
warning: 1,
|
|
380
|
+
error: 2
|
|
381
|
+
};
|
|
382
|
+
function contentDiagnostics(manifest, minimumSeverity = "info") {
|
|
383
|
+
const lessonFileByHint = /* @__PURE__ */ new Map();
|
|
384
|
+
for (const lesson of manifest.lessons) {
|
|
385
|
+
lessonFileByHint.set(lesson.id, lesson.file);
|
|
386
|
+
lessonFileByHint.set(lesson.slug, lesson.file);
|
|
387
|
+
lessonFileByHint.set(lesson.file, lesson.file);
|
|
388
|
+
}
|
|
389
|
+
const groups = Object.entries(manifest.diagnostics);
|
|
390
|
+
const diagnostics = [];
|
|
391
|
+
for (const [group, values] of groups) {
|
|
392
|
+
for (const value of values) {
|
|
393
|
+
const diagnostic = value;
|
|
394
|
+
const severity = diagnosticSeverity(diagnostic.severity);
|
|
395
|
+
if (SEVERITY_RANK[severity] < SEVERITY_RANK[minimumSeverity]) continue;
|
|
396
|
+
const source = diagnostic.source !== null && typeof diagnostic.source === "object" ? diagnostic.source : void 0;
|
|
397
|
+
const rawFile = typeof diagnostic.file === "string" ? diagnostic.file : typeof source?.file === "string" ? source.file : void 0;
|
|
398
|
+
const lessonId = typeof diagnostic.lessonId === "string" ? diagnostic.lessonId : void 0;
|
|
399
|
+
const file = (rawFile === void 0 ? void 0 : lessonFileByHint.get(rawFile)) ?? (lessonId === void 0 ? void 0 : lessonFileByHint.get(lessonId)) ?? rawFile;
|
|
400
|
+
const line = numericLocation(diagnostic.line) ?? numericLocation(source?.line);
|
|
401
|
+
const column = numericLocation(diagnostic.column) ?? numericLocation(source?.column);
|
|
402
|
+
const detail = typeof diagnostic.code === "string" ? diagnostic.code : typeof diagnostic.kind === "string" ? diagnostic.kind : "diagnostic";
|
|
403
|
+
const equation = equationLink(diagnostic.equation);
|
|
404
|
+
diagnostics.push({
|
|
405
|
+
code: `${group}:${detail}`,
|
|
406
|
+
severity,
|
|
407
|
+
message: String(diagnostic.message ?? detail),
|
|
408
|
+
...file === void 0 ? {} : { file },
|
|
409
|
+
...line === void 0 ? {} : { line },
|
|
410
|
+
...column === void 0 ? {} : { column },
|
|
411
|
+
...equation === void 0 ? {} : { equation }
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
diagnostics.sort(
|
|
416
|
+
(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)
|
|
417
|
+
);
|
|
418
|
+
return {
|
|
419
|
+
minimumSeverity,
|
|
420
|
+
count: diagnostics.length,
|
|
421
|
+
diagnostics
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function contentWarnings(manifest) {
|
|
425
|
+
const warnings = contentDiagnostics(manifest, "warning").diagnostics.filter(
|
|
426
|
+
(diagnostic) => diagnostic.severity === "warning"
|
|
427
|
+
);
|
|
428
|
+
return { count: warnings.length, warnings };
|
|
429
|
+
}
|
|
363
430
|
function contentContext(manifest, rawId) {
|
|
364
431
|
const lesson = findLesson(manifest, rawId);
|
|
365
432
|
const { frontmatter, body } = lessonRaw(lesson);
|
|
@@ -599,7 +666,9 @@ export {
|
|
|
599
666
|
changedLessonIds,
|
|
600
667
|
contentCheck,
|
|
601
668
|
contentContext,
|
|
669
|
+
contentDiagnostics,
|
|
602
670
|
contentStatus,
|
|
671
|
+
contentWarnings,
|
|
603
672
|
estimateTokens,
|
|
604
673
|
findLesson,
|
|
605
674
|
lessonDiagnostics,
|
|
@@ -25,12 +25,24 @@ function filesBelow(directory, extensions) {
|
|
|
25
25
|
return entry.isFile() && extensions.some((extension) => entry.name.endsWith(extension)) ? [path] : [];
|
|
26
26
|
}).sort();
|
|
27
27
|
}
|
|
28
|
+
function bodyStartLine(raw, body) {
|
|
29
|
+
const offset = raw.endsWith(body) ? raw.length - body.length : raw.indexOf(body);
|
|
30
|
+
if (offset <= 0) return 1;
|
|
31
|
+
let line = 1;
|
|
32
|
+
for (let index = 0; index < offset; index += 1) {
|
|
33
|
+
if (raw.charCodeAt(index) === 10) line += 1;
|
|
34
|
+
}
|
|
35
|
+
return line;
|
|
36
|
+
}
|
|
28
37
|
function parseDocument(path) {
|
|
29
|
-
const
|
|
38
|
+
const raw = readFileSync(path, "utf8");
|
|
39
|
+
const parsed = parseFrontmatter(raw);
|
|
40
|
+
const body = parsed.content.replace(/^\n/, "");
|
|
30
41
|
return {
|
|
31
42
|
file: relative(REPOSITORY_ROOT, path),
|
|
32
43
|
data: parsed.frontmatter ?? {},
|
|
33
|
-
body
|
|
44
|
+
body,
|
|
45
|
+
bodyLine: bodyStartLine(raw, body)
|
|
34
46
|
};
|
|
35
47
|
}
|
|
36
48
|
function record(value, label) {
|
|
@@ -322,7 +334,7 @@ function notationLessonEntries() {
|
|
|
322
334
|
for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
|
|
323
335
|
const slug = docSlug(path);
|
|
324
336
|
if (!isLessonSlug(slug)) continue;
|
|
325
|
-
const { file, data, body } = parseDocument(path);
|
|
337
|
+
const { file, data, body, bodyLine } = parseDocument(path);
|
|
326
338
|
const notation = record(data.notation ?? {}, `${file} notation`);
|
|
327
339
|
const local = Array.isArray(notation.local) ? notation.local : [];
|
|
328
340
|
lessons.push({
|
|
@@ -333,6 +345,7 @@ function notationLessonEntries() {
|
|
|
333
345
|
),
|
|
334
346
|
references: extractNotationReferences(body, file, void 0),
|
|
335
347
|
body,
|
|
348
|
+
bodyLine,
|
|
336
349
|
source: { file }
|
|
337
350
|
});
|
|
338
351
|
}
|
|
@@ -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,119 @@
|
|
|
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
|
+
/** `{ key, href }` when the finding belongs to a `\label{eq:…}` equation. */
|
|
71
|
+
readonly equation?: {
|
|
72
|
+
readonly key: string;
|
|
73
|
+
readonly href: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** `manifest.diagnostics`, structurally — one array per subsystem. */
|
|
77
|
+
type DiagnosticGroups = Readonly<Record<string, readonly AnyDiagnostic[]>>;
|
|
78
|
+
interface RunLog {
|
|
79
|
+
/** Absolute path of this run's directory. */
|
|
80
|
+
readonly directory: string;
|
|
81
|
+
/** Records at `ERROR` and returns `message`. */
|
|
82
|
+
error(source: string, message: string): string;
|
|
83
|
+
/** Records at `WARNING` and returns `message`. */
|
|
84
|
+
warning(source: string, message: string): string;
|
|
85
|
+
/** Records at `INFO` and returns `message`. */
|
|
86
|
+
info(source: string, message: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Sweeps every severity-`info` diagnostic in a manifest's `diagnostics` into
|
|
89
|
+
* `info.log`. Errors and warnings are deliberately **not** taken from here:
|
|
90
|
+
* they are recorded at the call site that also prints the console line, so
|
|
91
|
+
* the two can never drift.
|
|
92
|
+
*/
|
|
93
|
+
infoDiagnostics(diagnostics: DiagnosticGroups): number;
|
|
94
|
+
/** Refreshes `latest` and reports what the run recorded. */
|
|
95
|
+
close(): RunLogSummary;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `[code] file:line:column: message`, matching what the console has always
|
|
99
|
+
* printed, with ` (eq:<key> -> <href>)` appended when the finding belongs to a
|
|
100
|
+
* keyed equation — so a size-only finding inside a long `aligned` block names
|
|
101
|
+
* the result it is about, not just its coordinates.
|
|
102
|
+
*/
|
|
103
|
+
declare function diagnosticLine(diagnostic: AnyDiagnostic): string;
|
|
104
|
+
interface RunLogOptions {
|
|
105
|
+
/** Overridden in tests; defaults to `build/logs` under the repository root. */
|
|
106
|
+
readonly root?: string;
|
|
107
|
+
/** Overridden in tests so the directory name is deterministic. */
|
|
108
|
+
readonly now?: Date;
|
|
109
|
+
/** Set `false` to skip the `latest` symlink. */
|
|
110
|
+
readonly latest?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Opens a run directory and returns its log. The directory and its three files
|
|
114
|
+
* are created up front, so a run that crashes before {@link RunLog.close} still
|
|
115
|
+
* leaves everything recorded up to that point on disk.
|
|
116
|
+
*/
|
|
117
|
+
declare function createRunLog(options?: RunLogOptions): RunLog;
|
|
118
|
+
|
|
119
|
+
export { type AnyDiagnostic, type DiagnosticGroups, LOGS_DIR, type LogLevel, type RunLog, type RunLogOptions, type RunLogSummary, createRunLog, diagnosticLine, formatRecord, formatRunStamp, formatTimestamp, loggerName };
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
const equation = diagnostic.equation === void 0 ? "" : ` (eq:${diagnostic.equation.key} -> ${diagnostic.equation.href})`;
|
|
42
|
+
return at === "" ? `${prefix}${diagnostic.message}${equation}` : `${prefix}${at}: ${diagnostic.message}${equation}`;
|
|
43
|
+
}
|
|
44
|
+
function createRunLog(options = {}) {
|
|
45
|
+
const root = options.root ?? LOGS_DIR;
|
|
46
|
+
const started = options.now ?? /* @__PURE__ */ new Date();
|
|
47
|
+
const stamp = formatRunStamp(started);
|
|
48
|
+
let directory = join(root, stamp);
|
|
49
|
+
for (let suffix = 2; existsSync(directory); suffix += 1) {
|
|
50
|
+
directory = join(root, `${stamp}-${suffix}`);
|
|
51
|
+
}
|
|
52
|
+
mkdirSync(directory, { recursive: true });
|
|
53
|
+
for (const file of FILES) writeFileSync(join(directory, file.name), "");
|
|
54
|
+
const counts = { ERROR: 0, WARNING: 0, INFO: 0 };
|
|
55
|
+
const record = (level, source, message) => {
|
|
56
|
+
counts[level] += 1;
|
|
57
|
+
const line = `${formatRecord(level, source, message, /* @__PURE__ */ new Date())}
|
|
58
|
+
`;
|
|
59
|
+
for (const file of FILES) {
|
|
60
|
+
if (includes(file.level, level)) {
|
|
61
|
+
appendFileSync(join(directory, file.name), line);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return message;
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
directory,
|
|
68
|
+
error: (source, message) => record("ERROR", source, message),
|
|
69
|
+
warning: (source, message) => record("WARNING", source, message),
|
|
70
|
+
info: (source, message) => record("INFO", source, message),
|
|
71
|
+
infoDiagnostics(diagnostics) {
|
|
72
|
+
let written = 0;
|
|
73
|
+
for (const [source, group] of Object.entries(diagnostics)) {
|
|
74
|
+
if (!Array.isArray(group)) continue;
|
|
75
|
+
for (const diagnostic of group) {
|
|
76
|
+
if (diagnostic.severity !== "info") continue;
|
|
77
|
+
record("INFO", source, diagnosticLine(diagnostic));
|
|
78
|
+
written += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return written;
|
|
82
|
+
},
|
|
83
|
+
close() {
|
|
84
|
+
if (options.latest !== false) {
|
|
85
|
+
const link = join(root, "latest");
|
|
86
|
+
try {
|
|
87
|
+
rmSync(link, { force: true });
|
|
88
|
+
symlinkSync(directory, link, "dir");
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
directory,
|
|
94
|
+
relativeDirectory: relative(REPOSITORY_ROOT, directory),
|
|
95
|
+
counts: { ...counts }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
LOGS_DIR,
|
|
102
|
+
createRunLog,
|
|
103
|
+
diagnosticLine,
|
|
104
|
+
formatRecord,
|
|
105
|
+
formatRunStamp,
|
|
106
|
+
formatTimestamp,
|
|
107
|
+
loggerName
|
|
108
|
+
};
|
|
@@ -96,8 +96,12 @@ import '../reference/keyed-float-validate.js';
|
|
|
96
96
|
* engine-authored sections, not written by a lesson, so they get their own
|
|
97
97
|
* default independent of a course's `##` heading default; closed unless the
|
|
98
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.
|
|
99
103
|
*/
|
|
100
|
-
declare const MANIFEST_SCHEMA_VERSION =
|
|
104
|
+
declare const MANIFEST_SCHEMA_VERSION = 16;
|
|
101
105
|
declare const MANIFEST_PATH: string;
|
|
102
106
|
interface ManifestCitation {
|
|
103
107
|
readonly id: string;
|
|
@@ -324,11 +328,11 @@ interface Manifest {
|
|
|
324
328
|
readonly notation: readonly NotationDiagnostic[];
|
|
325
329
|
readonly alignment: readonly NotationDiagnostic[];
|
|
326
330
|
readonly math: readonly MathGateDiagnostic[];
|
|
327
|
-
/** Tier-3 corpus consistency checks (D6). Warnings
|
|
331
|
+
/** Tier-3 corpus consistency checks (D6). Warnings and optional info. */
|
|
328
332
|
readonly consistency: readonly ConsistencyDiagnostic[];
|
|
329
333
|
/** Knowledge-version checks (C3): `about.version` vs `content/CHANGELOG.md`. Warnings only. */
|
|
330
334
|
readonly knowledge: readonly KnowledgeVersionDiagnostic[];
|
|
331
|
-
/** Equation
|
|
335
|
+
/** Equation identity: broken identities are errors; unused keys are info. */
|
|
332
336
|
readonly equations: readonly EquationDiagnostic[];
|
|
333
337
|
/** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
|
|
334
338
|
readonly tables: readonly TableDiagnostic[];
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
isLessonSlug,
|
|
76
76
|
lessonIdFromSlug
|
|
77
77
|
} from "./lesson-derivation.js";
|
|
78
|
-
const MANIFEST_SCHEMA_VERSION =
|
|
78
|
+
const MANIFEST_SCHEMA_VERSION = 16;
|
|
79
79
|
const REPOSITORY_ROOT = process.cwd();
|
|
80
80
|
const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
|
|
81
81
|
const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
|
|
@@ -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;
|
|
@@ -17,23 +17,6 @@ function mathSpans(body) {
|
|
|
17
17
|
}
|
|
18
18
|
return spans;
|
|
19
19
|
}
|
|
20
|
-
function tableRows(body) {
|
|
21
|
-
const rows = [];
|
|
22
|
-
for (const line of stripCode(body).split("\n")) {
|
|
23
|
-
const trimmed = line.trim();
|
|
24
|
-
if (!trimmed.startsWith("|") || !trimmed.slice(1).includes("|")) continue;
|
|
25
|
-
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) continue;
|
|
26
|
-
rows.push(line);
|
|
27
|
-
}
|
|
28
|
-
return rows;
|
|
29
|
-
}
|
|
30
|
-
const NUMERAL_RE = /(?<![\w.])-?\d+(?:\.\d+)?/g;
|
|
31
|
-
function numeralCount(text) {
|
|
32
|
-
return (text.replace(/\\[a-zA-Z]+/g, " ").match(NUMERAL_RE) ?? []).length;
|
|
33
|
-
}
|
|
34
|
-
function isTagged(text) {
|
|
35
|
-
return /\[@[a-z0-9]/i.test(text) || /<!--\s*domain:/i.test(text);
|
|
36
|
-
}
|
|
37
20
|
function glyphMatcher(latex) {
|
|
38
21
|
const trimmed = latex.trim();
|
|
39
22
|
if (trimmed === "") return void 0;
|
|
@@ -141,31 +124,6 @@ function checkConventionSingleDefinition(input, out) {
|
|
|
141
124
|
});
|
|
142
125
|
}
|
|
143
126
|
}
|
|
144
|
-
function checkNumeralsTagged(input, out) {
|
|
145
|
-
for (const lesson of input.notationInput.lessons) {
|
|
146
|
-
if (input.lintIgnore.matches("numerals-tagged", lesson.lessonId)) continue;
|
|
147
|
-
let math = 0;
|
|
148
|
-
for (const span of mathSpans(lesson.body)) {
|
|
149
|
-
if (isTagged(span.latex)) continue;
|
|
150
|
-
math += numeralCount(span.latex);
|
|
151
|
-
}
|
|
152
|
-
let table = 0;
|
|
153
|
-
for (const row of tableRows(lesson.body)) {
|
|
154
|
-
if (isTagged(row)) continue;
|
|
155
|
-
table += numeralCount(row.replace(/\$[^$\n]*\$/g, " "));
|
|
156
|
-
}
|
|
157
|
-
const count = math + table;
|
|
158
|
-
if (count === 0) continue;
|
|
159
|
-
out.push({
|
|
160
|
-
code: "numerals-tagged",
|
|
161
|
-
severity: "warning",
|
|
162
|
-
lessonId: lesson.lessonId,
|
|
163
|
-
file: lesson.source.file,
|
|
164
|
-
count,
|
|
165
|
-
message: `${lesson.lessonId}: ${count} numeral(s) in $\u2026$ / result tables (${math} in math, ${table} in tables) are not tagged to a src/domain/ call or a source locator`
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
127
|
function checkWeakLocal(input, out) {
|
|
170
128
|
for (const lesson of input.notationInput.lessons) {
|
|
171
129
|
const spans = mathSpans(lesson.body).map((span) => span.latex);
|
|
@@ -177,7 +135,7 @@ function checkWeakLocal(input, out) {
|
|
|
177
135
|
if (uses > 1) continue;
|
|
178
136
|
out.push({
|
|
179
137
|
code: "weak-local",
|
|
180
|
-
severity: "
|
|
138
|
+
severity: "info",
|
|
181
139
|
lessonId: lesson.lessonId,
|
|
182
140
|
file: lesson.source.file,
|
|
183
141
|
key: definition.key,
|
|
@@ -228,7 +186,7 @@ function checkGlossWantsPromoting(input, out) {
|
|
|
228
186
|
if (input.lintIgnore.matches("gloss-wants-promoting", glyph)) continue;
|
|
229
187
|
out.push({
|
|
230
188
|
code: "gloss-wants-promoting",
|
|
231
|
-
severity: "
|
|
189
|
+
severity: "info",
|
|
232
190
|
glyph,
|
|
233
191
|
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`
|
|
234
192
|
});
|
|
@@ -250,7 +208,7 @@ function checkCardWantsDemoting(input, out) {
|
|
|
250
208
|
}
|
|
251
209
|
out.push({
|
|
252
210
|
code: "card-wants-demoting",
|
|
253
|
-
severity: "
|
|
211
|
+
severity: "info",
|
|
254
212
|
key: definition.key,
|
|
255
213
|
file: definition.source.file,
|
|
256
214
|
count: words,
|
|
@@ -381,7 +339,6 @@ const CHECKS = [
|
|
|
381
339
|
checkGlyphUniqueness,
|
|
382
340
|
checkNotationUnits,
|
|
383
341
|
checkConventionSingleDefinition,
|
|
384
|
-
checkNumeralsTagged,
|
|
385
342
|
checkWeakLocal,
|
|
386
343
|
checkNotationSourceLocator,
|
|
387
344
|
checkGlossWantsPromoting,
|
|
@@ -403,7 +360,6 @@ function consistencyCounts(diagnostics) {
|
|
|
403
360
|
"notation-units": 0,
|
|
404
361
|
"units-vocab": 0,
|
|
405
362
|
"convention-single-definition": 0,
|
|
406
|
-
"numerals-tagged": 0,
|
|
407
363
|
"weak-local": 0,
|
|
408
364
|
"notation-source-locator": 0,
|
|
409
365
|
"gloss-wants-promoting": 0,
|