explico 0.8.0 → 0.11.1

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.
Files changed (230) hide show
  1. package/CHANGELOG.md +1257 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +4 -1
  15. package/dist/compiler/logging.js +4 -1
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +5 -3
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1152 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1467 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -0,0 +1,3 @@
1
+ import './catalog.js';
2
+ import './course-rules.js';
3
+ export { e as ConfigDiagnostic, j as CourseRulesConfig, M as ManifestRules, R as ResolvedRules, k as RuleSettingsInput, l as catalogIds, m as disabledCodes, n as dropDisabled, p as partPageLimits, r as resolveRules, o as rulesJsonSchema, q as rulesSnapshot } from '../course-config-Dsnri9Tk.js';
@@ -0,0 +1,290 @@
1
+ import {
2
+ CATALOG,
3
+ ELEMENTS,
4
+ enforcementOf,
5
+ entryById,
6
+ isCourseRuleId,
7
+ retiredRule,
8
+ ruleById,
9
+ rules as catalogRules
10
+ } from "./catalog.js";
11
+ import { resolveCourseRules } from "./course-rules.js";
12
+ const LEGACY_KEYS = {
13
+ operators: "N8",
14
+ romanLabels: "N10",
15
+ abbreviations: "N14"
16
+ };
17
+ const EXTRA_VALIDATION = {
18
+ S2: (s) => {
19
+ for (const slot of ["bridge", "scope"]) {
20
+ const min = s[`${slot}MinWords`];
21
+ const max = s[`${slot}MaxWords`];
22
+ if (min > max) {
23
+ return `${slot}MinWords (${min}) is larger than ${slot}MaxWords (${max}).`;
24
+ }
25
+ }
26
+ return void 0;
27
+ }
28
+ };
29
+ function describe(value) {
30
+ return JSON.stringify(value) ?? String(value);
31
+ }
32
+ function validateSetting(id, spec, value) {
33
+ const at = `CourseConfig rules.${id}.${spec.key}`;
34
+ const { schema } = spec;
35
+ switch (schema.type) {
36
+ case "integer": {
37
+ if (!Number.isInteger(value)) {
38
+ throw new Error(`${at} must be an integer; received ${describe(value)}.`);
39
+ }
40
+ if (schema.minimum !== void 0 && value < schema.minimum) {
41
+ throw new Error(`${at} must be at least ${schema.minimum}; received ${describe(value)}.`);
42
+ }
43
+ return;
44
+ }
45
+ case "boolean": {
46
+ if (typeof value !== "boolean") {
47
+ throw new Error(`${at} must be true or false; received ${describe(value)}.`);
48
+ }
49
+ return;
50
+ }
51
+ case "array": {
52
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
53
+ throw new Error(`${at} must be an array of strings; received ${describe(value)}.`);
54
+ }
55
+ return;
56
+ }
57
+ }
58
+ }
59
+ function suggestion(id) {
60
+ const area = id.charAt(0).toUpperCase();
61
+ const ids = catalogRules().filter((rule) => rule.id.startsWith(area)).map((rule) => rule.id);
62
+ return ids.length === 0 ? `Rule IDs are a letter (N, V, C, S, P) and a number, e.g. N9.` : `Rules in area ${area}: ${ids.join(", ")}.`;
63
+ }
64
+ function defaultsOf(rule) {
65
+ const out = {};
66
+ for (const spec of rule.settings ?? []) out[spec.key] = spec.schema.default;
67
+ return out;
68
+ }
69
+ function resolveRules(config) {
70
+ const diagnostics = [];
71
+ const given = { ...config.rules ?? {} };
72
+ for (const [legacyKey, id] of Object.entries(LEGACY_KEYS)) {
73
+ const value = config[legacyKey];
74
+ if (value === void 0) continue;
75
+ if (given[id] !== void 0) {
76
+ throw new Error(
77
+ `CourseConfig sets both the deprecated \`${legacyKey}\` and \`rules.${id}\`; keep \`rules.${id}\` and delete \`${legacyKey}\`.`
78
+ );
79
+ }
80
+ given[id] = value;
81
+ diagnostics.push({
82
+ code: "config-deprecated-key",
83
+ severity: "warning",
84
+ message: `CourseConfig \`${legacyKey}\` is deprecated \u2014 move it to \`rules.${id}\` (same \`allow\` / \`deny\` shape). The old key is honoured for now.`
85
+ });
86
+ }
87
+ const enabled = /* @__PURE__ */ new Map();
88
+ const settings = /* @__PURE__ */ new Map();
89
+ for (const rule of catalogRules()) {
90
+ enabled.set(rule.id, true);
91
+ settings.set(rule.id, defaultsOf(rule));
92
+ }
93
+ for (const [id, block] of Object.entries(given)) {
94
+ const rule = ruleById(id);
95
+ if (rule === void 0) {
96
+ const gone = retiredRule(id);
97
+ if (gone !== void 0) {
98
+ diagnostics.push({
99
+ code: "config-retired-rule",
100
+ severity: "warning",
101
+ message: `CourseConfig rules.${id}: rule ${id} was retired on ${gone.date}${gone.into === void 0 ? "" : ` into rule ${gone.into}`}; the block has no effect. See docs/authoring/retired.md.`
102
+ });
103
+ continue;
104
+ }
105
+ const invariant = entryById(id);
106
+ if (isCourseRuleId(id)) {
107
+ throw new Error(
108
+ `CourseConfig rules.${id}: \`${id}\` is one of this course's own rules. A course rule is defined *and* switched in \`courseRules\` (its \`enabled\` key); \`rules\` is for the rules the engine ships.`
109
+ );
110
+ }
111
+ throw new Error(
112
+ invariant !== void 0 ? `CourseConfig rules.${id}: \`${id}\` is an invariant \u2014 always on, with no settings. Remove the block.` : `CourseConfig rules.${id}: no such rule. ${suggestion(id)}`
113
+ );
114
+ }
115
+ if (block === null || typeof block !== "object" || Array.isArray(block)) {
116
+ throw new Error(
117
+ `CourseConfig rules.${id} must be an object such as { "enabled": false }; received ${describe(block)}.`
118
+ );
119
+ }
120
+ const specs = new Map((rule.settings ?? []).map((spec) => [spec.key, spec]));
121
+ const resolved = defaultsOf(rule);
122
+ for (const [key, value] of Object.entries(block)) {
123
+ if (key === "enabled") {
124
+ if (typeof value !== "boolean") {
125
+ throw new Error(`CourseConfig rules.${id}.enabled must be true or false; received ${describe(value)}.`);
126
+ }
127
+ if (value === false && rule.locked !== void 0) {
128
+ throw new Error(
129
+ `CourseConfig rules.${id}: rule ${id} is locked and cannot be turned off \u2014 ${rule.locked}`
130
+ );
131
+ }
132
+ enabled.set(id, value);
133
+ continue;
134
+ }
135
+ const spec = specs.get(key);
136
+ if (spec === void 0) {
137
+ const known = [...specs.keys()];
138
+ throw new Error(
139
+ `CourseConfig rules.${id}: unknown setting \`${key}\`. ` + (known.length === 0 ? `Rule ${id} has no settings besides \`enabled\`.` : `Rule ${id} takes: enabled, ${known.join(", ")}.`)
140
+ );
141
+ }
142
+ validateSetting(id, spec, value);
143
+ resolved[key] = value;
144
+ }
145
+ const problem = EXTRA_VALIDATION[rule.id]?.(resolved);
146
+ if (problem !== void 0) throw new Error(`CourseConfig rules.${id}: ${problem}`);
147
+ settings.set(id, resolved);
148
+ }
149
+ const course = resolveCourseRules(config);
150
+ const disabled = [
151
+ ...catalogRules().map((rule) => rule.id).filter((id) => enabled.get(id) === false),
152
+ ...course.filter((rule) => !rule.enabled).map((rule) => rule.id)
153
+ ];
154
+ for (const rule of course) enabled.set(rule.id, rule.enabled);
155
+ const settingsOf = (id) => {
156
+ const found = settings.get(id);
157
+ if (found === void 0) {
158
+ if (entryById(id) !== void 0 || isCourseRuleId(id)) return {};
159
+ throw new Error(`resolveRules: no rule ${id} in the catalog.`);
160
+ }
161
+ return found;
162
+ };
163
+ return {
164
+ isEnabled: (id) => enabled.get(id) ?? true,
165
+ course,
166
+ settings: settingsOf,
167
+ number: (id, key) => {
168
+ const value = settingsOf(id)[key];
169
+ if (typeof value !== "number") throw new Error(`resolveRules: rules.${id}.${key} is not a number setting.`);
170
+ return value;
171
+ },
172
+ strings: (id, key) => {
173
+ const value = settingsOf(id)[key];
174
+ if (!Array.isArray(value)) throw new Error(`resolveRules: rules.${id}.${key} is not a string-array setting.`);
175
+ return value;
176
+ },
177
+ disabled,
178
+ diagnostics
179
+ };
180
+ }
181
+ function partPageLimits(rules) {
182
+ return {
183
+ bridge: {
184
+ minWords: rules.number("S2", "bridgeMinWords"),
185
+ maxWords: rules.number("S2", "bridgeMaxWords")
186
+ },
187
+ scope: {
188
+ minWords: rules.number("S2", "scopeMinWords"),
189
+ maxWords: rules.number("S2", "scopeMaxWords")
190
+ }
191
+ };
192
+ }
193
+ function settingSchema(spec) {
194
+ const { schema } = spec;
195
+ const base = { title: spec.title, description: spec.description };
196
+ switch (schema.type) {
197
+ case "integer":
198
+ return {
199
+ ...base,
200
+ type: "integer",
201
+ ...schema.minimum === void 0 ? {} : { minimum: schema.minimum },
202
+ default: schema.default
203
+ };
204
+ case "boolean":
205
+ return { ...base, type: "boolean", default: schema.default };
206
+ case "array":
207
+ return {
208
+ ...base,
209
+ type: "array",
210
+ items: { title: "Entry", type: "string", minLength: 1 },
211
+ default: [...schema.default]
212
+ };
213
+ }
214
+ }
215
+ function ruleSchema(rule) {
216
+ const enforcement = enforcementOf(rule);
217
+ const properties = {};
218
+ if (rule.locked === void 0) {
219
+ properties.enabled = {
220
+ title: "Enabled",
221
+ description: enforcement === "guide" ? "Off: the rule leaves the rules-in-effect page and an agent neither applies it nor flags it in review." : "Off: the check does not run, and the rule leaves the rules-in-effect page.",
222
+ type: "boolean",
223
+ default: true
224
+ };
225
+ }
226
+ for (const spec of rule.settings ?? []) properties[spec.key] = settingSchema(spec);
227
+ const description = rule.summary + (rule.locked === void 0 ? "" : ` Locked \u2014 ${rule.locked}`) + (rule.unchecked === void 0 ? "" : ` Not checked by the build: ${rule.unchecked}`);
228
+ return {
229
+ title: `${rule.id} \u2014 ${rule.short ?? rule.title}`,
230
+ description,
231
+ "x-element": rule.elements[0],
232
+ "x-elements": [...rule.elements],
233
+ "x-enforcement": rule.locked === void 0 ? enforcement : "locked",
234
+ "x-doc": `docs/authoring/${rule.doc}`,
235
+ type: "object",
236
+ additionalProperties: false,
237
+ properties
238
+ };
239
+ }
240
+ function rulesJsonSchema() {
241
+ const properties = {};
242
+ const byElement = /* @__PURE__ */ new Map();
243
+ for (const rule of catalogRules()) {
244
+ const primary = rule.elements[0];
245
+ if (!byElement.has(primary)) byElement.set(primary, []);
246
+ byElement.get(primary).push(rule);
247
+ }
248
+ for (const element of ELEMENTS) {
249
+ for (const rule of byElement.get(element) ?? []) properties[rule.id] = ruleSchema(rule);
250
+ }
251
+ return {
252
+ title: "Rules",
253
+ description: "Authoring rules this course turns off or tunes, by rule ID. List only what differs from the defaults: every rule is on, with the defaults shown, until a block here says otherwise. A locked rule has no `enabled`. Invariants (always-on syntax checks) are not listed. See docs/authoring/README.md for the index.",
254
+ type: "object",
255
+ additionalProperties: false,
256
+ properties
257
+ };
258
+ }
259
+ function catalogIds() {
260
+ return CATALOG.map((entry) => entry.id);
261
+ }
262
+ function rulesSnapshot(rules) {
263
+ const settings = {};
264
+ for (const rule of catalogRules()) settings[rule.id] = rules.settings(rule.id);
265
+ return { disabled: rules.disabled, settings, course: rules.course };
266
+ }
267
+ function disabledCodes(rules) {
268
+ const codes = /* @__PURE__ */ new Set();
269
+ for (const id of rules.disabled) {
270
+ if (isCourseRuleId(id)) codes.add(id);
271
+ for (const check of ruleById(id)?.checks ?? []) codes.add(check.code);
272
+ }
273
+ return codes;
274
+ }
275
+ function dropDisabled(diagnostics, disabled) {
276
+ if (disabled.size === 0) return [...diagnostics];
277
+ return diagnostics.filter((d) => {
278
+ const code = d.code ?? d.kind;
279
+ return code === void 0 || !disabled.has(code);
280
+ });
281
+ }
282
+ export {
283
+ catalogIds,
284
+ disabledCodes,
285
+ dropDisabled,
286
+ partPageLimits,
287
+ resolveRules,
288
+ rulesJsonSchema,
289
+ rulesSnapshot
290
+ };
@@ -0,0 +1,53 @@
1
+ import { RuleId, RuleEntry } from './catalog.js';
2
+ import { CourseRule } from './course-rules.js';
3
+
4
+ /**
5
+ * The course's own rules, when it has any — a waiver may name one (`X1`) just
6
+ * like an engine rule, so both the validation and the "silenced nothing" check
7
+ * need to see them.
8
+ */
9
+ interface WaiverRuleSet {
10
+ readonly courseRules?: readonly CourseRule[];
11
+ }
12
+ interface Waiver {
13
+ readonly file: string;
14
+ /** Line of the comment itself. */
15
+ readonly line: number;
16
+ readonly ids: readonly RuleId[];
17
+ readonly reason: string;
18
+ /** Inclusive line range of the block the waiver covers, in file coordinates. */
19
+ readonly fromLine: number;
20
+ readonly toLine: number;
21
+ }
22
+ type WaiverDiagnosticCode = 'waiver-malformed' | 'waiver-invalid' | 'waiver-unused';
23
+ interface WaiverDiagnostic {
24
+ readonly code: WaiverDiagnosticCode;
25
+ readonly severity: 'error' | 'warning';
26
+ readonly message: string;
27
+ readonly file: string;
28
+ readonly line: number;
29
+ }
30
+ interface WaiverLessonInput {
31
+ readonly body: string;
32
+ readonly bodyLine?: number;
33
+ readonly source: {
34
+ readonly file: string;
35
+ };
36
+ }
37
+ /** True when `rule` may be waived with an inline comment. */
38
+ declare function isWaivable(rule: RuleEntry): boolean;
39
+ declare function collectWaivers(lesson: WaiverLessonInput, set?: WaiverRuleSet): {
40
+ readonly waivers: Waiver[];
41
+ readonly diagnostics: WaiverDiagnostic[];
42
+ };
43
+ /** Drop the located diagnostics the waivers cover; report waivers that covered nothing. */
44
+ declare function applyWaivers<T extends {
45
+ readonly code?: string;
46
+ readonly file?: string;
47
+ readonly line?: number;
48
+ }>(diagnostics: readonly T[], waivers: readonly Waiver[], set?: WaiverRuleSet): {
49
+ readonly kept: T[];
50
+ readonly unused: WaiverDiagnostic[];
51
+ };
52
+
53
+ export { type Waiver, type WaiverDiagnostic, type WaiverDiagnosticCode, type WaiverLessonInput, type WaiverRuleSet, applyWaivers, collectWaivers, isWaivable };
@@ -0,0 +1,132 @@
1
+ import { lessonLine } from "../reference/gate-prose-notation.js";
2
+ import {
3
+ entryById,
4
+ isCourseRuleId,
5
+ retiredRule,
6
+ ruleById,
7
+ ruleIdOf
8
+ } from "./catalog.js";
9
+ import { courseRuleEntry } from "./course-rules.js";
10
+ function ruleEntry(id, set) {
11
+ const shipped = ruleById(id);
12
+ if (shipped !== void 0) return shipped;
13
+ const own = set.courseRules?.find((rule) => rule.id === id);
14
+ return own === void 0 ? void 0 : courseRuleEntry(own);
15
+ }
16
+ function isWaivable(rule) {
17
+ if (rule.locked !== void 0) return false;
18
+ return rule.checks.length === 0 || rule.checks.every((check) => check.located);
19
+ }
20
+ const WAIVER_COMMENT = /\{\/\*\s*explico-allow\b([\s\S]*?)\*\/\}/g;
21
+ const RULE_ID = /^[A-Z]\d+$/;
22
+ function notWaivableBecause(id, set) {
23
+ const rule = ruleEntry(id, set);
24
+ if (rule === void 0) {
25
+ if (entryById(id) !== void 0) {
26
+ return `\`${id}\` is an invariant \u2014 always on, never waived`;
27
+ }
28
+ const gone = retiredRule(id);
29
+ if (gone !== void 0) {
30
+ return `rule ${id} was retired on ${gone.date}${gone.into === void 0 ? "" : ` into rule ${gone.into}, which is what to name instead`}`;
31
+ }
32
+ return isCourseRuleId(id) ? `no rule \`${id}\` \u2014 the \`X\` area is this course's own, and \`courseRules\` defines no ${id}` : `no rule \`${id}\``;
33
+ }
34
+ if (rule.locked !== void 0) return `rule ${id} is locked \u2014 ${rule.locked}`;
35
+ if (!isWaivable(rule)) {
36
+ return `rule ${id} is not waivable inline \u2014 its check is corpus- or file-level; silence it per key in \`lint-ignore.yml\`, or turn it off in \`rules.${id}\``;
37
+ }
38
+ return void 0;
39
+ }
40
+ function blockAfter(body, commentEnd) {
41
+ const lines = body.split("\n");
42
+ const commentLine = body.slice(0, commentEnd).split("\n").length;
43
+ let from = commentLine + 1;
44
+ while (from <= lines.length && lines[from - 1].trim() === "") from += 1;
45
+ if (from > lines.length) return void 0;
46
+ let to = from;
47
+ while (to + 1 <= lines.length && lines[to].trim() !== "") to += 1;
48
+ return { from, to };
49
+ }
50
+ function collectWaivers(lesson, set = {}) {
51
+ const waivers = [];
52
+ const diagnostics = [];
53
+ const file = lesson.source.file;
54
+ const offset = (lesson.bodyLine ?? 1) - 1;
55
+ for (const match of lesson.body.matchAll(WAIVER_COMMENT)) {
56
+ const line = lessonLine(lesson, match.index);
57
+ const inner = (match[1] ?? "").trim();
58
+ const colon = inner.indexOf(":");
59
+ const idsText = colon === -1 ? inner : inner.slice(0, colon);
60
+ const reason = colon === -1 ? "" : inner.slice(colon + 1).trim();
61
+ const ids = idsText.split(/[,\s]+/).map((id) => id.trim()).filter(Boolean);
62
+ if (ids.length === 0 || !ids.every((id) => RULE_ID.test(id)) || reason === "") {
63
+ diagnostics.push({
64
+ code: "waiver-malformed",
65
+ severity: "error",
66
+ file,
67
+ line,
68
+ message: `a waiver is \`{/* explico-allow <ID>[, <ID>]: <reason> */}\` \u2014 rule IDs, a colon, and a non-empty reason; got ${JSON.stringify(inner)}.`
69
+ });
70
+ continue;
71
+ }
72
+ const problems = ids.map((id) => notWaivableBecause(id, set)).filter((p) => p !== void 0);
73
+ if (problems.length > 0) {
74
+ diagnostics.push({
75
+ code: "waiver-invalid",
76
+ severity: "error",
77
+ file,
78
+ line,
79
+ message: `waiver cannot apply \u2014 ${problems.join("; ")}.`
80
+ });
81
+ continue;
82
+ }
83
+ const block = blockAfter(lesson.body, match.index + match[0].length);
84
+ waivers.push({
85
+ file,
86
+ line,
87
+ ids,
88
+ reason,
89
+ fromLine: block === void 0 ? line : block.from + offset,
90
+ toLine: block === void 0 ? line : block.to + offset
91
+ });
92
+ }
93
+ return { waivers, diagnostics };
94
+ }
95
+ function applyWaivers(diagnostics, waivers, set = {}) {
96
+ if (waivers.length === 0) return { kept: [...diagnostics], unused: [] };
97
+ const used = new Map(waivers.map((w) => [w, /* @__PURE__ */ new Set()]));
98
+ const kept = diagnostics.filter((diagnostic) => {
99
+ if (diagnostic.code === void 0 || diagnostic.file === void 0 || diagnostic.line === void 0) {
100
+ return true;
101
+ }
102
+ const ownerId = ruleIdOf(diagnostic.code);
103
+ if (ownerId === void 0) return true;
104
+ for (const waiver of waivers) {
105
+ if (waiver.file === diagnostic.file && diagnostic.line >= waiver.fromLine && diagnostic.line <= waiver.toLine && waiver.ids.includes(ownerId)) {
106
+ used.get(waiver).add(ownerId);
107
+ return false;
108
+ }
109
+ }
110
+ return true;
111
+ });
112
+ const unused = [];
113
+ for (const waiver of waivers) {
114
+ const silent = waiver.ids.filter(
115
+ (id) => (ruleEntry(id, set)?.checks.length ?? 0) > 0 && !used.get(waiver).has(id)
116
+ );
117
+ if (silent.length === 0) continue;
118
+ unused.push({
119
+ code: "waiver-unused",
120
+ severity: "warning",
121
+ file: waiver.file,
122
+ line: waiver.line,
123
+ message: `waiver for ${silent.join(", ")} silenced nothing on lines ${waiver.fromLine}\u2013${waiver.toLine}; remove it, or move it above the block it is for.`
124
+ });
125
+ }
126
+ return { kept, unused };
127
+ }
128
+ export {
129
+ applyWaivers,
130
+ collectWaivers,
131
+ isWaivable
132
+ };
@@ -15,6 +15,11 @@
15
15
  interface PreferenceStore {
16
16
  get(key: string): string | null;
17
17
  set(key: string, value: string): void;
18
+ /**
19
+ * Forget `key`. Optional, so a store written for an older version still
20
+ * fits; a caller without it writes an empty value instead.
21
+ */
22
+ remove?(key: string): void;
18
23
  }
