@heroiclands/package-build 23.1.0 → 23.2.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 (248) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/bin/content-commands.mjs +68 -149
  3. package/bin/package-build.mjs +56 -10
  4. package/ci/check-banners.mjs +42 -6
  5. package/ci/confirm-npm-version.mjs +135 -0
  6. package/ci/content-format-reference.mjs +14 -5
  7. package/config.mjs +1 -0
  8. package/container.mjs +185 -4
  9. package/content-config.mjs +333 -210
  10. package/deploy.mjs +116 -10
  11. package/docs/api.md +238 -246
  12. package/docs/authoring/assets.md +11 -5
  13. package/docs/authoring/frontmatter.md +1 -1
  14. package/docs/authoring/links-and-markup.md +87 -0
  15. package/docs/commands.md +138 -99
  16. package/docs/configuration.md +277 -125
  17. package/docs/content-format.md +2 -0
  18. package/docs/diagnostics.md +48 -13
  19. package/docs/guides/book.md +341 -21
  20. package/docs/guides/e2e.md +13 -2
  21. package/docs/guides/icons.md +14 -4
  22. package/docs/guides/module-project.md +18 -8
  23. package/docs/guides/release-and-deploy.md +54 -6
  24. package/docs/guides/site.md +5 -2
  25. package/docs/project-setup.md +30 -10
  26. package/docs/reference/format-details.md +373 -170
  27. package/docs/reference/note-types.md +128 -109
  28. package/docs/reference/theme-pages.md +31 -6
  29. package/e2e.mjs +140 -28
  30. package/engine/actor-compiler.mjs +22 -31
  31. package/engine/address-charset.mjs +4 -17
  32. package/engine/address-fields.mjs +1 -1
  33. package/engine/address-render.mjs +12 -1
  34. package/engine/address-values.mjs +6 -0
  35. package/engine/address.mjs +46 -33
  36. package/engine/art-fields.mjs +30 -5
  37. package/engine/art-slots.mjs +10 -1
  38. package/engine/asset-bindings.mjs +12 -1
  39. package/engine/base-compiler.mjs +34 -65
  40. package/engine/being-age.mjs +2 -7
  41. package/engine/being-measurements.mjs +12 -1
  42. package/engine/calendar-artifacts.mjs +6 -0
  43. package/engine/calendar-choice.mjs +12 -1
  44. package/engine/calendar-format.mjs +12 -1
  45. package/engine/calendar-human.mjs +12 -1
  46. package/engine/compile-corpus.mjs +3 -1
  47. package/engine/content-address.mjs +9 -12
  48. package/engine/content-alerts.mjs +13 -1
  49. package/engine/content-blocks.mjs +14 -3
  50. package/engine/content-embeds.mjs +11 -3
  51. package/engine/content-figures.mjs +9 -0
  52. package/engine/content-footnotes.mjs +12 -1
  53. package/engine/content-format-check.mjs +11 -262
  54. package/engine/content-format.yaml +5 -2
  55. package/engine/content-images.mjs +3 -0
  56. package/engine/content-index.mjs +443 -20
  57. package/engine/content-links.mjs +29 -35
  58. package/engine/content-lint.mjs +2 -3
  59. package/engine/content-poetry.mjs +12 -1
  60. package/engine/content-slug.mjs +2 -14
  61. package/engine/content-spans.mjs +13 -1
  62. package/engine/content-tables.mjs +12 -1
  63. package/engine/culture-choice.mjs +210 -13
  64. package/engine/data-keys.mjs +19 -16
  65. package/engine/date-conversion.mjs +12 -1
  66. package/engine/declaration-check.mjs +9 -0
  67. package/engine/derived-fields.mjs +6 -7
  68. package/engine/diagnostics.mjs +34 -4
  69. package/engine/docs-checks.mjs +9 -0
  70. package/engine/document-subtypes.mjs +47 -34
  71. package/engine/event-views.mjs +139 -14
  72. package/engine/exported-scene.mjs +92 -28
  73. package/engine/extension-attributes.mjs +12 -1
  74. package/engine/field-spec.mjs +11 -102
  75. package/engine/folder-notes.mjs +0 -64
  76. package/engine/foreign-catalog.mjs +2 -1
  77. package/engine/format-generated.mjs +9 -0
  78. package/engine/foundry-entries.mjs +10 -28
  79. package/engine/frontmatter-lint.mjs +83 -392
  80. package/engine/frontmatter.mjs +20 -70
  81. package/engine/generate.mjs +10 -15
  82. package/engine/heading-attributes.mjs +12 -1
  83. package/engine/helpers.mjs +41 -102
  84. package/engine/holdings.mjs +1 -1
  85. package/engine/homepage.mjs +3 -10
  86. package/engine/ids.mjs +1 -167
  87. package/engine/index-records.mjs +6 -4
  88. package/engine/index.mjs +0 -3
  89. package/engine/infobox-render.mjs +15 -3
  90. package/engine/infobox.mjs +13 -14
  91. package/engine/item-compiler.mjs +4 -10
  92. package/engine/item-docs.mjs +3 -5
  93. package/engine/item-registry.mjs +2 -10
  94. package/engine/journals.mjs +1 -5
  95. package/engine/literature-notes.mjs +4 -12
  96. package/engine/macros.mjs +1 -2
  97. package/engine/map-graphviz.mjs +12 -1
  98. package/engine/markdown-expressions.mjs +12 -1
  99. package/engine/markdownlint-run.mjs +567 -0
  100. package/engine/metadata-index.mjs +42 -12
  101. package/engine/note-addresses.mjs +143 -35
  102. package/engine/note-claims.mjs +14 -28
  103. package/engine/note-dates.mjs +3 -3
  104. package/engine/note-frontmatter.mjs +6 -0
  105. package/engine/note-ids.mjs +8 -14
  106. package/engine/note-package.mjs +8 -25
  107. package/engine/note-state.mjs +1 -3
  108. package/engine/note-vocabulary.mjs +148 -84
  109. package/engine/number-words.mjs +12 -1
  110. package/engine/pack-config.mjs +15 -25
  111. package/engine/pack-router.mjs +10 -16
  112. package/engine/pathnames.mjs +8 -22
  113. package/engine/pdf-build.mjs +773 -121
  114. package/engine/pdf-coverage.mjs +188 -0
  115. package/engine/pdf-index.mjs +54 -7
  116. package/engine/pdf-pages.mjs +247 -0
  117. package/engine/pdf-render.mjs +114 -4
  118. package/engine/pdf-toc.mjs +29 -32
  119. package/engine/project-init.mjs +46 -6
  120. package/engine/prose-config.mjs +1 -1
  121. package/engine/prose-lint.mjs +52 -88
  122. package/engine/readability-lint.mjs +9 -0
  123. package/engine/reckoning-markers.mjs +12 -1
  124. package/engine/reference-targets.mjs +7 -0
  125. package/engine/runtime-only-fields.mjs +5 -8
  126. package/engine/scene-levels.mjs +2 -2
  127. package/engine/scenes.mjs +1 -1
  128. package/engine/shortcode-references.mjs +7 -0
  129. package/engine/site-build.mjs +30 -16
  130. package/engine/site-index.mjs +10 -16
  131. package/engine/site-preview.mjs +6 -0
  132. package/engine/site-root.mjs +8 -1
  133. package/engine/social-tie-terms.mjs +12 -1
  134. package/engine/social-ties.mjs +3 -0
  135. package/engine/sql-tables.mjs +56 -20
  136. package/engine/standing-terms.mjs +11 -1
  137. package/engine/standings.mjs +11 -27
  138. package/engine/system-block.mjs +39 -141
  139. package/engine/system-vocabulary.mjs +6 -0
  140. package/engine/walk-files.mjs +69 -0
  141. package/engine/web-wikilinks.mjs +23 -3
  142. package/engine/wikilink-syntax.mjs +32 -21
  143. package/engine/wikilinks.mjs +27 -16
  144. package/engine/yaml-lint.mjs +7 -4
  145. package/githooks/attribution.mjs +8 -0
  146. package/githooks/commit-message.mjs +9 -0
  147. package/hm3/actors.mjs +9 -48
  148. package/hm3/default-item-art.mjs +9 -9
  149. package/hm3/item-fields.mjs +2 -9
  150. package/hm3/template-priority.mjs +6 -7
  151. package/hugo-theme/data/banner-kinds.yaml +78 -0
  152. package/hugo-theme/data/banners.yaml +2 -3
  153. package/hugo-theme/layouts/404.html +3 -3
  154. package/hugo-theme/layouts/partials/banner-url.html +31 -7
  155. package/hugo-theme/layouts/partials/hero-banner.html +13 -14
  156. package/hugo-theme/layouts/partials/related.html +1 -1
  157. package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
  158. package/hugo-theme/static/css/style.css +1 -1
  159. package/manifest.mjs +11 -4
  160. package/markdownlint-config.mjs +1 -1
  161. package/package.json +6 -3
  162. package/release.mjs +83 -18
  163. package/sohl/actors.mjs +16 -27
  164. package/sohl/default-item-art.mjs +3 -3
  165. package/sohl/document-subtypes.mjs +1 -16
  166. package/sohl/infobox.mjs +2 -3
  167. package/sohl/item-builders.mjs +5 -8
  168. package/sohl/item-fields.mjs +5 -35
  169. package/sohl/items.mjs +0 -4
  170. package/sohl/kb-passes.mjs +3 -3
  171. package/types/container.d.mts +116 -5
  172. package/types/content-config.d.mts +83 -35
  173. package/types/deploy.d.mts +42 -6
  174. package/types/e2e.d.mts +35 -3
  175. package/types/engine/address-charset.d.mts +4 -17
  176. package/types/engine/address.d.mts +17 -8
  177. package/types/engine/art-fields.d.mts +7 -4
  178. package/types/engine/art-slots.d.mts +6 -0
  179. package/types/engine/base-compiler.d.mts +15 -17
  180. package/types/engine/content-address.d.mts +9 -12
  181. package/types/engine/content-embeds.d.mts +4 -2
  182. package/types/engine/content-format-check.d.mts +0 -64
  183. package/types/engine/content-index.d.mts +82 -3
  184. package/types/engine/content-links.d.mts +2 -4
  185. package/types/engine/culture-choice.d.mts +51 -8
  186. package/types/engine/data-keys.d.mts +2 -2
  187. package/types/engine/derived-fields.d.mts +1 -2
  188. package/types/engine/diagnostics.d.mts +27 -4
  189. package/types/engine/document-subtypes.d.mts +12 -31
  190. package/types/engine/event-views.d.mts +14 -2
  191. package/types/engine/exported-scene.d.mts +3 -2
  192. package/types/engine/field-spec.d.mts +13 -122
  193. package/types/engine/folder-notes.d.mts +0 -39
  194. package/types/engine/foreign-catalog.d.mts +2 -1
  195. package/types/engine/foundry-entries.d.mts +4 -5
  196. package/types/engine/frontmatter-lint.d.mts +2 -2
  197. package/types/engine/frontmatter.d.mts +19 -45
  198. package/types/engine/generate.d.mts +1 -1
  199. package/types/engine/helpers.d.mts +25 -42
  200. package/types/engine/homepage.d.mts +3 -10
  201. package/types/engine/ids.d.mts +0 -114
  202. package/types/engine/index-records.d.mts +6 -4
  203. package/types/engine/index.d.mts +0 -1
  204. package/types/engine/infobox-render.d.mts +7 -1
  205. package/types/engine/infobox.d.mts +3 -2
  206. package/types/engine/item-docs.d.mts +1 -1
  207. package/types/engine/item-registry.d.mts +2 -4
  208. package/types/engine/literature-notes.d.mts +1 -1
  209. package/types/engine/markdownlint-run.d.mts +40 -0
  210. package/types/engine/metadata-index.d.mts +9 -1
  211. package/types/engine/note-addresses.d.mts +27 -2
  212. package/types/engine/note-claims.d.mts +3 -5
  213. package/types/engine/note-ids.d.mts +2 -4
  214. package/types/engine/note-package.d.mts +1 -1
  215. package/types/engine/note-state.d.mts +1 -3
  216. package/types/engine/note-vocabulary.d.mts +28 -21
  217. package/types/engine/pack-config.d.mts +2 -6
  218. package/types/engine/pack-router.d.mts +4 -4
  219. package/types/engine/pathnames.d.mts +3 -3
  220. package/types/engine/pdf-build.d.mts +98 -12
  221. package/types/engine/pdf-coverage.d.mts +60 -0
  222. package/types/engine/pdf-index.d.mts +34 -5
  223. package/types/engine/pdf-pages.d.mts +143 -0
  224. package/types/engine/pdf-render.d.mts +45 -1
  225. package/types/engine/pdf-toc.d.mts +12 -14
  226. package/types/engine/prose-config.d.mts +1 -1
  227. package/types/engine/prose-lint.d.mts +11 -10
  228. package/types/engine/runtime-only-fields.d.mts +2 -2
  229. package/types/engine/site-build.d.mts +9 -5
  230. package/types/engine/site-index.d.mts +4 -5
  231. package/types/engine/site-root.d.mts +1 -1
  232. package/types/engine/sql-tables.d.mts +31 -2
  233. package/types/engine/standing-terms.d.mts +2 -1
  234. package/types/engine/standings.d.mts +7 -9
  235. package/types/engine/system-block.d.mts +10 -44
  236. package/types/engine/walk-files.d.mts +15 -0
  237. package/types/engine/wikilink-syntax.d.mts +18 -19
  238. package/types/engine/yaml-lint.d.mts +5 -4
  239. package/types/hm3/actors.d.mts +1 -2
  240. package/types/hm3/item-fields.d.mts +2 -4
  241. package/types/hm3/template-priority.d.mts +6 -7
  242. package/types/manifest.d.mts +3 -2
  243. package/types/release.d.mts +12 -7
  244. package/types/sohl/actors.d.mts +2 -1
  245. package/types/sohl/document-subtypes.d.mts +1 -2
  246. package/types/sohl/item-fields.d.mts +3 -7
  247. package/engine/retired-fields.mjs +0 -631
  248. package/types/engine/retired-fields.d.mts +0 -354
