@heroiclands/package-build 20.3.0 → 20.4.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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/CONTENT.md +111 -92
  3. package/README.md +2 -9
  4. package/bin/content-build.mjs +70 -61
  5. package/bin/package-build.mjs +2 -2
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +187 -64
  8. package/docs/content-format.md +57 -68
  9. package/e2e.mjs +4 -4
  10. package/engine/actor-compiler.mjs +36 -36
  11. package/engine/address-charset.mjs +6 -6
  12. package/engine/address-diff.mjs +12 -11
  13. package/engine/anchored-sections.mjs +1 -1
  14. package/engine/anchors.mjs +1 -1
  15. package/engine/base-compiler.mjs +36 -38
  16. package/engine/bundle-notes.mjs +4 -4
  17. package/engine/bundles.mjs +8 -8
  18. package/engine/code-fences.mjs +3 -3
  19. package/engine/compendiums.mjs +8 -8
  20. package/engine/compile-corpus.mjs +4 -5
  21. package/engine/content-address.mjs +15 -15
  22. package/engine/content-charset.mjs +1 -1
  23. package/engine/content-format-check.mjs +24 -25
  24. package/engine/content-format.mjs +8 -8
  25. package/engine/content-html.mjs +202 -0
  26. package/engine/content-icons.mjs +225 -98
  27. package/engine/content-index.mjs +17 -17
  28. package/engine/content-links.mjs +30 -30
  29. package/engine/content-lint.mjs +20 -24
  30. package/engine/content-package.mjs +7 -9
  31. package/engine/content-slug.mjs +2 -2
  32. package/engine/content-tables.mjs +7 -8
  33. package/engine/content-tree.mjs +1 -1
  34. package/engine/derived-fields.mjs +174 -0
  35. package/engine/diagnostics.mjs +3 -4
  36. package/engine/document-subtypes.mjs +6 -7
  37. package/engine/field-reference.mjs +8 -8
  38. package/engine/field-spec.mjs +29 -30
  39. package/engine/folder-notes.mjs +14 -14
  40. package/engine/foreign-catalog.mjs +8 -9
  41. package/engine/foundry-entries.mjs +19 -20
  42. package/engine/frontmatter-lint.mjs +64 -69
  43. package/engine/frontmatter.mjs +11 -11
  44. package/engine/generate.mjs +39 -39
  45. package/engine/helpers.mjs +63 -54
  46. package/engine/homepage.mjs +20 -22
  47. package/engine/ids.mjs +15 -15
  48. package/engine/index-records.mjs +4 -4
  49. package/engine/index.mjs +14 -8
  50. package/engine/item-compiler.mjs +54 -29
  51. package/engine/item-docs.mjs +11 -13
  52. package/engine/item-registry.mjs +13 -13
  53. package/engine/journals.mjs +15 -15
  54. package/engine/macros.mjs +3 -3
  55. package/engine/map-notes.mjs +6 -6
  56. package/engine/metadata-index.mjs +5 -5
  57. package/engine/note-claims.mjs +36 -34
  58. package/engine/note-ids.mjs +5 -5
  59. package/engine/note-package.mjs +4 -4
  60. package/engine/note-renames.mjs +3 -3
  61. package/engine/note-schemas.mjs +3 -3
  62. package/engine/note-vocabulary.mjs +27 -26
  63. package/engine/pack-config.mjs +13 -13
  64. package/engine/pack-router.mjs +0 -0
  65. package/engine/prose-config.mjs +9 -9
  66. package/engine/prose-lint.mjs +12 -13
  67. package/engine/region-events.mjs +2 -2
  68. package/engine/retired-fields.mjs +24 -22
  69. package/engine/runtime-only-fields.mjs +1 -1
  70. package/engine/scene-levels.mjs +1 -1
  71. package/engine/scenes.mjs +19 -20
  72. package/engine/schema-check.mjs +20 -20
  73. package/engine/schema-extract.mjs +1 -1
  74. package/engine/site-build.mjs +45 -47
  75. package/engine/site-index.mjs +17 -18
  76. package/engine/sql-tables.mjs +21 -13
  77. package/engine/subtype-registry.mjs +5 -4
  78. package/engine/system-block.mjs +18 -19
  79. package/engine/systems.mjs +2 -2
  80. package/engine/web-wikilinks.mjs +35 -35
  81. package/engine/wikilink-syntax.mjs +16 -16
  82. package/engine/wikilinks.mjs +39 -39
  83. package/hm3/actors.mjs +22 -22
  84. package/hm3/document-subtypes.mjs +5 -5
  85. package/hm3/index.mjs +2 -2
  86. package/hm3/item-builders.mjs +1 -1
  87. package/hm3/item-fields.mjs +3 -3
  88. package/hm3/items.mjs +20 -8
  89. package/hm3/template-priority.mjs +2 -2
  90. package/lang.mjs +3 -3
  91. package/manifest.mjs +17 -20
  92. package/package.json +1 -2
  93. package/release.mjs +3 -3
  94. package/sohl/actors.mjs +28 -28
  95. package/sohl/affiliation-standings.mjs +2 -2
  96. package/sohl/being-info.mjs +5 -5
  97. package/sohl/default-item-art.mjs +5 -5
  98. package/sohl/document-subtypes.mjs +5 -5
  99. package/sohl/index.mjs +3 -3
  100. package/sohl/item-builders.mjs +7 -7
  101. package/sohl/item-fields.mjs +8 -9
  102. package/sohl/items.mjs +20 -6
  103. package/sohl/kb-passes.mjs +5 -5
  104. package/sohl/note-schemas.mjs +7 -7
  105. package/sohl/skill-base.mjs +1 -1
  106. package/types/content-config.d.mts +25 -24
  107. package/types/e2e.d.mts +3 -3
  108. package/types/engine/actor-compiler.d.mts +14 -13
  109. package/types/engine/address-charset.d.mts +6 -6
  110. package/types/engine/address-diff.d.mts +3 -3
  111. package/types/engine/base-compiler.d.mts +17 -19
  112. package/types/engine/bundle-notes.d.mts +3 -3
  113. package/types/engine/bundles.d.mts +1 -1
  114. package/types/engine/code-fences.d.mts +3 -3
  115. package/types/engine/compendiums.d.mts +3 -3
  116. package/types/engine/compile-corpus.d.mts +1 -1
  117. package/types/engine/content-address.d.mts +11 -11
  118. package/types/engine/content-format-check.d.mts +6 -6
  119. package/types/engine/content-format.d.mts +2 -2
  120. package/types/engine/content-html.d.mts +78 -0
  121. package/types/engine/content-icons.d.mts +150 -62
  122. package/types/engine/content-index.d.mts +2 -2
  123. package/types/engine/content-links.d.mts +8 -8
  124. package/types/engine/content-lint.d.mts +2 -2
  125. package/types/engine/content-package.d.mts +6 -8
  126. package/types/engine/derived-fields.d.mts +101 -0
  127. package/types/engine/diagnostics.d.mts +2 -2
  128. package/types/engine/document-subtypes.d.mts +3 -3
  129. package/types/engine/field-spec.d.mts +37 -39
  130. package/types/engine/folder-notes.d.mts +4 -5
  131. package/types/engine/foreign-catalog.d.mts +3 -3
  132. package/types/engine/foundry-entries.d.mts +6 -7
  133. package/types/engine/frontmatter-lint.d.mts +7 -7
  134. package/types/engine/frontmatter.d.mts +7 -7
  135. package/types/engine/generate.d.mts +7 -7
  136. package/types/engine/helpers.d.mts +25 -27
  137. package/types/engine/homepage.d.mts +16 -18
  138. package/types/engine/ids.d.mts +13 -13
  139. package/types/engine/index-records.d.mts +3 -3
  140. package/types/engine/index.d.mts +2 -0
  141. package/types/engine/item-compiler.d.mts +21 -5
  142. package/types/engine/item-docs.d.mts +2 -2
  143. package/types/engine/item-registry.d.mts +6 -6
  144. package/types/engine/journals.d.mts +3 -3
  145. package/types/engine/map-notes.d.mts +2 -2
  146. package/types/engine/metadata-index.d.mts +4 -4
  147. package/types/engine/note-claims.d.mts +16 -15
  148. package/types/engine/note-ids.d.mts +4 -4
  149. package/types/engine/note-package.d.mts +1 -1
  150. package/types/engine/note-renames.d.mts +3 -3
  151. package/types/engine/note-vocabulary.d.mts +9 -8
  152. package/types/engine/pack-config.d.mts +3 -3
  153. package/types/engine/pack-router.d.mts +1 -1
  154. package/types/engine/prose-config.d.mts +9 -9
  155. package/types/engine/prose-lint.d.mts +3 -4
  156. package/types/engine/region-events.d.mts +2 -2
  157. package/types/engine/retired-fields.d.mts +10 -9
  158. package/types/engine/schema-check.d.mts +11 -11
  159. package/types/engine/site-build.d.mts +18 -19
  160. package/types/engine/site-index.d.mts +3 -4
  161. package/types/engine/sql-tables.d.mts +11 -5
  162. package/types/engine/subtype-registry.d.mts +3 -3
  163. package/types/engine/system-block.d.mts +3 -3
  164. package/types/engine/web-wikilinks.d.mts +7 -7
  165. package/types/engine/wikilink-syntax.d.mts +17 -17
  166. package/types/engine/wikilinks.d.mts +13 -12
  167. package/types/hm3/actors.d.mts +1 -1
  168. package/types/hm3/document-subtypes.d.mts +1 -1
  169. package/types/hm3/items.d.mts +1 -2
  170. package/types/hm3/template-priority.d.mts +1 -1
  171. package/types/manifest.d.mts +8 -8
  172. package/types/sohl/actors.d.mts +2 -2
  173. package/types/sohl/affiliation-standings.d.mts +2 -2
  174. package/types/sohl/being-info.d.mts +2 -2
  175. package/types/sohl/document-subtypes.d.mts +1 -1
  176. package/types/sohl/items.d.mts +1 -2
  177. package/types/sohl/note-schemas.d.mts +1 -1
  178. package/MIGRATING.md +0 -608
