@heroiclands/package-build 20.3.1 → 20.5.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.
Files changed (195) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/CONTENT.md +322 -110
  3. package/README.md +21 -10
  4. package/bin/content-build.mjs +204 -92
  5. package/bin/package-build.mjs +48 -15
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +518 -151
  8. package/docs/api.md +1352 -0
  9. package/docs/commands.md +1609 -0
  10. package/docs/configuration.md +1432 -0
  11. package/docs/content-format.md +73 -74
  12. package/docs/diagnostics.md +356 -0
  13. package/docs/getting-started.md +813 -0
  14. package/docs/project-setup.md +469 -0
  15. package/e2e.mjs +4 -4
  16. package/engine/actor-compiler.mjs +64 -61
  17. package/engine/address-charset.mjs +6 -6
  18. package/engine/address-diff.mjs +57 -52
  19. package/engine/anchored-sections.mjs +1 -1
  20. package/engine/anchors.mjs +1 -1
  21. package/engine/base-compiler.mjs +42 -38
  22. package/engine/bundle-notes.mjs +4 -4
  23. package/engine/bundles.mjs +17 -8
  24. package/engine/code-fences.mjs +3 -3
  25. package/engine/compendiums.mjs +8 -8
  26. package/engine/compile-corpus.mjs +4 -5
  27. package/engine/content-address.mjs +24 -24
  28. package/engine/content-charset.mjs +1 -1
  29. package/engine/content-format-check.mjs +24 -25
  30. package/engine/content-format.mjs +8 -8
  31. package/engine/content-html.mjs +202 -0
  32. package/engine/content-icons.mjs +196 -165
  33. package/engine/content-index.mjs +61 -40
  34. package/engine/content-links.mjs +74 -41
  35. package/engine/content-lint.mjs +64 -34
  36. package/engine/content-package.mjs +7 -9
  37. package/engine/content-slug.mjs +2 -2
  38. package/engine/content-tables.mjs +38 -34
  39. package/engine/content-tree.mjs +1 -1
  40. package/engine/derived-fields.mjs +174 -0
  41. package/engine/diagnostics.mjs +3 -4
  42. package/engine/document-subtypes.mjs +6 -7
  43. package/engine/field-reference.mjs +8 -8
  44. package/engine/field-spec.mjs +29 -30
  45. package/engine/folder-notes.mjs +18 -16
  46. package/engine/foreign-catalog.mjs +8 -9
  47. package/engine/foundry-entries.mjs +19 -20
  48. package/engine/frontmatter-lint.mjs +95 -103
  49. package/engine/frontmatter.mjs +11 -11
  50. package/engine/generate.mjs +44 -39
  51. package/engine/helpers.mjs +147 -84
  52. package/engine/homepage.mjs +20 -22
  53. package/engine/ids.mjs +15 -15
  54. package/engine/index-records.mjs +4 -4
  55. package/engine/index.mjs +26 -10
  56. package/engine/item-compiler.mjs +54 -29
  57. package/engine/item-docs.mjs +11 -13
  58. package/engine/item-registry.mjs +13 -13
  59. package/engine/journals.mjs +24 -15
  60. package/engine/macros.mjs +3 -3
  61. package/engine/map-notes.mjs +6 -6
  62. package/engine/metadata-index.mjs +5 -5
  63. package/engine/note-claims.mjs +51 -41
  64. package/engine/note-ids.mjs +5 -5
  65. package/engine/note-package.mjs +4 -4
  66. package/engine/note-renames.mjs +3 -3
  67. package/engine/note-schemas.mjs +3 -8
  68. package/engine/note-vocabulary.mjs +56 -54
  69. package/engine/pack-config.mjs +39 -25
  70. package/engine/pack-router.mjs +0 -0
  71. package/engine/pdf-build.mjs +464 -0
  72. package/engine/pdf-fonts.mjs +420 -0
  73. package/engine/pdf-render.mjs +798 -0
  74. package/engine/pdf-toc.mjs +525 -0
  75. package/engine/prose-config.mjs +9 -9
  76. package/engine/prose-lint.mjs +12 -13
  77. package/engine/region-events.mjs +2 -2
  78. package/engine/retired-fields.mjs +24 -22
  79. package/engine/runtime-only-fields.mjs +1 -1
  80. package/engine/scene-levels.mjs +1 -1
  81. package/engine/scenes.mjs +33 -25
  82. package/engine/schema-check.mjs +21 -21
  83. package/engine/schema-extract.mjs +1 -1
  84. package/engine/site-build.mjs +66 -50
  85. package/engine/site-index.mjs +17 -18
  86. package/engine/sql-tables.mjs +21 -13
  87. package/engine/subtype-registry.mjs +5 -4
  88. package/engine/system-block.mjs +18 -19
  89. package/engine/systems.mjs +2 -2
  90. package/engine/web-wikilinks.mjs +41 -38
  91. package/engine/wikilink-syntax.mjs +16 -16
  92. package/engine/wikilinks.mjs +40 -42
  93. package/hm3/actors.mjs +30 -22
  94. package/hm3/document-subtypes.mjs +5 -5
  95. package/hm3/index.mjs +2 -2
  96. package/hm3/item-builders.mjs +1 -1
  97. package/hm3/item-fields.mjs +3 -3
  98. package/hm3/items.mjs +28 -8
  99. package/hm3/template-priority.mjs +2 -2
  100. package/lang.mjs +3 -3
  101. package/manifest.mjs +17 -20
  102. package/package.json +1 -2
  103. package/release.mjs +66 -6
  104. package/sohl/actors.mjs +36 -28
  105. package/sohl/affiliation-standings.mjs +2 -2
  106. package/sohl/being-info.mjs +5 -5
  107. package/sohl/default-item-art.mjs +5 -5
  108. package/sohl/document-subtypes.mjs +5 -5
  109. package/sohl/index.mjs +3 -3
  110. package/sohl/item-builders.mjs +7 -7
  111. package/sohl/item-fields.mjs +8 -9
  112. package/sohl/items.mjs +28 -6
  113. package/sohl/kb-passes.mjs +5 -5
  114. package/sohl/note-schemas.mjs +12 -12
  115. package/sohl/skill-base.mjs +1 -1
  116. package/types/content-config.d.mts +91 -39
  117. package/types/e2e.d.mts +3 -3
  118. package/types/engine/actor-compiler.d.mts +45 -40
  119. package/types/engine/address-charset.d.mts +6 -6
  120. package/types/engine/address-diff.d.mts +60 -6
  121. package/types/engine/base-compiler.d.mts +27 -21
  122. package/types/engine/bundle-notes.d.mts +3 -3
  123. package/types/engine/bundles.d.mts +10 -1
  124. package/types/engine/code-fences.d.mts +3 -3
  125. package/types/engine/compendiums.d.mts +3 -3
  126. package/types/engine/compile-corpus.d.mts +1 -1
  127. package/types/engine/content-address.d.mts +20 -20
  128. package/types/engine/content-format-check.d.mts +6 -6
  129. package/types/engine/content-format.d.mts +2 -2
  130. package/types/engine/content-html.d.mts +78 -0
  131. package/types/engine/content-icons.d.mts +139 -92
  132. package/types/engine/content-index.d.mts +59 -15
  133. package/types/engine/content-links.d.mts +8 -8
  134. package/types/engine/content-lint.d.mts +8 -6
  135. package/types/engine/content-package.d.mts +6 -8
  136. package/types/engine/content-tables.d.mts +49 -18
  137. package/types/engine/derived-fields.d.mts +101 -0
  138. package/types/engine/diagnostics.d.mts +2 -2
  139. package/types/engine/document-subtypes.d.mts +3 -3
  140. package/types/engine/field-spec.d.mts +37 -39
  141. package/types/engine/folder-notes.d.mts +4 -5
  142. package/types/engine/foreign-catalog.d.mts +3 -3
  143. package/types/engine/foundry-entries.d.mts +6 -7
  144. package/types/engine/frontmatter-lint.d.mts +10 -9
  145. package/types/engine/frontmatter.d.mts +7 -7
  146. package/types/engine/generate.d.mts +7 -7
  147. package/types/engine/helpers.d.mts +128 -56
  148. package/types/engine/homepage.d.mts +16 -18
  149. package/types/engine/ids.d.mts +13 -13
  150. package/types/engine/index-records.d.mts +3 -3
  151. package/types/engine/index.d.mts +6 -0
  152. package/types/engine/item-compiler.d.mts +21 -5
  153. package/types/engine/item-docs.d.mts +2 -2
  154. package/types/engine/item-registry.d.mts +6 -6
  155. package/types/engine/journals.d.mts +12 -3
  156. package/types/engine/map-notes.d.mts +2 -2
  157. package/types/engine/metadata-index.d.mts +4 -4
  158. package/types/engine/note-claims.d.mts +30 -22
  159. package/types/engine/note-ids.d.mts +4 -4
  160. package/types/engine/note-package.d.mts +1 -1
  161. package/types/engine/note-renames.d.mts +3 -3
  162. package/types/engine/note-vocabulary.d.mts +31 -203
  163. package/types/engine/pack-config.d.mts +7 -7
  164. package/types/engine/pack-router.d.mts +1 -1
  165. package/types/engine/pdf-build.d.mts +42 -0
  166. package/types/engine/pdf-fonts.d.mts +30 -0
  167. package/types/engine/pdf-render.d.mts +144 -0
  168. package/types/engine/pdf-toc.d.mts +114 -0
  169. package/types/engine/prose-config.d.mts +9 -9
  170. package/types/engine/prose-lint.d.mts +3 -4
  171. package/types/engine/region-events.d.mts +2 -2
  172. package/types/engine/retired-fields.d.mts +10 -9
  173. package/types/engine/scenes.d.mts +10 -1
  174. package/types/engine/schema-check.d.mts +13 -13
  175. package/types/engine/site-build.d.mts +52 -25
  176. package/types/engine/site-index.d.mts +3 -4
  177. package/types/engine/sql-tables.d.mts +11 -5
  178. package/types/engine/subtype-registry.d.mts +3 -3
  179. package/types/engine/system-block.d.mts +3 -3
  180. package/types/engine/web-wikilinks.d.mts +7 -7
  181. package/types/engine/wikilink-syntax.d.mts +17 -17
  182. package/types/engine/wikilinks.d.mts +14 -14
  183. package/types/hm3/actors.d.mts +9 -1
  184. package/types/hm3/document-subtypes.d.mts +1 -1
  185. package/types/hm3/items.d.mts +9 -2
  186. package/types/hm3/template-priority.d.mts +1 -1
  187. package/types/manifest.d.mts +8 -8
  188. package/types/release.d.mts +15 -4
  189. package/types/sohl/actors.d.mts +12 -4
  190. package/types/sohl/affiliation-standings.d.mts +2 -2
  191. package/types/sohl/being-info.d.mts +2 -2
  192. package/types/sohl/document-subtypes.d.mts +1 -1
  193. package/types/sohl/items.d.mts +9 -2
  194. package/types/sohl/note-schemas.d.mts +1 -1
  195. package/MIGRATING.md +0 -608