@@ -83,63 +83,39 @@ export function sohlField(fm, key, defaultValue = undefined) {
83
83
  * `relations` and a projectile's impact die — because each validates a *shape
84
84
  * spread over several keys* rather than coercing one scalar.
85
85
  *
86
- * That was equivalent while every note authored inside the block, and stopped
87
- * being so once a note may author at `sohl.system.<to>` instead: {@link
88
- * sohlField} sees `sohl.<key>` and the top level, never inside `sohl.system`.
89
- * So those five read as unset and shipped their empty value — a missing
90
- * `subType` is a thrown build error, and the other four ship empty in silence,
91
- * which is the failure class the passthrough exists to prevent.
86
+ * {@link sohlField} sees `sohl.<key>` and the top level, never inside
87
+ * `sohl.system`, so this reads the destination first: a note may author at
88
+ * `sohl.system.<to>`, and a reader that could not see it would ship the
89
+ * field's empty value in silence.
92
90
  *
93
91
  * The **destination wins**, matching every other field's resolution order.
94
92
  *
95
- * `legacyKey` is for the one pair whose two positions are spelled differently:
96
- * a projectile authors `impact.die` and stores `impactBase.die`, so the reader
97
- * has to be told both. It is the same split `FieldSpec.name`/`legacyKey` makes,
98
- * for the same reason — one name cannot key two positions.
93
+ * `name` is for the one pair whose two positions are spelled differently: a
94
+ * projectile authors `impact.die` and stores `impactBase.die`, so the reader
95
+ * has to be told both.
99
96
  *
100
- * `shared` is the field's declared shared source, and it completes the order the
101
- * three other positions are two thirds of. A field whose source is a path into
102
- * `data:` is authored there by the specification's own mapping tables, and a
103
- * reader that knew the destination, the block and the top level alone skipped
104
- * straight past the container: every authored value read as unset, and the
105
- * document shipped the field's default with nothing said. It sits **between**
106
- * the block and the top level, exactly where
107
- * {@link module:engine/system-block.resolveFieldValue} puts it — the block is the
108
- * position a tree still writes and wins while it does, and the bare top-level
109
- * key is the spelling `data:` gathered the fact off.
97
+ * `shared` is the field's declared shared source when it is a path into
98
+ * `data:`. Such a field is authored in that container and nowhere else, so the
99
+ * block and the top level are not consulted.
110
100
  *
111
101
  * @param {object} fm - The note's frontmatter.
112
102
  * @param {string} to - The key at the destination, dotted for a nested one.
113
103
  * @param {any} [defaultValue] - What an unauthored field reads as.
114
104
  * @param {object} [options] - Options.
115
- * @param {string} [options.legacyKey] - The key the block still carries, when
116
- * it is not spelled `to`. Defaults to `to`.
105
+ * @param {string} [options.name] - The key the block carries, when it is not
106
+ * spelled `to`. Defaults to `to`.
117
107
  * @param {string} [options.shared] - The field's declared shared source, when it
118
- * is a path into a container (`data.relations`). Omitted by a field whose
119
- * shared position is the note's top level, which `sohlField` already reads.
108
+ * is a path into a container (`data.relations`).
120
109
  * @returns {any} The value.
121
110
  */
122
- export function sohlSystemField(fm, to, defaultValue = undefined, { legacyKey = to, shared } = {}) {
111
+ export function sohlSystemField(fm, to, defaultValue = undefined, { name = to, shared } = {}) {
123
112
  const system = fm?.sohl?.system;
124
113
  if (system && typeof system === "object" && !Array.isArray(system)) {
125
114
  const found = getFrontmatter(system, to, undefined);
126
115
  if (found !== undefined) return found;
127
116
  }
128
- if (shared !== undefined) {
129
- const block = fm?.sohl;
130
- const inBlock =
131
- block && typeof block === "object" ?
132
- getFrontmatter(block, legacyKey, undefined)
133
- : undefined;
134
- // Consulted only where the block does not carry the key, so step 2 keeps
135
- // winning; `sohlField` below then reads the block and the retiring
136
- // top-level spelling exactly as it always has.
137
- if (inBlock === undefined) {
138
- const container = getFrontmatter(fm, shared, undefined);
139
- if (container !== undefined) return container;
140
- }
141
- }
142
- return sohlField(fm, legacyKey, defaultValue);
117
+ if (shared !== undefined) return getFrontmatter(fm, shared, defaultValue);
118
+ return sohlField(fm, name, defaultValue);
143
119
  }
144
120
 
145
121
  /**
@@ -184,9 +160,6 @@ function readMapEntries(fm, key, { shared } = {}) {
184
160
  * so absent frontmatter must resolve to `null` — coercing it to `0` would ship
185
161
  * every item as an uncapped charge-user.
186
162
  *
187
- * A legacy `usesCharges` flag in authored frontmatter is ignored: it was inert
188
- * and has been dropped from the schema.
189
- *
190
163
  * @param {object} fm - The item frontmatter.
191
164
  * @returns {{value: number|null, max: number|null}} The persisted charges block.
192
165
  */
@@ -249,7 +222,7 @@ export function resolveSkillAptitudes(fm, ctx = "item") {
249
222
  }
250
223
 
251
224
  /**
252
- * Resolve an affiliation's `relation` map — the shortcode of another
225
+ * Resolve an affiliation's `relations` map — the shortcode of another
253
226
  * affiliation → this one's standing toward it.
254
227
  *
255
228
  * An unrecognized standing would fail the schema's `choices` validation at load
@@ -266,18 +239,7 @@ export function resolveSkillAptitudes(fm, ctx = "item") {
266
239
  * @throws {Error} When the map is malformed or names an unknown standing.
267
240
  */
268
241
  export function resolveRelation(fm, ctx = "item") {
269
- // `relations` is the field's name; `relation` is the retired spelling, read
270
- // underneath it so a tree converts on its own schedule. The
271
- // current name wins wherever a note writes both, and the lint reports the
272
- // old one through {@link RETIRED_FIELD_ALIASES}.
273
- // Probed at the destination too: a note that has moved to
274
- // `sohl.system.relations` carries the current spelling, and a probe that
275
- // could not see it would fall through to the retired one and read `{}`.
276
- // Each spelling is probed at all four of its positions, the `data:` source
277
- // the specification names included, so a retirement and a container move are
278
- // independent of each other.
279
- const current = sohlSystemField(fm, "relations", undefined, { shared: "data.relations" });
280
- const key = current == null ? "relation" : "relations";
242
+ const key = "relations";
281
243
  const entries = readMapEntries(fm, key, { shared: `data.${key}` });
282
244
  if (entries === null) {
283
245
  throw new Error(`${ctx}: ${key} must be a map of shortcode → standing`);
@@ -362,20 +324,8 @@ export function parseValueDesc(raw) {
362
324
  *
363
325
  * `data.packFolder` is a folder note's **address**
364
326
  * (`folder-poisonsandtoxins`), resolved through the address index the whole
365
- * build shares. The `folder:` Foundry-id spelling this function once read
366
- * beside it, and the per-pack `*-folders.yaml` that id was resolved against,
367
- * are retired together — a note declaring `folder:` is refused by
368
- * {@link module:engine/folder-notes.assertNoDeclaredFolder} rather than
369
- * reaching here, so there is no second source for a value to come from.
370
- *
371
- * **`packFolder` was a path for one release** (`Possessions/Misc_Gear/Cooking`)
372
- * and is an address now. A path encoded the hierarchy *in the value*, so
373
- * reparenting a folder made every note naming it wrong — a structural edit
374
- * became a corpus-wide rewrite. An address is stable under reparenting, which
375
- * is exactly why a note is addressed by `(type, shortcode)` and never by
376
- * `file.path`. The path form is **removed rather than deprecated**: it had no
377
- * authors to migrate, which is the whole reason the change was cheap enough to
378
- * make.
327
+ * build shares. An address is stable under reparenting, which is exactly why a
328
+ * note is addressed by `(type, shortcode)` and never by `file.path`.
379
329
  *
380
330
  * @param {object|null|undefined} fm - Parsed frontmatter.
381
331
  * @returns {{value: string|null, isAddress: boolean}} The authored value, and
@@ -28,15 +28,14 @@
28
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
30
  * naming it, its ancestors with it. So no pack loads a folder list, and
31
- * two packs can no longer disagree about a folder they both hold.
31
+ * two packs cannot 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
36
  * output states that on its compiler and is scheduled after it.
37
37
  *
38
- * This replaces the retired `packs:export` (vault → committed `_source/`); the
39
- * HeroicLands vault is no longer a build input for SoHL content.
38
+ * The HeroicLands vault is not a build input for SoHL content.
40
39
  */
41
40
 
42
41
  import { noteAddressContext } from "./note-addresses.mjs";
@@ -73,7 +72,7 @@ import { routerFor } from "./pack-router.mjs";
73
72
  import { NEVER_PACKED_TYPES, unclaimedNoteFindings } from "./note-claims.mjs";
74
73
  // Which document a content type compiles into, so the art declaration below is
75
74
  // answered from the same routing the compile uses.
76
- import { RETIRED_TYPES, currentType, packForType } from "./ids.mjs";
75
+ import { packForType } from "./ids.mjs";
77
76
  import { contentPackage } from "./content-package.mjs";
78
77
 
79
78
  /**
@@ -230,22 +229,22 @@ export function compilerFor(docType, system = null) {
230
229
  * @param {string} type - The note's content type.
231
230
  * @returns {{document: string|null, art: readonly string[]}|null} What the type
232
231
  * compiles into and the art it carries there, or `null` where no claim can be
233
- * made — a retired type, which is reported as retired instead.
232
+ * made.
234
233
  */
235
234
  export function emittedArtFor(type) {
236
235
  const name = String(type ?? "");
237
- if (!name || Object.hasOwn(RETIRED_TYPES, name)) return null;
236
+ if (!name) return null;
238
237
 
239
238
  // A homepage compiles into a *page*, not a compendium document, so nothing
240
239
  // it authors reaches one. It is the one type whose absence from every pack
241
240
  // is the intended state (`NEVER_PACKED_TYPES`).
242
- if (NEVER_PACKED_TYPES.has(currentType(name))) return { document: null, art: [] };
241
+ if (NEVER_PACKED_TYPES.has(name)) return { document: null, art: [] };
243
242
 
244
243
  // A folder reaches a pack by a route of its own — it materialises in every
245
244
  // pack holding a document that references it — so `packForType` has no
246
245
  // answer for it and no compiler class writes it. `folderDocument` does, and
247
246
  // a Foundry `Folder` has no artwork at all.
248
- if (currentType(name) === FOLDER_TYPE) return { document: "Folder", art: [] };
247
+ if (name === FOLDER_TYPE) return { document: "Folder", art: [] };
249
248
 
250
249
  const { docType } = packForType(name);
251
250
  const passes = [
@@ -516,11 +515,8 @@ async function generatePack(
516
515
  /**
517
516
  * The Foundry folder id a note names, by its folder note's address.
518
517
  *
519
- * There is one spelling. `packFolder` names a folder **note**, resolved
520
- * through the address index shared by the whole build; the `folder:`
521
- * Foundry-id spelling and the per-pack `*-folders.yaml` it resolved
522
- * against are retired together, so there is no second source left
523
- * for a value to come from.
518
+ * `packFolder` names a folder **note**, resolved through the address index
519
+ * shared by the whole build.
524
520
  *
525
521
  * @param {string|null|undefined} value - As authored.
526
522
  * @returns {string|null} The folder id, or `null` for an absent value.
@@ -799,8 +795,7 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
799
795
  emitDiagnostic(finding);
800
796
 
801
797
  // A note whose `type:` no configured pack claims compiles into nothing, and
802
- // used to say nothing — no pass got far enough to reject it, so the
803
- // silence had no owner. Asked once, of the whole configuration, because
798
+ // no pass gets far enough to reject it, so the silence has no owner. Asked once, of the whole configuration, because
804
799
  // that is the only place it can be answered: a per-pass check would report
805
800
  // every type a system deliberately does not map, which is exactly the
806
801
  // silence the rule requires. Independent of `only`, since it is a fact about the
@@ -1,4 +1,15 @@
1
- /* SPDX-License-Identifier: GPL-3.0-or-later */
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
2
13
 
3
14
  /**
4
15
  * The braced attribute block a heading may end with, read once.
@@ -303,7 +303,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
303
303
  *
304
304
  * The rule in one place, so every reader of the tree refuses the same
305
305
  * way and says so in the same words. It is shared rather than repeated because
306
- * the corpus is no longer read only by {@link walkMarkdownTree}: a pass that
306
+ * the corpus is read by more than {@link walkMarkdownTree}: a pass that
307
307
  * reads the content index instead is making the identical claim about which
308
308
  * files it is looking at, and must be held to the identical requirement — a
309
309
  * scope that quietly defaulted there would reintroduce exactly the second
@@ -423,68 +423,45 @@ export function* walkMarkdownTree(rootDir, { skipDirectories, addressContext } =
423
423
  * block, in the order they answer — and whichever of them the note actually
424
424
  * wrote.
425
425
  *
426
- * Three places, in the order the migration runs. The specification calls this
427
- * `data.templatePriority`; `sohl-thalorna` already writes it there on 941
428
- * notes, beside the `archetype` the build reads — so a tree that has authored
429
- * forward is read from the key it authored, and only then does the retiring
430
- * spelling answer.
426
+ * Three places: the specification calls this `data.templatePriority`, and the
427
+ * system block and the note's top level answer after it.
431
428
  *
432
429
  * **The block is a parameter because the value is shared, not per-system.** One
433
430
  * `data.templatePriority` is the note's statement that it is a template; SoHL
434
431
  * records it as `system.templatePriority` and HM3 as `flags.hm3.templatePriority`.
435
- * The legacy in-block position is therefore read from *the block being compiled*
432
+ * The in-block position is therefore read from *the block being compiled*
436
433
  * rather than always from `sohl:`, so an HM3-only note is not asked to author a
437
434
  * SoHL block to be read.
438
435
  *
439
436
  * @param {object} fm Parsed frontmatter.
440
437
  * @param {string} block The system block being compiled.
441
- * @returns {{found: [object, string]|undefined, retiring: [object, string]|undefined,
442
- * NEW: string, OLD: string}} The answering position, the retiring spelling's
443
- * position if the note also carries it, and the two key names.
438
+ * @returns {{found: [object, string]|undefined, KEY: string}} The answering
439
+ * position and the key name.
444
440
  */
445
441
  function findTemplatePriority(fm, block) {
446
442
  const inBlock =
447
443
  fm != null && typeof fm[block] === "object" && fm[block] !== null ? fm[block] : null;
448
444
  const data = fm != null && typeof fm.data === "object" && fm.data !== null ? fm.data : null;
449
445
 
450
- const NEW = "templatePriority";
451
- const OLD = "archetype";
446
+ const KEY = "templatePriority";
452
447
  const sources = /** @type {[object|null, string][]} */ ([
453
- [data, NEW],
454
- [inBlock, NEW],
455
- [fm, NEW],
456
- [inBlock, OLD],
457
- [fm, OLD],
448
+ [data, KEY],
449
+ [inBlock, KEY],
450
+ [fm, KEY],
458
451
  ]);
459
452
  const found = sources.find(([where, key]) => where != null && key in where);
460
- // A note part-way through the rename may carry both spellings, and they may
461
- // *disagree*: 145 of `sohl-thalorna`'s 941 dual-spelled notes say
462
- // `templatePriority: null` where `archetype: 0` says the opposite — "not a
463
- // template" against "a template at priority 0". Preferring the new key
464
- // silently would flip those documents, and preferring the old would ignore
465
- // what an author wrote most recently. Neither is a decision this function
466
- // gets to make quietly, so a contradiction is refused and named.
467
- const retiring = sources
468
- .slice(sources.findIndex(([, key]) => key === OLD))
469
- .find(([where, key]) => where != null && key in where);
470
- return {
471
- found: /** @type {[object, string]|undefined} */ (found),
472
- retiring: /** @type {[object, string]|undefined} */ (retiring),
473
- NEW,
474
- OLD,
475
- };
453
+ return { found: /** @type {[object, string]|undefined} */ (found), KEY };
476
454
  }
477
455
 
478
456
  /**
479
457
  * The template priority a note states, for a system that treats an unstated one
480
458
  * as "not a template" rather than as an authoring error.
481
459
  *
482
- * Reads exactly the positions {@link resolveTemplatePriority} reads, including
483
- * the retiring `archetype` spelling, and refuses the same contradiction — so
484
- * the two systems cannot disagree about what a note said. It differs only in
485
- * what silence means: SoHL requires the statement, while HM3 keeps the value in
486
- * a flag it simply omits, so there is no tri-state for an absent value to
487
- * corrupt and nothing to demand.
460
+ * Reads exactly the positions {@link resolveTemplatePriority} reads, so the two
461
+ * systems cannot disagree about what a note said. It differs only in what
462
+ * silence means: SoHL requires the statement, while HM3 keeps the value in a
463
+ * flag it simply omits, so there is no tri-state for an absent value to corrupt
464
+ * and nothing to demand.
488
465
  *
489
466
  * @param {object} fm Parsed frontmatter.
490
467
  * @param {string} label Human-readable context for error messages.
@@ -492,18 +469,9 @@ function findTemplatePriority(fm, block) {
492
469
  * @param {string} [options.block="sohl"] The system block being compiled.
493
470
  * @returns {number|null} The priority, or `null` when the note is not a
494
471
  * template or states nothing.
495
- * @throws {Error} When both spellings are present and disagree.
496
472
  */
497
473
  export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
498
- const { found, retiring, NEW, OLD } = findTemplatePriority(fm, block);
499
- if (found && retiring && found[1] !== OLD && found[0][found[1]] !== retiring[0][OLD]) {
500
- throw new Error(
501
- `Conflicting ${NEW} for ${label}: ` +
502
- `${NEW} is ${JSON.stringify(found[0][found[1]])} and the retiring ` +
503
- `${OLD} is ${JSON.stringify(retiring[0][OLD])}. Both are read and ` +
504
- `${NEW} wins, so they must agree — delete ${OLD}, or correct it`,
505
- );
506
- }
474
+ const { found } = findTemplatePriority(fm, block);
507
475
  if (!found) return null;
508
476
  const raw = found[0][found[1]];
509
477
  return raw === null || raw === "" || raw === undefined ? null : raw;
@@ -519,31 +487,21 @@ export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
519
487
  * silently assumed.
520
488
  *
521
489
  * Reads the positions {@link findTemplatePriority} lists: `data.templatePriority`
522
- * first — the specified home — then the system block and the top level, and
523
- * finally the retiring `archetype` spelling in the same two places.
490
+ * first — the specified home — then the system block and the top level.
524
491
  *
525
492
  * @param {object} fm Parsed frontmatter.
526
493
  * @param {string} label Human-readable context for error messages.
527
494
  * @param {object} [options] Options.
528
495
  * @param {string} [options.block="sohl"] The system block being compiled.
529
496
  * @returns {number|undefined} The template priority, or `undefined` when null.
530
- * @throws {Error} When the property is absent, is not a number/null, or both
531
- * spellings are present and disagree.
497
+ * @throws {Error} When the property is absent or is not a number/null.
532
498
  */
533
499
  export function resolveTemplatePriority(fm, label, { block = "sohl" } = {}) {
534
- const { found, retiring, NEW, OLD } = findTemplatePriority(fm, block);
500
+ const { found, KEY } = findTemplatePriority(fm, block);
535
501
 
536
- if (found && retiring && found[1] !== OLD && found[0][found[1]] !== retiring[0][OLD]) {
537
- throw new Error(
538
- `Conflicting ${NEW} for ${label}: ` +
539
- `${NEW} is ${JSON.stringify(found[0][found[1]])} and the retiring ` +
540
- `${OLD} is ${JSON.stringify(retiring[0][OLD])}. Both are read and ` +
541
- `${NEW} wins, so they must agree — delete ${OLD}, or correct it`,
542
- );
543
- }
544
502
  if (!found) {
545
503
  throw new Error(
546
- `Missing required ${NEW} for ${label} — set a number (this is a template, at that priority) or null (it is not)`,
504
+ `Missing required ${KEY} for ${label} — set a number (this is a template, at that priority) or null (it is not)`,
547
505
  );
548
506
  }
549
507
  const raw = found[0][found[1]];
@@ -558,7 +516,7 @@ export function resolveTemplatePriority(fm, label, { block = "sohl" } = {}) {
558
516
 
559
517
  /**
560
518
  * The value a document's `system.templatePriority` carries, from the required
561
- * `templatePriority` frontmatter (`archetype` is the legacy spelling).
519
+ * `templatePriority` frontmatter.
562
520
  *
563
521
  * A **schema field**, so the tri-state is written out in full rather than
564
522
  * expressed by a key's presence: a number is a template at that priority, and
@@ -602,15 +560,11 @@ export function makeFilename(name, id) {
602
560
  * and nothing else, and because the default a caller applies to an unset one is
603
561
  * domain-specific — actors default differently from items, and gear differently
604
562
  * again — so each compiler owns its default and applies it with **nullish**
605
- * coalescing: `resolveImg(fm.img) ?? <default>`. Not `||`, which would collapse
606
- * a deliberate blank back into the default.
563
+ * coalescing: `resolveImg(fm.img) ?? <default>`.
607
564
  *
608
- * **Two empties, and they mean opposite things.** `null` — or an absent key,
609
- * which reaches here as `undefined` — means _unset_: the note names no art and
610
- * the caller's default applies. `""` means _blank on purpose_: the note names
611
- * no art **and wants none**, so no default may replace it. Both come back
612
- * distinguishable, `null` and `""` respectively, and neither is invented from
613
- * the other.
565
+ * `null` — or an absent key, which reaches here as `undefined` — means _unset_:
566
+ * the note names no art and the caller's default applies. An empty string is
567
+ * not a pathname and is refused like any other value that names no file.
614
568
  *
615
569
  * **`title` does not follow this rule**, and must not be made to. On a
616
570
  * `type: affiliation` note `title` is a declared item field with its own
@@ -633,11 +587,11 @@ export function makeFilename(name, id) {
633
587
  * @param {string | null | undefined} raw - The pathname from frontmatter.
634
588
  * @param {{assetRoot: string|null}} [config] - The resolved build configuration.
635
589
  * Defaults to this repository's.
636
- * @returns {string | null} the Foundry-relative path; `""` for a deliberate
637
- * blank, and `null` when the note names no art at all.
638
- * @throws {Error} When no Foundry address can be derived — a `documentation`
639
- * package, which Foundry installs nothing of, or a pathname naming a package
640
- * this build declares no relationship with.
590
+ * @returns {string | null} the Foundry-relative path, and `null` when the
591
+ * note names no art at all.
592
+ * @throws {Error} When the pathname is empty, or when no Foundry address can
593
+ * be derived — a `documentation` package, which Foundry installs nothing of,
594
+ * or a pathname naming a package this build declares no relationship with.
641
595
  */
642
596
  export function resolveImg(raw, config = loadPackConfig()) {
643
597
  const forms = resolvePathname(raw, config);
@@ -672,10 +626,8 @@ export function resolveName(fm, defaultValue = "Unnamed") {
672
626
  * every v14 build and so left all shipped content permanently eligible for
673
627
  * every v14 migration.
674
628
  *
675
- * **Read from the configuration, not from the shipped manifest.** It used to
676
- * open `paths.packageManifest` and take `compatibility.minimum` out of it,
677
- * because the configuration had no way to state the fact. Now that it does, the
678
- * manifest is generated *from* the configuration — so reading it back would be
629
+ * **Read from the configuration, not from the shipped manifest.** The
630
+ * manifest is generated *from* the configuration, so reading it back would be
679
631
  * a round trip through an artifact that need not exist yet: `build:db` can run
680
632
  * before the manifest is written.
681
633
  *
@@ -704,9 +656,7 @@ export function supportedCoreVersion(config = loadPackConfig()) {
704
656
  /**
705
657
  * Default `_stats` block for compiled compendium entries.
706
658
  *
707
- * Every stamped identity is configuration: four compilers used to pass
708
- * the same frozen `"0.6.0"` literal, and `systemId` / `lastModifiedBy` were
709
- * written into this function. `coreVersion` alone is *not* configuration — it
659
+ * Every stamped identity is configuration. `coreVersion` alone is *not* configuration — it
710
660
  * comes from {@link supportedCoreVersion}, the configured Foundry floor,
711
661
  * so a document never claims to predate the migrations that would rewrite it.
712
662
  *
@@ -771,11 +721,9 @@ let cachedDefaultStats;
771
721
  /**
772
722
  * The `_stats` block every compiler stamps on an entry it emits, built once.
773
723
  *
774
- * Each compiler used to hoist `const STATS = buildStats()` at module scope,
775
- * which read the shipped package manifest the moment the module was imported —
776
- * so importing a compiler required a manifest to exist even when nothing was
777
- * going to be compiled. Deferred to first use and memoised here, the
778
- * cost and the identity are what they always were; only the moment moved.
724
+ * Built on first use and memoised, not at module scope: building it reads the
725
+ * shipped package manifest, and importing a compiler must not require a
726
+ * manifest to exist when nothing is going to be compiled.
779
727
  *
780
728
  * @returns {object} The default `_stats` block, shared by every compiler.
781
729
  */
@@ -802,7 +750,7 @@ export { makeId } from "./ids.mjs";
802
750
 
803
751
  // The content-type → document-type map, which decides *which* pack list a
804
752
  // note's own document is routed against.
805
- import { assertTypeNotRetired, packForType } from "./ids.mjs";
753
+ import { packForType } from "./ids.mjs";
806
754
  import { collectAnchors } from "./anchors.mjs";
807
755
 
808
756
  /* ------------------------------------------------------------------------ */
@@ -820,10 +768,7 @@ import { collectAnchors } from "./anchors.mjs";
820
768
  * would otherwise address every one of them as the first. A note whose
821
769
  * declaration is unroutable is indexed against the conventional name and left
822
770
  * for the compile pass to report — the index has no business failing a build,
823
- * and the pass fails it with a far better message. The one exception is a
824
- * **retired** content type: this walk is the first to see every
825
- * note together with its path, and unlike an unroutable declaration there is
826
- * no pass that would ever claim such a note and report it.
771
+ * and the pass fails it with a far better message.
827
772
  *
828
773
  * @param {string} contentBase - Root of the content tree.
829
774
  * @param {object} [router] - The pack router. Supplied by the calling pass so
@@ -861,9 +806,8 @@ export function buildContentLinkIndex(
861
806
  // A documentation journal is a document this tree emits, not a note in
862
807
  // it; the note it documents is indexed here and carries its address.
863
808
  if (!isNoteRecord(record)) continue;
864
- // The note as its author wrote it — the router, the draft tag and the
865
- // retired-type check all read authored fields, and none of them may be
866
- // handed the keys the index derived.
809
+ // The note as its author wrote it — the router and the draft tag read
810
+ // authored fields, and neither may be handed the keys the index derived.
867
811
  const fm = authoredFrontmatter(record);
868
812
  const absPath = noteFile(contentBase, record);
869
813
  // The id a note's document is filed under: its authored pin, or the
@@ -876,11 +820,6 @@ export function buildContentLinkIndex(
876
820
  // no shortcode — which is not an addressable note and has no document
877
821
  // to link to.
878
822
  if (!fm?.id) continue;
879
- // The first walk of every note in the tree, and the only one holding
880
- // both the declared type and the file that declares it — so a note
881
- // left on a retired type is reported here, by name, rather than
882
- // several frames deeper with nothing to go on.
883
- assertTypeNotRetired(fm.type, absPath);
884
823
  const base = String(record.file.name).replace(/_/g, " ");
885
824
  const documentation = recordsByAddress.get(encodeAddresses(record.documentation));
886
825
  const noOwnDocument =
@@ -84,7 +84,7 @@ function referenceKey(value, node, type) {
84
84
  type,
85
85
  types: new Set([type]),
86
86
  },
87
- { declared: true, legacyShortcodeCase: true },
87
+ { declared: true },
88
88
  );
89
89
  if (tuple.reason || tuple.type !== type) return "";
90
90
  return renderAddress({ ...tuple, system: "note" }).toLowerCase();
@@ -138,10 +138,6 @@ export function isHomepage(fm) {
138
138
  * closed vocabulary, so repeating that judgement per type would be a second
139
139
  * answer to one question.
140
140
  *
141
- * `aliases` is deliberately not in the class: it is a **retired** field, refused
142
- * on every note whatever its type, so it is answered there rather than
143
- * here.
144
- *
145
141
  * @type {ReadonlyMap<string, string>}
146
142
  */
147
143
  export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
@@ -184,8 +180,7 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
184
180
  *
185
181
  * Each finding carries the `locator` key to position it at, because the two
186
182
  * things that would resolve one — the raw note text and the position helper —
187
- * belong to the caller. This mirrors {@link module:engine/retired-fields},
188
- * whose retired-field messages are likewise positioned by whoever reports them.
183
+ * belong to the caller.
189
184
  *
190
185
  * **A refused field must be one the note *wrote*.** `resolveNoteId` fills
191
186
  * `fm.id` **in place** so every downstream reader sees one derived value —
@@ -364,8 +359,7 @@ export function homepageTitle(fm, config) {
364
359
  *
365
360
  * The note's own, plus the two derived values every emitted page carries: the
366
361
  * resolved `title`, and the package the build derived — no note declares one
367
- * (`package:` is retired) and the theme's breadcrumb partial reads
368
- * `.Params.package`.
362
+ * and the theme's breadcrumb partial reads `.Params.package`.
369
363
  *
370
364
  * **No `url` and no `slug`.** Hugo publishes the `home` kind at `baseURL`,
371
365
  * whose path is already where the package is served, so the page has no
@@ -374,8 +368,7 @@ export function homepageTitle(fm, config) {
374
368
  *
375
369
  * An authored `aliases` is dropped for the same reason it is on every other
376
370
  * page: Hugo reads it as URL redirects, so passing it through would publish a
377
- * redirect stub at each one. The field is retired and refused before a
378
- * build reaches here, which makes this a guard rather than a working path.
371
+ * redirect stub at each one.
379
372
  *
380
373
  * `related` is derived like every other page's — what links to the homepage
381
374
  * is a fact about the rest of the tree — so an authored value is dropped and