@@ -22,18 +22,18 @@
22
22
  *
23
23
  * Each `*` compiler walks the whole content tree and selects its own entries by
24
24
  * the note's `type` — every note in the tree belongs to this repository's
25
- * `contentPackage` (#56) — so routing is directory-agnostic: a file lands in a
25
+ * `contentPackage` — so routing is directory-agnostic: a file lands in a
26
26
  * pack because of its `type`, not its location. Which packs exist is declared
27
27
  * in `package-build.config.yaml`; the **folder hierarchy is not declared
28
- * anywhere**. A folder is a note like any other (#256), named by `packFolder`
28
+ * anywhere**. A folder is a note like any other, named by `packFolder`
29
29
  * — its address — and it materialises in every pack that holds a document
30
- * naming it, its ancestors with it (#257). So no pack loads a folder list, and
30
+ * naming it, its ancestors with it. So no pack loads a folder list, and
31
31
  * two packs can no longer disagree about a folder they both hold.
32
32
  *
33
33
  * **The order the passes run in is derived, not declared** — see
34
34
  * {@link orderPassesByDependency}. The declared list is the manifest's `packs`
35
35
  * array as well, so it is ordered for a reader; a pass that reads another's
36
- * output states that on its compiler and is scheduled after it (#73).
36
+ * output states that on its compiler and is scheduled after it.
37
37
  *
38
38
  * This replaces the retired `packs:export` (vault → committed `_source/`); the
39
39
  * HeroicLands vault is no longer a build input for SoHL content.
@@ -62,15 +62,15 @@ import {
62
62
  } from "./folder-notes.mjs";
63
63
  import { countContentNotes } from "./content-tree.mjs";
64
64
  import { emitDiagnostic } from "./diagnostics.mjs";
65
- // The corpus every pass runs over, derived once (#243).
65
+ // The corpus every pass runs over, derived once.
66
66
  import { buildCompileCorpus } from "./compile-corpus.mjs";
67
- // The record accessors only — see `engine/index-records.mjs` (#243).
67
+ // The record accessors only — see `engine/index-records.mjs`.
68
68
  import { isNoteRecord, noteFile } from "./index-records.mjs";
69
69
  import { loadPackConfig } from "./pack-config.mjs";
70
70
  import { routerFor } from "./pack-router.mjs";
71
71
  import { NEVER_PACKED_TYPES, unclaimedNoteFindings } from "./note-claims.mjs";
72
72
  // Which document a content type compiles into, so the art declaration below is
73
- // answered from the same routing the compile uses (#349).
73
+ // answered from the same routing the compile uses.
74
74
  import { RETIRED_TYPES, currentType, packForType } from "./ids.mjs";
75
75
  import { contentPackage } from "./content-package.mjs";
76
76
 
@@ -90,7 +90,7 @@ import { contentPackage } from "./content-package.mjs";
90
90
  * system-neutral passes have one implementation because a JournalEntry, a
91
91
  * Macro, a Scene and an Adventure are Foundry's documents rather than any
92
92
  * system's — an `Adventure` does not even have a `system` field, which is why a
93
- * bundle spanning two systems is two documents (#259).
93
+ * bundle spanning two systems is two documents.
94
94
  */
95
95
  const COMPILERS = {
96
96
  Item: Items,
@@ -102,7 +102,7 @@ const COMPILERS = {
102
102
  };
103
103
 
104
104
  /**
105
- * The system-specific compilers, by the system a pack declares (#139).
105
+ * The system-specific compilers, by the system a pack declares.
106
106
  *
107
107
  * A repository feeding two systems declares one Item pack and one Actor pack
108
108
  * per system — `harn-ensemble` has `actors-hm3` and `actors-sohl` — and each
@@ -138,7 +138,7 @@ export function compilerFor(docType, system = null) {
138
138
 
139
139
  /**
140
140
  * The art fields a note of one content type reaches its document through, and
141
- * the document it reaches (#349).
141
+ * the document it reaches.
142
142
  *
143
143
  * **Derived, never listed.** A note's type routes to a document type
144
144
  * ({@link packForType}), a document type routes to the pass that compiles it
@@ -201,7 +201,7 @@ export const packJsonDir = (name, config = loadPackConfig()) =>
201
201
  * The generated JSON of **every** configured Item pack — what the actors pass
202
202
  * reads its predefined items from.
203
203
  *
204
- * All of them, not the first: a repository may ship several Item packs (#1566),
204
+ * All of them, not the first: a repository may ship several Item packs,
205
205
  * and an actor's embedded items may be sourced from any of them. Finding one
206
206
  * pack and stopping is how embedded-item resolution would silently miss every
207
207
  * item that landed in another. Returned in configured order, which is also the
@@ -209,7 +209,7 @@ export const packJsonDir = (name, config = loadPackConfig()) =>
209
209
  * order among packs of one type — and every one of them is written before the
210
210
  * actors pass that reads them.
211
211
  *
212
- * **Scoped to one system when the caller has one (#58).** A being addresses an
212
+ * **Scoped to one system when the caller has one.** A being addresses an
213
213
  * item by `(type, shortcode)`, and that address is unique within one system and
214
214
  * not across two: `skill:sword` is an HM3 skill *and* a SoHL skill, with
215
215
  * different data models behind them. The reference itself is unambiguous — it
@@ -227,7 +227,7 @@ export const packJsonDir = (name, config = loadPackConfig()) =>
227
227
  * @returns {string[]} Each Item pack's JSON directory. Empty when the
228
228
  * repository ships no items at all, which is a legitimate package: the actors
229
229
  * pass accepts an empty list and reports an item it cannot resolve per
230
- * `(type, shortcode)` instead, naming the being (#49).
230
+ * `(type, shortcode)` instead, naming the being.
231
231
  */
232
232
  export function itemPackJsonDirs(config = loadPackConfig(), system = null) {
233
233
  return config.packs
@@ -242,7 +242,7 @@ export function itemPackJsonDirs(config = loadPackConfig(), system = null) {
242
242
  * An `Adventure` carries **copies**, not references, so a bundle resolves its
243
243
  * `contents` against compiled output rather than against the content tree — the
244
244
  * same arrangement the actors pass has for `itemsSourceDirs`, generalised to
245
- * every document class an Adventure can hold (#259).
245
+ * every document class an Adventure can hold.
246
246
  *
247
247
  * Two kinds of pack are left out, each because it holds nothing a note
248
248
  * addresses. A **prebuilt** pack's JSON is checked in rather than compiled, so
@@ -294,7 +294,7 @@ function readsOutputOf(type) {
294
294
  /**
295
295
  * The passes to run, ordered so that each one follows the output it reads.
296
296
  *
297
- * **Declaration order is presentation, not compile order (#73).** The same
297
+ * **Declaration order is presentation, not compile order.** The same
298
298
  * `packs:` list is the manifest's `packs` array, which a consumer orders for a
299
299
  * reader browsing compendiums; the actors pass, meanwhile, resolves each
300
300
  * being's embedded items against the item passes' *output*. Making one list
@@ -431,7 +431,7 @@ async function generatePack(
431
431
  // Which folder notes this pack turned out to hold something for. A folder
432
432
  // materialises in every pack holding a document that references it, so the
433
433
  // set is not knowable until the pass has compiled, which is why these
434
- // documents are written after `compile()` (#257).
434
+ // documents are written after `compile()`.
435
435
  /** @type {Set<import("./folder-notes.mjs").FolderNote>} */
436
436
  const referencedFolders = new Set();
437
437
 
@@ -441,7 +441,7 @@ async function generatePack(
441
441
  * There is one spelling. `packFolder` names a folder **note**, resolved
442
442
  * through the address index shared by the whole build; the `folder:`
443
443
  * Foundry-id spelling and the per-pack `*-folders.yaml` it resolved
444
- * against are retired together (#260), so there is no second source left
444
+ * against are retired together, so there is no second source left
445
445
  * for a value to come from.
446
446
  *
447
447
  * @param {string|null|undefined} value - As authored.
@@ -481,38 +481,38 @@ async function generatePack(
481
481
  const pack = new packClass({
482
482
  contentBase,
483
483
  dest,
484
- // The corpus this compile derived once, shared by every pass (#243).
484
+ // The corpus this compile derived once, shared by every pass.
485
485
  corpus,
486
486
  companionDests,
487
487
  // The actors pass resolves each being's embedded items against the items
488
- // passes' output. That used to be an unwritten sibling-directory contract
488
+ // passes' output. An unwritten sibling-directory contract
489
489
  // (`path.resolve(dest, "..", "items")`); the configured pack list names
490
490
  // the Item packs, so the dependency is stated rather than assumed
491
- // (#1508) — and it is every Item pack, since a repository may ship more
492
- // than one (#1566).
491
+ // — and it is every Item pack, since a repository may ship more
492
+ // than one.
493
493
  // Scoped to this pack's system, so a being resolves `(type, shortcode)`
494
- // against its own system's catalogue and the neutral one (#58).
494
+ // against its own system's catalogue and the neutral one.
495
495
  itemsSourceDirs: itemPackJsonDirs(config, system ?? null),
496
496
  // The catalogue of a package this repository depends on but does
497
497
  // not contain, for a repository that authors beings without
498
498
  // holding the items they are assembled from. Cache-only: a cold
499
499
  // cache throws naming `content-build deps fetch` rather than
500
500
  // downloading inside a compile. Scoped to this pack's system for the
501
- // reason the local half is (#58): both halves answer one lookup, so a
501
+ // reason the local half is: both halves answer one lookup, so a
502
502
  // dependency shipping two systems' items would otherwise supply the
503
503
  // wrong vocabulary's document for an address that exists in both.
504
504
  foreignSourceDirs: foreignItemCatalogDirs(config, system ?? null),
505
505
  // The bundles pass resolves each Adventure's members against the output
506
506
  // of every pass that produces one. Stated from the configured pack list
507
- // for the same reason `itemsSourceDirs` is (#1508), and scoped to this
507
+ // for the same reason `itemsSourceDirs` is, and scoped to this
508
508
  // pack's system so a bundle holds the catalogue it is compiled for.
509
509
  bundleSourceDirs: bundleSourceJsonDirs(config, system ?? null),
510
510
  folderResolver: resolver,
511
511
  // One answer to "which files are the corpus?", from the configuration
512
- // this build resolved rather than from the working directory (#243).
512
+ // this build resolved rather than from the working directory.
513
513
  skipDirectories: config.skipDirectories,
514
514
  packName: name,
515
- // Which system this pack's documents are stamped for (#48).
515
+ // Which system this pack's documents are stamped for.
516
516
  packSystem: system ?? null,
517
517
  docType: type,
518
518
  router,
@@ -525,7 +525,7 @@ async function generatePack(
525
525
  // journals pack both hold it when both hold something filed in it — and
526
526
  // every copy carries the same `_id`, which is what files a documentation
527
527
  // journal beside the item it describes rather than in a folder that merely
528
- // looks alike (#257).
528
+ // looks alike.
529
529
  writeFolderNoteDocs(
530
530
  referencedFolders,
531
531
  folderNotes,
@@ -579,7 +579,7 @@ function writeFolderNoteDocs(referenced, folderNotes, stats, dest, documentType,
579
579
  *
580
580
  * A pack ships blank whenever every note in a full tree was rejected — by a
581
581
  * `selects` that claims nothing, or a `pack:` that routes everything elsewhere
582
- * — and the build then exits 0 (#1502). The empty-tree guard in
582
+ * — and the build then exits 0. The empty-tree guard in
583
583
  * {@link generatePacksJson} cannot see that: the tree is full, it is the
584
584
  * *output* that is empty.
585
585
  *
@@ -644,11 +644,11 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
644
644
  // goes, and the first pack of each document type owns the error message for
645
645
  // a note of that type that goes nowhere. Resolved here because the corpus
646
646
  // below is derived against it, and the corpus is what every reader from
647
- // this point on reads (#243).
647
+ // this point on reads.
648
648
  const router = routerFor(config);
649
649
 
650
650
  // The corpus every pass runs over, and the three whole-tree indexes built
651
- // over it, derived **once** for the whole compile (#243). Each is a pure
651
+ // over it, derived **once** for the whole compile. Each is a pure
652
652
  // function of (tree, scope, router), none of which varies between passes —
653
653
  // `router` is one object, handed to all of them — so the passes were
654
654
  // deriving the same answers over and over. Compiling `sohl` read every note
@@ -668,11 +668,11 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
668
668
  for (const problem of corpusProblems) emitDiagnostic(problem);
669
669
 
670
670
  // A note whose `type:` no configured pack claims compiles into nothing, and
671
- // used to say nothing (#146) — no pass got far enough to reject it, so the
671
+ // used to say nothing — no pass got far enough to reject it, so the
672
672
  // silence had no owner. Asked once, of the whole configuration, because
673
673
  // that is the only place it can be answered: a per-pass check would report
674
674
  // every type a system deliberately does not map, which is exactly the
675
- // silence #79 requires. Independent of `only`, since it is a fact about the
675
+ // silence the rule requires. Independent of `only`, since it is a fact about the
676
676
  // configured pack list rather than about which passes this run executes.
677
677
  const unclaimed = unclaimedNoteFindings(config, undefined, { records: corpus.records });
678
678
  for (const finding of unclaimed) emitDiagnostic(finding);
@@ -684,7 +684,7 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
684
684
  //
685
685
  // A **prebuilt** pack has no pass either, and for a plainer reason: its
686
686
  // per-document JSON is checked in. Passed over rather than compiled — which
687
- // it could not be before #259, since the only prebuilt pack in the wild
687
+ // it could not be otherwise, since the only prebuilt pack in the wild
688
688
  // holds Adventures and no compiler was registered for that document type,
689
689
  // so the pack failed the build with "no compiler for document type". Now
690
690
  // one is registered, and running it would wipe `build/packs-json/<name>/`
@@ -700,12 +700,12 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
700
700
  // definition with one address, and every pass resolves against the same
701
701
  // index. A dangling `parent` or a parent cycle is therefore reported once,
702
702
  // as a fact about the tree, rather than once per pass that happened to walk
703
- // it (#256).
703
+ // it.
704
704
  let folderNotes;
705
705
  try {
706
706
  folderNotes = buildFolderNoteIndex(
707
707
  collectFolderNotes(
708
- // The corpus this compile derived picks the notes (#243); the
708
+ // The corpus this compile derived picks the notes; the
709
709
  // file supplies their frontmatter, and this is one of the few
710
710
  // places where that distinction is load-bearing rather than
711
711
  // incidental.
@@ -713,8 +713,8 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
713
713
  // `collectFolderNotes` treats `fm.id` as an **authored pin**,
714
714
  // which wins over the id it derives under the folder namespace.
715
715
  // A record's `id` is not that: the index fills it in for every
716
- // addressable note (#270), so handing records straight over
717
- // would make every folder look pinned. Since #310 the *value*
716
+ // addressable note, so handing records straight over
717
+ // would make every folder look pinned. The *value*
718
718
  // would be right either way — the index derives a folder's id
719
719
  // under the folder namespace, so the two agree — but `derivedId`
720
720
  // would not, and it is what tells an author whether a duplicate
@@ -734,7 +734,7 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
734
734
  // The package this build resolved, not the ambient accessor:
735
735
  // they are the same value in a real repository and different
736
736
  // ones under `PACKAGE_BUILD_CONFIG`, in a worktree, or in a
737
- // test (#243).
737
+ // test.
738
738
  config.contentPackage,
739
739
  ),
740
740
  );
@@ -754,7 +754,7 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
754
754
 
755
755
  // Compile order is derived from what each pass reads, not from the order
756
756
  // `packs:` declares — that list is also the manifest's, which a consumer
757
- // orders for a reader (#73).
757
+ // orders for a reader.
758
758
  const ordered = orderPassesByDependency(packs);
759
759
  if (ordered.some((pack, index) => pack !== packs[index])) {
760
760
  log.info(
@@ -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);
@@ -123,13 +135,13 @@ export function parseMarkdownFile(filePath) {
123
135
  * Yields `{ frontmatter, body, description, file, absPath, bodyLine,
124
136
  * bodyColumn }` for each match — the last two from
125
137
  * {@link parseMarkdownFile}, so a caller can report a position inside the
126
- * body as a position in the file (#17).
138
+ * body as a position in the file.
127
139
  * Silently skips directories that don't exist.
128
140
  *
129
141
  * Directory names in `skipDirectories` are ignored wherever they appear. The
130
142
  * walk itself knows nothing about what they mean: `Templates/` is an Obsidian
131
143
  * 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).
144
+ * of a content tree, so it is configured rather than hard-coded.
133
145
  *
134
146
  * @param {string} rootDir - Root of the tree to walk.
135
147
  * @param {object} [opts]
@@ -139,7 +151,7 @@ export function parseMarkdownFile(filePath) {
139
151
  /**
140
152
  * Refuse a corpus read whose scope its caller did not state.
141
153
  *
142
- * The rule of #243 in one place, so every reader of the tree refuses the same
154
+ * The rule in one place, so every reader of the tree refuses the same
143
155
  * way and says so in the same words. It is shared rather than repeated because
144
156
  * the corpus is no longer read only by {@link walkMarkdownTree}: a pass that
145
157
  * reads the content index instead is making the identical claim about which
@@ -172,7 +184,7 @@ export function assertStatedScope(skipDirectories, who) {
172
184
  * cycle. They take the records their caller already holds.
173
185
  *
174
186
  * 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
187
+ * over one tree, and the whole point is that they must not each answer "which
176
188
  * files are the corpus?" for themselves. Requiring the answer to be handed in
177
189
  * makes the sharing structural rather than remembered.
178
190
  *
@@ -192,7 +204,7 @@ export function assertSuppliedCorpus(records, who) {
192
204
  }
193
205
 
194
206
  export function* walkMarkdownTree(rootDir, { skipDirectories } = {}) {
195
- // Stated by the caller, never resolved here. The default this used to carry
207
+ // Stated by the caller, never resolved here. A default here
196
208
  // — `loadPackConfig().skipDirectories` — read whichever configuration
197
209
  // resolved from the working directory rather than the one the caller was
198
210
  // working under. In an ordinary build those are the same object and nothing
@@ -200,9 +212,9 @@ export function* walkMarkdownTree(rootDir, { skipDirectories } = {}) {
200
212
  // `PACKAGE_BUILD_CONFIG` names one, or when the command runs from a
201
213
  // worktree. Six of this function's twelve callers were on that default, so
202
214
  // "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.
215
+ // — the same defect class as `entriesForNote` reading `docEntryTypes` from
216
+ // the ambient config rather than the passed one, which a fixture can pass
217
+ // on indefinitely.
206
218
  assertStatedScope(skipDirectories, "walkMarkdownTree");
207
219
  if (!fs.existsSync(rootDir)) return;
208
220
  const stack = [rootDir];
@@ -234,7 +246,7 @@ export function* walkMarkdownTree(rootDir, { skipDirectories } = {}) {
234
246
  /**
235
247
  * Every position a note may state its template priority at, for one system
236
248
  * block, in the order they answer — and whichever of them the note actually
237
- * wrote (#266).
249
+ * wrote.
238
250
  *
239
251
  * Three places, in the order the migration runs. The specification calls this
240
252
  * `data.templatePriority`; `sohl-thalorna` already writes it there on 941
@@ -290,7 +302,7 @@ function findTemplatePriority(fm, block) {
290
302
 
291
303
  /**
292
304
  * 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).
305
+ * as "not a template" rather than as an authoring error.
294
306
  *
295
307
  * Reads exactly the positions {@link resolveTemplatePriority} reads, including
296
308
  * the retiring `archetype` spelling, and refuses the same contradiction — so
@@ -324,7 +336,7 @@ export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
324
336
 
325
337
  /**
326
338
  * Resolve the required `templatePriority` frontmatter for an Item/Actor entry
327
- * (the archetype contract, #604). The property is a nullable number that
339
+ * (the archetype contract). The property is a nullable number that
328
340
  * authors must state explicitly:
329
341
  * - a number → the document is a template of that priority.
330
342
  * - `null` → the document is not a template.
@@ -333,7 +345,7 @@ export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
333
345
  *
334
346
  * Reads the positions {@link findTemplatePriority} lists: `data.templatePriority`
335
347
  * 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).
348
+ * finally the retiring `archetype` spelling in the same two places.
337
349
  *
338
350
  * @param {object} fm Parsed frontmatter.
339
351
  * @param {string} label Human-readable context for error messages.
@@ -371,8 +383,7 @@ export function resolveTemplatePriority(fm, label, { block = "sohl" } = {}) {
371
383
 
372
384
  /**
373
385
  * 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).
386
+ * `templatePriority` frontmatter (`archetype` is the legacy spelling).
376
387
  *
377
388
  * A **schema field**, so the tri-state is written out in full rather than
378
389
  * expressed by a key's presence: a number is a template at that priority, and
@@ -459,7 +470,7 @@ function addressesAnotherPackage(s) {
459
470
  *
460
471
  * Content frontmatter (`img` / `portrait`) authors a single path that has to
461
472
  * work for Foundry, the knowledgebase, and the website. **Its first segment
462
- * says which package owns the file** (#331), and there are exactly three
473
+ * says which package owns the file**, and there are exactly three
463
474
  * answers:
464
475
  *
465
476
  * | Authored path starts with | Owner | Emitted |
@@ -471,7 +482,7 @@ function addressesAnotherPackage(s) {
471
482
  * So `icons/relic.svg` compiles to `systems/sohl/assets/icons/relic.svg` here
472
483
  * and to `modules/sohl-thalorna/assets/icons/relic.svg` in a module — the asset
473
484
  * root is derived from the configuration, and is the one place `systems/sohl`
474
- * is ever spelled (#1508). An authored
485
+ * is ever spelled. An authored
475
486
  * `systems/sohl/assets/icons/noun/shield.svg` is left exactly as written,
476
487
  * whichever package is compiling it.
477
488
  *
@@ -482,13 +493,13 @@ function addressesAnotherPackage(s) {
482
493
  * `assets/artwork/`, so an authored `artwork/deity.webp` would have shipped
483
494
  * unprefixed: a 404 in Foundry, reported by nothing. That a package owns its
484
495
  * own tree is the fact; the directory names inside it are that package's
485
- * business (#331).
496
+ * business.
486
497
  *
487
498
  * **Off-install addresses pass through too**, which is the same rule rather
488
499
  * than a fourth: a URL, a `data:` URI, or a `/`-rooted path names something no
489
500
  * package owns. See {@link addressesAnotherPackage}.
490
501
  *
491
- * **`banner:` does not follow this rule, deliberately (#331).** It is not an
502
+ * **`banner:` does not follow this rule, deliberately.** It is not an
492
503
  * asset path inside a Foundry install at all: it reaches no compiled document,
493
504
  * and its only consumer is the Hugo theme, which prefixes a relative value with
494
505
  * `images/` and joins it onto `params.cdnBaseURL`. The two fields look alike
@@ -496,7 +507,7 @@ function addressesAnotherPackage(s) {
496
507
  * the CDN serves — so they are documented apart rather than reconciled into one
497
508
  * rule that would be true of neither.
498
509
  *
499
- * **Two empties, and they mean opposite things (#218).** `null` — or an absent
510
+ * **Two empties, and they mean opposite things.** `null` — or an absent
500
511
  * key, which reaches here as `undefined` — means _unset_: the note names no art
501
512
  * and the caller's default applies. `""` means _blank on purpose_: the note
502
513
  * names no art **and wants none**, so no default may replace it. Both come back
@@ -515,7 +526,7 @@ function addressesAnotherPackage(s) {
515
526
  * is `""` (`sohl/item-fields.mjs`), resolved from the very same shared top-level
516
527
  * key the site emitter reads as the page title — so `title: null` stringifies
517
528
  * into the compiled document as the literal `"null"`. One key, two destinations
518
- * that disagree about what empty means; see #218.
529
+ * that disagree about what empty means; see.
519
530
  *
520
531
  * This is translation only: the default for an unset path is domain-specific
521
532
  * (actors default differently from items, and gear differently again), so each
@@ -564,7 +575,7 @@ export function resolveName(fm, defaultValue = "Unnamed") {
564
575
  * its migration shims on: a record stamped older than a shim is rewritten by it
565
576
  * on load. Every pack once shipped `coreVersion: "14"`, which sorts *below*
566
577
  * every v14 build and so left all shipped content permanently eligible for
567
- * every v14 migration (#1533).
578
+ * every v14 migration.
568
579
  *
569
580
  * **Read from the configuration, not from the shipped manifest.** It used to
570
581
  * open `paths.packageManifest` and take `compatibility.minimum` out of it,
@@ -598,7 +609,7 @@ export function supportedCoreVersion(config = loadPackConfig()) {
598
609
  /**
599
610
  * Default `_stats` block for compiled compendium entries.
600
611
  *
601
- * Every stamped identity is configuration (#1508): four compilers used to pass
612
+ * Every stamped identity is configuration: four compilers used to pass
602
613
  * the same frozen `"0.6.0"` literal, and `systemId` / `lastModifiedBy` were
603
614
  * written into this function. `coreVersion` alone is *not* configuration — it
604
615
  * comes from {@link supportedCoreVersion}, the configured Foundry floor,
@@ -623,13 +634,12 @@ export function buildStats(systemVersion = undefined, config = loadPackConfig())
623
634
  }
624
635
 
625
636
  /**
626
- * The `_stats` block for one pack, stamped with the system that pack is for
627
- * (#48).
637
+ * The `_stats` block for one pack, stamped with the system that pack is for.
628
638
  *
629
639
  * **`systemId` travels with `systemVersion`.** They are one decision, so where
630
640
  * one is omitted both are. Stamping a per-pack version against a package-wide
631
641
  * 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
642
+ * *plausible lie*, which is worse than a missing value, because
633
643
  * nothing about it looks wrong.
634
644
  *
635
645
  * Resolution, in order:
@@ -708,11 +718,11 @@ import { collectAnchors } from "./anchors.mjs";
708
718
  *
709
719
  * Each note's pack is resolved here, once, and stored on its index entry: a
710
720
  * 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
721
+ * would otherwise address every one of them as the first. A note whose
712
722
  * declaration is unroutable is indexed against the conventional name and left
713
723
  * for the compile pass to report — the index has no business failing a build,
714
724
  * 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
725
+ * **retired** content type: this walk is the first to see every
716
726
  * note together with its path, and unlike an unroutable declaration there is
717
727
  * no pass that would ever claim such a note and report it.
718
728
  *
@@ -740,11 +750,11 @@ export function buildContentLinkIndex(
740
750
  const fm = authoredFrontmatter(record);
741
751
  const absPath = noteFile(contentBase, record);
742
752
  // 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).
753
+ // one derived from its canonical address. Derived by the index
754
+ // against the configuration this build resolved — never here through
755
+ // `resolveNoteId(fm)` with no package, which falls back to the ambient
756
+ // `contentPackage()` and so to whichever configuration the working
757
+ // directory answers with.
748
758
  // What is left after that is a file with **no address** — no type, or
749
759
  // no shortcode — which is not an addressable note and has no document
750
760
  // to link to.
@@ -752,30 +762,30 @@ export function buildContentLinkIndex(
752
762
  // The first walk of every note in the tree, and the only one holding
753
763
  // both the declared type and the file that declares it — so a note
754
764
  // left on a retired type is reported here, by name, rather than
755
- // several frames deeper with nothing to go on (SoHL#1580).
765
+ // several frames deeper with nothing to go on.
756
766
  assertTypeNotRetired(fm.type, absPath);
757
767
  const base = String(record.file.name).replace(/_/g, " ");
758
768
  docs.push({
759
769
  type: fm.type,
760
770
  id: fm.id,
761
771
  // Where this note's own document lands, and where the JournalEntry
762
- // its prose compiles into lands — two documents, two packs (#1362).
772
+ // its prose compiles into lands — two documents, two packs.
763
773
  pack: router.resolveOrNull(fm, packForType(fm.type).docType),
764
774
  docPack: router.resolveOrNull(fm, "JournalEntry"),
765
775
  shortcode: fm.shortcode ?? null,
766
776
  name: fm.name?.full ?? base,
767
- // Whether the note is tagged `draft` (#183). Read from the tag
777
+ // Whether the note is tagged `draft`. Read from the tag
768
778
  // vocabulary that declares it, and used for one thing: a link
769
779
  // *into* this note renders marked. It takes no part in resolution,
770
780
  // so the note is indexed, compiled and published as any other.
771
781
  draft: isDraftNote(fm),
772
782
  // The anchors this note declares, carried so the *builds* can check
773
- // a `#section` link and not only the checker (#193). A foreign
783
+ // a `#section` link and not only the checker. A foreign
774
784
  // anchor has always been checked, because a fetched index
775
785
  // publishes the map; a local one was not, because the set was
776
786
  // discarded here — the walk yields the body and nothing read it.
777
787
  // Read from the record rather than from a second reading of the
778
- // note's headings — the one-anchor-reader rule (#243).
788
+ // note's headings — the one-anchor-reader rule.
779
789
  anchors: new Set((record.anchors ?? []).map((anchor) => anchor.slug)),
780
790
  });
781
791
  }
@@ -783,7 +793,7 @@ export function buildContentLinkIndex(
783
793
  // its own content index and this build fetched the ones it depends on, so
784
794
  // a contributor without every repository checked out resolves the same
785
795
  // links CI does — from an artifact the producer shipped rather than a copy
786
- // this repository committed (#239).
796
+ // this repository committed.
787
797
  const { index: foreign, stale } = loadForeignIndexes(
788
798
  resolved,
789
799
  [resolved.contentPackage],
@@ -810,7 +820,7 @@ export function buildContentLinkIndex(
810
820
  * target in the content tree. Every compiler funnels through this so the
811
821
  * diagnostic text and the leave-it-alone fallback are identical everywhere.
812
822
  *
813
- * Each report names the **file, line and column** the link sits on (#17), so
823
+ * Each report names the **file, line and column** the link sits on, so
814
824
  * it can be opened and fixed — and so two identical links on one note are
815
825
  * tellable apart. That needs `file` and the note's `bodyLine` / `bodyColumn`;
816
826
  * without them the diagnostic still reports, one field shorter, rather than
@@ -875,8 +885,8 @@ export function convertNoteWikilinks(
875
885
  };
876
886
 
877
887
  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
888
+ // Every class fails, and every class is worded by the shared table.
889
+ // The three resolvers read one authored link, so an author who
880
890
  // ran the pack build first and the link checker second must not be told
881
891
  // two different things about the same mistake — and a class the pack
882
892
  // build alone knew how to describe is how they came apart before.
@@ -916,9 +926,8 @@ export function collectContentDocs(
916
926
  docs.push({
917
927
  // With its package supplied for a `WHERE … package = "…"` query —
918
928
  // 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).
929
+ // note declares it and the ambient one is a different
930
+ // configuration in a worktree or under `PACKAGE_BUILD_CONFIG`.
922
931
  fm: searchableFrontmatter(fm, resolved.contentPackage),
923
932
  // POSIX-separated and relative to the content root — what a
924
933
  // `path:` search term globs, on every platform.
@@ -953,7 +962,7 @@ const packLinkable = (doc) => Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type
953
962
  * A table searches the whole tree, which is one package's notes and nothing
954
963
  * else — so there is no longer a package to scope on. It used to filter, back
955
964
  * 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).
965
+ * which; that field is retired and the filter with it.
957
966
  *
958
967
  * @param {string} body - The note's markdown body.
959
968
  * @param {object} ctx
@@ -966,7 +975,7 @@ const packLinkable = (doc) => Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type
966
975
  * @returns {{markdown: string, lineMap: Array<{line: number,
967
976
  * generated: boolean}>}} The body with every table expanded, and where each
968
977
  * emitted line came from — which is what lets a diagnostic about the
969
- * expanded body name an authored position (#17).
978
+ * expanded body name an authored position.
970
979
  * @throws {Error} When a query is malformed or unsupported — the note fails to
971
980
  * compile rather than shipping a table-shaped hole. The error carries
972
981
  * `position`, the directive's own line.