explico 0.12.0 → 0.13.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 (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -25,6 +25,9 @@
25
25
  * on any renderability error (unparseable MDX, KaTeX error, unresolved
26
26
  * notation, unknown `[[key]]`, unknown citation), on top of the manifest's
27
27
  * schema / ref / math / equation diagnostics for that lesson.
28
+ * - Beside the manifest's diagnostics, every command lists what the last
29
+ * `astro build` found on the rendered pages (`render-diagnostics.ts`),
30
+ * under the `render:` source.
28
31
  */
29
32
 
30
33
  import { execFileSync } from 'node:child_process';
@@ -37,6 +40,7 @@ import {
37
40
  parseFrontmatter,
38
41
  } from '@astrojs/markdown-remark';
39
42
  import rehypeKatex from 'rehype-katex';
43
+ import remarkDirective from 'remark-directive';
40
44
  import remarkMath from 'remark-math';
41
45
  import remarkMdx from 'remark-mdx';
42
46
 
@@ -51,6 +55,7 @@ import remarkShortNote from '../reference/remark-short-note.mjs';
51
55
  import type { DiagnosticSeverity } from '../reference/types';
52
56
  import type { EquationLink } from '../reference/gate-math';
53
57
  import type { Manifest, ManifestLesson } from './manifest';
58
+ import { readRenderDiagnostics, type RenderDiagnostic } from './render-diagnostics';
54
59
  import { isLessonSlug, lessonIdFromSlug } from './lesson-derivation';
55
60
  import { isCourseRuleId, ownerOf, ELEMENTS, type Element, type Enforcement } from '../rules/catalog';
56
61
  import {
@@ -59,6 +64,7 @@ import {
59
64
  type RuleInEffect,
60
65
  type RulesFilter,
61
66
  } from '../rules/in-effect';
67
+ import { RESERVED_KEY_PREFIXES, reservedKeyPrefix } from '../reference/references';
62
68
 
63
69
  const REPOSITORY_ROOT = process.cwd();
64
70
  // Content and its course math are the top-level `content/` folder (Phase F).
@@ -132,6 +138,17 @@ function withOwner(diagnostic: CliDiagnostic): CliDiagnostic {
132
138
  return { ...diagnostic, ...ownerFields(diagnostic.code) };
133
139
  }
134
140
 
141
+ /** A finding the last `astro build` made on a rendered page, as a CLI diagnostic. */
142
+ function renderedCliDiagnostic(diagnostic: RenderDiagnostic): CliDiagnostic {
143
+ return {
144
+ code: `render:${diagnostic.code}`,
145
+ ...ownerFields(diagnostic.code),
146
+ severity: diagnosticSeverity(diagnostic.severity),
147
+ message: diagnostic.message,
148
+ file: diagnostic.file,
149
+ };
150
+ }
151
+
135
152
  function mentions(message: string, lessonId: string, file: string): boolean {
136
153
  return (
137
154
  message.includes(lessonId) ||
@@ -149,10 +166,19 @@ function mentions(message: string, lessonId: string, file: string): boolean {
149
166
  export function lessonDiagnostics(
150
167
  manifest: Manifest,
151
168
  lesson: ManifestLesson,
169
+ rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
152
170
  ): CliDiagnostic[] {
153
171
  const out: CliDiagnostic[] = [];
154
172
  const { file, id } = lesson;
155
173
  const assessments = new Set(lesson.assessments);
174
+ // The shared entries this page shows a card of: a fault in one (a formula
175
+ // naming an unknown key, say) is the page's too, as `validate:content` says.
176
+ const bundle = new Set(lesson.notation.definitionIds);
177
+ const entryFiles = new Set(
178
+ manifest.notation.definitions
179
+ .filter((definition) => definition.scope.kind === 'shared' && bundle.has(definition.id))
180
+ .map((definition) => definition.source.file),
181
+ );
156
182
  const withFile = (value?: string) =>
157
183
  value === undefined ? {} : { file: value };
158
184
 
@@ -174,6 +200,7 @@ export function lessonDiagnostics(
174
200
  if (
175
201
  entry.lessonId !== id &&
176
202
  entryFile !== file &&
203
+ !(entryFile !== undefined && entryFiles.has(entryFile)) &&
177
204
  !mentions(entry.message, id, file)
178
205
  ) {
179
206
  continue;
@@ -189,6 +216,7 @@ export function lessonDiagnostics(
189
216
  for (const diagnostic of manifest.diagnostics.math) {
190
217
  const forLesson =
191
218
  diagnostic.file === file ||
219
+ entryFiles.has(diagnostic.file) ||
192
220
  (diagnostic.context === 'assessment' &&
193
221
  [...assessments].some((assessmentId) =>
194
222
  diagnostic.file.startsWith(`${assessmentId} `),
@@ -257,6 +285,11 @@ export function lessonDiagnostics(
257
285
  });
258
286
  }
259
287
 
288
+ for (const diagnostic of rendered) {
289
+ if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
290
+ out.push(renderedCliDiagnostic(diagnostic));
291
+ }
292
+
260
293
  return out
261
294
  .map(withOwner)
262
295
  .sort(
@@ -280,6 +313,11 @@ function processorFor(manifest: Manifest) {
280
313
  // as the real Astro/MDX build, so `remarkNotation` sees the same tree
281
314
  // shape here that it does there (D7/F-tables-figures parity).
282
315
  remarkMdx,
316
+ // Starlight parses `:::note` asides as directives in the real build
317
+ // (on every page, not only its own `src/content/docs/`); without this
318
+ // an aside's body read as top-level lesson flow here, so its equation
319
+ // took a number the build does not give it.
320
+ remarkDirective,
283
321
  remarkMath,
284
322
  remarkShortNote,
285
323
  remarkMemo,
@@ -303,9 +341,12 @@ function processorFor(manifest: Manifest) {
303
341
  return processorPromise;
304
342
  }
305
343
 
306
- /** Drop `import` / `export` lines so they don't render as stray prose. */
344
+ /**
345
+ * Empty `import` / `export` lines so they don't render as stray prose. The
346
+ * line itself stays, so a render error names the line it has in the file.
347
+ */
307
348
  function stripEsm(body: string): string {
308
- return body.replace(/^(?:import|export)\s[^\n]*\n?/gm, '');
349
+ return body.replace(/^(?:import|export)\s[^\n]*/gm, '');
309
350
  }
310
351
 
311
352
  /** Classify a render error by which pipeline stage raised it. */
@@ -347,22 +388,60 @@ export async function renderLesson(
347
388
  relative(DOCS_ROOT, input.path).replace(/\\/g, '/'),
348
389
  );
349
390
  try {
350
- await processor.render(stripEsm(input.body), {
351
- frontmatter: { ...input.frontmatter, lessonId },
352
- fileURL: pathToFileURL(input.path),
353
- });
391
+ await withoutParseLogs(() =>
392
+ processor.render(stripEsm(input.body), {
393
+ frontmatter: { ...input.frontmatter, lessonId },
394
+ fileURL: pathToFileURL(input.path),
395
+ }),
396
+ );
354
397
  return { rendered: true, diagnostics: [] };
355
398
  } catch (error) {
356
399
  const message = error instanceof Error ? error.message : String(error);
400
+ // A VFileMessage knows its place in the file (the body keeps the
401
+ // frontmatter's lines, `lessonSource`).
402
+ const { line, column } = (error ?? {}) as { line?: unknown; column?: unknown };
357
403
  return {
358
404
  rendered: false,
359
405
  diagnostics: [
360
- { code: renderErrorCode(message), severity: 'error', message },
406
+ {
407
+ code: renderErrorCode(message),
408
+ severity: 'error',
409
+ message,
410
+ ...(typeof line === 'number' ? { line } : {}),
411
+ ...(typeof column === 'number' ? { column } : {}),
412
+ },
361
413
  ],
362
414
  };
363
415
  }
364
416
  }
365
417
 
418
+ /**
419
+ * Astro's processor `console.error`s a failed render (the whole VFileMessage,
420
+ * its syntax tree included) before rethrowing it; the check reports the error
421
+ * itself, once, as a diagnostic. Renders run concurrently, so the swap is
422
+ * counted and only that one log is dropped.
423
+ */
424
+ let quietRenders = 0;
425
+ let consoleError: typeof console.error | undefined;
426
+ async function withoutParseLogs<T>(work: () => Promise<T>): Promise<T> {
427
+ if (quietRenders++ === 0) {
428
+ const original = console.error;
429
+ consoleError = original;
430
+ console.error = (...args: unknown[]) => {
431
+ if (args[0] instanceof Error && args[0].message.startsWith('Failed to parse Markdown file')) return;
432
+ original(...args);
433
+ };
434
+ }
435
+ try {
436
+ return await work();
437
+ } finally {
438
+ if (--quietRenders === 0 && consoleError !== undefined) {
439
+ console.error = consoleError;
440
+ consoleError = undefined;
441
+ }
442
+ }
443
+ }
444
+
366
445
  // --- token estimate ----------------------------------------------------
367
446
 
368
447
  /** Rough token estimate — ~4 chars/token, the usual GPT-family heuristic. */
@@ -461,6 +540,22 @@ function lessonRaw(lesson: ManifestLesson): {
461
540
  };
462
541
  }
463
542
 
543
+ /**
544
+ * A lesson file's frontmatter and its body for rendering: the frontmatter is
545
+ * blanked with spaces, as Astro's MDX plugin does, so line N of the body is
546
+ * line N of the file and a render error points where the build's would.
547
+ */
548
+ export function lessonSource(text: string): {
549
+ frontmatter: Record<string, unknown>;
550
+ body: string;
551
+ } {
552
+ const parsed = parseFrontmatter(text, { frontmatter: 'empty-with-spaces' });
553
+ return {
554
+ frontmatter: (parsed.frontmatter ?? {}) as Record<string, unknown>,
555
+ body: parsed.content,
556
+ };
557
+ }
558
+
464
559
  // --- domain signatures ------------------------------------------------
465
560
 
466
561
  /**
@@ -546,10 +641,6 @@ export interface StatusReport {
546
641
  readonly keyedDiagrams: number;
547
642
  readonly keyedExamples: number;
548
643
  };
549
- readonly draftDebt: {
550
- readonly lessons: readonly string[];
551
- readonly notation: readonly string[];
552
- };
553
644
  readonly orphans: readonly CliDiagnostic[];
554
645
  readonly diagnostics: {
555
646
  readonly errors: number;
@@ -558,16 +649,10 @@ export interface StatusReport {
558
649
  };
559
650
  }
560
651
 
561
- export function contentStatus(manifest: Manifest): StatusReport {
562
- const draftLessons = manifest.lessons
563
- .filter((lesson) => lesson.status === 'draft')
564
- .map((lesson) => lesson.id)
565
- .sort();
566
- const draftNotation = manifest.notation.definitions
567
- .filter((definition) => definition.status === 'draft')
568
- .map((definition) => definition.id)
569
- .sort();
570
-
652
+ export function contentStatus(
653
+ manifest: Manifest,
654
+ rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
655
+ ): StatusReport {
571
656
  const orphans: CliDiagnostic[] = manifest.notation.diagnostics
572
657
  .filter((diagnostic) => diagnostic.code === 'unused-definition')
573
658
  .map((diagnostic) => ({
@@ -598,6 +683,7 @@ export function contentStatus(manifest: Manifest): StatusReport {
598
683
  ...manifest.diagnostics.knowledge.map((d) => d.severity),
599
684
  ...manifest.diagnostics.config.map((d) => d.severity),
600
685
  ...manifest.diagnostics.waivers.map((d) => d.severity),
686
+ ...rendered.map((d) => diagnosticSeverity(d.severity)),
601
687
  ];
602
688
 
603
689
  const keyedEquations = manifest.lessons.reduce(
@@ -639,7 +725,6 @@ export function contentStatus(manifest: Manifest): StatusReport {
639
725
  keyedDiagrams,
640
726
  keyedExamples,
641
727
  },
642
- draftDebt: { lessons: draftLessons, notation: draftNotation },
643
728
  orphans,
644
729
  diagnostics: {
645
730
  errors: allDiagnostics.filter((severity) => severity === 'error').length,
@@ -699,10 +784,14 @@ const SEVERITY_RANK: Readonly<Record<DiagnosticSeverity, number>> = {
699
784
  error: 2,
700
785
  };
701
786
 
702
- /** Manifest diagnostics at or above a severity, normalized for tooling. */
787
+ /**
788
+ * Manifest diagnostics at or above a severity, and those the last
789
+ * `astro build` found on rendered pages, normalized for tooling.
790
+ */
703
791
  export function contentDiagnostics(
704
792
  manifest: Manifest,
705
793
  minimumSeverity: DiagnosticThreshold = 'info',
794
+ rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
706
795
  ): DiagnosticsReport {
707
796
  const lessonFileByHint = new Map<string, string>();
708
797
  for (const lesson of manifest.lessons) {
@@ -762,6 +851,10 @@ export function contentDiagnostics(
762
851
  });
763
852
  }
764
853
  }
854
+ for (const diagnostic of rendered) {
855
+ const cli = renderedCliDiagnostic(diagnostic);
856
+ if (SEVERITY_RANK[cli.severity] >= SEVERITY_RANK[minimumSeverity]) diagnostics.push(cli);
857
+ }
765
858
 
766
859
  diagnostics.sort(
767
860
  (left, right) =>
@@ -779,8 +872,11 @@ export function contentDiagnostics(
779
872
  }
780
873
 
781
874
  /** Every warning exactly; errors and informational advice are excluded. */
782
- export function contentWarnings(manifest: Manifest): WarningsReport {
783
- const warnings = contentDiagnostics(manifest, 'warning').diagnostics.filter(
875
+ export function contentWarnings(
876
+ manifest: Manifest,
877
+ rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
878
+ ): WarningsReport {
879
+ const warnings = contentDiagnostics(manifest, 'warning', rendered).diagnostics.filter(
784
880
  (diagnostic) => diagnostic.severity === 'warning',
785
881
  );
786
882
  return { count: warnings.length, warnings };
@@ -850,7 +946,6 @@ export interface ContextReport {
850
946
  readonly file: string;
851
947
  readonly title: string;
852
948
  readonly description: string;
853
- readonly status: string;
854
949
  readonly teaches: readonly string[];
855
950
  readonly requires: readonly string[];
856
951
  readonly assumptions: readonly string[];
@@ -928,7 +1023,6 @@ export function contentContext(
928
1023
  file: lesson.file,
929
1024
  title: lesson.title,
930
1025
  description: lesson.description,
931
- status: lesson.status,
932
1026
  teaches: lesson.teaches,
933
1027
  requires: lesson.requires,
934
1028
  assumptions: lesson.assumptions,
@@ -999,7 +1093,9 @@ async function checkOneLesson(
999
1093
  lesson: ManifestLesson,
1000
1094
  ): Promise<CheckLessonResult> {
1001
1095
  const manifestDiagnostics = lessonDiagnostics(manifest, lesson);
1002
- const { frontmatter, body } = lessonRaw(lesson);
1096
+ const { frontmatter, body } = lessonSource(
1097
+ readFileSync(lessonAbsolutePath(lesson), 'utf8'),
1098
+ );
1003
1099
  const render = await renderLesson(manifest, {
1004
1100
  body,
1005
1101
  frontmatter,
@@ -1057,20 +1153,26 @@ export function changedLessonIds(): string[] {
1057
1153
 
1058
1154
  export async function contentCheck(
1059
1155
  manifest: Manifest,
1060
- options: { lesson?: string; changed?: boolean },
1156
+ options: {
1157
+ lesson?: string;
1158
+ /** Several lessons, each checked; `lesson` is the one-lesson form. */
1159
+ lessons?: readonly string[];
1160
+ changed?: boolean;
1161
+ },
1061
1162
  ): Promise<CheckReport> {
1062
1163
  let targets: ManifestLesson[];
1164
+ const named = [...(options.lesson ? [options.lesson] : []), ...(options.lessons ?? [])];
1063
1165
  if (options.changed) {
1064
1166
  const ids = changedLessonIds();
1065
1167
  targets = manifest.lessons.filter((lesson) => ids.includes(lesson.id));
1066
1168
  } else {
1067
- if (!options.lesson) {
1169
+ if (named.length === 0) {
1068
1170
  throw new ContentCliError(
1069
1171
  'usage',
1070
1172
  'content check needs a <lesson> id or --changed.',
1071
1173
  );
1072
1174
  }
1073
- targets = [findLesson(manifest, options.lesson)];
1175
+ targets = named.map((id) => findLesson(manifest, id));
1074
1176
  }
1075
1177
 
1076
1178
  const lessons = await Promise.all(
@@ -1129,7 +1231,6 @@ export function scaffoldLesson(rawId: string): Scaffold {
1129
1231
  const contents = `---
1130
1232
  title: ${title}
1131
1233
  description: One sentence on what the reader can do after this lesson.
1132
- editorialStatus: draft
1133
1234
  teaches: []
1134
1235
  assumptions: []
1135
1236
  ---
@@ -1172,6 +1273,14 @@ export function scaffoldTerm(rawKey: string): Scaffold {
1172
1273
  `A notation key is lower-case, dot/hyphen separated, got ${JSON.stringify(rawKey)}.`,
1173
1274
  );
1174
1275
  }
1276
+ const reserved = reservedKeyPrefix(key);
1277
+ if (reserved !== undefined) {
1278
+ throw new ContentCliError(
1279
+ 'invalid-id',
1280
+ `A notation key may not start with ${RESERVED_KEY_PREFIXES.join(', ')}: [[${key}]] would be ` +
1281
+ `read as a reference to that kind of float. Got ${JSON.stringify(rawKey)}.`,
1282
+ );
1283
+ }
1175
1284
  const relPath = `content/notation/${key}.md`;
1176
1285
  if (existsSync(join(REPOSITORY_ROOT, relPath))) {
1177
1286
  throw new ContentCliError(
@@ -1190,13 +1299,12 @@ sources: []
1190
1299
  seeAlso: []
1191
1300
  alignment:
1192
1301
  kind: general
1193
- rationale: Draft scaffold — set a real alignment before review.
1194
- editorialStatus: draft
1302
+ rationale: Draft scaffold — set a real alignment.
1195
1303
  aiAssisted: true
1196
1304
  ---
1197
1305
 
1198
1306
  Draft definition for [[${key}]]. Replace this body and the placeholder
1199
- frontmatter before moving the entry past \`draft\`.
1307
+ frontmatter.
1200
1308
  `;
1201
1309
 
1202
1310
  return { path: relPath, contents, companions: [] };
@@ -25,7 +25,6 @@ import type {
25
25
  } from '../curriculum/validation';
26
26
  import type {
27
27
  AbbreviationDefinitionInput,
28
- EditorialStatus,
29
28
  LocalNotationDefinitionInput,
30
29
  NotationAlignment,
31
30
  NotationLessonInput,
@@ -48,6 +47,9 @@ import { parseMemoRef } from '../reference/remark-memo.mjs';
48
47
  import { parseSectionRef } from '../reference/sections.mjs';
49
48
  import { resolveLabel } from '../reference/label';
50
49
  import { resolveGlosses } from '../reference/gloss';
50
+ import { indexEntryFormula } from '../reference/derived-notation.mjs';
51
+ import { isIndexEntryLatex } from './content-schemas';
52
+ import type { ContentRecord } from '../reference/gate-retired-keys';
51
53
  import {
52
54
  deriveRequires,
53
55
  deriveSources,
@@ -194,13 +196,6 @@ function boolean(value: unknown, label: string): boolean {
194
196
  return value;
195
197
  }
196
198
 
197
- function status(value: unknown, label: string): EditorialStatus {
198
- if (value !== 'draft' && value !== 'in-review' && value !== 'reviewed') {
199
- throw new TypeError(`${label} must be draft, in-review, or reviewed`);
200
- }
201
- return value;
202
- }
203
-
204
199
  function alignment(value: unknown, label: string): NotationAlignment {
205
200
  const data = record(value, label);
206
201
  if (data.kind === 'general') {
@@ -400,7 +395,6 @@ function lessonEntries(
400
395
  const teaches = (data.teaches as string[] | undefined) ?? [];
401
396
  lessons.push({
402
397
  id: lessonIdFromSlug(slug),
403
- status: (data.editorialStatus as LessonDefinition['status']) ?? 'draft',
404
398
  requires: deriveRequires(teaches, prerequisitesById),
405
399
  teaches,
406
400
  assessments: lessonCheckIds(path),
@@ -471,6 +465,41 @@ function glossEntries(
471
465
  return glosses;
472
466
  }
473
467
 
468
+ /**
469
+ * Rule N23's optional fields: `reads` (index entry), `range`, `fixedIndex`.
470
+ * An index entry's `formula` names the symbol it indexes as `{base}`: the
471
+ * entry's own card shows it over a square bound to the entry, and the raw
472
+ * template stays as `formulaTemplate` for the derived cards.
473
+ */
474
+ function indexFields(
475
+ data: Record<string, unknown>,
476
+ at: string,
477
+ key: string,
478
+ latex: string,
479
+ formula: string | undefined,
480
+ ): {
481
+ reads?: string;
482
+ range?: string;
483
+ fixedIndex?: true;
484
+ formula?: string;
485
+ formulaTemplate?: string;
486
+ } {
487
+ const reads = optionalString(data.reads, `${at}.reads`);
488
+ const range = optionalString(data.range, `${at}.range`);
489
+ const fixedIndex =
490
+ data.fixedIndex === undefined ? false : boolean(data.fixedIndex, `${at}.fixedIndex`);
491
+ const template =
492
+ formula !== undefined && isIndexEntryLatex(latex) ? formula : undefined;
493
+ return {
494
+ ...(reads === undefined ? {} : { reads }),
495
+ ...(range === undefined ? {} : { range }),
496
+ ...(fixedIndex ? { fixedIndex: true as const } : {}),
497
+ ...(template === undefined
498
+ ? {}
499
+ : { formula: indexEntryFormula(template, key), formulaTemplate: template }),
500
+ };
501
+ }
502
+
474
503
  function localDefinition(
475
504
  raw: unknown,
476
505
  file: string,
@@ -483,10 +512,6 @@ function localDefinition(
483
512
  const meaning = meaningText(data.meaning, `${at}.meaning`);
484
513
  const formula = optionalString(data.formula, `${at}.formula`);
485
514
  const units = optionalString(data.units, `${at}.units`);
486
- const definitionText = [meaning, formula]
487
- .filter((value): value is string => value !== undefined)
488
- .join('\n');
489
-
490
515
  return {
491
516
  key,
492
517
  notation: latex,
@@ -498,11 +523,15 @@ function localDefinition(
498
523
  data.alignment === undefined
499
524
  ? { kind: 'general', rationale: 'Lesson-local symbol.' }
500
525
  : alignment(data.alignment, `${at}.alignment`),
501
- references: extractNotationReferences(definitionText, file, 'definition'),
526
+ references: [
527
+ ...extractNotationReferences(meaning, file, 'definition'),
528
+ ...(formula === undefined ? [] : extractNotationReferences(formula, file, 'formula')),
529
+ ],
502
530
  glosses: glossEntries(data.glosses, key, `${at}.glosses`),
503
531
  source: { file },
504
532
  ...(formula === undefined ? {} : { formula }),
505
533
  ...(units === undefined ? {} : { units }),
534
+ ...indexFields(data, at, key, latex, formula),
506
535
  };
507
536
  }
508
537
 
@@ -527,15 +556,22 @@ function sharedNotationEntries(): SharedNotationDefinitionInput[] {
527
556
  domain: string(data.domain, `${file} domain`),
528
557
  sources: sourceIds(data.sources, `${file} sources`),
529
558
  seeAlso: strings(data.seeAlso, `${file} seeAlso`),
530
- alignment: alignment(data.alignment, `${file} alignment`),
531
- status: status(data.editorialStatus, `${file} editorialStatus`),
532
- aiAssisted: boolean(data.aiAssisted, `${file} aiAssisted`),
559
+ // The collection schema's defaults (`compiler/content-schemas.ts`).
560
+ alignment:
561
+ data.alignment === undefined
562
+ ? { kind: 'general', rationale: 'General notation.' }
563
+ : alignment(data.alignment, `${file} alignment`),
564
+ aiAssisted:
565
+ data.aiAssisted === undefined
566
+ ? false
567
+ : boolean(data.aiAssisted, `${file} aiAssisted`),
533
568
  body,
534
569
  references: extractNotationReferences(body, file, undefined),
535
570
  glosses: glossEntries(data.glosses, key, `${file} glosses`),
536
571
  source: { file },
537
572
  ...(formula === undefined ? {} : { formula }),
538
573
  ...(units === undefined ? {} : { units }),
574
+ ...indexFields(data, file, key, string(data.latex, `${file} latex`), formula),
539
575
  };
540
576
  });
541
577
  }
@@ -629,7 +665,6 @@ function notationLessonEntries(): NotationLessonInput[] {
629
665
  );
630
666
  lessons.push({
631
667
  lessonId: lessonIdFromSlug(slug),
632
- status: status(data.editorialStatus, `${file} editorialStatus`),
633
668
  localDefinitions,
634
669
  suspended: notationSuspensions(notation.suspend, localDefinitions, file),
635
670
  references: extractNotationReferences(body, file, undefined),
@@ -680,7 +715,6 @@ export function loadNonChapterPages(): NonChapterPage[] {
680
715
  bodyLine,
681
716
  notation: {
682
717
  lessonId: id,
683
- status: status(data.editorialStatus ?? 'draft', `${file} editorialStatus`),
684
718
  localDefinitions: [],
685
719
  suspended: [],
686
720
  references: extractNotationReferences(body, file, undefined),
@@ -712,6 +746,45 @@ export function loadNotationDefinitions(): Record<string, unknown>[] {
712
746
  );
713
747
  }
714
748
 
749
+ /**
750
+ * Every content file's top-level fields and text: the frontmatter of a page,
751
+ * a notation entry or an abbreviation, and the object of a competency,
752
+ * assessment, source or track. The typed loaders above drop keys they do not
753
+ * know; the retired-key gate (`reference/gate-retired-keys.ts`) needs them.
754
+ */
755
+ export function loadContentRecords(): ContentRecord[] {
756
+ const below = (directory: string, extensions: readonly string[]) => {
757
+ const path = join(CONTENT_ROOT, directory);
758
+ return existsSync(path) ? filesBelow(path, extensions) : [];
759
+ };
760
+ const records: ContentRecord[] = [];
761
+ for (const directory of ['docs', 'notation', 'abbreviations']) {
762
+ for (const path of below(directory, ['.md', '.mdx'])) {
763
+ const text = readFileSync(path, 'utf8');
764
+ records.push({
765
+ file: relative(REPOSITORY_ROOT, path),
766
+ data: (parseFrontmatter(text).frontmatter ?? {}) as Record<string, unknown>,
767
+ text,
768
+ });
769
+ }
770
+ }
771
+ for (const directory of ['competencies', 'assessments', 'sources', 'tracks']) {
772
+ for (const path of below(directory, ['.json'])) {
773
+ const text = readFileSync(path, 'utf8');
774
+ const data: unknown = JSON.parse(text);
775
+ records.push({
776
+ file: relative(REPOSITORY_ROOT, path),
777
+ data:
778
+ data !== null && typeof data === 'object' && !Array.isArray(data)
779
+ ? (data as Record<string, unknown>)
780
+ : {},
781
+ text,
782
+ });
783
+ }
784
+ }
785
+ return records;
786
+ }
787
+
715
788
  /** Parsed `src/content/sources/*.json` records for `remark-citation`. */
716
789
  export function loadSourceRecords(): Record<string, unknown>[] {
717
790
  return jsonEntries<SourceDefinition & { readonly id: string }>(
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The content-schema check (invariant `content-schema`). Every content file
3
+ * must satisfy its collection's schema — the same zod schema `astro build`
4
+ * loads it with (`content-schemas.ts`) — so `pnpm validate:content` and
5
+ * `pnpm content check` reject what the build would, at the key's line,
6
+ * instead of passing it and leaving `astro build` to fail with
7
+ * `InvalidContentEntryDataError`.
8
+ */
9
+ import type { CurriculumIssue } from '../curriculum/validation';
10
+ import { keyLine, type ContentRecord } from '../reference/gate-retired-keys';
11
+ import { CONTENT_SCHEMAS } from './content-schemas';
12
+
13
+ export type ContentCollection = keyof typeof CONTENT_SCHEMAS;
14
+
15
+ /**
16
+ * The collection a repository-relative content file belongs to, or `null`
17
+ * for one Astro's loaders skip (`src/content-config.ts`): a docs page whose
18
+ * name starts with `_`, a notation or abbreviation file that is not `.md`.
19
+ */
20
+ export function collectionOf(file: string): ContentCollection | null {
21
+ const parts = file.replace(/\\/g, '/').split('/');
22
+ if (parts[0] !== 'content' || parts.length < 3) return null;
23
+ const directory = parts[1] ?? '';
24
+ const name = parts[parts.length - 1] ?? '';
25
+ if (!Object.hasOwn(CONTENT_SCHEMAS, directory)) return null;
26
+ const collection = directory as ContentCollection;
27
+ if (collection === 'docs') {
28
+ return /\.mdx?$/.test(name) && !name.startsWith('_') ? collection : null;
29
+ }
30
+ if (collection === 'notation' || collection === 'abbreviations') {
31
+ return name.endsWith('.md') ? collection : null;
32
+ }
33
+ return name.endsWith('.json') ? collection : null;
34
+ }
35
+
36
+ /**
37
+ * The 1-based line of the value at `path` in a YAML or JSON `text`: each name
38
+ * is searched for after the previous one, and an array index skips that many
39
+ * `- ` items of a YAML list. Falls back to the deepest line found.
40
+ */
41
+ export function lineOfPath(
42
+ text: string,
43
+ path: readonly (string | number)[],
44
+ ): number | undefined {
45
+ const first = path[0];
46
+ if (typeof first !== 'string') return undefined;
47
+ const start = keyLine(text, first);
48
+ if (start === undefined) return undefined;
49
+ const lines = text.split('\n');
50
+ let at = start - 1;
51
+ for (const segment of path.slice(1)) {
52
+ if (typeof segment === 'number') {
53
+ let seen = -1;
54
+ let found = -1;
55
+ for (let index = at + 1; index < lines.length; index += 1) {
56
+ if (/^\s*-\s/.test(lines[index] ?? '')) seen += 1;
57
+ if (seen === segment) {
58
+ found = index;
59
+ break;
60
+ }
61
+ }
62
+ if (found === -1) break;
63
+ at = found;
64
+ continue;
65
+ }
66
+ const name = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
67
+ const pattern = new RegExp(`^[\\s-]*(?:"${name}"|${name})[ \\t]*:`);
68
+ let found = -1;
69
+ for (let index = at; index < lines.length; index += 1) {
70
+ if (pattern.test(lines[index] ?? '')) {
71
+ found = index;
72
+ break;
73
+ }
74
+ }
75
+ if (found === -1) break;
76
+ at = found;
77
+ }
78
+ return at + 1;
79
+ }
80
+
81
+ /** One `content-schema` issue per schema problem in each record, in record order. */
82
+ export function validateContentSchemas(
83
+ records: readonly ContentRecord[],
84
+ ): CurriculumIssue[] {
85
+ const issues: CurriculumIssue[] = [];
86
+ for (const { file, data, text } of records) {
87
+ const collection = collectionOf(file);
88
+ if (!collection) continue;
89
+ const result = CONTENT_SCHEMAS[collection].safeParse(data);
90
+ if (result.success) continue;
91
+ for (const problem of result.error.issues) {
92
+ const path = problem.path.filter(
93
+ (segment): segment is string | number =>
94
+ typeof segment === 'string' || typeof segment === 'number',
95
+ );
96
+ const line = lineOfPath(text, path);
97
+ const at = line === undefined ? file : `${file}:${line}`;
98
+ const field = path.length > 0 ? `${path.join('.')}: ` : '';
99
+ issues.push({
100
+ kind: 'content-schema',
101
+ message: `${at}: ${field}${problem.message} (the ${collection} collection's schema, which astro build applies too)`,
102
+ file,
103
+ ...(line === undefined ? {} : { line }),
104
+ });
105
+ }
106
+ }
107
+ return issues;
108
+ }