@heroiclands/package-build 20.3.1 → 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 +75 -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 +196 -165
  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 +139 -92
  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
@@ -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
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
588
586
  * the same value: a value outside `^[A-Za-z0-9]+$` is refused whatever the type
589
587
  * declares, and only once it is a well-formed term is the type's own list the
590
588
  * 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
@@ -717,13 +714,13 @@ function checkTags(note, { type }) {
717
714
  * empty value belongs to the translator, not to whichever key happens to be
718
715
  * more common. Eleven `sohl-kethira-basic` beings write `portrait: ""` and no
719
716
  * note in any tree writes `img: ""` on a being; a check keyed on `img` alone
720
- * would have called that tree clean (#218).
717
+ * would have called that tree clean.
721
718
  *
722
719
  * **Each carries where it is authored**, because the two no longer agree. The
723
720
  * specification puts an actor's portrait under `data:` and leaves its token art
724
721
  * at the note's top level, so `portrait` has a third position to read and `img`
725
722
  * does not — and a check that read only the two they share would pass a
726
- * `data.portrait: ""` it could not see (#332).
723
+ * `data.portrait: ""` it could not see.
727
724
  *
728
725
  * @type {readonly {key: string, inData: boolean}[]}
729
726
  */
@@ -749,7 +746,7 @@ const ART_FIELDS = Object.freeze([
749
746
  * "Ajaw", "Warden". Twenty-eight `sohl-kethira-basic` affiliations author
750
747
  * `sohl.title: ""` — an office with no style of address, which is ordinary —
751
748
  * 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).
749
+ * of them does; their pages take `name.full` exactly as intended.
753
750
  *
754
751
  * Keyed on the **in-block** key — `legacyKey` where a field declares one, and
755
752
  * its first segment where that is dotted — because that is the position a note
@@ -764,13 +761,13 @@ const ART_FIELDS = Object.freeze([
764
761
  *
765
762
  * The first segment of each field's in-block key: `impact.die` is authored as
766
763
  * `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
764
+ * the `legacyKey` it declares rather than at its dotted name. Keying on
768
765
  * the name instead would report `sohl.species` as a property no `being` has,
769
766
  * against exactly the notes the sweep has not reached yet.
770
767
  *
771
768
  * Written once and read twice: the note type's own declaration answers for the
772
769
  * 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
770
+ * registry answers for its block. Two derivations of one thing would be
774
771
  * free to disagree about which position a note authors.
775
772
  *
776
773
  * @param {readonly object[]|null|undefined} schema - A type's declarations.
@@ -798,12 +795,11 @@ function collidingBlockKeys(schema) {
798
795
  * restated here rather than imported so this module stays a leaf the linter can
799
796
  * load without a resolved build configuration. Unlike that resolver it
800
797
  * 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).
798
+ * authored `null` as `null` — which is the whole point of the caller below.
803
799
  *
804
800
  * **`blockCollides` drops the first position**, where the note's type declares
805
801
  * a system field of that name meaning something else — the resolver's
806
- * `topLevelMeans` exemption, applied from the note-level side (#312). See
802
+ * `topLevelMeans` exemption, applied from the note-level side. See
807
803
  * {@link collidingBlockKeys}. The caller decides per key rather than this
808
804
  * function deciding for itself, because this module knows no type's vocabulary:
809
805
  * the declarations arrive from the caller, as `schemas` and `vocabulary` do.
@@ -831,7 +827,7 @@ function authoredValue(fm, key, { inData = false, blockCollides = false } = {})
831
827
  }
832
828
 
833
829
  /**
834
- * Two embedded items on one actor may not share `(type, shortcode)` (#228).
830
+ * Two embedded items on one actor may not share `(type, shortcode)`.
835
831
  *
836
832
  * SoHL treats `(type, shortcode)` as a **logical identity**, not a lookup
837
833
  * convenience: two documents of one type bearing one shortcode denote *the same
@@ -916,7 +912,7 @@ function checkEmbeddedShortcodes(note, blockName) {
916
912
  * @param {object} [opts.index] - The link index, for the reference check. Its
917
913
  * absence skips that check rather than reporting every reference as dead.
918
914
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
919
- * it declares, as `engine/note-vocabulary.mjs` states them (#128). Supplied
915
+ * it declares, as `engine/note-vocabulary.mjs` states them. Supplied
920
916
  * by the caller for the same reason `schemas` is: this module validates a
921
917
  * note against whatever its type declares and knows no type names of its
922
918
  * own. Its absence skips the `data:` and `subType` checks rather than
@@ -926,7 +922,7 @@ function checkEmbeddedShortcodes(note, blockName) {
926
922
  * declaration, asked through `engine/generate.mjs`'s `emittedArtFor`.
927
923
  * Supplied by the caller like `schemas`, so this module states no list of
928
924
  * iconless types of its own; absent it, an inert `img:` goes unreported
929
- * rather than every note's being (#349).
925
+ * rather than every note's being.
930
926
  * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
931
927
  * The system blocks to check, and what each accepts. Supplied by the caller
932
928
  * for the same reason `schemas` is — a build derives them from its
@@ -950,7 +946,7 @@ export function lintNote(
950
946
  /**
951
947
  * The in-block keys this note's own type claims for something other than
952
948
  * 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
949
+ * reads past. Resolved once: the type is fixed for the note, and
954
950
  * each check would otherwise ask the same question of the same
955
951
  * declarations.
956
952
  */
@@ -959,7 +955,7 @@ export function lintNote(
959
955
  // The retired top-level fields, checked before the type: a note may carry
960
956
  // one whatever its type is, and each finding stands on its own. Reported
961
957
  // 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).
958
+ // every finding in the tree at once, rather than one note at a time.
963
959
  if (Object.hasOwn(fm, "package")) {
964
960
  findings.push({
965
961
  file: note.file,
@@ -973,7 +969,7 @@ export function lintNote(
973
969
  });
974
970
  }
975
971
  // `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
972
+ // per-pack `*-folders.yaml`. Both halves are retired together: the
977
973
  // id spelling has nothing left to resolve against once the YAML is gone.
978
974
  //
979
975
  // Checked here as well as refused at compile because this is where an
@@ -1002,7 +998,7 @@ export function lintNote(
1002
998
  }
1003
999
  // `img: ""` was how a note said "I name no art" while `resolveImg`
1004
1000
  // 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
1001
+ // says the opposite — "ship no art, and do not default me" — so a
1006
1002
  // note carrying the old spelling has quietly changed meaning. Forty-five
1007
1003
  // `sohl-thalorna` notes were written under the old reading and would have
1008
1004
  // lost their default art with no error and no warning; this is the guard
@@ -1038,14 +1034,14 @@ export function lintNote(
1038
1034
  // reads; it says nothing about which position a *check* reads, and
1039
1035
  // `authoredValue` went on resolving through the block regardless — so an
1040
1036
  // office with no style of address answered for its note's heading, in
1041
- // twenty-eight `sohl-kethira-basic` affiliations (#312). Hence
1037
+ // twenty-eight `sohl-kethira-basic` affiliations. Hence
1042
1038
  // `blockCollisions`: a note-level check reads past a block key its type
1043
1039
  // claims for something else.
1044
1040
 
1045
1041
  // The template priority is a *shared source* — the specification states it
1046
1042
  // once for every type, as it does `pack` — so its retirement is reported
1047
1043
  // here rather than by the per-type loop below, which only reaches a field
1048
- // some type's vocabulary declares (#266).
1044
+ // some type's vocabulary declares.
1049
1045
  if (declaresRetiredAlias(fm, "templatePriority")) {
1050
1046
  findings.push({
1051
1047
  file: note.file,
@@ -1065,7 +1061,7 @@ export function lintNote(
1065
1061
  });
1066
1062
  }
1067
1063
 
1068
- // An art field a note's own type never emits (#349). `img` is a *shared
1064
+ // An art field a note's own type never emits. `img` is a *shared
1069
1065
  // top-level* field — `BLOCK_DOCUMENT_PROPERTIES` maps it onto
1070
1066
  // `document.img`, so it is legal on every note whatever the type — and a
1071
1067
  // note whose document has no such property authors it, validates, compiles,
@@ -1084,7 +1080,7 @@ export function lintNote(
1084
1080
  // `index` and `vocabulary` set.
1085
1081
  //
1086
1082
  // **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
1083
+ // for "this note names no art", and on a type with no art that is a
1088
1084
  // true and harmless thing to say — it compiles identically to writing
1089
1085
  // nothing. Twenty-six `sohl-thalorna` place notes are in exactly that
1090
1086
  // state, and telling each of them to delete a key that already means
@@ -1140,7 +1136,7 @@ export function lintNote(
1140
1136
  "meant to have no image",
1141
1137
  });
1142
1138
  }
1143
- // `title: ""` publishes a blank heading (#218). The rule the art fields
1139
+ // `title: ""` publishes a blank heading. The rule the art fields
1144
1140
  // follow — `null` falls back, `""` is blank on purpose — reads the same way
1145
1141
  // here, and for a *page heading* the deliberate blank is almost never what
1146
1142
  // anyone wants: the emitter is `fm.title ?? name`, so `""` survives, the
@@ -1154,7 +1150,7 @@ export function lintNote(
1154
1150
  // **The emitter reads `fm.title`, so this reads the note level.** On an
1155
1151
  // `affiliation` `sohl.title` is the office's style of address, which the
1156
1152
  // 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
1153
+ // two apart. On every other type nothing claims the block key, so the
1158
1154
  // resolution is the unchanged one.
1159
1155
  if (authoredValue(fm, "title", { blockCollides: blockCollisions.has("title") }) === "") {
1160
1156
  findings.push({
@@ -1179,7 +1175,7 @@ export function lintNote(
1179
1175
  }
1180
1176
  // Anchored at column 1 for the same reason `aliases` is: `section` names a
1181
1177
  // configuration key too (`site.trees[].section`), and a nested one under
1182
- // some other block is not this field (#202).
1178
+ // some other block is not this field.
1183
1179
  if (Object.hasOwn(fm, "section")) {
1184
1180
  findings.push({
1185
1181
  file: note.file,
@@ -1191,7 +1187,7 @@ export function lintNote(
1191
1187
  // Anchored at column 1 for the same reason `section` is, and with more at
1192
1188
  // stake: `sohl.traits` is a *different field that shares the name* —
1193
1189
  // `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).
1190
+ // a finding about the retired top-level block must never open on it.
1195
1191
  if (Object.hasOwn(fm, "traits")) {
1196
1192
  findings.push({
1197
1193
  file: note.file,
@@ -1202,7 +1198,7 @@ export function lintNote(
1202
1198
  }
1203
1199
  // Only the top-level `aliases` is retired. `name.aliases` writes the same
1204
1200
  // key indented under `name:` and is **permitted** — reserved and unread —
1205
- // so both the test and the locator are anchored at column 1 (#180).
1201
+ // so both the test and the locator are anchored at column 1.
1206
1202
  if (declaresRetiredAliasesField(fm)) {
1207
1203
  findings.push({
1208
1204
  file: note.file,
@@ -1213,7 +1209,7 @@ export function lintNote(
1213
1209
  }
1214
1210
 
1215
1211
  // 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
1212
+ // `shortcode` it owes, and the `id` it may not write. Reported
1217
1213
  // beside the retired fields above because it is the same kind of statement
1218
1214
  // about the same note, and, like them, it must survive the two early
1219
1215
  // returns below: the finding stands whatever else the type is.
@@ -1224,7 +1220,7 @@ export function lintNote(
1224
1220
 
1225
1221
  // A refused field must be one the note *wrote*: `resolveNoteId` fills
1226
1222
  // `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
1223
+ // author never typed. The raw text is the only place that
1228
1224
  // distinguishes them, and `positionInFrontmatter` already answers it —
1229
1225
  // `topLevel` so a nested `id:` under some other key is not mistaken for the
1230
1226
  // note's own.
@@ -1242,7 +1238,7 @@ export function lintNote(
1242
1238
  });
1243
1239
  }
1244
1240
 
1245
- // The type's charset, before anything that looks the type up (#206). A
1241
+ // The type's charset, before anything that looks the type up. A
1246
1242
  // hyphenated type is unaddressable, and every lookup below would report it
1247
1243
  // as a type nobody declared — true, but not the reason, and it would send
1248
1244
  // the author to declare one rather than to rename it.
@@ -1278,7 +1274,7 @@ export function lintNote(
1278
1274
  // A **renamed** type is the opposite case, and the opposite answer: the
1279
1275
  // note compiles into exactly the document it always did, so refusing it
1280
1276
  // would fail a build over a note that is not wrong. It is reported, and
1281
- // every lookup below reads the current spelling (#78).
1277
+ // every lookup below reads the current spelling.
1282
1278
  const renamedTo = RENAMED_TYPES[type];
1283
1279
  if (renamedTo) {
1284
1280
  findings.push({
@@ -1286,7 +1282,7 @@ export function lintNote(
1286
1282
  ...at("type", type),
1287
1283
  // A warning, for the reason the retired *field* alias below is one:
1288
1284
  // the sweep is the content trees' work and the refusal comes after
1289
- // it, as `package:`'s did (#56).
1285
+ // it, as `package:`'s did.
1290
1286
  severity: "warning",
1291
1287
  message: renamedTypeMessage(type, renamedTo),
1292
1288
  });
@@ -1308,7 +1304,7 @@ export function lintNote(
1308
1304
  return findings;
1309
1305
  }
1310
1306
 
1311
- // The closed frontmatter regions (#128), checked beside the `sohl:` block
1307
+ // The closed frontmatter regions, checked beside the `sohl:` block
1312
1308
  // because they are the same statement about the same note: this key is not
1313
1309
  // one this type may write. Skipped entirely when the caller declares no
1314
1310
  // vocabulary — reporting every key as unknown because nothing was loaded
@@ -1326,7 +1322,7 @@ export function lintNote(
1326
1322
  // The retired spelling of a field this type declares → what to write now.
1327
1323
  // Built from the type's own vocabulary, so a renamed field is retired
1328
1324
  // exactly where its replacement exists and the old name stays an unknown
1329
- // key everywhere else (#142).
1325
+ // key everywhere else.
1330
1326
  const renamed = new Map();
1331
1327
  for (const name of declared) {
1332
1328
  const retired = RETIRED_FIELD_ALIASES[name];
@@ -1344,13 +1340,13 @@ export function lintNote(
1344
1340
  // A warning, not an error: the note compiles to the correct
1345
1341
  // document, so failing a build over it would red a tree that has
1346
1342
  // done nothing wrong yet. The refusal comes after the sweep, as
1347
- // `package:`'s did (#56).
1343
+ // `package:`'s did.
1348
1344
  severity: "warning",
1349
1345
  message: retiredAliasMessage(retired, current),
1350
1346
  });
1351
1347
  }
1352
1348
 
1353
- // Every declared system's block, each against its own vocabulary (#58). A
1349
+ // Every declared system's block, each against its own vocabulary. A
1354
1350
  // block carries the shared keys any system's does — `system`, `type`,
1355
1351
  // `img`, `effects`, `flags`, `pack` — plus whatever that system declares:
1356
1352
  // the note type's own field names for the system those schemas describe,
@@ -1358,7 +1354,7 @@ export function lintNote(
1358
1354
  // here is the configuration's answer, not this module's; see
1359
1355
  // {@link systemBlocksFor}.
1360
1356
  for (const [blockName, spec] of Object.entries(systems ?? {})) {
1361
- // Two embedded items denoting one entity (#228). Per block, because
1357
+ // Two embedded items denoting one entity. Per block, because
1362
1358
  // `items` is a block key and a second system's actor carries its own.
1363
1359
  // Before the `continue` below, because it is a statement about the
1364
1360
  // block's *shape* and holds whether or not this system declares a
@@ -1406,7 +1402,7 @@ export function lintNote(
1406
1402
  // reached through its parent, and reporting the parent twice — once as
1407
1403
  // itself and once as its child — helps nobody.
1408
1404
  const [head] = field.name.split(".");
1409
- // Resolved exactly as the compiler resolves it (#58): the system path
1405
+ // Resolved exactly as the compiler resolves it: the system path
1410
1406
  // first, then the block, then the declared shared source. A lint that
1411
1407
  // read only one of the three would report a note's own field as missing
1412
1408
  // the moment it moved to another of them. A **shared** field needs
@@ -1416,7 +1412,7 @@ export function lintNote(
1416
1412
  // A **renamed** field may still be written under its retired spelling,
1417
1413
  // which that order knows nothing about. It resolves through the reader
1418
1414
  // the compiler uses, so the lint cannot disagree with the build about
1419
- // which value a note carries (#142).
1415
+ // which value a note carries.
1420
1416
  if ((from === "default" || value == null) && RETIRED_FIELD_ALIASES[field.name]) {
1421
1417
  const aliased = readAliasedField(fm, field.name);
1422
1418
  if (aliased !== undefined) {
@@ -1424,7 +1420,7 @@ export function lintNote(
1424
1420
  from = "block";
1425
1421
  }
1426
1422
  }
1427
- // The sweep's progress signal (#305). A **warning**, for the reason a
1423
+ // The sweep's progress signal. A **warning**, for the reason a
1428
1424
  // retired spelling is one: the note compiles to the correct document,
1429
1425
  // so failing a build over it would red a tree that has done nothing
1430
1426
  // wrong yet. The refusal comes once no tree writes the position.
@@ -1441,7 +1437,7 @@ export function lintNote(
1441
1437
  // under `sohl:`, so telling an author to write `sohl.img` would send
1442
1438
  // them to the wrong region. Nor is a field whose shared source is a
1443
1439
  // 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).
1440
+ // exist, and the home of that field is the container it names.
1445
1441
  const label =
1446
1442
  field.shared || (field.name.includes(".") && field.legacyKey !== undefined) ?
1447
1443
  `\`${field.name}\``
@@ -1476,11 +1472,10 @@ export function lintNote(
1476
1472
  // would — rather than through a second, subtly different rule.
1477
1473
  //
1478
1474
  // **As an address, always** — which is now the only namespace there
1479
- // is (#180). A frontmatter reference is a bare address by construction:
1475
+ // is. A frontmatter reference is a bare address by construction:
1480
1476
  // there is no pipe to read intent from, and the field supplies the
1481
1477
  // 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).
1478
+ // read every `ref:` value as an alias, which `type-shortcode` never was.
1484
1479
  if (field.ref && index && typeof value === "string" && value) {
1485
1480
  const target = `${field.ref}-${value}`;
1486
1481
  if (!index.resolve(target) && !index.manifestHit(target)) {
@@ -1506,7 +1501,7 @@ export function lintNote(
1506
1501
  * @param {object} opts
1507
1502
  * @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
1508
1503
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
1509
- * it declares (#128); see {@link lintNote}.
1504
+ * it declares; see {@link lintNote}.
1510
1505
  * @param {boolean} [opts.references=true] - Whether to check references.
1511
1506
  * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
1512
1507
  * 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