explico 0.14.0 → 0.15.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 +112 -1
- package/dist/compiler/cli-main.d.ts +61 -4
- package/dist/compiler/cli-main.js +77 -36
- package/dist/compiler/cli-reference.d.ts +50 -0
- package/dist/compiler/cli-reference.js +213 -0
- package/dist/compiler/cli-verbs.d.ts +45 -0
- package/dist/compiler/cli-verbs.js +224 -0
- package/dist/compiler/cli-verify.d.ts +51 -0
- package/dist/compiler/cli-verify.js +151 -0
- package/dist/compiler/cli.d.ts +19 -50
- package/dist/compiler/cli.js +45 -6
- package/dist/compiler/content-release.d.ts +49 -0
- package/dist/compiler/content-release.js +63 -0
- package/dist/compiler/content-schemas.d.ts +2 -2
- package/dist/compiler/manifest.js +29 -24
- package/dist/compiler/page-versions.d.ts +25 -1
- package/dist/compiler/page-versions.js +19 -0
- package/dist/compiler/validate-content.js +2 -2
- package/dist/reference/consistency.js +7 -1
- package/dist/reference/diagram-fit.d.ts +5 -1
- package/dist/reference/diagram-fit.js +5 -1
- package/dist/rules/catalog.js +16 -0
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/in-effect.js +5 -4
- package/dist/rules/rules.schema.json +21 -0
- package/docs/authoring/README.md +2 -0
- package/docs/authoring/agents.md +18 -11
- package/docs/authoring/cli-reference.json +2172 -0
- package/docs/authoring/cli.md +887 -0
- package/docs/authoring/deck.md +24 -0
- package/docs/authoring/migrating.md +168 -18
- package/docs/authoring/versioning.md +2 -1
- package/docs/course-agent/AGENTS.md +10 -7
- package/docs/course-agent/agents/rules-reviewer.md +2 -2
- package/docs/course-agent/skills/draft-lesson/SKILL.md +7 -6
- package/docs/course-agent/skills/migrate-course/SKILL.md +5 -4
- package/docs/course-agent/skills/release-content/SKILL.md +5 -3
- package/docs/diagram-scaling.md +2 -2
- package/package.json +1 -1
- package/src/compiler/cli-main.ts +135 -47
- package/src/compiler/cli-reference.ts +285 -0
- package/src/compiler/cli-verbs.ts +270 -0
- package/src/compiler/cli-verify.ts +241 -0
- package/src/compiler/cli.ts +77 -17
- package/src/compiler/content-release.ts +91 -0
- package/src/compiler/manifest.ts +34 -24
- package/src/compiler/page-versions.ts +42 -0
- package/src/compiler/validate-content.ts +5 -2
- package/src/reference/consistency.ts +13 -2
- package/src/reference/diagram-fit.ts +18 -4
- package/src/rules/catalog.ts +17 -0
- package/src/rules/in-effect.ts +5 -4
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
const VERIFY_DIR = join("node_modules", ".cache", "explico", "verify");
|
|
6
|
+
const RECORD = "last-pass.json";
|
|
7
|
+
const TAIL_LINES = 20;
|
|
8
|
+
function verifyTreeHash(cwd) {
|
|
9
|
+
try {
|
|
10
|
+
const git = (args) => execFileSync("git", args, { cwd, maxBuffer: 1 << 30, stdio: ["ignore", "pipe", "ignore"] });
|
|
11
|
+
const hash = createHash("sha1");
|
|
12
|
+
hash.update(git(["rev-parse", "HEAD"]));
|
|
13
|
+
hash.update(git(["diff", "HEAD", "--binary"]));
|
|
14
|
+
const untracked = git(["ls-files", "--others", "--exclude-standard", "-z"]).toString("utf8").split("\0").filter(Boolean).sort();
|
|
15
|
+
for (const file of untracked) {
|
|
16
|
+
hash.update(`\0${file}\0`);
|
|
17
|
+
if (existsSync(join(cwd, file))) hash.update(readFileSync(join(cwd, file)));
|
|
18
|
+
}
|
|
19
|
+
return hash.digest("hex");
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function scriptCommand(script, args) {
|
|
25
|
+
const execpath = process.env.npm_execpath ?? "";
|
|
26
|
+
const manager = /pnpm/.test(execpath) ? "pnpm" : /yarn/.test(execpath) ? "yarn" : /npm/.test(execpath) ? "npm" : "pnpm";
|
|
27
|
+
const tail = manager === "npm" && args.length > 0 ? ["run", script, "--", ...args] : ["run", script, ...args];
|
|
28
|
+
const display = `${manager} ${tail.join(" ")}`;
|
|
29
|
+
if (execpath === "" || !execpath.includes(manager)) return { file: manager, argv: tail, display };
|
|
30
|
+
return /\.[cm]?js$/.test(execpath) ? { file: process.execPath, argv: [execpath, ...tail], display } : { file: execpath, argv: tail, display };
|
|
31
|
+
}
|
|
32
|
+
function binCommand(cwd, bin, args) {
|
|
33
|
+
return { file: join(cwd, "node_modules", ".bin", process.platform === "win32" ? `${bin}.cmd` : bin), argv: [...args], display: [bin, ...args].join(" ") };
|
|
34
|
+
}
|
|
35
|
+
function slug(name) {
|
|
36
|
+
return name.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
37
|
+
}
|
|
38
|
+
function tailOf(file) {
|
|
39
|
+
const lines = readFileSync(file, "utf8").split("\n").map((line) => line.trimEnd()).filter((line) => line !== "");
|
|
40
|
+
return lines.slice(-TAIL_LINES);
|
|
41
|
+
}
|
|
42
|
+
function runVerify(options = {}) {
|
|
43
|
+
const cwd = options.cwd ?? process.cwd();
|
|
44
|
+
const e2e = options.e2e ?? false;
|
|
45
|
+
const dir = join(cwd, VERIFY_DIR);
|
|
46
|
+
const recordFile = join(dir, RECORD);
|
|
47
|
+
if (!options.force && existsSync(recordFile)) {
|
|
48
|
+
try {
|
|
49
|
+
const record = JSON.parse(readFileSync(recordFile, "utf8"));
|
|
50
|
+
const tree2 = verifyTreeHash(cwd);
|
|
51
|
+
if (tree2 !== null && record.tree === tree2 && (record.e2e || !e2e)) {
|
|
52
|
+
return { ok: true, cached: true, since: record.when, tree: tree2, e2e: record.e2e, steps: record.steps };
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
59
|
+
const scripts = pkg.scripts ?? {};
|
|
60
|
+
const has = (path) => existsSync(join(cwd, path));
|
|
61
|
+
const playwrightConfig = ["playwright.config.ts", "playwright.config.mts", "playwright.config.js", "playwright.config.mjs"].some(has);
|
|
62
|
+
const plans = [
|
|
63
|
+
scripts["validate:content"] === void 0 ? { name: "validate:content", reason: "package.json has no validate:content script", fail: true } : { name: "validate:content", command: scriptCommand("validate:content", []) },
|
|
64
|
+
scripts.content === void 0 ? { name: "check --changed", reason: "package.json has no content script", fail: true } : { name: "check --changed", command: scriptCommand("content", ["check", "--changed"]) },
|
|
65
|
+
// Without @astrojs/check, `astro check` stops to ask whether to install it.
|
|
66
|
+
has(join("node_modules", "@astrojs", "check", "package.json")) ? { name: "astro check", command: binCommand(cwd, "astro", ["check"]) } : { name: "astro check", reason: "@astrojs/check is not installed (pnpm add -D @astrojs/check typescript)" },
|
|
67
|
+
scripts.test === void 0 ? { name: "tests", reason: "package.json has no test script" } : { name: "tests", command: scriptCommand("test", []) },
|
|
68
|
+
{ name: "astro build", command: binCommand(cwd, "astro", ["build"]) }
|
|
69
|
+
];
|
|
70
|
+
if (e2e) {
|
|
71
|
+
plans.push(
|
|
72
|
+
scripts["test:e2e"] !== void 0 ? { name: "e2e", command: scriptCommand("test:e2e", []) } : playwrightConfig ? { name: "e2e", command: binCommand(cwd, "playwright", ["test"]) } : { name: "e2e", reason: "no test:e2e script and no playwright.config" }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const steps = [];
|
|
76
|
+
for (const plan of plans) {
|
|
77
|
+
options.onStep?.({ phase: "start", name: plan.name });
|
|
78
|
+
let step;
|
|
79
|
+
if (plan.command === void 0) {
|
|
80
|
+
step = { name: plan.name, status: plan.fail ? "fail" : "skipped", reason: plan.reason };
|
|
81
|
+
} else {
|
|
82
|
+
const log = join(dir, `${slug(plan.name)}.log`);
|
|
83
|
+
const fd = openSync(log, "w");
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
let result;
|
|
86
|
+
try {
|
|
87
|
+
result = spawnSync(plan.command.file, plan.command.argv, {
|
|
88
|
+
cwd,
|
|
89
|
+
stdio: ["ignore", fd, fd],
|
|
90
|
+
shell: process.platform === "win32",
|
|
91
|
+
env: process.env
|
|
92
|
+
});
|
|
93
|
+
} finally {
|
|
94
|
+
closeSync(fd);
|
|
95
|
+
}
|
|
96
|
+
const seconds = Math.round((Date.now() - started) / 100) / 10;
|
|
97
|
+
const passed = result.error === void 0 && result.status === 0;
|
|
98
|
+
step = {
|
|
99
|
+
name: plan.name,
|
|
100
|
+
status: passed ? "pass" : "fail",
|
|
101
|
+
command: plan.command.display,
|
|
102
|
+
seconds,
|
|
103
|
+
log: relative(cwd, log),
|
|
104
|
+
...result.error === void 0 ? {} : { reason: result.error.message },
|
|
105
|
+
...passed ? {} : { tail: tailOf(log) }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
steps.push(step);
|
|
109
|
+
options.onStep?.({ phase: "end", step });
|
|
110
|
+
}
|
|
111
|
+
const ok = steps.every((step) => step.status !== "fail");
|
|
112
|
+
const tree = verifyTreeHash(cwd);
|
|
113
|
+
if (ok && tree !== null) {
|
|
114
|
+
const record = { tree, e2e, when: (/* @__PURE__ */ new Date()).toISOString(), steps };
|
|
115
|
+
writeFileSync(recordFile, `${JSON.stringify(record, null, 2)}
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
return { ok, cached: false, tree, e2e, steps };
|
|
119
|
+
}
|
|
120
|
+
function formatVerifyReport(report) {
|
|
121
|
+
if (report.cached) {
|
|
122
|
+
return `verify: PASS, unchanged since ${report.since} (${report.e2e ? "with" : "without"} e2e); --force reruns
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const step of report.steps) {
|
|
127
|
+
if (step.status !== "fail") continue;
|
|
128
|
+
out.push("", `${step.name} failed${step.reason === void 0 ? "" : `: ${step.reason}`}${step.log === void 0 ? "" : ` \u2014 last lines of ${step.log}:`}`);
|
|
129
|
+
for (const line of step.tail ?? []) out.push(` ${line}`);
|
|
130
|
+
}
|
|
131
|
+
const failed = report.steps.filter((step) => step.status === "fail").map((step) => step.name);
|
|
132
|
+
out.push(
|
|
133
|
+
"",
|
|
134
|
+
failed.length === 0 ? `verify: PASS${report.tree === null ? " (not a git repository: nothing recorded)" : ""}` : `verify: FAIL \u2014 ${failed.join(", ")}; whole output under ${VERIFY_DIR}/`
|
|
135
|
+
);
|
|
136
|
+
return `${out.join("\n").replace(/^\n/, "")}
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
function formatVerifyStep(step) {
|
|
140
|
+
const verdict = step.status === "pass" ? "ok" : step.status === "fail" ? "FAIL" : "skipped";
|
|
141
|
+
const detail = step.seconds !== void 0 ? `${step.seconds.toFixed(1)}s` : step.reason ?? "";
|
|
142
|
+
return ` ${step.name.padEnd(18)} ${verdict.padEnd(7)} ${detail}
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
VERIFY_DIR,
|
|
147
|
+
formatVerifyReport,
|
|
148
|
+
formatVerifyStep,
|
|
149
|
+
runVerify,
|
|
150
|
+
verifyTreeHash
|
|
151
|
+
};
|
package/dist/compiler/cli.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DiagnosticSeverity } from '../reference/types.js';
|
|
2
2
|
import { EquationLink, ScopeEntry } from '../reference/gate-math.js';
|
|
3
|
-
import { ManifestLesson, Manifest } from './manifest.js';
|
|
3
|
+
import { ManifestLesson, ManifestLessonFloat, Manifest } from './manifest.js';
|
|
4
4
|
import { RenderDiagnostic } from './render-diagnostics.js';
|
|
5
5
|
import { RetiredRule } from '../rules/catalog.js';
|
|
6
|
-
import {
|
|
6
|
+
import { RulesFilter, RuleInEffect } from '../rules/in-effect.js';
|
|
7
7
|
import '../curriculum/validation.js';
|
|
8
8
|
import '../reference/consistency.js';
|
|
9
9
|
import '../course-config-BLtIdkei.js';
|
|
@@ -64,7 +64,7 @@ import './lesson-derivation.js';
|
|
|
64
64
|
* under the `render:` source.
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
|
-
type ContentCliCode = 'unknown-command' | 'usage' | 'unknown-lesson' | 'unknown-kind' | 'invalid-id' | 'exists' | 'not-a-lesson' | 'no-git' | 'unknown-version' | 'migration-gap' | 'unknown-key';
|
|
67
|
+
type ContentCliCode = 'unknown-command' | 'usage' | 'unknown-lesson' | 'unknown-kind' | 'invalid-id' | 'exists' | 'not-a-lesson' | 'no-git' | 'unknown-version' | 'migration-gap' | 'unknown-key' | 'no-changelog';
|
|
68
68
|
/** A user-facing failure with a stable code and no stack in `--json` output. */
|
|
69
69
|
declare class ContentCliError extends Error {
|
|
70
70
|
readonly code: ContentCliCode;
|
|
@@ -312,22 +312,10 @@ interface ContextReport {
|
|
|
312
312
|
units?: string;
|
|
313
313
|
formula?: string;
|
|
314
314
|
}[];
|
|
315
|
-
readonly equations: readonly
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
readonly tables: readonly {
|
|
320
|
-
key: string;
|
|
321
|
-
number: string;
|
|
322
|
-
}[];
|
|
323
|
-
readonly figures: readonly {
|
|
324
|
-
key: string;
|
|
325
|
-
number: string;
|
|
326
|
-
}[];
|
|
327
|
-
readonly diagrams: readonly {
|
|
328
|
-
key: string;
|
|
329
|
-
number: string;
|
|
330
|
-
}[];
|
|
315
|
+
readonly equations: readonly ManifestLessonFloat[];
|
|
316
|
+
readonly tables: readonly ManifestLessonFloat[];
|
|
317
|
+
readonly figures: readonly ManifestLessonFloat[];
|
|
318
|
+
readonly diagrams: readonly ManifestLessonFloat[];
|
|
331
319
|
/** Worked-example sets: `short` is the title `[[ex-key]]` names. */
|
|
332
320
|
readonly examples: readonly {
|
|
333
321
|
key: string;
|
|
@@ -357,13 +345,6 @@ interface ContextReport {
|
|
|
357
345
|
readonly diagnostics: readonly CliDiagnostic[];
|
|
358
346
|
readonly tokens: TokenCounts;
|
|
359
347
|
}
|
|
360
|
-
/**
|
|
361
|
-
* The migration guide, cut to the sections an upgrade needs: from the
|
|
362
|
-
* version a course pins to the version it has installed, oldest first. The
|
|
363
|
-
* guide is one newest-first file of every release; an upgrade reads one or
|
|
364
|
-
* two sections of it, and the heading grammar `## <from> → <to>` (a `0.4.x`
|
|
365
|
-
* range on the left, `Unreleased` allowed on the right) says which.
|
|
366
|
-
*/
|
|
367
348
|
interface MigrationSection {
|
|
368
349
|
readonly from: string;
|
|
369
350
|
readonly to: string;
|
|
@@ -401,14 +382,6 @@ declare function contentMigrate(options?: {
|
|
|
401
382
|
}): MigrateReport;
|
|
402
383
|
/** The sections, oldest first, under one line that says how much is coming. */
|
|
403
384
|
declare function formatMigrateReport(report: MigrateReport): string;
|
|
404
|
-
/**
|
|
405
|
-
* What a lesson's math binds, span by span, before a build says so: each
|
|
406
|
-
* glyph and the card it resolves to, how (`canonical`, `family`, `derived`,
|
|
407
|
-
* `base`, `operator`, or an explicit `\explain` wrap), the wraps the bare
|
|
408
|
-
* symbol would have resolved without, and every unresolved glyph with where
|
|
409
|
-
* `content symbol` finds it defined. The same resolver and scope the
|
|
410
|
-
* completeness gate uses, so what it prints is what the build will do.
|
|
411
|
-
*/
|
|
412
385
|
interface SpanBinding {
|
|
413
386
|
readonly token: string;
|
|
414
387
|
readonly key: string;
|
|
@@ -503,11 +476,6 @@ declare function contentEquation(manifest: Manifest, latex: string, options?: {
|
|
|
503
476
|
readonly lesson?: string;
|
|
504
477
|
}): EquationReport;
|
|
505
478
|
declare function formatEquationReport(report: EquationReport): string;
|
|
506
|
-
/**
|
|
507
|
-
* The refactors a model gets wrong most and reads most for, as deterministic
|
|
508
|
-
* edits: every one is planned first (`FileEdit`s with the whole text before
|
|
509
|
-
* and after), shown as a unified diff, and written only on `--write`.
|
|
510
|
-
*/
|
|
511
479
|
interface FileEdit {
|
|
512
480
|
/** Course-relative path. */
|
|
513
481
|
readonly file: string;
|
|
@@ -537,10 +505,17 @@ declare function removeLocalItem(text: string, key: string): {
|
|
|
537
505
|
text: string;
|
|
538
506
|
removed: boolean;
|
|
539
507
|
};
|
|
540
|
-
/**
|
|
508
|
+
/**
|
|
509
|
+
* A shared card's file from a page-local entry's fields, nothing invented: no
|
|
510
|
+
* body (the glossary shows the meaning; what a reader needs beyond it is the
|
|
511
|
+
* author's to write, and a placeholder would publish), and the label only
|
|
512
|
+
* when the entry has one. `domain` is the caller's pick, else the key's
|
|
513
|
+
* prefix, else `general`.
|
|
514
|
+
*/
|
|
541
515
|
declare function sharedCardFromLocal(local: Record<string, unknown>, options: {
|
|
542
516
|
readonly key: string;
|
|
543
517
|
readonly fromLesson: string;
|
|
518
|
+
readonly domain?: string;
|
|
544
519
|
}): string;
|
|
545
520
|
/**
|
|
546
521
|
* `content move <lesson>:<key> [--to <key>]`: a page-local entry becomes a
|
|
@@ -559,6 +534,8 @@ declare function planRename(manifest: Manifest, oldKey: string, newKey: string):
|
|
|
559
534
|
* becomes `inner`.
|
|
560
535
|
*/
|
|
561
536
|
declare function planFix(manifest: Manifest, lessonId: string): EditPlan;
|
|
537
|
+
/** `content fix` over several lessons (or every one, `--all`) as one plan: the lessons with nothing to fix are counted, not listed. */
|
|
538
|
+
declare function planFixMany(manifest: Manifest, lessonIds: readonly string[]): EditPlan;
|
|
562
539
|
/** The plan as a unified diff (git's), file by file, under its summary lines. */
|
|
563
540
|
declare function renderEditPlan(plan: EditPlan): string;
|
|
564
541
|
/** Write a plan's edits. Returns the files touched. */
|
|
@@ -614,15 +591,7 @@ declare function contentRule(manifest: Manifest, rawId: string): RuleReport;
|
|
|
614
591
|
/** The text form of a rule report: the entry's facts, then the page section. */
|
|
615
592
|
declare function formatRuleReport(report: RuleReport): string;
|
|
616
593
|
declare function contentContext(manifest: Manifest, rawId: string): ContextReport;
|
|
617
|
-
/**
|
|
618
|
-
* Where a symbol is already defined, asked before an author declares it
|
|
619
|
-
* again. Reads the registry the manifest holds: shared cards, page-local
|
|
620
|
-
* entries (`notation.local`) and glosses (a card's `glosses:`), matched on
|
|
621
|
-
* the latex as written, a shared card's `aliases`, or the key. A `contains`
|
|
622
|
-
* list follows for a letter that sits inside a longer notation (`v` in
|
|
623
|
-
* `v_0`), so a near miss is visible too. Read-only; every list is sorted,
|
|
624
|
-
* so two runs print the same report.
|
|
625
|
-
*/
|
|
594
|
+
/** Where a hit is defined: a shared card, a lesson's `notation.local`, or a card's gloss. */
|
|
626
595
|
type SymbolTier = 'shared' | 'local' | 'gloss';
|
|
627
596
|
interface SymbolHit {
|
|
628
597
|
readonly tier: SymbolTier;
|
|
@@ -716,4 +685,4 @@ declare function scaffoldLesson(rawId: string): Scaffold;
|
|
|
716
685
|
declare function scaffoldDeck(rawId: string): Scaffold;
|
|
717
686
|
declare function scaffoldTerm(rawKey: string): Scaffold;
|
|
718
687
|
|
|
719
|
-
export { type BindingsReport, type BindingsSpan, type CheckLessonResult, type CheckReport, type CheckTarget, type CliDiagnostic, type ContentCliCode, ContentCliError, type ContextReport, type DiagnosticThreshold, type DiagnosticsReport, type DiagnosticsSummary, type DomainFileSignatures, type EditPlan, type EquationReport, type ExplicitWrap, type FileEdit, type MigrateReport, type MigrationSection, type RenderResult, type RuleReport, type RulesReport, type Scaffold, type SpanBinding, type SpanBindings, type StatusReport, type SymbolHit, type SymbolReport, type SymbolTier, type TokenCounts, type UnresolvedGlyph, type WarningsReport, applyEditPlan, authoringDocsDir, changedLessonIds, contentBindings, contentCheck, contentContext, contentDiagnostics, contentEquation, contentMigrate, contentRule, contentRules, contentStatus, contentSymbol, contentWarnings, courseAgentDir, estimateTokens, filterDiagnostics, findCheckTarget, findLesson, formatBindingsReport, formatCliDiagnostic, formatDiagnosticsSummary, formatEquationReport, formatMigrateReport, formatRuleReport, formatSymbolReport, installedEngineVersion, lessonDiagnostics, lessonSource, normalizeLessonId, ownerTag, parseMigrationSections, parseRulesFilter, pinnedEngineVersion, planFix, planInitAgent, planMove, planRename, removeLocalItem, renderEditPlan, renderLesson, rewriteKeyReferences, ruleSection, scaffoldDeck, scaffoldLesson, scaffoldTerm, selectMigrationPath, sharedCardFromLocal, spanBindings, summarizeDiagnostics };
|
|
688
|
+
export { type BindingsReport, type BindingsSpan, type CheckLessonResult, type CheckReport, type CheckTarget, type CliDiagnostic, type ContentCliCode, ContentCliError, type ContextReport, type DiagnosticThreshold, type DiagnosticsReport, type DiagnosticsSummary, type DomainFileSignatures, type EditPlan, type EquationReport, type ExplicitWrap, type FileEdit, type MigrateReport, type MigrationSection, type RenderResult, type RuleReport, type RulesReport, type Scaffold, type SpanBinding, type SpanBindings, type StatusReport, type SymbolHit, type SymbolReport, type SymbolTier, type TokenCounts, type UnresolvedGlyph, type WarningsReport, applyEditPlan, authoringDocsDir, changedLessonIds, contentBindings, contentCheck, contentContext, contentDiagnostics, contentEquation, contentMigrate, contentRule, contentRules, contentStatus, contentSymbol, contentWarnings, courseAgentDir, estimateTokens, filterDiagnostics, findCheckTarget, findLesson, formatBindingsReport, formatCliDiagnostic, formatDiagnosticsSummary, formatEquationReport, formatMigrateReport, formatRuleReport, formatSymbolReport, installedEngineVersion, lessonDiagnostics, lessonSource, normalizeLessonId, ownerTag, parseMigrationSections, parseRulesFilter, pinnedEngineVersion, planFix, planFixMany, planInitAgent, planMove, planRename, removeLocalItem, renderEditPlan, renderLesson, rewriteKeyReferences, ruleSection, scaffoldDeck, scaffoldLesson, scaffoldTerm, selectMigrationPath, sharedCardFromLocal, spanBindings, summarizeDiagnostics };
|
package/dist/compiler/cli.js
CHANGED
|
@@ -1121,7 +1121,7 @@ function yamlBlock(value, indent) {
|
|
|
1121
1121
|
}
|
|
1122
1122
|
function sharedCardFromLocal(local, options) {
|
|
1123
1123
|
const key = options.key;
|
|
1124
|
-
const domain = key.includes(".") ? key.slice(0, key.indexOf(".")) : "general";
|
|
1124
|
+
const domain = options.domain ?? (key.includes(".") ? key.slice(0, key.indexOf(".")) : "general");
|
|
1125
1125
|
const front = {
|
|
1126
1126
|
key,
|
|
1127
1127
|
latex: local.latex,
|
|
@@ -1143,10 +1143,22 @@ function sharedCardFromLocal(local, options) {
|
|
|
1143
1143
|
return `---
|
|
1144
1144
|
${yamlBlock(front, 0).join("\n")}
|
|
1145
1145
|
---
|
|
1146
|
-
|
|
1147
|
-
Promoted from \`${options.fromLesson}\`'s \`notation.local\`; the meaning above is what that page said. Extend this body with what a glossary reader needs.
|
|
1148
1146
|
`;
|
|
1149
1147
|
}
|
|
1148
|
+
function promotedDomain(manifest, lesson, key) {
|
|
1149
|
+
const shared = manifest.notation.definitions.filter((definition) => definition.kind === "shared");
|
|
1150
|
+
const prefix = key.includes(".") ? key.slice(0, key.indexOf(".")) : void 0;
|
|
1151
|
+
if (prefix !== void 0 && shared.some((definition) => definition.domain === prefix)) return prefix;
|
|
1152
|
+
const mostCommon = (definitions) => {
|
|
1153
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1154
|
+
for (const definition of definitions) {
|
|
1155
|
+
if (definition.domain) counts.set(definition.domain, (counts.get(definition.domain) ?? 0) + 1);
|
|
1156
|
+
}
|
|
1157
|
+
return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0];
|
|
1158
|
+
};
|
|
1159
|
+
const bound = new Set(lesson.notation.definitionIds);
|
|
1160
|
+
return mostCommon(shared.filter((definition) => bound.has(definition.id))) ?? mostCommon(shared) ?? prefix ?? "general";
|
|
1161
|
+
}
|
|
1150
1162
|
function courseTextFiles() {
|
|
1151
1163
|
const out = [];
|
|
1152
1164
|
const walk = (dir) => {
|
|
@@ -1195,8 +1207,20 @@ function planMove(manifest, lessonId, key, options = {}) {
|
|
|
1195
1207
|
}
|
|
1196
1208
|
const summary = [];
|
|
1197
1209
|
const edits = [];
|
|
1198
|
-
|
|
1199
|
-
|
|
1210
|
+
const domain = promotedDomain(manifest, lesson, newKey);
|
|
1211
|
+
const glyph = typeof source.latex === "string" ? source.latex.trim() : "";
|
|
1212
|
+
const owner = manifest.notation.definitions.find(
|
|
1213
|
+
(definition) => definition.kind === "shared" && definition.key !== newKey && definition.notation.trim() === glyph
|
|
1214
|
+
);
|
|
1215
|
+
if (owner !== void 0) {
|
|
1216
|
+
summary.push(
|
|
1217
|
+
`CAUTION: ${glyph} is already shared card ${owner.key}'s \u2014 a second shared card on one glyph is a glyph-unique-in-corpus finding; a reused letter stays local (rule N4). Write this only if the two cards are meant to share the glyph, and acknowledge the pair.`
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
edits.push({ file: target, before: null, after: sharedCardFromLocal(source, { key: newKey, fromLesson: lesson.id, domain }) });
|
|
1221
|
+
summary.push(
|
|
1222
|
+
`${target}: new shared card from ${lesson.id}'s entry${newKey === key ? "" : ` (key ${key} \u2192 ${newKey})`}, domain ${domain}; no body yet \u2014 add what a glossary reader needs beyond the meaning` + (source.label === void 0 ? "; no label in the entry \u2014 the card shows one made from the key until you add `label:`" : "")
|
|
1223
|
+
);
|
|
1200
1224
|
const touched = /* @__PURE__ */ new Set();
|
|
1201
1225
|
for (const other of manifest.lessons) {
|
|
1202
1226
|
const copy = local(other);
|
|
@@ -1285,6 +1309,20 @@ function planFix(manifest, lessonId) {
|
|
|
1285
1309
|
next: [`pnpm content check ${lesson.id}`]
|
|
1286
1310
|
};
|
|
1287
1311
|
}
|
|
1312
|
+
function planFixMany(manifest, lessonIds) {
|
|
1313
|
+
if (lessonIds.length === 1) return planFix(manifest, lessonIds[0]);
|
|
1314
|
+
const plans = lessonIds.map((id) => planFix(manifest, id));
|
|
1315
|
+
const fixing = plans.filter((plan) => plan.edits.length > 0);
|
|
1316
|
+
const clean = plans.length - fixing.length;
|
|
1317
|
+
return {
|
|
1318
|
+
summary: [
|
|
1319
|
+
...fixing.flatMap((plan) => plan.summary),
|
|
1320
|
+
...clean > 0 ? [`${clean} lesson(s) with nothing to fix`] : []
|
|
1321
|
+
],
|
|
1322
|
+
edits: fixing.flatMap((plan) => plan.edits),
|
|
1323
|
+
next: ["pnpm content check --changed"]
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1288
1326
|
function renderEditPlan(plan) {
|
|
1289
1327
|
const lines = plan.summary.map((line) => ` ${line}`);
|
|
1290
1328
|
const scratch = join(REPOSITORY_ROOT, ".agent", "content-edits");
|
|
@@ -1354,7 +1392,7 @@ function planInitAgent() {
|
|
|
1354
1392
|
const before = existsSync(target) ? readFileSync(target, "utf8") : null;
|
|
1355
1393
|
if (before === after) return;
|
|
1356
1394
|
edits.push({ file: to, before, after });
|
|
1357
|
-
summary.push(`${to}: ${before === null ? "
|
|
1395
|
+
summary.push(`${to}: ${before === null ? "new" : "differs from the engine's copy"}`);
|
|
1358
1396
|
};
|
|
1359
1397
|
for (const file of filesUnder(join(source, "skills"))) install(`skills/${file}`, `.claude/skills/${file}`);
|
|
1360
1398
|
for (const file of filesUnder(join(source, "agents"))) install(`agents/${file}`, `.claude/agents/${file}`);
|
|
@@ -2079,6 +2117,7 @@ export {
|
|
|
2079
2117
|
parseRulesFilter,
|
|
2080
2118
|
pinnedEngineVersion,
|
|
2081
2119
|
planFix,
|
|
2120
|
+
planFixMany,
|
|
2082
2121
|
planInitAgent,
|
|
2083
2122
|
planMove,
|
|
2084
2123
|
planRename,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EditPlan } from './cli.js';
|
|
2
|
+
import '../reference/types.js';
|
|
3
|
+
import '../reference/gate-math.js';
|
|
4
|
+
import './manifest.js';
|
|
5
|
+
import '../curriculum/validation.js';
|
|
6
|
+
import '../reference/consistency.js';
|
|
7
|
+
import '../course-config-BLtIdkei.js';
|
|
8
|
+
import '../rules/catalog.js';
|
|
9
|
+
import '../rules/course-rules.js';
|
|
10
|
+
import './collections.js';
|
|
11
|
+
import '../reference/gate-retired-keys.js';
|
|
12
|
+
import '../reference/lint-ignore.js';
|
|
13
|
+
import '../reference/equations-validate.js';
|
|
14
|
+
import '../reference/tables-validate.js';
|
|
15
|
+
import '../reference/keyed-float-validate.js';
|
|
16
|
+
import '../reference/figures-validate.js';
|
|
17
|
+
import '../reference/diagrams-validate.js';
|
|
18
|
+
import '../reference/examples-validate.js';
|
|
19
|
+
import '../reference/folds-validate.js';
|
|
20
|
+
import '../reference/knowledge-checks-validate.js';
|
|
21
|
+
import '../reference/raw-floats-validate.js';
|
|
22
|
+
import '../reference/lab-katex-validate.js';
|
|
23
|
+
import '../reference/mixed-content.js';
|
|
24
|
+
import '../reference/gloss.js';
|
|
25
|
+
import '../rules/waivers.js';
|
|
26
|
+
import './knowledge-version.js';
|
|
27
|
+
import './page-versions.js';
|
|
28
|
+
import './decks.js';
|
|
29
|
+
import '../reference/decks-validate.js';
|
|
30
|
+
import '../reference/deck-source.js';
|
|
31
|
+
import './lesson-derivation.js';
|
|
32
|
+
import './render-diagnostics.js';
|
|
33
|
+
import '../rules/in-effect.js';
|
|
34
|
+
|
|
35
|
+
type ReleaseBump = 'major' | 'minor' | 'patch';
|
|
36
|
+
interface ReleaseEdit {
|
|
37
|
+
readonly from: string;
|
|
38
|
+
readonly to: string;
|
|
39
|
+
readonly bump: ReleaseBump;
|
|
40
|
+
/** True when the bump was read from the section's headings, not asked for. */
|
|
41
|
+
readonly inferred: boolean;
|
|
42
|
+
readonly changelog: string;
|
|
43
|
+
readonly config: string;
|
|
44
|
+
}
|
|
45
|
+
/** The release of these two files' text on `date` (`YYYY-MM-DD`); `null` when `[Unreleased]` has no entry. */
|
|
46
|
+
declare function releaseEdit(changelog: string, config: string, date: string, requested?: ReleaseBump): ReleaseEdit | null;
|
|
47
|
+
declare function planContentRelease(requested?: string, date?: string): EditPlan;
|
|
48
|
+
|
|
49
|
+
export { type ReleaseBump, type ReleaseEdit, planContentRelease, releaseEdit };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ContentCliError } from "./cli.js";
|
|
4
|
+
const CHANGELOG = "content/CHANGELOG.md";
|
|
5
|
+
const CONFIG = "content/course.config.ts";
|
|
6
|
+
function releaseEdit(changelog, config, date, requested) {
|
|
7
|
+
const unreleased = /^## \[Unreleased\]\r?$/m.exec(changelog);
|
|
8
|
+
if (!unreleased) throw new ContentCliError("no-changelog", `${CHANGELOG} has no "## [Unreleased]" heading.`);
|
|
9
|
+
const bodyStart = unreleased.index + unreleased[0].length;
|
|
10
|
+
const released = /^## \[\d+\.\d+\.\d+\](?: - .*?)?\r?$/m.exec(changelog.slice(bodyStart));
|
|
11
|
+
if (!released) throw new ContentCliError("no-changelog", `${CHANGELOG} has no released "## [x.y.z]" heading below "## [Unreleased]".`);
|
|
12
|
+
const bodyEnd = bodyStart + released.index;
|
|
13
|
+
const body = changelog.slice(bodyStart, bodyEnd);
|
|
14
|
+
if (!/^-\s+/m.test(body)) return null;
|
|
15
|
+
const version = /(about:\s*\{[\s\S]*?\bversion:\s*['"])(\d+)\.(\d+)\.(\d+)(['"])/.exec(config);
|
|
16
|
+
if (!version) throw new ContentCliError("unknown-version", `No about.version "x.y.z" in ${CONFIG}.`);
|
|
17
|
+
const [major, minor, patch] = version.slice(2, 5).map(Number);
|
|
18
|
+
const implied = /^### (Breaking|Removed)\s*$/m.test(body) ? "major" : /^### Added\s*$/m.test(body) ? "minor" : "patch";
|
|
19
|
+
const bump = requested ?? implied;
|
|
20
|
+
const to = bump === "major" ? `${major + 1}.0.0` : bump === "minor" ? `${major}.${minor + 1}.0` : `${major}.${minor}.${patch + 1}`;
|
|
21
|
+
return {
|
|
22
|
+
from: `${major}.${minor}.${patch}`,
|
|
23
|
+
to,
|
|
24
|
+
bump,
|
|
25
|
+
inferred: requested === void 0,
|
|
26
|
+
changelog: `${changelog.slice(0, bodyStart)}
|
|
27
|
+
|
|
28
|
+
## [${to}] - ${date}${body}${changelog.slice(bodyEnd)}`,
|
|
29
|
+
config: config.replace(version[0], `${version[1]}${to}${version[5]}`)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function localDate(now = /* @__PURE__ */ new Date()) {
|
|
33
|
+
return [now.getFullYear(), now.getMonth() + 1, now.getDate()].map((n, i) => String(n).padStart(i === 0 ? 4 : 2, "0")).join("-");
|
|
34
|
+
}
|
|
35
|
+
function planContentRelease(requested, date = localDate()) {
|
|
36
|
+
if (requested !== void 0 && requested !== "major" && requested !== "minor" && requested !== "patch") {
|
|
37
|
+
throw new ContentCliError("usage", `content release takes major, minor or patch; got ${requested}.`);
|
|
38
|
+
}
|
|
39
|
+
for (const file of [CHANGELOG, CONFIG]) {
|
|
40
|
+
if (!existsSync(join(process.cwd(), file))) throw new ContentCliError("no-changelog", `No ${file} in this course.`);
|
|
41
|
+
}
|
|
42
|
+
const changelog = readFileSync(join(process.cwd(), CHANGELOG), "utf8");
|
|
43
|
+
const config = readFileSync(join(process.cwd(), CONFIG), "utf8");
|
|
44
|
+
const edit = releaseEdit(changelog, config, date, requested);
|
|
45
|
+
if (edit === null) {
|
|
46
|
+
return { summary: ["## [Unreleased] has no entries: nothing to release"], edits: [], next: [] };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
summary: [
|
|
50
|
+
`about.version ${edit.from} \u2192 ${edit.to} (${edit.bump}${edit.inferred ? ", from the section's headings" : ""})`,
|
|
51
|
+
`${CHANGELOG}: ## [Unreleased] \u2192 ## [${edit.to}] - ${date}, a fresh ## [Unreleased] above it`
|
|
52
|
+
],
|
|
53
|
+
edits: [
|
|
54
|
+
{ file: CHANGELOG, before: changelog, after: edit.changelog },
|
|
55
|
+
{ file: CONFIG, before: config, after: edit.config }
|
|
56
|
+
],
|
|
57
|
+
next: ["pnpm validate:content (records the release in content/versions.json)", "commit the changelog, the config and content/versions.json together"]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
planContentRelease,
|
|
62
|
+
releaseEdit
|
|
63
|
+
};
|
|
@@ -178,9 +178,9 @@ declare const trackSchema: z.ZodObject<{
|
|
|
178
178
|
declare const sourceSchema: z.ZodObject<{
|
|
179
179
|
id: z.ZodString;
|
|
180
180
|
type: z.ZodEnum<{
|
|
181
|
+
paper: "paper";
|
|
181
182
|
primary: "primary";
|
|
182
183
|
"official-guidance": "official-guidance";
|
|
183
|
-
paper: "paper";
|
|
184
184
|
book: "book";
|
|
185
185
|
secondary: "secondary";
|
|
186
186
|
}>;
|
|
@@ -359,9 +359,9 @@ declare const CONTENT_SCHEMAS: {
|
|
|
359
359
|
readonly sources: z.ZodObject<{
|
|
360
360
|
id: z.ZodString;
|
|
361
361
|
type: z.ZodEnum<{
|
|
362
|
+
paper: "paper";
|
|
362
363
|
primary: "primary";
|
|
363
364
|
"official-guidance": "official-guidance";
|
|
364
|
-
paper: "paper";
|
|
365
365
|
book: "book";
|
|
366
366
|
secondary: "secondary";
|
|
367
367
|
}>;
|
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
resolveKnowledgeVersion
|
|
113
113
|
} from "./knowledge-version.js";
|
|
114
114
|
import {
|
|
115
|
+
deckStaleDiagnostics,
|
|
115
116
|
nextPageVersions,
|
|
116
117
|
PAGE_VERSIONS_FILE,
|
|
117
118
|
pageSourceDigest,
|
|
@@ -1170,6 +1171,32 @@ async function compileManifest(courseConfig) {
|
|
|
1170
1171
|
disclaimer: courseConfig.about.disclaimer,
|
|
1171
1172
|
...courseConfig.about.changelogHref === void 0 ? {} : { changelogHref: courseConfig.about.changelogHref }
|
|
1172
1173
|
};
|
|
1174
|
+
const lessonsWithSince = lessons.map((lesson) => ({ ...lesson, since: sinceOfPage(lesson.slug) }));
|
|
1175
|
+
const decksWithSince = deckModel.decks.map((deck) => {
|
|
1176
|
+
const page = pageInputById.get(deck.pageId);
|
|
1177
|
+
return {
|
|
1178
|
+
...deck,
|
|
1179
|
+
since: sinceOfPage(deck.slug),
|
|
1180
|
+
notation: lessonNotationBundle(
|
|
1181
|
+
bundlesByLesson.get(deck.pageId),
|
|
1182
|
+
definitionsById,
|
|
1183
|
+
page === void 0 ? [] : lessonNotationResolution(page, definitionsById, availableDefinitionIds),
|
|
1184
|
+
cardsByKey,
|
|
1185
|
+
// A deck shows its lessons' symbols: the derived cards (rule N23)
|
|
1186
|
+
// its chapters' bodies and checks use ship with it, as its bundle
|
|
1187
|
+
// holds the chapters' entries.
|
|
1188
|
+
[
|
|
1189
|
+
...indexGate.derivedByLesson.get(deck.pageId) ?? [],
|
|
1190
|
+
...(page?.scopeLessonIds ?? []).flatMap((lessonId) => [
|
|
1191
|
+
...indexGate.derivedByLesson.get(lessonId) ?? [],
|
|
1192
|
+
...assessmentDerived.get(lessonId)?.values() ?? []
|
|
1193
|
+
])
|
|
1194
|
+
].filter((use, index, all) => all.findIndex((other) => other.key === use.key) === index)
|
|
1195
|
+
)
|
|
1196
|
+
};
|
|
1197
|
+
});
|
|
1198
|
+
const chapterSince = new Map(lessonsWithSince.map((lesson) => [lesson.slug, lesson.since]));
|
|
1199
|
+
mathIssues.push(...deckStaleDiagnostics(decksWithSince, chapterSince));
|
|
1173
1200
|
return {
|
|
1174
1201
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
1175
1202
|
lessonExports: normalizeLessonExports(courseConfig),
|
|
@@ -1182,29 +1209,7 @@ async function compileManifest(courseConfig) {
|
|
|
1182
1209
|
readingMemory: normalizeReadingMemory(courseConfig),
|
|
1183
1210
|
appearance: normalizeAppearance(courseConfig),
|
|
1184
1211
|
deck: deckSettings,
|
|
1185
|
-
decks:
|
|
1186
|
-
const page = pageInputById.get(deck.pageId);
|
|
1187
|
-
return {
|
|
1188
|
-
...deck,
|
|
1189
|
-
since: sinceOfPage(deck.slug),
|
|
1190
|
-
notation: lessonNotationBundle(
|
|
1191
|
-
bundlesByLesson.get(deck.pageId),
|
|
1192
|
-
definitionsById,
|
|
1193
|
-
page === void 0 ? [] : lessonNotationResolution(page, definitionsById, availableDefinitionIds),
|
|
1194
|
-
cardsByKey,
|
|
1195
|
-
// A deck shows its lessons' symbols: the derived cards (rule N23)
|
|
1196
|
-
// its chapters' bodies and checks use ship with it, as its bundle
|
|
1197
|
-
// holds the chapters' entries.
|
|
1198
|
-
[
|
|
1199
|
-
...indexGate.derivedByLesson.get(deck.pageId) ?? [],
|
|
1200
|
-
...(page?.scopeLessonIds ?? []).flatMap((lessonId) => [
|
|
1201
|
-
...indexGate.derivedByLesson.get(lessonId) ?? [],
|
|
1202
|
-
...assessmentDerived.get(lessonId)?.values() ?? []
|
|
1203
|
-
])
|
|
1204
|
-
].filter((use, index, all) => all.findIndex((other) => other.key === use.key) === index)
|
|
1205
|
-
)
|
|
1206
|
-
};
|
|
1207
|
-
}),
|
|
1212
|
+
decks: decksWithSince,
|
|
1208
1213
|
about,
|
|
1209
1214
|
notation: {
|
|
1210
1215
|
definitions: registry.definitions,
|
|
@@ -1221,7 +1226,7 @@ async function compileManifest(courseConfig) {
|
|
|
1221
1226
|
parts: manifestParts.map(
|
|
1222
1227
|
({ overview, ...part }) => overview ? { ...part, overview: { ...overview, since: sinceOfPage(`${part.dir}/index`) } } : part
|
|
1223
1228
|
),
|
|
1224
|
-
lessons:
|
|
1229
|
+
lessons: lessonsWithSince,
|
|
1225
1230
|
pageVersions,
|
|
1226
1231
|
pages,
|
|
1227
1232
|
prereqEdges,
|
|
@@ -156,5 +156,29 @@ declare function syncPageVersions(manifest: {
|
|
|
156
156
|
readonly frozen?: boolean;
|
|
157
157
|
readonly root?: string;
|
|
158
158
|
}): PageVersionsSync;
|
|
159
|
+
/**
|
|
160
|
+
* Rule D11, shaped like a `MathGateDiagnostic` (`src/reference/gate-math.ts`)
|
|
161
|
+
* so it can join `diagnostics.math` beside D1–D10's frame codes, without
|
|
162
|
+
* this module importing from `src/reference/`.
|
|
163
|
+
*/
|
|
164
|
+
interface DeckStaleDiagnostic {
|
|
165
|
+
readonly context: 'structure';
|
|
166
|
+
readonly severity: 'info';
|
|
167
|
+
readonly code: 'deck-stale';
|
|
168
|
+
readonly file: string;
|
|
169
|
+
readonly message: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Rule D11: nothing else would notice a lesson's chapter and deck drifting
|
|
173
|
+
* apart, since two files keep each edition readable at the cost of that one
|
|
174
|
+
* check. Flags a deck whose chapter's `since` (rule C5) is newer than the
|
|
175
|
+
* deck's own. A part's deck has no one chapter to compare against, so a
|
|
176
|
+
* deck without a `chapter` (its `BuiltDeck.chapter`) is skipped.
|
|
177
|
+
*/
|
|
178
|
+
declare function deckStaleDiagnostics(decks: readonly {
|
|
179
|
+
readonly file: string;
|
|
180
|
+
readonly chapter?: string;
|
|
181
|
+
readonly since: string | null;
|
|
182
|
+
}[], chapterSince: ReadonlyMap<string, string | null>): DeckStaleDiagnostic[];
|
|
159
183
|
|
|
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 };
|
|
184
|
+
export { type DeckStaleDiagnostic, type ManifestPageVersions, type NextPageVersionsInput, PAGE_VERSIONS_FILE, PAGE_VERSIONS_FORMAT, PAGE_VERSIONS_GENERATED_BY, type PageVersionEntry, type PageVersionsRecord, type PageVersionsStaleDiagnostic, type PageVersionsSync, compareKnowledgeVersions, deckStaleDiagnostics, newestSince, nextPageVersions, pageSourceDigest, pageVersionsFrozen, readPageVersions, serializePageVersions, sinceOf, syncPageVersions, versionPhrase };
|