@@ -43,19 +43,19 @@ import { resolveNoteId } from "./note-ids.mjs";
43
43
  import { loadForeignIndexes } from "./metadata-index.mjs";
44
44
  // The record accessors only — deriving records reaches the pack router and the
45
45
  // manifest emitter, which reach the compilers, which load this module. Reading
46
- // a record needs none of that (#243).
46
+ // a record needs none of that.
47
47
  import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
48
48
  import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
49
49
  // One vocabulary of link findings, and one message per class, so the three
50
- // resolvers cannot word the same defect differently (#184).
50
+ // resolvers cannot word the same defect differently.
51
51
  import { linkFindingMessage } from "./wikilink-syntax.mjs";
52
- // The declared tag vocabulary (#172), which is where `draft` is stated. Read
52
+ // The declared tag vocabulary, which is where `draft` is stated. Read
53
53
  // from there rather than respelt, so the tag and its one reader cannot drift.
54
54
  import { isDraftNote } from "./note-vocabulary.mjs";
55
55
  import { expandContentTables } from "./content-tables.mjs";
56
56
  import { positionInBody } from "./diagnostics.mjs";
57
57
  // The pure `sohl:` frontmatter readers live in a leaf module so the item-type
58
- // registry can import them without reaching back through this one (#1504).
58
+ // registry can import them without reaching back through this one.
59
59
  // Re-exported here so every existing importer keeps its single import path.