19
24
  /**
20
25
  * A `localStorage`-backed store. Every call is wrapped: storage can be absent
@@ -12,6 +12,12 @@ function createBrowserPreferenceStore() {
12
12
  window.localStorage.setItem(key, value);
13
13
  } catch {
14
14
  }
15
+ },
16
+ remove(key) {
17
+ try {
18
+ window.localStorage.removeItem(key);
19
+ } catch {
20
+ }
15
21
  }
16
22
  };
17
23
  }
@@ -21,6 +27,9 @@ function createMemoryPreferenceStore() {
21
27
  get: (key) => map.get(key) ?? null,
22
28
  set: (key, value) => {
23
29
  map.set(key, value);
30
+ },
31
+ remove: (key) => {
32
+ map.delete(key);
24
33
  }
25
34
  };
26
35
  }
@@ -17,12 +17,12 @@ consumer-visible surface is:
17
17
  | Surface | Where |
18
18
  | --- | --- |
19
19
  | Published module entry points | `package.json` `publishConfig.exports` — the `explico/*` specifiers and their `.d.ts` |
20
- | Astro / React components | names + props of everything under `components/` that the README wires |
20
+ | Astro / React components | names + props of everything under `components/` that the README wires, and the functions a component documents for authors to call (`setFigureState` in `components/figure/figure-state.ts`) |
21
21
  | remark / rehype adapter options | the option objects of `reference/*.mjs` (`remarkNotation`, `remarkCitation`, `createNotationKatexOptions`, …) |
22
22
  | The compiler manifest | `MANIFEST_SCHEMA_VERSION` and the shape of `Manifest` |
23
23
  | The `content` CLI | the commands, their flags, and every diagnostic `code` string |
24
24
  | `CourseConfig` | the interface in `compiler/course-config.ts` |
25
- | Build-enforced authoring rules | the `N*` / `V*` / `C*` rules marked *Build-enforced* in `docs/authoring/` |
25
+ | Build-enforced authoring rules | the `N*` / `V*` / `C*` / `S*` / `P*` rules the catalog marks *script* or *partial* (`src/rules/catalog.ts`, indexed in `docs/authoring/README.md`). The `X*` area is a course's own (`courseRules`) and is not an engine surface — but the shape it is written in is, through `CourseConfig` |
26
26
 
27
27
  ## What each bump means
28
28
 
@@ -105,7 +105,9 @@ applies every intervening section in order.
105
105
  the `migrating.md` section.
106
106
  6. Run `pnpm build && pnpm test`, then inspect `pnpm pack --dry-run` and confirm
107
107
  that `docs/authoring/migrating.md` and `docs/authoring/agents.md` are listed.
108
- 7. Commit, then `git tag vx.y.z`.
108
+ 7. Commit, then tag it annotated: `git tag -a vx.y.z -m "explico x.y.z"`. With
109
+ `push.followTags` on, a plain `git push` sends an annotated tag with its
110
+ commit; a lightweight tag never travels that way.
109
111
 
110
112
  ## When you change the engine
111
113