explico 0.13.1 → 0.14.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 +879 -1
- package/README.md +200 -30
- package/dist/compiler/astro.d.ts +15 -2
- package/dist/compiler/astro.js +40 -11
- package/dist/compiler/cli-main.d.ts +43 -0
- package/dist/compiler/cli-main.js +392 -0
- package/dist/compiler/cli.d.ts +424 -9
- package/dist/compiler/cli.js +1289 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +105 -1
- package/dist/compiler/decks.d.ts +161 -0
- package/dist/compiler/decks.js +235 -0
- package/dist/compiler/exports/combined-html.d.ts +5 -1
- package/dist/compiler/exports/deck-fit.d.ts +55 -0
- package/dist/compiler/exports/deck-fit.js +90 -0
- package/dist/compiler/exports/export-chapters.d.ts +37 -6
- package/dist/compiler/exports/export-chapters.js +64 -5
- package/dist/compiler/exports/export-pages.d.ts +30 -3
- package/dist/compiler/exports/export-pages.js +24 -1
- package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/dist/compiler/exports/latex/cache.d.ts +78 -0
- package/dist/compiler/exports/latex/cache.js +129 -0
- package/dist/compiler/exports/latex/capture.d.ts +10 -1
- package/dist/compiler/exports/latex/capture.js +3 -1
- package/dist/compiler/exports/latex/compile.d.ts +13 -2
- package/dist/compiler/exports/latex/compile.js +12 -1
- package/dist/compiler/exports/latex/deck.d.ts +92 -0
- package/dist/compiler/exports/latex/deck.js +349 -0
- package/dist/compiler/exports/latex/document.d.ts +51 -6
- package/dist/compiler/exports/latex/document.js +71 -12
- package/dist/compiler/exports/latex/explico-common.sty +198 -0
- package/dist/compiler/exports/latex/explico.cls +13 -183
- package/dist/compiler/exports/latex/serialize.d.ts +60 -4
- package/dist/compiler/exports/latex/serialize.js +4 -2
- package/dist/compiler/exports/latex-export.d.ts +24 -2
- package/dist/compiler/exports/latex-export.js +107 -26
- package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
- package/dist/compiler/exports/markdown/serialize.js +85 -8
- package/dist/compiler/exports/markdown-export.d.ts +6 -1
- package/dist/compiler/exports/markdown-export.js +50 -11
- package/dist/compiler/exports/standalone-html.d.ts +10 -5
- package/dist/compiler/exports/standalone-html.js +3 -3
- package/dist/compiler/exports/static-tree.d.ts +38 -1
- package/dist/compiler/exports/static-tree.js +120 -4
- package/dist/compiler/knowledge-version.d.ts +26 -2
- package/dist/compiler/knowledge-version.js +27 -17
- package/dist/compiler/lesson-derivation.d.ts +143 -6
- package/dist/compiler/lesson-derivation.js +145 -13
- package/dist/compiler/manifest.d.ts +133 -4
- package/dist/compiler/manifest.js +359 -58
- package/dist/compiler/page-versions.d.ts +160 -0
- package/dist/compiler/page-versions.js +198 -0
- package/dist/compiler/render-diagnostics.d.ts +5 -1
- package/dist/compiler/resolution-report.d.ts +24 -6
- package/dist/compiler/resolution-report.js +22 -4
- package/dist/compiler/validate-content.d.ts +7 -0
- package/dist/compiler/validate-content.js +160 -0
- package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
- package/dist/curriculum/competency-graph.d.ts +54 -0
- package/dist/curriculum/competency-graph.js +41 -0
- package/dist/reference/base-library.d.mts +8 -0
- package/dist/reference/base-library.mjs +19 -0
- package/dist/reference/consistency.d.ts +8 -4
- package/dist/reference/consistency.js +76 -26
- package/dist/reference/deck-remark.d.mts +123 -0
- package/dist/reference/deck-remark.mjs +600 -0
- package/dist/reference/deck-source.d.ts +107 -0
- package/dist/reference/deck-source.js +140 -0
- package/dist/reference/decks-validate.d.ts +133 -0
- package/dist/reference/decks-validate.js +359 -0
- package/dist/reference/derived-entries.d.ts +5 -1
- package/dist/reference/diagram-fit.d.ts +1 -1
- package/dist/reference/figure-symbols.d.ts +23 -6
- package/dist/reference/figure-symbols.js +51 -15
- package/dist/reference/float-name.d.ts +43 -0
- package/dist/reference/float-name.js +58 -0
- package/dist/reference/folds-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-indices.js +5 -4
- package/dist/reference/gate-math.d.ts +29 -2
- package/dist/reference/gate-math.js +99 -18
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/lab-katex-validate.d.ts +10 -6
- package/dist/reference/lab-katex-validate.js +17 -9
- package/dist/reference/lab-math-scope.js +29 -4
- package/dist/reference/math-glyphs.d.mts +25 -0
- package/dist/reference/math-glyphs.mjs +189 -25
- package/dist/reference/page-notation.d.ts +22 -3
- package/dist/reference/page-notation.js +32 -10
- package/dist/reference/references.d.ts +14 -1
- package/dist/reference/references.js +13 -0
- package/dist/reference/registry-scope.d.mts +33 -0
- package/dist/reference/registry-scope.mjs +34 -0
- package/dist/reference/registry.js +29 -10
- package/dist/reference/rehype-deck.d.mts +6 -0
- package/dist/reference/rehype-deck.mjs +286 -0
- package/dist/reference/rehype-heading-anchors.d.mts +20 -0
- package/dist/reference/rehype-heading-anchors.mjs +48 -6
- package/dist/reference/remark-notation.mjs +249 -40
- package/dist/reference/source-lines.d.mts +12 -0
- package/dist/reference/source-lines.mjs +65 -0
- package/dist/reference/types.d.ts +28 -0
- package/dist/rules/catalog.d.ts +21 -4
- package/dist/rules/catalog.js +221 -2
- package/dist/rules/course-rules.d.ts +5 -1
- package/dist/rules/course-rules.js +17 -3
- package/dist/rules/course-rules.schema.json +12 -0
- package/dist/rules/in-effect.d.ts +13 -3
- package/dist/rules/in-effect.js +30 -7
- package/dist/rules/rules.schema.json +259 -1
- package/dist/rules/settings.d.ts +1 -1
- package/dist/session/PreferenceStore.d.ts +6 -0
- package/dist/session/PreferenceStore.js +18 -1
- package/dist/session/deck-memory.d.ts +116 -0
- package/dist/session/deck-memory.js +143 -0
- package/dist/session/reading-memory.d.ts +96 -0
- package/dist/session/reading-memory.js +133 -0
- package/docs/VERSIONING.md +21 -18
- package/docs/authoring/README.md +29 -9
- package/docs/authoring/agents.md +63 -16
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +588 -0
- package/docs/authoring/migrating.md +1045 -16
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +54 -2
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +37 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
- package/docs/course-agent/skills/release-content/SKILL.md +24 -0
- package/docs/diagram-scaling.md +148 -0
- package/docs/privacy.md +59 -0
- package/package.json +8 -2
- package/src/compiler/astro.ts +60 -21
- package/src/compiler/cli-main.ts +469 -0
- package/src/compiler/cli.ts +1905 -16
- package/src/compiler/collections.ts +284 -25
- package/src/compiler/course-config.ts +267 -0
- package/src/compiler/decks.ts +455 -0
- package/src/compiler/exports/deck-fit.ts +127 -0
- package/src/compiler/exports/export-chapters.ts +105 -6
- package/src/compiler/exports/export-pages.ts +46 -2
- package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/src/compiler/exports/latex/cache.ts +183 -0
- package/src/compiler/exports/latex/capture.ts +5 -2
- package/src/compiler/exports/latex/compile.ts +18 -0
- package/src/compiler/exports/latex/deck.ts +419 -0
- package/src/compiler/exports/latex/document.ts +117 -21
- package/src/compiler/exports/latex/explico-common.sty +198 -0
- package/src/compiler/exports/latex/explico.cls +13 -183
- package/src/compiler/exports/latex/serialize.ts +25 -21
- package/src/compiler/exports/latex/texlive-packages.txt +6 -2
- package/src/compiler/exports/latex-export.ts +187 -45
- package/src/compiler/exports/markdown/serialize.ts +118 -9
- package/src/compiler/exports/markdown-export.ts +56 -11
- package/src/compiler/exports/standalone-html.ts +8 -7
- package/src/compiler/exports/static-tree.ts +175 -3
- package/src/compiler/knowledge-version.ts +56 -21
- package/src/compiler/lesson-derivation.ts +282 -16
- package/src/compiler/manifest.ts +558 -63
- package/src/compiler/page-versions.ts +359 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +212 -0
- package/src/components/CourseContents.astro +124 -6
- package/src/components/CurriculumMap.astro +49 -1
- package/src/components/FloatIndex.astro +1 -1
- package/src/components/PartOverview.astro +51 -5
- package/src/components/assessments/AssessmentSet.astro +41 -15
- package/src/components/block-heading.ts +15 -6
- package/src/components/card-carry.ts +7 -4
- package/src/components/card-manager.ts +14 -2
- package/src/components/card-memory.ts +13 -9
- package/src/components/deck/DeckEnhancer.astro +13 -0
- package/src/components/deck/deck-context.ts +104 -0
- package/src/components/deck/deck-controller.ts +1287 -0
- package/src/components/deck/deck-map.ts +91 -0
- package/src/components/deck-lane.ts +163 -0
- package/src/components/diagram/Diagram.astro +131 -86
- package/src/components/disclosure/persisted-disclosure.ts +9 -1
- package/src/components/figure/Figure.astro +7 -2
- package/src/components/figure/FloatCaption.astro +5 -3
- package/src/components/fold/Fold.astro +2 -2
- package/src/components/notation/NotationFormula.astro +81 -11
- package/src/components/notation/NotationGlossary.astro +132 -1
- package/src/components/notation/NotationLayer.astro +15 -7
- package/src/components/starlight/AboutPanel.astro +186 -3
- package/src/components/starlight/CollapsibleSections.astro +24 -3
- package/src/components/starlight/DiagramEnhancer.astro +5 -1
- package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
- package/src/components/starlight/LayoutHeader.astro +17 -0
- package/src/components/starlight/LayoutPageTitle.astro +110 -0
- package/src/components/starlight/LayoutSidebar.astro +807 -21
- package/src/components/starlight/LayoutSocialIcons.astro +62 -6
- package/src/components/starlight/LessonExportActions.astro +27 -9
- package/src/components/starlight/LessonFooter.astro +59 -0
- package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
- package/src/components/starlight/TextSizeControl.astro +24 -13
- package/src/components/starlight/page-marks.ts +139 -0
- package/src/components/starlight/section-summary.ts +1 -1
- package/src/components/starlight/sidebar-editions.ts +245 -0
- package/src/components/table/Table.astro +15 -7
- package/src/content-config.ts +87 -1
- package/src/curriculum/competency-graph.ts +95 -0
- package/src/reference/base-library.mjs +19 -0
- package/src/reference/consistency.ts +126 -39
- package/src/reference/deck-remark.mjs +600 -0
- package/src/reference/deck-source.ts +273 -0
- package/src/reference/decks-validate.ts +604 -0
- package/src/reference/diagram-layout.ts +3 -1
- package/src/reference/figure-symbols.ts +108 -31
- package/src/reference/folds-validate.ts +1 -1
- package/src/reference/gate-indices.ts +5 -3
- package/src/reference/gate-math.ts +169 -25
- package/src/reference/lab-katex-validate.ts +30 -15
- package/src/reference/lab-math-scope.ts +34 -3
- package/src/reference/math-glyphs.mjs +189 -25
- package/src/reference/page-notation.ts +69 -15
- package/src/reference/references.ts +28 -0
- package/src/reference/registry-scope.mjs +34 -0
- package/src/reference/registry.ts +41 -11
- package/src/reference/rehype-deck.mjs +286 -0
- package/src/reference/rehype-heading-anchors.mjs +48 -6
- package/src/reference/remark-notation.mjs +249 -40
- package/src/reference/source-lines.mjs +65 -0
- package/src/reference/types.ts +28 -0
- package/src/rules/catalog.ts +257 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +54 -8
- package/src/session/PreferenceStore.ts +23 -0
- package/src/session/deck-memory.ts +263 -0
- package/src/session/reading-memory.ts +243 -0
- package/src/styles/deck.css +1150 -0
- package/src/styles/global.css +958 -27
- package/src/styles/zones.css +277 -0
- /package/src/{components → reference}/float-name.ts +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { KnowledgeVersionDiagnostic } from './knowledge-version.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A page's own content version (rule C5): the knowledge version its content
|
|
5
|
+
* last changed in, as against the course's, which moves at every release.
|
|
6
|
+
*
|
|
7
|
+
* A build sees one commit, and "unchanged since 0.2.0" is history, so the
|
|
8
|
+
* record is a committed file, `content/versions.json`, kept like a lockfile:
|
|
9
|
+
* `pnpm validate:content` works the next record out from the committed one
|
|
10
|
+
* and the content tree, writes it back on a developer's machine, and in CI
|
|
11
|
+
* (`CI` set) fails when the committed file is not the one it would write.
|
|
12
|
+
* The manifest carries each chapter's and part page's `since` either way,
|
|
13
|
+
* computed from the committed record by the same rules, so a build never
|
|
14
|
+
* depends on the file having been rewritten.
|
|
15
|
+
*
|
|
16
|
+
* What counts as the page: its own file, its `.checks.yml` and the
|
|
17
|
+
* assessments it lists, the shared notation entries its text uses, the
|
|
18
|
+
* sources it cites, and its numbers (`part.chapter` and the float offsets its
|
|
19
|
+
* earlier chapters leave it). A lab, `content/domain/`, a part's label or the
|
|
20
|
+
* engine change a page's PDF without changing its stamp.
|
|
21
|
+
*
|
|
22
|
+
* The three rules, applied to the committed entry of each page:
|
|
23
|
+
*
|
|
24
|
+
* 1. The record's `version` is not `about.version`: a release came after the
|
|
25
|
+
* file was written, and a release takes every change `## [Unreleased]`
|
|
26
|
+
* held. Every page with a `pending` hash, or never released, is now
|
|
27
|
+
* `since` that version.
|
|
28
|
+
* 2. The page hashes as recorded: `since` stays. A pending edit undone
|
|
29
|
+
* (the page hashes as it did at `since`) is unchanged again.
|
|
30
|
+
* 3. The page changed: with bullets under `## [Unreleased]` the change is
|
|
31
|
+
* not yet released (`pending`); with none it is the release's own, or a
|
|
32
|
+
* fix rule C1 lets go unlisted, and `since` is `about.version`.
|
|
33
|
+
*
|
|
34
|
+
* Pure except `readPageVersions` and `syncPageVersions`, which do the file IO.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Repo-relative path of the record. */
|
|
38
|
+
declare const PAGE_VERSIONS_FILE = "content/versions.json";
|
|
39
|
+
declare const PAGE_VERSIONS_GENERATED_BY = "pnpm validate:content (rule C5); do not edit";
|
|
40
|
+
/**
|
|
41
|
+
* The format of the record and of the hashes in it. A record of another
|
|
42
|
+
* format keeps every page's `since` and takes the hashes as they are now, so
|
|
43
|
+
* a change to what is hashed never marks every page changed.
|
|
44
|
+
*/
|
|
45
|
+
declare const PAGE_VERSIONS_FORMAT = 1;
|
|
46
|
+
interface PageVersionEntry {
|
|
47
|
+
/** The release the page's content last changed in; `null` before its first. */
|
|
48
|
+
readonly since: string | null;
|
|
49
|
+
/** `sha256:…` of the page's inputs as of `since`. */
|
|
50
|
+
readonly source: string;
|
|
51
|
+
/** The inputs' hash now, while they differ from `source` and `## [Unreleased]` has bullets. */
|
|
52
|
+
readonly pending?: string;
|
|
53
|
+
}
|
|
54
|
+
interface PageVersionsRecord {
|
|
55
|
+
readonly generatedBy: string;
|
|
56
|
+
readonly format: number;
|
|
57
|
+
/** `about.version` when the record was written. */
|
|
58
|
+
readonly version: string;
|
|
59
|
+
/** By docs slug (`motion/constant-acceleration`, `motion/index`), sorted. */
|
|
60
|
+
readonly pages: Readonly<Record<string, PageVersionEntry>>;
|
|
61
|
+
}
|
|
62
|
+
/** What the manifest carries for the record (`manifest.pageVersions`). */
|
|
63
|
+
interface ManifestPageVersions {
|
|
64
|
+
/** False when rule C5 is off: every page then carries the course's version. */
|
|
65
|
+
readonly enabled: boolean;
|
|
66
|
+
/** Repo-relative path of the record, `content/versions.json`. */
|
|
67
|
+
readonly file: string;
|
|
68
|
+
/** The record as `validate:content` should write it. */
|
|
69
|
+
readonly record: PageVersionsRecord;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The hash of one page's inputs: each labelled file digest, in label order,
|
|
73
|
+
* then the page's facts (its numbers) as sorted JSON.
|
|
74
|
+
*/
|
|
75
|
+
declare function pageSourceDigest(inputs: readonly {
|
|
76
|
+
readonly label: string;
|
|
77
|
+
readonly digest: string;
|
|
78
|
+
}[], facts?: Record<string, unknown>): string;
|
|
79
|
+
/**
|
|
80
|
+
* Semver order over `x.y.z`, a prerelease below its release; `null` (not
|
|
81
|
+
* yet released) above everything, since it is the newest state a page can
|
|
82
|
+
* be in. A string that is not semver sorts by text, after every semver.
|
|
83
|
+
*/
|
|
84
|
+
declare function compareKnowledgeVersions(a: string | null, b: string | null): number;
|
|
85
|
+
/**
|
|
86
|
+
* The version of a document made of several pages: the newest among them,
|
|
87
|
+
* `null` when any page has a change not yet released, `null` for no pages.
|
|
88
|
+
*/
|
|
89
|
+
declare function newestSince(values: readonly (string | null)[]): string | null;
|
|
90
|
+
/** The version a page's entry gives it: `null` while it has a change not yet released. */
|
|
91
|
+
declare function sinceOf(entry: PageVersionEntry): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* The phrase every export prints for a document's version (rule C5):
|
|
94
|
+
* "unchanged since version 0.2.0", or, for a change not yet released, "with
|
|
95
|
+
* changes after version 0.4.0, not yet released", 0.4.0 being the newest
|
|
96
|
+
* release, the last one the document agrees with.
|
|
97
|
+
*/
|
|
98
|
+
declare function versionPhrase(since: string | null, current: string): string;
|
|
99
|
+
interface NextPageVersionsInput {
|
|
100
|
+
/** The committed record, or `undefined` for a course without one. */
|
|
101
|
+
readonly previous: PageVersionsRecord | undefined;
|
|
102
|
+
/** `about.version`. */
|
|
103
|
+
readonly version: string;
|
|
104
|
+
/** Whether `## [Unreleased]` holds bullets. */
|
|
105
|
+
readonly unreleased: boolean;
|
|
106
|
+
/** Every tracked page, with its inputs' hash now. */
|
|
107
|
+
readonly pages: readonly {
|
|
108
|
+
readonly slug: string;
|
|
109
|
+
readonly source: string;
|
|
110
|
+
}[];
|
|
111
|
+
}
|
|
112
|
+
/** The record `validate:content` writes: the committed one, brought to the content as it is now. */
|
|
113
|
+
declare function nextPageVersions(input: NextPageVersionsInput): PageVersionsRecord;
|
|
114
|
+
/** The record's text as `syncPageVersions` writes it. */
|
|
115
|
+
declare function serializePageVersions(record: PageVersionsRecord): string;
|
|
116
|
+
/**
|
|
117
|
+
* The committed record, or `undefined` when there is none. A file that is
|
|
118
|
+
* not a record (hand-edited, truncated) is reported as `page-versions-invalid`
|
|
119
|
+
* and treated as none: every page then counts as new.
|
|
120
|
+
*/
|
|
121
|
+
declare function readPageVersions(path: string, file?: string): {
|
|
122
|
+
readonly record: PageVersionsRecord | undefined;
|
|
123
|
+
readonly diagnostics: readonly KnowledgeVersionDiagnostic[];
|
|
124
|
+
};
|
|
125
|
+
/** The one error rule C5 raises: frozen, and the committed record is not the content's. */
|
|
126
|
+
interface PageVersionsStaleDiagnostic {
|
|
127
|
+
readonly code: 'page-versions-stale';
|
|
128
|
+
readonly severity: 'error';
|
|
129
|
+
readonly message: string;
|
|
130
|
+
readonly file: string;
|
|
131
|
+
}
|
|
132
|
+
interface PageVersionsSync {
|
|
133
|
+
/** Absolute path of the record. */
|
|
134
|
+
readonly path: string;
|
|
135
|
+
/** The record on disk was not the one the content gives. */
|
|
136
|
+
readonly stale: boolean;
|
|
137
|
+
/** The record was written (never when frozen). */
|
|
138
|
+
readonly written: boolean;
|
|
139
|
+
/** Pages whose entry changed, or `['*']` for a record written first. */
|
|
140
|
+
readonly changed: readonly string[];
|
|
141
|
+
/** Set when frozen and stale: the error that fails the build. */
|
|
142
|
+
readonly diagnostic?: PageVersionsStaleDiagnostic;
|
|
143
|
+
}
|
|
144
|
+
/** `CI` set (and not `false` or `0`): the record is frozen, like a lockfile. */
|
|
145
|
+
declare function pageVersionsFrozen(): boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Bring `content/versions.json` to what the manifest computed. On a
|
|
148
|
+
* developer's machine a stale record is rewritten and the caller logs the
|
|
149
|
+
* pages that moved; frozen (CI by default) a stale record is left as it is
|
|
150
|
+
* and the result carries a `page-versions-stale` error, so a content change
|
|
151
|
+
* cannot land without its record.
|
|
152
|
+
*/
|
|
153
|
+
declare function syncPageVersions(manifest: {
|
|
154
|
+
readonly pageVersions: ManifestPageVersions;
|
|
155
|
+
}, options?: {
|
|
156
|
+
readonly frozen?: boolean;
|
|
157
|
+
readonly root?: string;
|
|
158
|
+
}): PageVersionsSync;
|
|
159
|
+
|
|
160
|
+
export { type ManifestPageVersions, type NextPageVersionsInput, PAGE_VERSIONS_FILE, PAGE_VERSIONS_FORMAT, PAGE_VERSIONS_GENERATED_BY, type PageVersionEntry, type PageVersionsRecord, type PageVersionsStaleDiagnostic, type PageVersionsSync, compareKnowledgeVersions, newestSince, nextPageVersions, pageSourceDigest, pageVersionsFrozen, readPageVersions, serializePageVersions, sinceOf, syncPageVersions, versionPhrase };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const PAGE_VERSIONS_FILE = "content/versions.json";
|
|
5
|
+
const PAGE_VERSIONS_GENERATED_BY = "pnpm validate:content (rule C5); do not edit";
|
|
6
|
+
const PAGE_VERSIONS_FORMAT = 1;
|
|
7
|
+
const ENTRY_HASH = /^sha256:[0-9a-f]{64}$/u;
|
|
8
|
+
function compareSlugs(a, b) {
|
|
9
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
10
|
+
}
|
|
11
|
+
function stableJson(value) {
|
|
12
|
+
const sortKeys = (node) => {
|
|
13
|
+
if (Array.isArray(node)) return node.map(sortKeys);
|
|
14
|
+
if (node && typeof node === "object") {
|
|
15
|
+
return Object.fromEntries(
|
|
16
|
+
Object.keys(node).sort().map((key) => [key, sortKeys(node[key])])
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return node;
|
|
20
|
+
};
|
|
21
|
+
return `${JSON.stringify(sortKeys(value), null, 2)}
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
function pageSourceDigest(inputs, facts = {}) {
|
|
25
|
+
const hash = createHash("sha256");
|
|
26
|
+
for (const { label, digest } of [...inputs].sort((a, b) => compareSlugs(a.label, b.label))) {
|
|
27
|
+
hash.update(`${label} ${digest}
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
hash.update(stableJson(facts));
|
|
31
|
+
return `sha256:${hash.digest("hex")}`;
|
|
32
|
+
}
|
|
33
|
+
function compareKnowledgeVersions(a, b) {
|
|
34
|
+
if (a === b) return 0;
|
|
35
|
+
if (a === null) return 1;
|
|
36
|
+
if (b === null) return -1;
|
|
37
|
+
const parse = (value) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/u.exec(value.trim());
|
|
38
|
+
const left = parse(a);
|
|
39
|
+
const right = parse(b);
|
|
40
|
+
if (!left || !right) {
|
|
41
|
+
if (left) return -1;
|
|
42
|
+
if (right) return 1;
|
|
43
|
+
return compareSlugs(a, b);
|
|
44
|
+
}
|
|
45
|
+
for (const index of [1, 2, 3]) {
|
|
46
|
+
const difference = Number(left[index]) - Number(right[index]);
|
|
47
|
+
if (difference !== 0) return Math.sign(difference);
|
|
48
|
+
}
|
|
49
|
+
const leftPre = left[4];
|
|
50
|
+
const rightPre = right[4];
|
|
51
|
+
if (leftPre === rightPre) return 0;
|
|
52
|
+
if (leftPre === void 0) return 1;
|
|
53
|
+
if (rightPre === void 0) return -1;
|
|
54
|
+
const leftParts = leftPre.split(".");
|
|
55
|
+
const rightParts = rightPre.split(".");
|
|
56
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
57
|
+
const l = leftParts[index];
|
|
58
|
+
const r = rightParts[index];
|
|
59
|
+
if (l === void 0) return -1;
|
|
60
|
+
if (r === void 0) return 1;
|
|
61
|
+
const numeric = /^\d+$/u.test(l) && /^\d+$/u.test(r);
|
|
62
|
+
const difference = numeric ? Number(l) - Number(r) : compareSlugs(l, r);
|
|
63
|
+
if (difference !== 0) return Math.sign(difference);
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
function newestSince(values) {
|
|
68
|
+
if (values.length === 0) return null;
|
|
69
|
+
return values.reduce(
|
|
70
|
+
(newest, value) => compareKnowledgeVersions(value, newest) > 0 ? value : newest,
|
|
71
|
+
values[0]
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
function sinceOf(entry) {
|
|
75
|
+
return entry.pending !== void 0 || entry.since === null ? null : entry.since;
|
|
76
|
+
}
|
|
77
|
+
function versionPhrase(since, current) {
|
|
78
|
+
return since === null ? `with changes after version ${current}, not yet released` : `unchanged since version ${since}`;
|
|
79
|
+
}
|
|
80
|
+
function nextPageVersions(input) {
|
|
81
|
+
const { previous, version, unreleased } = input;
|
|
82
|
+
const released = previous !== void 0 && previous.version !== version;
|
|
83
|
+
const sameFormat = previous !== void 0 && previous.format === PAGE_VERSIONS_FORMAT;
|
|
84
|
+
const pages = {};
|
|
85
|
+
for (const page of [...input.pages].sort((a, b) => compareSlugs(a.slug, b.slug))) {
|
|
86
|
+
const current = page.source;
|
|
87
|
+
let entry = previous?.pages[page.slug];
|
|
88
|
+
if (entry && !sameFormat) {
|
|
89
|
+
entry = { since: entry.since, source: current, ...entry.pending === void 0 ? {} : { pending: current } };
|
|
90
|
+
}
|
|
91
|
+
if (entry && released && (entry.pending !== void 0 || entry.since === null)) {
|
|
92
|
+
entry = { since: version, source: entry.pending ?? entry.source };
|
|
93
|
+
}
|
|
94
|
+
if (entry === void 0) {
|
|
95
|
+
pages[page.slug] = unreleased ? { since: null, source: current } : { since: version, source: current };
|
|
96
|
+
} else if ((entry.pending ?? entry.source) === current) {
|
|
97
|
+
pages[page.slug] = entry;
|
|
98
|
+
} else if (entry.pending !== void 0 && entry.source === current) {
|
|
99
|
+
pages[page.slug] = { since: entry.since, source: entry.source };
|
|
100
|
+
} else if (unreleased) {
|
|
101
|
+
pages[page.slug] = entry.since === null ? { since: null, source: current } : { since: entry.since, source: entry.source, pending: current };
|
|
102
|
+
} else {
|
|
103
|
+
pages[page.slug] = { since: version, source: current };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { generatedBy: PAGE_VERSIONS_GENERATED_BY, format: PAGE_VERSIONS_FORMAT, version, pages };
|
|
107
|
+
}
|
|
108
|
+
function serializePageVersions(record) {
|
|
109
|
+
return stableJson(record);
|
|
110
|
+
}
|
|
111
|
+
function readPageVersions(path, file = PAGE_VERSIONS_FILE) {
|
|
112
|
+
if (!existsSync(path)) return { record: void 0, diagnostics: [] };
|
|
113
|
+
const invalid = (reason) => ({
|
|
114
|
+
record: void 0,
|
|
115
|
+
diagnostics: [
|
|
116
|
+
{
|
|
117
|
+
code: "page-versions-invalid",
|
|
118
|
+
severity: "warning",
|
|
119
|
+
message: `${file} is not a record validate:content wrote (${reason}); it is rebuilt, and every page counts as new. Restore the committed file to keep the pages' versions (rule C5).`,
|
|
120
|
+
file
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return invalid(error instanceof Error ? error.message : String(error));
|
|
129
|
+
}
|
|
130
|
+
if (!parsed || typeof parsed !== "object") return invalid("not an object");
|
|
131
|
+
const { version, format, pages, generatedBy } = parsed;
|
|
132
|
+
if (typeof version !== "string" || typeof format !== "number" || !pages || typeof pages !== "object") {
|
|
133
|
+
return invalid("missing version, format or pages");
|
|
134
|
+
}
|
|
135
|
+
const entries = {};
|
|
136
|
+
for (const [slug, value] of Object.entries(pages)) {
|
|
137
|
+
if (!value || typeof value !== "object") return invalid(`page ${slug} is not an object`);
|
|
138
|
+
const { since, source, pending } = value;
|
|
139
|
+
if (since !== null && typeof since !== "string") return invalid(`page ${slug}: since must be a version or null`);
|
|
140
|
+
if (typeof source !== "string" || !ENTRY_HASH.test(source)) return invalid(`page ${slug}: source is not a sha256`);
|
|
141
|
+
if (pending !== void 0 && (typeof pending !== "string" || !ENTRY_HASH.test(pending))) {
|
|
142
|
+
return invalid(`page ${slug}: pending is not a sha256`);
|
|
143
|
+
}
|
|
144
|
+
entries[slug] = { since, source, ...pending === void 0 ? {} : { pending } };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
record: { generatedBy: typeof generatedBy === "string" ? generatedBy : "", format, version, pages: entries },
|
|
148
|
+
diagnostics: []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function pageVersionsFrozen() {
|
|
152
|
+
const ci = process.env.CI;
|
|
153
|
+
return !!ci && ci !== "false" && ci !== "0";
|
|
154
|
+
}
|
|
155
|
+
function syncPageVersions(manifest, options = {}) {
|
|
156
|
+
const root = options.root ?? process.cwd();
|
|
157
|
+
const { enabled, file, record } = manifest.pageVersions;
|
|
158
|
+
const path = join(root, file);
|
|
159
|
+
if (!enabled) return { path, stale: false, written: false, changed: [] };
|
|
160
|
+
const expected = serializePageVersions(record);
|
|
161
|
+
const onDisk = existsSync(path) ? readFileSync(path, "utf8") : void 0;
|
|
162
|
+
if (onDisk === expected) return { path, stale: false, written: false, changed: [] };
|
|
163
|
+
const previous = onDisk === void 0 ? void 0 : readPageVersions(path, file).record;
|
|
164
|
+
const changed = previous === void 0 ? ["*"] : [.../* @__PURE__ */ new Set([...Object.keys(previous.pages), ...Object.keys(record.pages)])].filter((slug) => JSON.stringify(previous.pages[slug] ?? null) !== JSON.stringify(record.pages[slug] ?? null)).sort(compareSlugs);
|
|
165
|
+
const frozen = options.frozen ?? pageVersionsFrozen();
|
|
166
|
+
if (frozen) {
|
|
167
|
+
const moved = changed[0] === "*" ? "the file is missing" : `${changed.length} page(s): ${changed.slice(0, 6).join(", ")}${changed.length > 6 ? ", \u2026" : ""}`;
|
|
168
|
+
return {
|
|
169
|
+
path,
|
|
170
|
+
stale: true,
|
|
171
|
+
written: false,
|
|
172
|
+
changed,
|
|
173
|
+
diagnostic: {
|
|
174
|
+
code: "page-versions-stale",
|
|
175
|
+
severity: "error",
|
|
176
|
+
message: `${file} is out of date (${moved}). Run \`pnpm validate:content\` and commit ${file} with the change (rule C5).`,
|
|
177
|
+
file
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
writeFileSync(path, expected);
|
|
182
|
+
return { path, stale: true, written: true, changed };
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
PAGE_VERSIONS_FILE,
|
|
186
|
+
PAGE_VERSIONS_FORMAT,
|
|
187
|
+
PAGE_VERSIONS_GENERATED_BY,
|
|
188
|
+
compareKnowledgeVersions,
|
|
189
|
+
newestSince,
|
|
190
|
+
nextPageVersions,
|
|
191
|
+
pageSourceDigest,
|
|
192
|
+
pageVersionsFrozen,
|
|
193
|
+
readPageVersions,
|
|
194
|
+
serializePageVersions,
|
|
195
|
+
sinceOf,
|
|
196
|
+
syncPageVersions,
|
|
197
|
+
versionPhrase
|
|
198
|
+
};
|
|
@@ -2,7 +2,7 @@ import { DiagnosticSeverity } from '../reference/types.js';
|
|
|
2
2
|
import { Manifest } from './manifest.js';
|
|
3
3
|
import '../curriculum/validation.js';
|
|
4
4
|
import '../reference/consistency.js';
|
|
5
|
-
import '../course-config-
|
|
5
|
+
import '../course-config-BLtIdkei.js';
|
|
6
6
|
import '../rules/catalog.js';
|
|
7
7
|
import '../rules/course-rules.js';
|
|
8
8
|
import './collections.js';
|
|
@@ -23,6 +23,10 @@ import '../reference/mixed-content.js';
|
|
|
23
23
|
import '../reference/gloss.js';
|
|
24
24
|
import '../rules/waivers.js';
|
|
25
25
|
import './knowledge-version.js';
|
|
26
|
+
import './page-versions.js';
|
|
27
|
+
import './decks.js';
|
|
28
|
+
import '../reference/decks-validate.js';
|
|
29
|
+
import '../reference/deck-source.js';
|
|
26
30
|
import './lesson-derivation.js';
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Manifest } from './manifest.js';
|
|
2
2
|
import '../curriculum/validation.js';
|
|
3
3
|
import '../reference/consistency.js';
|
|
4
|
-
import '../course-config-
|
|
4
|
+
import '../course-config-BLtIdkei.js';
|
|
5
5
|
import '../rules/catalog.js';
|
|
6
6
|
import '../rules/course-rules.js';
|
|
7
7
|
import './collections.js';
|
|
@@ -23,6 +23,10 @@ import '../reference/mixed-content.js';
|
|
|
23
23
|
import '../reference/gloss.js';
|
|
24
24
|
import '../rules/waivers.js';
|
|
25
25
|
import './knowledge-version.js';
|
|
26
|
+
import './page-versions.js';
|
|
27
|
+
import './decks.js';
|
|
28
|
+
import '../reference/decks-validate.js';
|
|
29
|
+
import '../reference/deck-source.js';
|
|
26
30
|
import './lesson-derivation.js';
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -36,7 +40,9 @@ import './lesson-derivation.js';
|
|
|
36
40
|
* lesson — plus `resolution/consistency-report.json` for the Tier-3 corpus
|
|
37
41
|
* checks. `pnpm validate:content` regenerates them and **fails if a committed
|
|
38
42
|
* file is stale**, so a drift between content and the checked-in report cannot
|
|
39
|
-
* pass CI unnoticed.
|
|
43
|
+
* pass CI unnoticed. A report git does not track (a course that ignores
|
|
44
|
+
* `build/`) is refreshed and reported, never a failure: nothing could be
|
|
45
|
+
* committed, so nothing can be stale, and the first run passes.
|
|
40
46
|
*
|
|
41
47
|
* The reports are deterministic: keys are sorted recursively, rows follow the
|
|
42
48
|
* manifest's stable ordering, and nothing records a timestamp or a content
|
|
@@ -48,14 +54,26 @@ declare const RESOLUTION_DIR: string;
|
|
|
48
54
|
declare function buildResolutionReports(manifest: Manifest): Map<string, string>;
|
|
49
55
|
interface ResolutionReportSync {
|
|
50
56
|
readonly written: number;
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Repo-relative paths git tracks that were out of date (created, changed,
|
|
59
|
+
* or removed): the caller fails on these so the checked-in reports stay honest.
|
|
60
|
+
*/
|
|
52
61
|
readonly stale: readonly string[];
|
|
62
|
+
/** Paths rewritten or removed that git does not track: refreshed, nothing to commit. */
|
|
63
|
+
readonly refreshed: readonly string[];
|
|
53
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The report files git tracks under `dir`, by name. Empty when `dir` is
|
|
67
|
+
* ignored, outside a repository, or git is not on the path: then no report
|
|
68
|
+
* can be committed, so none can be stale.
|
|
69
|
+
*/
|
|
70
|
+
declare function trackedReportNames(dir: string): Set<string>;
|
|
54
71
|
/**
|
|
55
72
|
* Write every expected report and prune orphans. Returns the paths that did not
|
|
56
|
-
* already match on disk — the caller turns a
|
|
57
|
-
* failure so the checked-in reports stay
|
|
73
|
+
* already match on disk, split by whether git tracks them — the caller turns a
|
|
74
|
+
* non-empty `stale` list into a build failure so the checked-in reports stay
|
|
75
|
+
* honest, and prints `refreshed` as information.
|
|
58
76
|
*/
|
|
59
77
|
declare function syncResolutionReports(manifest: Manifest): ResolutionReportSync;
|
|
60
78
|
|
|
61
|
-
export { RESOLUTION_DIR, type ResolutionReportSync, buildResolutionReports, syncResolutionReports };
|
|
79
|
+
export { RESOLUTION_DIR, type ResolutionReportSync, buildResolutionReports, syncResolutionReports, trackedReportNames };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import {
|
|
2
3
|
existsSync,
|
|
3
4
|
mkdirSync,
|
|
@@ -63,13 +64,27 @@ function buildResolutionReports(manifest) {
|
|
|
63
64
|
);
|
|
64
65
|
return files;
|
|
65
66
|
}
|
|
67
|
+
function trackedReportNames(dir) {
|
|
68
|
+
try {
|
|
69
|
+
const out = execFileSync("git", ["ls-files", "-z", "--", "."], {
|
|
70
|
+
cwd: dir,
|
|
71
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
72
|
+
encoding: "utf8"
|
|
73
|
+
});
|
|
74
|
+
return new Set(out.split("\0").filter((name) => name !== ""));
|
|
75
|
+
} catch {
|
|
76
|
+
return /* @__PURE__ */ new Set();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
66
79
|
function syncResolutionReports(manifest) {
|
|
67
80
|
const expected = buildResolutionReports(manifest);
|
|
68
81
|
mkdirSync(RESOLUTION_DIR, { recursive: true });
|
|
69
82
|
const onDisk = new Set(
|
|
70
83
|
existsSync(RESOLUTION_DIR) ? readdirSync(RESOLUTION_DIR).filter((name) => name.endsWith(".json")) : []
|
|
71
84
|
);
|
|
85
|
+
const tracked = trackedReportNames(RESOLUTION_DIR);
|
|
72
86
|
const stale = [];
|
|
87
|
+
const refreshed = [];
|
|
73
88
|
for (const [relPath, content] of [...expected].sort(
|
|
74
89
|
([a], [b]) => a.localeCompare(b)
|
|
75
90
|
)) {
|
|
@@ -79,19 +94,22 @@ function syncResolutionReports(manifest) {
|
|
|
79
94
|
const current = existsSync(absolute) ? readFileSync(absolute, "utf8") : void 0;
|
|
80
95
|
if (current !== content) {
|
|
81
96
|
writeFileSync(absolute, content);
|
|
82
|
-
stale.push(
|
|
97
|
+
(tracked.has(name) ? stale : refreshed).push(
|
|
83
98
|
`${relPath} (${current === void 0 ? "missing" : "changed"})`
|
|
84
99
|
);
|
|
85
100
|
}
|
|
86
101
|
}
|
|
87
102
|
for (const orphan of [...onDisk].sort()) {
|
|
88
103
|
rmSync(join(RESOLUTION_DIR, orphan));
|
|
89
|
-
|
|
104
|
+
(tracked.has(orphan) ? stale : refreshed).push(
|
|
105
|
+
`${RESOLUTION_PREFIX}${orphan} (stale, removed)`
|
|
106
|
+
);
|
|
90
107
|
}
|
|
91
|
-
return { written: expected.size, stale };
|
|
108
|
+
return { written: expected.size, stale, refreshed };
|
|
92
109
|
}
|
|
93
110
|
export {
|
|
94
111
|
RESOLUTION_DIR,
|
|
95
112
|
buildResolutionReports,
|
|
96
|
-
syncResolutionReports
|
|
113
|
+
syncResolutionReports,
|
|
114
|
+
trackedReportNames
|
|
97
115
|
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { notationSourceLocatorGaps } from "./collections.js";
|
|
2
|
+
import { createRunLog, diagnosticLine } from "./logging.js";
|
|
3
|
+
import { compileManifest, MANIFEST_PATH, writeManifest } from "./manifest.js";
|
|
4
|
+
import { syncPageVersions } from "./page-versions.js";
|
|
5
|
+
import { syncResolutionReports } from "./resolution-report.js";
|
|
6
|
+
import { consistencyCounts } from "../reference/consistency.js";
|
|
7
|
+
import { curriculumErrors } from "../curriculum/validation.js";
|
|
8
|
+
import { writeRulesInEffect } from "../rules/in-effect.js";
|
|
9
|
+
async function runValidateContent(courseConfig) {
|
|
10
|
+
const log = createRunLog();
|
|
11
|
+
let manifest;
|
|
12
|
+
try {
|
|
13
|
+
manifest = await compileManifest(courseConfig);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
+
log.error("manifest", `Manifest compilation failed: ${message}`);
|
|
17
|
+
const summary2 = log.close();
|
|
18
|
+
console.error(`Manifest compilation failed: ${message}
|
|
19
|
+
Run log: ${summary2.relativeDirectory}.`);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
writeManifest(manifest);
|
|
23
|
+
const {
|
|
24
|
+
curriculum,
|
|
25
|
+
notation,
|
|
26
|
+
alignment,
|
|
27
|
+
math,
|
|
28
|
+
consistency,
|
|
29
|
+
equations,
|
|
30
|
+
tables,
|
|
31
|
+
figures,
|
|
32
|
+
diagrams,
|
|
33
|
+
checks,
|
|
34
|
+
examples,
|
|
35
|
+
folds,
|
|
36
|
+
knowledge,
|
|
37
|
+
rawFloats,
|
|
38
|
+
config,
|
|
39
|
+
waivers
|
|
40
|
+
} = manifest.diagnostics;
|
|
41
|
+
for (const warning of curriculum.filter((issue) => issue.severity === "warning")) {
|
|
42
|
+
console.warn(log.warning("curriculum", `[${warning.kind}] ${warning.message}`));
|
|
43
|
+
}
|
|
44
|
+
for (const warning of notation.filter((d) => d.severity === "warning")) {
|
|
45
|
+
console.warn(log.warning("notation", `[${warning.code}] ${warning.message}`));
|
|
46
|
+
}
|
|
47
|
+
for (const warning of alignment.filter((d) => d.severity === "warning")) {
|
|
48
|
+
console.warn(log.warning("alignment", `[${warning.code}] ${warning.message}`));
|
|
49
|
+
}
|
|
50
|
+
const floatGroups = [
|
|
51
|
+
["equations", equations],
|
|
52
|
+
["tables", tables],
|
|
53
|
+
["figures", figures],
|
|
54
|
+
["diagrams", diagrams],
|
|
55
|
+
["checks", checks],
|
|
56
|
+
["examples", examples],
|
|
57
|
+
["folds", folds],
|
|
58
|
+
["rawFloats", rawFloats]
|
|
59
|
+
];
|
|
60
|
+
for (const [source, group] of floatGroups) {
|
|
61
|
+
for (const warning of group.filter((d) => d.severity === "warning")) {
|
|
62
|
+
console.warn(log.warning(source, `[${warning.code}] ${warning.message}`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const issue of math.filter((item) => item.severity === "warning")) {
|
|
66
|
+
console.warn(log.warning("math", diagnosticLine(issue)));
|
|
67
|
+
}
|
|
68
|
+
for (const warning of knowledge) {
|
|
69
|
+
console.warn(log.warning("knowledge", `[${warning.code}] ${warning.message}`));
|
|
70
|
+
}
|
|
71
|
+
for (const warning of config) {
|
|
72
|
+
console.warn(log.warning("config", `[${warning.code}] ${warning.message}`));
|
|
73
|
+
}
|
|
74
|
+
for (const warning of waivers.filter((d) => d.severity === "warning")) {
|
|
75
|
+
console.warn(log.warning("waivers", diagnosticLine(warning)));
|
|
76
|
+
}
|
|
77
|
+
const locatorGaps = notationSourceLocatorGaps();
|
|
78
|
+
if (locatorGaps.bare > 0) {
|
|
79
|
+
console.warn(
|
|
80
|
+
log.warning(
|
|
81
|
+
"consistency",
|
|
82
|
+
`[notation-source-locator] ${locatorGaps.bare} of ${locatorGaps.total} notation source citations lack a non-empty locator.`
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const consistencyWarnings = consistency.filter((diagnostic) => diagnostic.severity === "warning");
|
|
87
|
+
if (consistencyWarnings.length > 0) {
|
|
88
|
+
console.warn(
|
|
89
|
+
log.warning(
|
|
90
|
+
"consistency",
|
|
91
|
+
`[consistency] ${consistencyWarnings.length} Tier-3 finding(s): ` + Object.entries(consistencyCounts(consistencyWarnings)).filter(([, count]) => count > 0).map(([code, count]) => `${code}=${count}`).join(", ")
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
log.infoDiagnostics(manifest.diagnostics);
|
|
96
|
+
for (const diagnostic of consistencyWarnings) {
|
|
97
|
+
log.info("consistency", `[${diagnostic.code}] ${diagnostic.message}`);
|
|
98
|
+
}
|
|
99
|
+
const reports = syncResolutionReports(manifest);
|
|
100
|
+
console.log(
|
|
101
|
+
log.info(
|
|
102
|
+
"manifest",
|
|
103
|
+
`Resolution report: ${reports.written} file(s) under resolution/` + (reports.refreshed.length > 0 ? ` (${reports.refreshed.length} refreshed; git does not track them, nothing to commit).` : ".")
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
const pageVersions = syncPageVersions(manifest);
|
|
107
|
+
if (pageVersions.written) {
|
|
108
|
+
console.log(
|
|
109
|
+
log.info(
|
|
110
|
+
"page-versions",
|
|
111
|
+
`content/versions.json ${pageVersions.changed[0] === "*" ? "written" : `updated for ${pageVersions.changed.length} page(s)`} \u2014 commit it with the change (rule C5).`
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
writeRulesInEffect(manifest);
|
|
116
|
+
console.log(
|
|
117
|
+
log.info("manifest", `Rules in effect: build/rules-in-effect.md (${manifest.rules.disabled.length} rule(s) off).`)
|
|
118
|
+
);
|
|
119
|
+
const failures = [];
|
|
120
|
+
const block = (source, message) => {
|
|
121
|
+
failures.push(log.error(source, message));
|
|
122
|
+
};
|
|
123
|
+
for (const stalePath of reports.stale) {
|
|
124
|
+
block("resolution-report", `[resolution-report] ${stalePath} is out of date \u2014 commit the regenerated resolution/ files`);
|
|
125
|
+
}
|
|
126
|
+
if (pageVersions.diagnostic) block("page-versions", diagnosticLine(pageVersions.diagnostic));
|
|
127
|
+
for (const issue of curriculumErrors(curriculum)) block("curriculum", `[curriculum:${issue.kind}] ${issue.message}`);
|
|
128
|
+
for (const issue of notation.filter((d) => d.severity === "error")) block("notation", `[notation:${issue.code}] ${issue.message}`);
|
|
129
|
+
for (const issue of alignment.filter((d) => d.severity === "error")) block("alignment", `[alignment:${issue.code}] ${issue.message}`);
|
|
130
|
+
for (const issue of math.filter((item) => item.severity === "error")) block("math", diagnosticLine(issue));
|
|
131
|
+
for (const issue of waivers.filter((d) => d.severity === "error")) block("waivers", diagnosticLine(issue));
|
|
132
|
+
for (const [source, group] of floatGroups) {
|
|
133
|
+
for (const issue of group.filter((d) => d.severity === "error")) block(source, `[${issue.code}] ${issue.message}`);
|
|
134
|
+
}
|
|
135
|
+
if (failures.length > 0) {
|
|
136
|
+
const summary2 = log.close();
|
|
137
|
+
console.error(
|
|
138
|
+
`Content manifest has ${failures.length} blocking diagnostic(s):
|
|
139
|
+
${failures.map((line) => `- ${line}`).join("\n")}
|
|
140
|
+
Run log: ${summary2.relativeDirectory} (${summary2.counts.ERROR} error(s), ${summary2.counts.WARNING} warning(s)).`
|
|
141
|
+
);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
const itemCount = manifest.lessons.reduce((count, lesson) => count + lesson.assessments.length, 0);
|
|
145
|
+
const relPath = MANIFEST_PATH.replace(`${process.cwd()}/`, "");
|
|
146
|
+
console.log(
|
|
147
|
+
log.info(
|
|
148
|
+
"manifest",
|
|
149
|
+
`Manifest written to ${relPath} (schema v${manifest.schemaVersion}, knowledge v${manifest.about.knowledge.version}): ${manifest.competencies.length} competencies, ${manifest.lessons.length} lessons, ${itemCount} lesson assessment links, ${manifest.notation.definitions.length} notation definitions, ${manifest.sources.length} sources, ${manifest.tracks.length} track(s). Content fingerprint ${manifest.hashes.contentTree}.`
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
const summary = log.close();
|
|
153
|
+
console.log(
|
|
154
|
+
`Run log: ${summary.relativeDirectory} (${summary.counts.ERROR} error(s), ${summary.counts.WARNING} warning(s), ${summary.counts.INFO} info).`
|
|
155
|
+
);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
runValidateContent
|
|
160
|
+
};
|