60
60
  import { getFrontmatter } from "./frontmatter.mjs";
61
61
  export {
@@ -75,9 +75,21 @@ export {
75
75
  * `html: true` is long-standing and load-bearing — notes carry raw blocks — and
76
76
  * it is also why {@link module:engine/content-icons} exists rather than an
77
77
  * instruction to write `<i class="fa-solid …">` by hand: that would render on
78
- * the two HTML surfaces and be silently dropped by the third (#378).
78
+ * the two HTML surfaces and be silently dropped by the third.
79
79
  */
80
- export const md = markdownit({ html: true }).use(iconPlugin());
80
+ export const md = markdownit({ html: true }).use(
81
+ // Resolved per render, not at import: this constant is built before any
82
+ // configuration is read, and a package's own icons live in the
83
+ // configuration. A tree with none — or a caller with no configuration to
84
+ // find — falls back to the shipped table.
85
+ iconPlugin(() => {
86
+ try {
87
+ return loadPackConfig().icons;
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ }),
92
+ );
81
93
 
82
94
  /**
83
95
  * Parses a markdown file with YAML frontmatter.
@@ -86,7 +98,7 @@ export const md = markdownit({ html: true }).use(iconPlugin());
86
98
  * `body` is the trimmed raw markdown after the frontmatter block, and
87
99
  * `description` is `body` rendered to HTML. `bodyLine` / `bodyColumn` are the
88
100
  * 1-based **file** position of the body's first character, which is what turns
89
- * an offset within `body` into a position a diagnostic can name (#17) — see
101
+ * an offset within `body` into a position a diagnostic can name — see
90
102
  * {@link positionInBody}. If the file has no frontmatter block, returns
91
103
  * `{ frontmatter: null, body: "", description: "" }` with a warn log, and no
92
104
  * position: there is no body to have one.
@@ -108,7 +120,7 @@ export function parseMarkdownFile(filePath) {
108
120
  const body = raw.trim();
109
121
  const description = body ? md.render(body) : "";
110
122
  // Where the trimmed body starts in the *file*, so an offset within it can
111
- // be reported as a file position (#17). The frontmatter's lines and the
123
+ // be reported as a file position. The frontmatter's lines and the
112
124
  // blank lines `trim()` removes both sit in between, and the trim can take
113
125
  // indentation off the first line as well — hence a column, not just a line.
114
126
  const bodyStart = content.length - raw.length + (raw.length - raw.trimStart().length);
@@ -118,28 +130,10 @@ export function parseMarkdownFile(filePath) {
118
130
  return { frontmatter, body, description, bodyLine, bodyColumn };
119
131
  }
120
132
 
121
- /**
122
- * Recursively yields every `.md` file under `rootDir`, parsed.
123
- * Yields `{ frontmatter, body, description, file, absPath, bodyLine,
124
- * bodyColumn }` for each match — the last two from
125
- * {@link parseMarkdownFile}, so a caller can report a position inside the
126
- * body as a position in the file (#17).
127
- * Silently skips directories that don't exist.
128
- *
129
- * Directory names in `skipDirectories` are ignored wherever they appear. The
130
- * walk itself knows nothing about what they mean: `Templates/` is an Obsidian
131
- * templater convention this repository's vault happens to use, not a property
132
- * of a content tree, so it is configured rather than hard-coded (#1508).
133
- *
134
- * @param {string} rootDir - Root of the tree to walk.
135
- * @param {object} [opts]
136
- * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
137
- * Defaults to the configured list.
138
- */
139
133
  /**
140
134
  * Refuse a corpus read whose scope its caller did not state.
141
135
  *
142
- * The rule of #243 in one place, so every reader of the tree refuses the same
136
+ * The rule in one place, so every reader of the tree refuses the same
143
137
  * way and says so in the same words. It is shared rather than repeated because
144
138
  * the corpus is no longer read only by {@link walkMarkdownTree}: a pass that
145
139
  * reads the content index instead is making the identical claim about which
@@ -172,7 +166,7 @@ export function assertStatedScope(skipDirectories, who) {
172
166
  * cycle. They take the records their caller already holds.
173
167
  *
174
168
  * That is not a workaround dressed up as a rule. A compile runs several passes
175
- * over one tree, and the whole of #243 is that they must not each answer "which
169
+ * over one tree, and the whole point is that they must not each answer "which
176
170
  * files are the corpus?" for themselves. Requiring the answer to be handed in
177
171
  * makes the sharing structural rather than remembered.
178
172
  *
@@ -191,8 +185,33 @@ export function assertSuppliedCorpus(records, who) {
191
185
  }
192
186
  }
193
187
 
188
+ /**
189
+ * Recursively yields every `.md` file under `rootDir`, parsed.
190
+ *
191
+ * Yields `{ frontmatter, body, description, file, absPath, bodyLine,
192
+ * bodyColumn }` for each match — the last from {@link parseMarkdownFile}, so a
193
+ * caller can report a position inside the body as a position in the file. A
194
+ * root that does not exist yields nothing, and a directory that cannot be read
195
+ * is warned about and skipped.
196
+ *
197
+ * Directory names in `skipDirectories` are ignored wherever they appear. The
198
+ * walk itself knows nothing about what they mean: `Templates/` is an Obsidian
199
+ * templater convention this repository's vault happens to use, not a property
200
+ * of a content tree, so it is stated by the caller rather than hard-coded.
201
+ *
202
+ * @param {string} rootDir - Root of the tree to walk.
203
+ * @param {object} opts
204
+ * @param {readonly string[]} opts.skipDirectories - Directory names to ignore.
205
+ * Required: the scope is the caller's to state, so two passes cannot
206
+ * disagree about which files are the corpus.
207
+ * @yields {{frontmatter: object|null, body: string, description: string,
208
+ * file: string, absPath: string, bodyLine?: number, bodyColumn?: number}}
209
+ * One entry per `.md` file found.
210
+ * @throws {Error} When `skipDirectories` is not stated — see
211
+ * {@link assertStatedScope}.
212
+ */
194
213
  export function* walkMarkdownTree(rootDir, { skipDirectories } = {}) {
195
- // Stated by the caller, never resolved here. The default this used to carry
214
+ // Stated by the caller, never resolved here. A default here
196
215
  // — `loadPackConfig().skipDirectories` — read whichever configuration
197
216
  // resolved from the working directory rather than the one the caller was
198
217
  // working under. In an ordinary build those are the same object and nothing
@@ -200,9 +219,9 @@ export function* walkMarkdownTree(rootDir, { skipDirectories } = {}) {
200
219
  // `PACKAGE_BUILD_CONFIG` names one, or when the command runs from a
201
220
  // worktree. Six of this function's twelve callers were on that default, so
202
221
  // "which files are the corpus?" had two answers depending on who asked
203
- // (#243) — the same defect class as `entriesForNote` reading
204
- // `docEntryTypes` from the ambient config, fixed in #240 after a fixture
205
- // had been passing on the leak for as long as it existed.
222
+ // — the same defect class as `entriesForNote` reading `docEntryTypes` from
223
+ // the ambient config rather than the passed one, which a fixture can pass
224
+ // on indefinitely.
206
225
  assertStatedScope(skipDirectories, "walkMarkdownTree");
207
226
  if (!fs.existsSync(rootDir)) return;
208
227
  const stack = [rootDir];
@@ -234,7 +253,7 @@ export function* walkMarkdownTree(rootDir, { skipDirectories } = {}) {
234
253
  /**
235
254
  * Every position a note may state its template priority at, for one system
236
255
  * block, in the order they answer — and whichever of them the note actually
237
- * wrote (#266).
256
+ * wrote.
238
257
  *
239
258
  * Three places, in the order the migration runs. The specification calls this
240
259
  * `data.templatePriority`; `sohl-thalorna` already writes it there on 941
@@ -290,7 +309,7 @@ function findTemplatePriority(fm, block) {
290
309
 
291
310
  /**
292
311
  * The template priority a note states, for a system that treats an unstated one
293
- * as "not a template" rather than as an authoring error (#266).
312
+ * as "not a template" rather than as an authoring error.
294
313
  *
295
314
  * Reads exactly the positions {@link resolveTemplatePriority} reads, including
296
315
  * the retiring `archetype` spelling, and refuses the same contradiction — so
@@ -324,7 +343,7 @@ export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
324
343
 
325
344
  /**
326
345
  * Resolve the required `templatePriority` frontmatter for an Item/Actor entry
327
- * (the archetype contract, #604). The property is a nullable number that
346
+ * (the archetype contract). The property is a nullable number that
328
347
  * authors must state explicitly:
329
348
  * - a number → the document is a template of that priority.
330
349
  * - `null` → the document is not a template.
@@ -333,7 +352,7 @@ export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
333
352
  *
334
353
  * Reads the positions {@link findTemplatePriority} lists: `data.templatePriority`
335
354
  * first — the specified home — then the system block and the top level, and
336
- * finally the retiring `archetype` spelling in the same two places (#266).
355
+ * finally the retiring `archetype` spelling in the same two places.
337
356
  *
338
357
  * @param {object} fm Parsed frontmatter.
339
358
  * @param {string} label Human-readable context for error messages.
@@ -371,8 +390,7 @@ export function resolveTemplatePriority(fm, label, { block = "sohl" } = {}) {
371
390
 
372
391
  /**
373
392
  * The value a document's `system.templatePriority` carries, from the required
374
- * `templatePriority` frontmatter (#126, sohl#1780, renamed off `archetype` by
375
- * #266 / sohl#1836).
393
+ * `templatePriority` frontmatter (`archetype` is the legacy spelling).
376
394
  *
377
395
  * A **schema field**, so the tri-state is written out in full rather than
378
396
  * expressed by a key's presence: a number is a template at that priority, and
@@ -404,11 +422,6 @@ export function makeFilename(name, id) {
404
422
  return `${unidecode(name)}_${id}`.replace(/[^0-9a-zA-Z]+/g, "_") + ".json";
405
423
  }
406
424
 
407
- /**
408
- * Standardize a name into a slug: lowercase, apostrophes removed,
409
- * non-alphanumerics collapsed to single hyphens.
410
- */
411
-
412
425
  /**
413
426
  * The path prefixes that name a package other than the one being compiled.
414
427
  *
@@ -459,7 +472,7 @@ function addressesAnotherPackage(s) {
459
472
  *
460
473
  * Content frontmatter (`img` / `portrait`) authors a single path that has to
461
474
  * work for Foundry, the knowledgebase, and the website. **Its first segment
462
- * says which package owns the file** (#331), and there are exactly three
475
+ * says which package owns the file**, and there are exactly three
463
476
  * answers:
464
477
  *
465
478
  * | Authored path starts with | Owner | Emitted |
@@ -471,7 +484,7 @@ function addressesAnotherPackage(s) {
471
484
  * So `icons/relic.svg` compiles to `systems/sohl/assets/icons/relic.svg` here
472
485
  * and to `modules/sohl-thalorna/assets/icons/relic.svg` in a module — the asset
473
486
  * root is derived from the configuration, and is the one place `systems/sohl`
474
- * is ever spelled (#1508). An authored
487
+ * is ever spelled. An authored
475
488
  * `systems/sohl/assets/icons/noun/shield.svg` is left exactly as written,
476
489
  * whichever package is compiling it.
477
490
  *
@@ -482,13 +495,13 @@ function addressesAnotherPackage(s) {
482
495
  * `assets/artwork/`, so an authored `artwork/deity.webp` would have shipped
483
496
  * unprefixed: a 404 in Foundry, reported by nothing. That a package owns its
484
497
  * own tree is the fact; the directory names inside it are that package's
485
- * business (#331).
498
+ * business.
486
499
  *
487
500
  * **Off-install addresses pass through too**, which is the same rule rather
488
501
  * than a fourth: a URL, a `data:` URI, or a `/`-rooted path names something no
489
502
  * package owns. See {@link addressesAnotherPackage}.
490
503
  *
491
- * **`banner:` does not follow this rule, deliberately (#331).** It is not an
504
+ * **`banner:` does not follow this rule, deliberately.** It is not an
492
505
  * asset path inside a Foundry install at all: it reaches no compiled document,
493
506
  * and its only consumer is the Hugo theme, which prefixes a relative value with
494
507
  * `images/` and joins it onto `params.cdnBaseURL`. The two fields look alike
@@ -496,7 +509,7 @@ function addressesAnotherPackage(s) {
496
509
  * the CDN serves — so they are documented apart rather than reconciled into one
497
510
  * rule that would be true of neither.
498
511
  *
499
- * **Two empties, and they mean opposite things (#218).** `null` — or an absent
512
+ * **Two empties, and they mean opposite things.** `null` — or an absent
500
513
  * key, which reaches here as `undefined` — means _unset_: the note names no art
501
514
  * and the caller's default applies. `""` means _blank on purpose_: the note
502
515
  * names no art **and wants none**, so no default may replace it. Both come back
@@ -515,7 +528,7 @@ function addressesAnotherPackage(s) {
515
528
  * is `""` (`sohl/item-fields.mjs`), resolved from the very same shared top-level
516
529
  * key the site emitter reads as the page title — so `title: null` stringifies
517
530
  * into the compiled document as the literal `"null"`. One key, two destinations
518
- * that disagree about what empty means; see #218.
531
+ * that disagree about what empty means; see.
519
532
  *
520
533
  * This is translation only: the default for an unset path is domain-specific
521
534
  * (actors default differently from items, and gear differently again), so each
@@ -526,11 +539,19 @@ function addressesAnotherPackage(s) {
526
539
  * `itemArt()`, which runs the path back through this function so a registry
527
540
  * entry and a note's `img:` are spelled the same way (#7).
528
541
  *
542
+ * **A package with no asset root cannot answer at all.** `assetRoot` is derived
543
+ * from the package kind, and a `documentation` package has none: Foundry serves
544
+ * no files for it. Only a compiling pass reaches here, and a documentation
545
+ * package runs none, so a path arriving with no root to put it under is a pass
546
+ * running where it should not — reported as that, rather than emitted as
547
+ * `null/icons/relic.svg` into a document nobody would check.
548
+ *
529
549
  * @param {string | null | undefined} raw - content-relative path from frontmatter.
530
- * @param {{assetRoot: string}} [config] - The resolved build configuration.
550
+ * @param {{assetRoot: string|null}} [config] - The resolved build configuration.
531
551
  * Defaults to this repository's.
532
552
  * @returns {string | null} the Foundry-relative path; `""` for a deliberate
533
553
  * blank, and `null` when the note names no art at all.
554
+ * @throws {Error} When the configuration has no asset root.
534
555
  */
535
556
  export function resolveImg(raw, config = loadPackConfig()) {
536
557
  // Unset — the caller's default applies. An absent key arrives as
@@ -541,6 +562,14 @@ export function resolveImg(raw, config = loadPackConfig()) {
541
562
  if (s === "") return "";
542
563
  // Somebody else's to serve — emit it exactly as authored.
543
564
  if (addressesAnotherPackage(s)) return s;
565
+ if (!config.assetRoot) {
566
+ throw new Error(
567
+ `package-build: \`${s}\` names a file this package serves, and a ` +
568
+ `\`documentation\` package has no asset root to serve it from — ` +
569
+ `Foundry installs no such package. Address the owning package ` +
570
+ `(\`systems/…\`, \`modules/…\`) or a URL.`,
571
+ );
572
+ }
544
573
  // Ours, so root it where Foundry serves this package's files from.
545
574
  return `${config.assetRoot}/${s}`;
546
575
  }
@@ -564,7 +593,7 @@ export function resolveName(fm, defaultValue = "Unnamed") {
564
593
  * its migration shims on: a record stamped older than a shim is rewritten by it
565
594
  * on load. Every pack once shipped `coreVersion: "14"`, which sorts *below*
566
595
  * every v14 build and so left all shipped content permanently eligible for
567
- * every v14 migration (#1533).
596
+ * every v14 migration.
568
597
  *
569
598
  * **Read from the configuration, not from the shipped manifest.** It used to
570
599
  * open `paths.packageManifest` and take `compatibility.minimum` out of it,
@@ -598,7 +627,7 @@ export function supportedCoreVersion(config = loadPackConfig()) {
598
627
  /**
599
628
  * Default `_stats` block for compiled compendium entries.
600
629
  *
601
- * Every stamped identity is configuration (#1508): four compilers used to pass
630
+ * Every stamped identity is configuration: four compilers used to pass
602
631
  * the same frozen `"0.6.0"` literal, and `systemId` / `lastModifiedBy` were
603
632
  * written into this function. `coreVersion` alone is *not* configuration — it
604
633
  * comes from {@link supportedCoreVersion}, the configured Foundry floor,
@@ -623,13 +652,12 @@ export function buildStats(systemVersion = undefined, config = loadPackConfig())
623
652
  }
624
653
 
625
654
  /**
626
- * The `_stats` block for one pack, stamped with the system that pack is for
627
- * (#48).
655
+ * The `_stats` block for one pack, stamped with the system that pack is for.
628
656
  *
629
657
  * **`systemId` travels with `systemVersion`.** They are one decision, so where
630
658
  * one is omitted both are. Stamping a per-pack version against a package-wide
631
659
  * id would emit `systemId: sohl, systemVersion: 1.6.3` on HM3 documents — a
632
- * *plausible lie*, which is worse than the missing value #43 fixed, because
660
+ * *plausible lie*, which is worse than a missing value, because
633
661
  * nothing about it looks wrong.
634
662
  *
635
663
  * Resolution, in order:
@@ -679,6 +707,13 @@ export function defaultStats() {
679
707
  return cachedDefaultStats;
680
708
  }
681
709
 
710
+ // The one slug rule, re-exported so callers keep a single import path.
711
+ /**
712
+ * Standardize a name into a slug: lowercase, apostrophes removed,
713
+ * non-alphanumerics collapsed to single hyphens.
714
+ */
715
+ export { slugify } from "./content-slug.mjs";
716
+
682
717
  /**
683
718
  * Stable 16-char hex id derived from `${namespace}:${value}`.
684
719
  *
@@ -686,9 +721,6 @@ export function defaultStats() {
686
721
  * resolver this one imports can derive ids too — and re-exported here for the
687
722
  * passes that have always reached it through `helpers`.
688
723
  */
689
- // The one slug rule, re-exported so callers keep a single import path.
690
- export { slugify } from "./content-slug.mjs";
691
-
692
724
  export { makeId } from "./ids.mjs";
693
725
 
694
726
  // The content-type → document-type map, which decides *which* pack list a
@@ -708,11 +740,11 @@ import { collectAnchors } from "./anchors.mjs";
708
740
  *
709
741
  * Each note's pack is resolved here, once, and stored on its index entry: a
710
742
  * UUID carries a pack name, so a repository shipping several packs of one type
711
- * (#1566) would otherwise address every one of them as the first. A note whose
743
+ * would otherwise address every one of them as the first. A note whose
712
744
  * declaration is unroutable is indexed against the conventional name and left
713
745
  * for the compile pass to report — the index has no business failing a build,
714
746
  * and the pass fails it with a far better message. The one exception is a
715
- * **retired** content type (SoHL#1580): this walk is the first to see every
747
+ * **retired** content type: this walk is the first to see every
716
748
  * note together with its path, and unlike an unroutable declaration there is
717
749
  * no pass that would ever claim such a note and report it.
718
750
  *
@@ -720,6 +752,15 @@ import { collectAnchors } from "./anchors.mjs";
720
752
  * @param {object} [router] - The pack router. Supplied by the calling pass so
721
753
  * the index and the compile agree about where each note landed; defaults to
722
754
  * this repository's own.
755
+ * @param {object} [opts]
756
+ * @param {readonly string[]} [opts.skipDirectories] - Part of the options bag
757
+ * every corpus reader takes; the scope is already settled by `records`.
758
+ * @param {object} [opts.config] - The resolved build configuration; loaded when
759
+ * omitted.
760
+ * @param {readonly object[]} [opts.records] - The corpus, derived once per
761
+ * compile and handed in. Required: see {@link assertSuppliedCorpus}.
762
+ * @param {object[]} [opts.problems] - Part of the same options bag; the notes
763
+ * the index cannot record are collected where the corpus is derived.
723
764
  * @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
724
765
  */
725
766
  export function buildContentLinkIndex(
@@ -740,11 +781,11 @@ export function buildContentLinkIndex(
740
781
  const fm = authoredFrontmatter(record);
741
782
  const absPath = noteFile(contentBase, record);
742
783
  // The id a note's document is filed under: its authored pin, or the
743
- // one derived from its canonical address (#270). Derived by the index
744
- // against the configuration this build resolved — it used to be
745
- // derived here through `resolveNoteId(fm)` with no package, which falls
746
- // back to the ambient `contentPackage()` and so to whichever
747
- // configuration the working directory answers with (#243).
784
+ // one derived from its canonical address. Derived by the index
785
+ // against the configuration this build resolved — never here through
786
+ // `resolveNoteId(fm)` with no package, which falls back to the ambient
787
+ // `contentPackage()` and so to whichever configuration the working
788
+ // directory answers with.
748
789
  // What is left after that is a file with **no address** — no type, or
749
790
  // no shortcode — which is not an addressable note and has no document
750
791
  // to link to.
@@ -752,30 +793,30 @@ export function buildContentLinkIndex(
752
793
  // The first walk of every note in the tree, and the only one holding
753
794
  // both the declared type and the file that declares it — so a note
754
795
  // left on a retired type is reported here, by name, rather than
755
- // several frames deeper with nothing to go on (SoHL#1580).
796
+ // several frames deeper with nothing to go on.
756
797
  assertTypeNotRetired(fm.type, absPath);
757
798
  const base = String(record.file.name).replace(/_/g, " ");
758
799
  docs.push({
759
800
  type: fm.type,
760
801
  id: fm.id,
761
802
  // Where this note's own document lands, and where the JournalEntry
762
- // its prose compiles into lands — two documents, two packs (#1362).
803
+ // its prose compiles into lands — two documents, two packs.
763
804
  pack: router.resolveOrNull(fm, packForType(fm.type).docType),
764
805
  docPack: router.resolveOrNull(fm, "JournalEntry"),
765
806
  shortcode: fm.shortcode ?? null,
766
807
  name: fm.name?.full ?? base,
767
- // Whether the note is tagged `draft` (#183). Read from the tag
808
+ // Whether the note is tagged `draft`. Read from the tag
768
809
  // vocabulary that declares it, and used for one thing: a link
769
810
  // *into* this note renders marked. It takes no part in resolution,
770
811
  // so the note is indexed, compiled and published as any other.
771
812
  draft: isDraftNote(fm),
772
813
  // The anchors this note declares, carried so the *builds* can check
773
- // a `#section` link and not only the checker (#193). A foreign
814
+ // a `#section` link and not only the checker. A foreign
774
815
  // anchor has always been checked, because a fetched index
775
816
  // publishes the map; a local one was not, because the set was
776
817
  // discarded here — the walk yields the body and nothing read it.
777
818
  // Read from the record rather than from a second reading of the
778
- // note's headings — the one-anchor-reader rule (#243).
819
+ // note's headings — the one-anchor-reader rule.
779
820
  anchors: new Set((record.anchors ?? []).map((anchor) => anchor.slug)),
780
821
  });
781
822
  }
@@ -783,7 +824,7 @@ export function buildContentLinkIndex(
783
824
  // its own content index and this build fetched the ones it depends on, so
784
825
  // a contributor without every repository checked out resolves the same
785
826
  // links CI does — from an artifact the producer shipped rather than a copy
786
- // this repository committed (#239).
827
+ // this repository committed.
787
828
  const { index: foreign, stale } = loadForeignIndexes(
788
829
  resolved,
789
830
  [resolved.contentPackage],
@@ -810,18 +851,28 @@ export function buildContentLinkIndex(
810
851
  * target in the content tree. Every compiler funnels through this so the
811
852
  * diagnostic text and the leave-it-alone fallback are identical everywhere.
812
853
  *
813
- * Each report names the **file, line and column** the link sits on (#17), so
854
+ * Each report names the **file, line and column** the link sits on, so
814
855
  * it can be opened and fixed — and so two identical links on one note are
815
856
  * tellable apart. That needs `file` and the note's `bodyLine` / `bodyColumn`;
816
857
  * without them the diagnostic still reports, one field shorter, rather than
817
858
  * inventing a position.
818
859
  *
819
860
  * @param {string} body - The note's markdown body, tables already expanded.
820
- * @param {object} ctx - `{ type, id, pack, docPack, index, name }` — `name` is
821
- * used in the message, and the two pack names address a `[[#slug]]`
822
- * self-link, whose target is the source note itself and so has no index
823
- * entry. Position is carried by `{ file, bodyLine, bodyColumn, lineMap }`,
824
- * the last from {@link expandNoteTables}.
861
+ * @param {object} ctx
862
+ * @param {string} ctx.type - The source note's content type.
863
+ * @param {string} ctx.id - The source note's document id.
864
+ * @param {string} ctx.pack - The pack the note's own document lands in, which
865
+ * addresses a `[[#slug]]` self-link: its target is the source note itself, so
866
+ * it has no index entry.
867
+ * @param {string} ctx.docPack - The pack the note's documentation journal lands
868
+ * in, addressing a self-link the same way.
869
+ * @param {object} ctx.index - The address index every link resolves through.
870
+ * @param {string} ctx.name - The note, for the message.
871
+ * @param {string} [ctx.file] - The note's file, so a report names it.
872
+ * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line.
873
+ * @param {number} [ctx.bodyColumn] - 1-based file column of the same character.
874
+ * @param {Array<{line: number, generated: boolean}>} [ctx.lineMap] - Which
875
+ * authored line each body line came from, from {@link expandNoteTables}.
825
876
  * @returns {{markdown: string, unresolved: Array<object>}}
826
877
  * @throws {Error} On any link that does not resolve — an unlabelled one, a
827
878
  * target that is not an address, or an address nothing publishes. The error
@@ -875,8 +926,8 @@ export function convertNoteWikilinks(
875
926
  };
876
927
 
877
928
  for (const u of result.unresolved) {
878
- // Every class fails, and every class is worded by the shared table
879
- // (#184). The three resolvers read one authored link, so an author who
929
+ // Every class fails, and every class is worded by the shared table.
930
+ // The three resolvers read one authored link, so an author who
880
931
  // ran the pack build first and the link checker second must not be told
881
932
  // two different things about the same mistake — and a class the pack
882
933
  // build alone knew how to describe is how they came apart before.
@@ -898,6 +949,15 @@ export function convertNoteWikilinks(
898
949
  * a table that leaves rows tied still emits identically on every build.
899
950
  *
900
951
  * @param {string} contentBase - Root of the content tree.
952
+ * @param {object} [opts]
953
+ * @param {readonly string[]} [opts.skipDirectories] - Part of the options bag
954
+ * every corpus reader takes; the scope is already settled by `records`.
955
+ * @param {object} [opts.config] - The resolved build configuration; loaded when
956
+ * omitted.
957
+ * @param {readonly object[]} [opts.records] - The corpus, derived once per
958
+ * compile and handed in. Required: see {@link assertSuppliedCorpus}.
959
+ * @param {object[]} [opts.problems] - Part of the same options bag; the notes
960
+ * the walk cannot read are collected where the corpus is derived.
901
961
  * @returns {Array<{fm: object, path: string, tld: string, folder: string,
902
962
  * absPath: string}>}
903
963
  */
@@ -916,9 +976,8 @@ export function collectContentDocs(
916
976
  docs.push({
917
977
  // With its package supplied for a `WHERE … package = "…"` query —
918
978
  // synthesised from the configuration this build resolved, since no
919
- // note declares it (#56) and the ambient one is a different
920
- // configuration in a worktree or under `PACKAGE_BUILD_CONFIG`
921
- // (#243).
979
+ // note declares it and the ambient one is a different
980
+ // configuration in a worktree or under `PACKAGE_BUILD_CONFIG`.
922
981
  fm: searchableFrontmatter(fm, resolved.contentPackage),
923
982
  // POSIX-separated and relative to the content root — what a
924
983
  // `path:` search term globs, on every platform.
@@ -953,7 +1012,7 @@ const packLinkable = (doc) => Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type
953
1012
  * A table searches the whole tree, which is one package's notes and nothing
954
1013
  * else — so there is no longer a package to scope on. It used to filter, back
955
1014
  * when a tree could hold several packages' notes and `package:` said which was
956
- * which; that field is retired and the filter with it (#56).
1015
+ * which; that field is retired and the filter with it.
957
1016
  *
958
1017
  * @param {string} body - The note's markdown body.
959
1018
  * @param {object} ctx
@@ -963,10 +1022,14 @@ const packLinkable = (doc) => Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type
963
1022
  * query's `this` reads. Its entry in `docs` supplies the path as well.
964
1023
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
965
1024
  * so a failing directive can be reported at its position in the file.
1025
+ * @param {object[]} [ctx.sqlTables] - This note's prepared `sql` results, in
1026
+ * document order, from
1027
+ * {@link module:engine/sql-tables.prepareSqlTables}. An `sql` directive with
1028
+ * no prepared result fails the note: nothing here runs a query.
966
1029
  * @returns {{markdown: string, lineMap: Array<{line: number,
967
1030
  * generated: boolean}>}} The body with every table expanded, and where each
968
1031
  * emitted line came from — which is what lets a diagnostic about the
969
- * expanded body name an authored position (#17).
1032
+ * expanded body name an authored position.
970
1033
  * @throws {Error} When a query is malformed or unsupported — the note fails to
971
1034
  * compile rather than shipping a table-shaped hole. The error carries
972
1035
  * `position`, the directive's own line.