@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
@@ -12,8 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Checking a note's **frontmatter** against the schema its `type` declares
16
- * (#19).
15
+ * Checking a note's **frontmatter** against the schema its `type` declares.
17
16
  *
18
17
  * Until now nothing did. A note's type decides which properties are required,
19
18
  * what shape each value has, and which name another note — but that knowledge
@@ -43,7 +42,7 @@
43
42
  * The two rules that are not schema-driven sit on the *note format* side of
44
43
  * that line, which is why they are here and not in `sohl/`: the retired
45
44
  * top-level fields, and the address-bearing fields a `type: homepage` note
46
- * refuses (#53). Each supplies its own message from the module that owns the
45
+ * refuses. Each supplies its own message from the module that owns the
47
46
  * knowledge — `retired-fields.mjs` and `homepage.mjs` — and this module only
48
47
  * locates it in the file.
49
48
  *
@@ -69,7 +68,7 @@ import { RETIRED_TYPES, RENAMED_TYPES, currentType, renamedTypeMessage } from ".
69
68
  import { isAddressSegment } from "./address-charset.mjs";
70
69
  // The one place the "every pack not named" key is spelled. Imported rather
71
70
  // than repeated, because a linter holding its own copy of what the compiler
72
- // reads is exactly the disagreement #288 was.
71
+ // reads is exactly the disagreement to avoid.
73
72
  import { DEFAULT_PARENT } from "./folder-notes.mjs";
74
73
  import { declaredTags, subTypeCharsetMessage, typeCharsetMessage } from "./note-vocabulary.mjs";
75
74
  import {
@@ -95,7 +94,7 @@ import {
95
94
  *
96
95
  * - `folder` — the compendium folder the document is filed in.
97
96
  * - `pack` — which compendium of the note's document type receives it, for a
98
- * repository shipping several (#1566).
97
+ * repository shipping several.
99
98
  * - `archetype` — the archetype flag, read for any note by the walk itself.
100
99
  * - `kbcat` — the knowledgebase category a note is grouped under.
101
100
  *
@@ -162,13 +161,13 @@ export const DEFAULT_SYSTEM_BLOCKS = Object.freeze({
162
161
  */
163
162
 
164
163
  /**
165
- * Every system a configuration says its tree carries (#58).
164
+ * Every system a configuration says its tree carries.
166
165
  *
167
166
  * **Which systems a package ships for is already declared**, in three places
168
167
  * that answer different questions, so this reads all three rather than asking a
169
168
  * new one:
170
169
  *
171
- * - `systems:` (#48) declares them without requiring one, which is how a
170
+ * - `systems:` declares them without requiring one, which is how a
172
171
  * package ships for several;
173
172
  * - a **pack's** `system:` is the same statement made per pack, and it is the
174
173
  * one some trees make: `harn-ensemble` declares an `actors-sohl` and an
@@ -204,7 +203,7 @@ export function declaredSystems(config) {
204
203
 
205
204
  /**
206
205
  * The system blocks a configuration says its tree carries, and what each
207
- * accepts (#58).
206
+ * accepts.
208
207
  *
209
208
  * The lint checks the blocks its caller names, and for as long as there was one
210
209
  * system the only caller named none — so every tree took the `sohl:` of
@@ -406,7 +405,7 @@ function dataBlock(fm) {
406
405
 
407
406
  /**
408
407
  * Check a note's `data:` container against the closed vocabulary its type
409
- * declares (#128).
408
+ * declares.
410
409
  *
411
410
  * Unlike the top level, which is passed through to the published page and so
412
411
  * cannot be refused, `data:` holds the type-specific facts about the subject
@@ -526,7 +525,7 @@ function checkKeyedMap(note, { field, segments, entries, raw, packs }) {
526
525
 
527
526
  // `default` is the map's own key for "every pack not named", not a
528
527
  // pack — spelled out rather than left as an absent key, so a map
529
- // stating only exceptions still reads as a complete answer (#276).
528
+ // stating only exceptions still reads as a complete answer.
530
529
  if (known && key !== DEFAULT_PARENT && !known.has(key)) {
531
530
  const guess = nearest(key, known);
532
531
  findings.push({
@@ -563,38 +562,36 @@ function checkKeyedMap(note, { field, segments, entries, raw, packs }) {
563
562
  }
564
563
 
565
564
  /**
566
- * Check a note's top-level `subType` against the values its type declares
567
- * (#128).
565
+ * Check a note's top-level `subType` against the values its type declares.
568
566
  *
569
567
  * `subType` stays at the top level — it is what each system's map reads to
570
568
  * derive a document type, so it describes the note rather than the subject —
571
569
  * but it is not open like the rest of that region: a type either declares a
572
570
  * `subType` or does not, and a type that does declares its values.
573
571
  *
574
- * **It is a genre, and only a genre.** #197 gave the field a second reading: a
575
- * `README.md` was its section's landing page, and the segment it landed at was
576
- * its `subType`, so the value had to be checked against the sections that could
577
- * exist every content type, plus whatever a repository configured rather
572
+ * **It is a genre, and only a genre.** A second reading of the field a
573
+ * `README.md` as its section's landing page, whose `subType` is the segment it
574
+ * lands at would mean checking the value against the sections that could
575
+ * exist (every content type, plus whatever a repository configured) rather
578
576
  * than against the genres its type declares. Two vocabularies in one field is
579
- * what #198, #200 and #201 were each spent on, and #204 removed the cause rather
580
- * than the symptom: a section is a Hugo directory the note format does not
577
+ * the cause, and it is removed rather than the symptom: a section is a Hugo
578
+ * directory the note format does not
581
579
  * carry, and a page introducing a type is an ordinary note addressed
582
580
  * `doc-<type>`. So the closed list answers for every note, whatever it is
583
581
  * called, and `rules`, `userguide`, `reference` mean three genres and nothing
584
582
  * else.
585
583
  *
586
- * **Two checks, in this order** — the charset, then the closed set (#206,
587
- * #204). The charset is first because it is the more general statement about
588
- * the same value: a value outside `^[A-Za-z0-9]+$` is refused whatever the type
589
- * declares, and only once it is a well-formed term is the type's own list the
590
- * reason to refuse it.
584
+ * **Two checks, in this order** — the charset, then the closed set. The
585
+ * charset is first because it is the more general statement about
586
+ * the same value: a value outside `ADDRESS_SEGMENT_PATTERN` is refused
587
+ * whatever the type declares, and only once it is a well-formed term is the
588
+ * type's own list the reason to refuse it.
591
589
  *
592
- * There were three. #206 ran a retired-spelling check ahead of both, accepting
593
- * `user-guide` as a warning naming `userguide`, so the 43 `sohl` notes
594
- * authoring it were not invalidated by the release that renamed it. Every
595
- * consumer tree has swept, so the acceptance guarded nothing and is gone: the
596
- * old spelling now falls through to the charset check, which refuses it for the
597
- * reason that always applied — it contains a hyphen (#210).
590
+ * There is deliberately no third, retired-spelling check ahead of them
591
+ * accepting `user-guide` as a warning naming `userguide`. Every consumer tree
592
+ * has swept, so it would guard nothing: the old spelling falls through to the
593
+ * charset check, which refuses it for the reason that always applied it
594
+ * contains a hyphen.
598
595
  *
599
596
  * @param {object} note - The note.
600
597
  * @param {object} opts
@@ -658,7 +655,7 @@ function checkSubType(note, { type, entry }) {
658
655
  }
659
656
 
660
657
  /**
661
- * Check a note's `tags` for near misses against the tags that classify (#172).
658
+ * Check a note's `tags` for near misses against the tags that classify.
662
659
  *
663
660
  * `tags:` is top-level and the top level is open, so an unrecognised tag is
664
661
  * **not** a finding: a theme, a region or a working state is the author's own
@@ -677,7 +674,8 @@ function checkSubType(note, { type, entry }) {
677
674
  *
678
675
  * @param {object} note - The note.
679
676
  * @param {object} opts
680
- * @param {readonly string[]} opts.tags - Every declared tag, flattened.
677
+ * @param {string} opts.type - The note's declared `type`, which scopes the
678
+ * groups checked against.
681
679
  * @returns {object[]} Findings.
682
680
  */
683
681
  function checkTags(note, { type }) {
@@ -717,13 +715,13 @@ function checkTags(note, { type }) {
717
715
  * empty value belongs to the translator, not to whichever key happens to be
718
716
  * more common. Eleven `sohl-kethira-basic` beings write `portrait: ""` and no
719
717
  * note in any tree writes `img: ""` on a being; a check keyed on `img` alone
720
- * would have called that tree clean (#218).
718
+ * would have called that tree clean.
721
719
  *
722
720
  * **Each carries where it is authored**, because the two no longer agree. The
723
721
  * specification puts an actor's portrait under `data:` and leaves its token art
724
722
  * at the note's top level, so `portrait` has a third position to read and `img`
725
723
  * does not — and a check that read only the two they share would pass a
726
- * `data.portrait: ""` it could not see (#332).
724
+ * `data.portrait: ""` it could not see.
727
725
  *
728
726
  * @type {readonly {key: string, inData: boolean}[]}
729
727
  */
@@ -732,6 +730,27 @@ const ART_FIELDS = Object.freeze([
732
730
  Object.freeze({ key: "portrait", inData: true }),
733
731
  ]);
734
732
 
733
+ /**
734
+ * The keys a field declaration is authored at **inside a system block**.
735
+ *
736
+ * The first segment of each field's in-block key: `impact.die` is authored as
737
+ * `impact`, and a field whose shared source moved under `data:` is authored at
738
+ * the `legacyKey` it declares rather than at its dotted name. Keying on
739
+ * the name instead would report `sohl.species` as a property no `being` has,
740
+ * against exactly the notes the sweep has not reached yet.
741
+ *
742
+ * Written once and read twice: the note type's own declaration answers for the
743
+ * system whose vocabulary the caller's `schemas` are, and a second system's
744
+ * registry answers for its block. Two derivations of one thing would be
745
+ * free to disagree about which position a note authors.
746
+ *
747
+ * @param {readonly object[]|null|undefined} schema - A type's declarations.
748
+ * @returns {Set<string>} The in-block keys.
749
+ */
750
+ function inBlockKeys(schema) {
751
+ return new Set(authoredFields(schema ?? []).map((f) => legacyKeyOf(f).split(".")[0]));
752
+ }
753
+
735
754
  /**
736
755
  * The in-block keys a type's own declarations claim for a *different* quantity.
737
756
  *
@@ -749,7 +768,7 @@ const ART_FIELDS = Object.freeze([
749
768
  * "Ajaw", "Warden". Twenty-eight `sohl-kethira-basic` affiliations author
750
769
  * `sohl.title: ""` — an office with no style of address, which is ordinary —
751
770
  * and every one of them was reported as publishing a page with no heading. None
752
- * of them does; their pages take `name.full` exactly as intended (#312).
771
+ * of them does; their pages take `name.full` exactly as intended.
753
772
  *
754
773
  * Keyed on the **in-block** key — `legacyKey` where a field declares one, and
755
774
  * its first segment where that is dotted — because that is the position a note
@@ -759,27 +778,6 @@ const ART_FIELDS = Object.freeze([
759
778
  * @returns {Set<string>} The in-block keys that are not the note-level field of
760
779
  * the same name.
761
780
  */
762
- /**
763
- * The keys a field declaration is authored at **inside a system block**.
764
- *
765
- * The first segment of each field's in-block key: `impact.die` is authored as
766
- * `impact`, and a field whose shared source moved under `data:` is authored at
767
- * the `legacyKey` it declares rather than at its dotted name (#305). Keying on
768
- * the name instead would report `sohl.species` as a property no `being` has,
769
- * against exactly the notes the sweep has not reached yet.
770
- *
771
- * Written once and read twice: the note type's own declaration answers for the
772
- * system whose vocabulary the caller's `schemas` are, and a second system's
773
- * registry answers for its block (#58). Two derivations of one thing would be
774
- * free to disagree about which position a note authors.
775
- *
776
- * @param {readonly object[]|null|undefined} schema - A type's declarations.
777
- * @returns {Set<string>} The in-block keys.
778
- */
779
- function inBlockKeys(schema) {
780
- return new Set(authoredFields(schema ?? []).map((f) => legacyKeyOf(f).split(".")[0]));
781
- }
782
-
783
781
  function collidingBlockKeys(schema) {
784
782
  const keys = new Set();
785
783
  if (!Array.isArray(schema)) return keys;
@@ -798,12 +796,11 @@ function collidingBlockKeys(schema) {
798
796
  * restated here rather than imported so this module stays a leaf the linter can
799
797
  * load without a resolved build configuration. Unlike that resolver it
800
798
  * distinguishes the two empties — an authored `""` comes back as `""` and an
801
- * authored `null` as `null` — which is the whole point of the caller below
802
- * (#218).
799
+ * authored `null` as `null` — which is the whole point of the caller below.
803
800
  *
804
801
  * **`blockCollides` drops the first position**, where the note's type declares
805
802
  * a system field of that name meaning something else — the resolver's
806
- * `topLevelMeans` exemption, applied from the note-level side (#312). See
803
+ * `topLevelMeans` exemption, applied from the note-level side. See
807
804
  * {@link collidingBlockKeys}. The caller decides per key rather than this
808
805
  * function deciding for itself, because this module knows no type's vocabulary:
809
806
  * the declarations arrive from the caller, as `schemas` and `vocabulary` do.
@@ -831,7 +828,7 @@ function authoredValue(fm, key, { inData = false, blockCollides = false } = {})
831
828
  }
832
829
 
833
830
  /**
834
- * Two embedded items on one actor may not share `(type, shortcode)` (#228).
831
+ * Two embedded items on one actor may not share `(type, shortcode)`.
835
832
  *
836
833
  * SoHL treats `(type, shortcode)` as a **logical identity**, not a lookup
837
834
  * convenience: two documents of one type bearing one shortcode denote *the same
@@ -913,10 +910,11 @@ function checkEmbeddedShortcodes(note, blockName) {
913
910
  * @param {object} note - A note from the link index (`{fm, file, raw, type}`).
914
911
  * @param {object} opts
915
912
  * @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
916
- * @param {object} [opts.index] - The link index, for the reference check. Its
917
- * absence skips that check rather than reporting every reference as dead.
913
+ * @param {object} [opts.index] - The link index, for the reference check, which
914
+ * runs through its `referenceHit`. Its absence skips that check rather than
915
+ * reporting every reference as dead.
918
916
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
919
- * it declares, as `engine/note-vocabulary.mjs` states them (#128). Supplied
917
+ * it declares, as `engine/note-vocabulary.mjs` states them. Supplied
920
918
  * by the caller for the same reason `schemas` is: this module validates a
921
919
  * note against whatever its type declares and knows no type names of its
922
920
  * own. Its absence skips the `data:` and `subType` checks rather than
@@ -926,7 +924,7 @@ function checkEmbeddedShortcodes(note, blockName) {
926
924
  * declaration, asked through `engine/generate.mjs`'s `emittedArtFor`.
927
925
  * Supplied by the caller like `schemas`, so this module states no list of
928
926
  * iconless types of its own; absent it, an inert `img:` goes unreported
929
- * rather than every note's being (#349).
927
+ * rather than every note's being.
930
928
  * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
931
929
  * The system blocks to check, and what each accepts. Supplied by the caller
932
930
  * for the same reason `schemas` is — a build derives them from its
@@ -950,7 +948,7 @@ export function lintNote(
950
948
  /**
951
949
  * The in-block keys this note's own type claims for something other than
952
950
  * the note-level field of that name, which every note-level check below
953
- * reads past (#312). Resolved once: the type is fixed for the note, and
951
+ * reads past. Resolved once: the type is fixed for the note, and
954
952
  * each check would otherwise ask the same question of the same
955
953
  * declarations.
956
954
  */
@@ -959,7 +957,7 @@ export function lintNote(
959
957
  // The retired top-level fields, checked before the type: a note may carry
960
958
  // one whatever its type is, and each finding stands on its own. Reported
961
959
  // here as well as refused at compile because this is where an author meets
962
- // every finding in the tree at once, rather than one note at a time (#56).
960
+ // every finding in the tree at once, rather than one note at a time.
963
961
  if (Object.hasOwn(fm, "package")) {
964
962
  findings.push({
965
963
  file: note.file,
@@ -973,7 +971,7 @@ export function lintNote(
973
971
  });
974
972
  }
975
973
  // `folder:` named a compendium folder by the raw Foundry id declared in a
976
- // per-pack `*-folders.yaml`. Both halves are retired together (#260): the
974
+ // per-pack `*-folders.yaml`. Both halves are retired together: the
977
975
  // id spelling has nothing left to resolve against once the YAML is gone.
978
976
  //
979
977
  // Checked here as well as refused at compile because this is where an
@@ -1002,7 +1000,7 @@ export function lintNote(
1002
1000
  }
1003
1001
  // `img: ""` was how a note said "I name no art" while `resolveImg`
1004
1002
  // conflated the two empties and every caller defaulted with `||`. It now
1005
- // says the opposite — "ship no art, and do not default me" (#218) — so a
1003
+ // says the opposite — "ship no art, and do not default me" — so a
1006
1004
  // note carrying the old spelling has quietly changed meaning. Forty-five
1007
1005
  // `sohl-thalorna` notes were written under the old reading and would have
1008
1006
  // lost their default art with no error and no warning; this is the guard
@@ -1038,14 +1036,14 @@ export function lintNote(
1038
1036
  // reads; it says nothing about which position a *check* reads, and
1039
1037
  // `authoredValue` went on resolving through the block regardless — so an
1040
1038
  // office with no style of address answered for its note's heading, in
1041
- // twenty-eight `sohl-kethira-basic` affiliations (#312). Hence
1039
+ // twenty-eight `sohl-kethira-basic` affiliations. Hence
1042
1040
  // `blockCollisions`: a note-level check reads past a block key its type
1043
1041
  // claims for something else.
1044
1042
 
1045
1043
  // The template priority is a *shared source* — the specification states it
1046
1044
  // once for every type, as it does `pack` — so its retirement is reported
1047
1045
  // here rather than by the per-type loop below, which only reaches a field
1048
- // some type's vocabulary declares (#266).
1046
+ // some type's vocabulary declares.
1049
1047
  if (declaresRetiredAlias(fm, "templatePriority")) {
1050
1048
  findings.push({
1051
1049
  file: note.file,
@@ -1065,7 +1063,7 @@ export function lintNote(
1065
1063
  });
1066
1064
  }
1067
1065
 
1068
- // An art field a note's own type never emits (#349). `img` is a *shared
1066
+ // An art field a note's own type never emits. `img` is a *shared
1069
1067
  // top-level* field — `BLOCK_DOCUMENT_PROPERTIES` maps it onto
1070
1068
  // `document.img`, so it is legal on every note whatever the type — and a
1071
1069
  // note whose document has no such property authors it, validates, compiles,
@@ -1084,7 +1082,7 @@ export function lintNote(
1084
1082
  // `index` and `vocabulary` set.
1085
1083
  //
1086
1084
  // **Only an authored value, never `null`.** `null` is the blessed spelling
1087
- // for "this note names no art" (#218), and on a type with no art that is a
1085
+ // for "this note names no art", and on a type with no art that is a
1088
1086
  // true and harmless thing to say — it compiles identically to writing
1089
1087
  // nothing. Twenty-six `sohl-thalorna` place notes are in exactly that
1090
1088
  // state, and telling each of them to delete a key that already means
@@ -1140,7 +1138,7 @@ export function lintNote(
1140
1138
  "meant to have no image",
1141
1139
  });
1142
1140
  }
1143
- // `title: ""` publishes a blank heading (#218). The rule the art fields
1141
+ // `title: ""` publishes a blank heading. The rule the art fields
1144
1142
  // follow — `null` falls back, `""` is blank on purpose — reads the same way
1145
1143
  // here, and for a *page heading* the deliberate blank is almost never what
1146
1144
  // anyone wants: the emitter is `fm.title ?? name`, so `""` survives, the
@@ -1154,7 +1152,7 @@ export function lintNote(
1154
1152
  // **The emitter reads `fm.title`, so this reads the note level.** On an
1155
1153
  // `affiliation` `sohl.title` is the office's style of address, which the
1156
1154
  // heading has nothing to do with — and `blockCollisions` is what keeps the
1157
- // two apart (#312). On every other type nothing claims the block key, so the
1155
+ // two apart. On every other type nothing claims the block key, so the
1158
1156
  // resolution is the unchanged one.
1159
1157
  if (authoredValue(fm, "title", { blockCollides: blockCollisions.has("title") }) === "") {
1160
1158
  findings.push({
@@ -1179,7 +1177,7 @@ export function lintNote(
1179
1177
  }
1180
1178
  // Anchored at column 1 for the same reason `aliases` is: `section` names a
1181
1179
  // configuration key too (`site.trees[].section`), and a nested one under
1182
- // some other block is not this field (#202).
1180
+ // some other block is not this field.
1183
1181
  if (Object.hasOwn(fm, "section")) {
1184
1182
  findings.push({
1185
1183
  file: note.file,
@@ -1191,7 +1189,7 @@ export function lintNote(
1191
1189
  // Anchored at column 1 for the same reason `section` is, and with more at
1192
1190
  // stake: `sohl.traits` is a *different field that shares the name* —
1193
1191
  // `projectilegear` declares one and the theme's gear sidebar reads it — so
1194
- // a finding about the retired top-level block must never open on it (#291).
1192
+ // a finding about the retired top-level block must never open on it.
1195
1193
  if (Object.hasOwn(fm, "traits")) {
1196
1194
  findings.push({
1197
1195
  file: note.file,
@@ -1202,7 +1200,7 @@ export function lintNote(
1202
1200
  }
1203
1201
  // Only the top-level `aliases` is retired. `name.aliases` writes the same
1204
1202
  // key indented under `name:` and is **permitted** — reserved and unread —
1205
- // so both the test and the locator are anchored at column 1 (#180).
1203
+ // so both the test and the locator are anchored at column 1.
1206
1204
  if (declaresRetiredAliasesField(fm)) {
1207
1205
  findings.push({
1208
1206
  file: note.file,
@@ -1213,7 +1211,7 @@ export function lintNote(
1213
1211
  }
1214
1212
 
1215
1213
  // What the address rule says about a homepage's top-level fields: the
1216
- // `shortcode` it owes, and the `id` it may not write (#53, #182). Reported
1214
+ // `shortcode` it owes, and the `id` it may not write. Reported
1217
1215
  // beside the retired fields above because it is the same kind of statement
1218
1216
  // about the same note, and, like them, it must survive the two early
1219
1217
  // returns below: the finding stands whatever else the type is.
@@ -1224,7 +1222,7 @@ export function lintNote(
1224
1222
 
1225
1223
  // A refused field must be one the note *wrote*: `resolveNoteId` fills
1226
1224
  // `fm.id` in place, so the parsed frontmatter carries a derived id the
1227
- // author never typed (#319). The raw text is the only place that
1225
+ // author never typed. The raw text is the only place that
1228
1226
  // distinguishes them, and `positionInFrontmatter` already answers it —
1229
1227
  // `topLevel` so a nested `id:` under some other key is not mistaken for the
1230
1228
  // note's own.
@@ -1242,7 +1240,7 @@ export function lintNote(
1242
1240
  });
1243
1241
  }
1244
1242
 
1245
- // The type's charset, before anything that looks the type up (#206). A
1243
+ // The type's charset, before anything that looks the type up. A
1246
1244
  // hyphenated type is unaddressable, and every lookup below would report it
1247
1245
  // as a type nobody declared — true, but not the reason, and it would send
1248
1246
  // the author to declare one rather than to rename it.
@@ -1278,7 +1276,7 @@ export function lintNote(
1278
1276
  // A **renamed** type is the opposite case, and the opposite answer: the
1279
1277
  // note compiles into exactly the document it always did, so refusing it
1280
1278
  // would fail a build over a note that is not wrong. It is reported, and
1281
- // every lookup below reads the current spelling (#78).
1279
+ // every lookup below reads the current spelling.
1282
1280
  const renamedTo = RENAMED_TYPES[type];
1283
1281
  if (renamedTo) {
1284
1282
  findings.push({
@@ -1286,7 +1284,7 @@ export function lintNote(
1286
1284
  ...at("type", type),
1287
1285
  // A warning, for the reason the retired *field* alias below is one:
1288
1286
  // the sweep is the content trees' work and the refusal comes after
1289
- // it, as `package:`'s did (#56).
1287
+ // it, as `package:`'s did.
1290
1288
  severity: "warning",
1291
1289
  message: renamedTypeMessage(type, renamedTo),
1292
1290
  });
@@ -1308,7 +1306,7 @@ export function lintNote(
1308
1306
  return findings;
1309
1307
  }
1310
1308
 
1311
- // The closed frontmatter regions (#128), checked beside the `sohl:` block
1309
+ // The closed frontmatter regions, checked beside the `sohl:` block
1312
1310
  // because they are the same statement about the same note: this key is not
1313
1311
  // one this type may write. Skipped entirely when the caller declares no
1314
1312
  // vocabulary — reporting every key as unknown because nothing was loaded
@@ -1326,7 +1324,7 @@ export function lintNote(
1326
1324
  // The retired spelling of a field this type declares → what to write now.
1327
1325
  // Built from the type's own vocabulary, so a renamed field is retired
1328
1326
  // exactly where its replacement exists and the old name stays an unknown
1329
- // key everywhere else (#142).
1327
+ // key everywhere else.
1330
1328
  const renamed = new Map();
1331
1329
  for (const name of declared) {
1332
1330
  const retired = RETIRED_FIELD_ALIASES[name];
@@ -1344,13 +1342,13 @@ export function lintNote(
1344
1342
  // A warning, not an error: the note compiles to the correct
1345
1343
  // document, so failing a build over it would red a tree that has
1346
1344
  // done nothing wrong yet. The refusal comes after the sweep, as
1347
- // `package:`'s did (#56).
1345
+ // `package:`'s did.
1348
1346
  severity: "warning",
1349
1347
  message: retiredAliasMessage(retired, current),
1350
1348
  });
1351
1349
  }
1352
1350
 
1353
- // Every declared system's block, each against its own vocabulary (#58). A
1351
+ // Every declared system's block, each against its own vocabulary. A
1354
1352
  // block carries the shared keys any system's does — `system`, `type`,
1355
1353
  // `img`, `effects`, `flags`, `pack` — plus whatever that system declares:
1356
1354
  // the note type's own field names for the system those schemas describe,
@@ -1358,7 +1356,7 @@ export function lintNote(
1358
1356
  // here is the configuration's answer, not this module's; see
1359
1357
  // {@link systemBlocksFor}.
1360
1358
  for (const [blockName, spec] of Object.entries(systems ?? {})) {
1361
- // Two embedded items denoting one entity (#228). Per block, because
1359
+ // Two embedded items denoting one entity. Per block, because
1362
1360
  // `items` is a block key and a second system's actor carries its own.
1363
1361
  // Before the `continue` below, because it is a statement about the
1364
1362
  // block's *shape* and holds whether or not this system declares a
@@ -1406,7 +1404,7 @@ export function lintNote(
1406
1404
  // reached through its parent, and reporting the parent twice — once as
1407
1405
  // itself and once as its child — helps nobody.
1408
1406
  const [head] = field.name.split(".");
1409
- // Resolved exactly as the compiler resolves it (#58): the system path
1407
+ // Resolved exactly as the compiler resolves it: the system path
1410
1408
  // first, then the block, then the declared shared source. A lint that
1411
1409
  // read only one of the three would report a note's own field as missing
1412
1410
  // the moment it moved to another of them. A **shared** field needs
@@ -1416,7 +1414,7 @@ export function lintNote(
1416
1414
  // A **renamed** field may still be written under its retired spelling,
1417
1415
  // which that order knows nothing about. It resolves through the reader
1418
1416
  // the compiler uses, so the lint cannot disagree with the build about
1419
- // which value a note carries (#142).
1417
+ // which value a note carries.
1420
1418
  if ((from === "default" || value == null) && RETIRED_FIELD_ALIASES[field.name]) {
1421
1419
  const aliased = readAliasedField(fm, field.name);
1422
1420
  if (aliased !== undefined) {
@@ -1424,7 +1422,7 @@ export function lintNote(
1424
1422
  from = "block";
1425
1423
  }
1426
1424
  }
1427
- // The sweep's progress signal (#305). A **warning**, for the reason a
1425
+ // The sweep's progress signal. A **warning**, for the reason a
1428
1426
  // retired spelling is one: the note compiles to the correct document,
1429
1427
  // so failing a build over it would red a tree that has done nothing
1430
1428
  // wrong yet. The refusal comes once no tree writes the position.
@@ -1441,7 +1439,7 @@ export function lintNote(
1441
1439
  // under `sohl:`, so telling an author to write `sohl.img` would send
1442
1440
  // them to the wrong region. Nor is a field whose shared source is a
1443
1441
  // path into `data:` — `sohl.data.species` is a region that does not
1444
- // exist, and the home of that field is the container it names (#305).
1442
+ // exist, and the home of that field is the container it names.
1445
1443
  const label =
1446
1444
  field.shared || (field.name.includes(".") && field.legacyKey !== undefined) ?
1447
1445
  `\`${field.name}\``
@@ -1470,20 +1468,14 @@ export function lintNote(
1470
1468
  continue;
1471
1469
  }
1472
1470
 
1473
- // A reference names another note by shortcode. Resolved through the
1474
- // link index's own resolver, so a cross-package reference answered by a
1475
- // fetched index lands exactly as the same address in a wikilink
1476
- // would rather than through a second, subtly different rule.
1477
- //
1478
- // **As an address, always** — which is now the only namespace there
1479
- // is (#180). A frontmatter reference is a bare address by construction:
1480
- // there is no pipe to read intent from, and the field supplies the
1481
- // type. The resolver once took a namespace argument, and omitting it
1482
- // read every `ref:` value as an alias, which `type-shortcode` never was
1483
- // (#176).
1471
+ // A reference names another note by shortcode, and the field supplies
1472
+ // the type, so the resolver is handed the whole `type-shortcode` pair.
1473
+ // It resolves in any reachable package: the value is persisted as
1474
+ // written and looked up at runtime among one actor's embedded items,
1475
+ // which come from every package the actor draws on.
1484
1476
  if (field.ref && index && typeof value === "string" && value) {
1485
1477
  const target = `${field.ref}-${value}`;
1486
- if (!index.resolve(target) && !index.manifestHit(target)) {
1478
+ if (!index.referenceHit(target)) {
1487
1479
  findings.push({
1488
1480
  file: note.file,
1489
1481
  ...at(head, value),
@@ -1506,7 +1498,7 @@ export function lintNote(
1506
1498
  * @param {object} opts
1507
1499
  * @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
1508
1500
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
1509
- * it declares (#128); see {@link lintNote}.
1501
+ * it declares; see {@link lintNote}.
1510
1502
  * @param {boolean} [opts.references=true] - Whether to check references.
1511
1503
  * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
1512
1504
  * The system blocks to check; see {@link lintNote} and
@@ -19,8 +19,8 @@
19
19
  * list of constants, so the
20
20
  * item-type registry (`item-builders.mjs`) can build on it without dragging in
21
21
  * `helpers.mjs`, which reaches wikilinks — and through them back to
22
- * `item-docs.mjs`, the very module that derives `itemTypes()` from the registry
23
- * (#1504). Keeping these readers dependency-free is what makes that one-way.
22
+ * `item-docs.mjs`, the very module that derives `itemTypes()` from the registry.
23
+ * Keeping these readers dependency-free is what makes that one-way.
24
24
  *
25
25
  * `helpers.mjs` re-exports everything here, so existing importers are
26
26
  * unaffected: there is still one name for each reader.
@@ -28,7 +28,7 @@
28
28
 
29
29
  // The affiliation standings an authored `relation` map may use. Read from the
30
30
  // build package rather than restated here, so the pipeline and the runtime
31
- // enum cannot drift apart (#1510) — a value absent from the list is a build
31
+ // enum cannot drift apart — a value absent from the list is a build
32
32
  // error, never a silent ship.
33
33
  import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
34
34
 
@@ -65,7 +65,7 @@ export function sohlField(fm, key, defaultValue = undefined) {
65
65
  }
66
66
 
67
67
  /**
68
- * Read a `sohl:` field, seeing the **destination** position as well (#126).
68
+ * Read a `sohl:` field, seeing the **destination** position as well.
69
69
  *
70
70
  * Five declarations resolve their value by re-reading the note rather than by
71
71
  * taking the one {@link module:engine/system-block.resolveFieldValue} handed
@@ -84,8 +84,8 @@ export function sohlField(fm, key, defaultValue = undefined) {
84
84
  *
85
85
  * `legacyKey` is for the one pair whose two positions are spelled differently:
86
86
  * a projectile authors `impact.die` and stores `impactBase.die`, so the reader
87
- * has to be told both. It is the same split `FieldSpec.name`/`legacyKey` makes
88
- * (#305), for the same reason — one name cannot key two positions.
87
+ * has to be told both. It is the same split `FieldSpec.name`/`legacyKey` makes,
88
+ * for the same reason — one name cannot key two positions.
89
89
  *
90
90
  * @param {object} fm - The note's frontmatter.
91
91
  * @param {string} to - The key at the destination, dotted for a nested one.
@@ -136,7 +136,7 @@ function readMapEntries(fm, key) {
136
136
  /**
137
137
  * Resolve the `charges` block shared by Mystery and Mystical Ability items.
138
138
  *
139
- * Charge usage is carried by the **maximum** alone (#1129): a `null` max means
139
+ * Charge usage is carried by the **maximum** alone: a `null` max means
140
140
  * the item does not use charges at all, `0` means it is counted but uncapped,
141
141
  * and a positive number is a real cap. `value` is the current count, with
142
142
  * `null` meaning "infinite remaining". Both persist as nullable NumberFields,
@@ -205,7 +205,7 @@ export function resolveSkillAptitudes(fm, ctx = "item") {
205
205
 
206
206
  /**
207
207
  * Resolve an affiliation's `relation` map — the shortcode of another
208
- * affiliation → this one's standing toward it (#1404).
208
+ * affiliation → this one's standing toward it.
209
209
  *
210
210
  * An unrecognized standing would fail the schema's `choices` validation at load
211
211
  * and be dropped silently, shipping an affiliation whose authored hostility had
@@ -223,7 +223,7 @@ export function resolveSkillAptitudes(fm, ctx = "item") {
223
223
  */
224
224
  export function resolveRelation(fm, ctx = "item") {
225
225
  // `relations` is the field's name; `relation` is the retired spelling, read
226
- // underneath it so a tree converts on its own schedule (SoHL#1781). The
226
+ // underneath it so a tree converts on its own schedule. The
227
227
  // current name wins wherever a note writes both, and the lint reports the
228
228
  // old one through {@link RETIRED_FIELD_ALIASES}.
229
229
  // Probed at the destination too: a note that has moved to
@@ -307,12 +307,12 @@ export function parseValueDesc(raw) {
307
307
  * (`folder-poisonsandtoxins`), resolved through the address index the whole
308
308
  * build shares. The `folder:` Foundry-id spelling this function once read
309
309
  * beside it, and the per-pack `*-folders.yaml` that id was resolved against,
310
- * are retired together (#260) — a note declaring `folder:` is refused by
310
+ * are retired together — a note declaring `folder:` is refused by
311
311
  * {@link module:engine/folder-notes.assertNoDeclaredFolder} rather than
312
312
  * reaching here, so there is no second source for a value to come from.
313
313
  *
314
314
  * **`packFolder` was a path for one release** (`Possessions/Misc_Gear/Cooking`)
315
- * and is an address now (#255). A path encoded the hierarchy *in the value*, so
315
+ * and is an address now. A path encoded the hierarchy *in the value*, so
316
316
  * reparenting a folder made every note naming it wrong — a structural edit
317
317
  * became a corpus-wide rewrite. An address is stable under reparenting, which
318
318
  * is exactly why a note is addressed by `(type, shortcode)` and never by