@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
@@ -23,6 +23,9 @@
23
23
  * `icon` while its accepted set also takes `image`. A value naming a type
24
24
  * outside it is an error, against this set rather than against `type`.
25
25
  * @property {boolean} document - Whether the slot reaches a compiled document.
26
+ * @property {string} [declines] - The literal value that declines the slot's
27
+ * art on purpose. It is never read as an Address: it resolves to no image and
28
+ * is emitted as a blank, which a theme reads as "draw none".
26
29
  * @property {string} describe - One line, for the author-facing reference.
27
30
  */
28
31
 
@@ -76,6 +79,12 @@ export const ART_SLOTS = Object.freeze([
76
79
  // fitted to whatever room it has, so no slot but this one states a
77
80
  // size.
78
81
  size: Object.freeze({ width: 1792, height: 768 }),
79
- describe: "The page's hero image, cut to 1792×768. Reaches no compiled document.",
82
+ // `none` is the one way to decline a hero image. It is a word rather
83
+ // than a blank because `""` is refused: a blank is how an unfinished
84
+ // note looks, and declining has to be said on purpose.
85
+ declines: "none",
86
+ describe:
87
+ "The page's hero image, cut to 1792×768; `none` declines it. " +
88
+ "Reaches no compiled document.",
80
89
  }),
81
90
  ]);
@@ -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
  import fs from "node:fs";
4
15
  import path from "node:path";
@@ -92,20 +92,17 @@ import {
92
92
  import { isNoteRecord, noteFile } from "./index-records.mjs";
93
93
  import { reduceAddressFields } from "./address-fields.mjs";
94
94
  import { artPathname, artSlot, unacceptedArtMessage, unresolvedArtMessage } from "./art-fields.mjs";
95
- import { emitDiagnostic, positionOfFrontmatterPath, positionOfLiteral } from "./diagnostics.mjs";
96
- import { assertNoDeclaredPackage } from "./note-package.mjs";
97
- import { assertNoDeclaredFolder } from "./folder-notes.mjs";
98
95
  import {
99
- assertNoAliasesField,
100
- assertNoDraftField,
101
- assertNoSectionField,
102
- assertNoTraitsField,
103
- } from "./retired-fields.mjs";
104
- import { assertTypeNotRetired, packForType } from "./ids.mjs";
96
+ emitDiagnostic,
97
+ locateFrontmatterKey,
98
+ positionOfFrontmatterPath,
99
+ positionOfLiteral,
100
+ } from "./diagnostics.mjs";
101
+ import { assertNoDeclaredPackage } from "./note-package.mjs";
102
+ import { packForType } from "./ids.mjs";
105
103
  import { resolveNoteId } from "./note-ids.mjs";
106
104
  import { carriesSystemBlock } from "./system-block.mjs";
107
105
  import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.mjs";
108
- import { locateFrontmatterKey } from "./retired-fields.mjs";
109
106
  // A being's computed `age`, filled in against the package's own declared
110
107
  // present before this pass reads `data:` for anything else — see
111
108
  // `engine/being-age.mjs`.
@@ -116,8 +113,8 @@ import { reckoningContext } from "./reckoning-markers.mjs";
116
113
  * The tallies one pass accumulates while walking the tree.
117
114
  *
118
115
  * `declined` and `skippedOther` are deliberately separate numbers. A declined
119
- * note is one this build **refused** — it declares a retired frontmatter field
120
- * — and it is an error; a skipped one legitimately belongs to another pass, and
116
+ * note is one this build **refused** — it declares `package:` — and it is an
117
+ * error; a skipped one legitimately belongs to another pass, and
121
118
  * there are thousands of those. Folding the first into the second is what let a
122
119
  * whole tree be filtered out in silence.
123
120
  *
@@ -125,9 +122,9 @@ import { reckoningContext } from "./reckoning-markers.mjs";
125
122
  * @property {number} compiled - Notes that became a document.
126
123
  * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
127
124
  * @property {number} skippedOther - Notes this pass does not claim.
128
- * @property {number} declined - Notes this pack **refused** — one declaring a
129
- * retired frontmatter field, or one routed to a system pack whose system it
130
- * says nothing about. Counted as errors, never as skips.
125
+ * @property {number} declined - Notes this pack **refused** — one declaring
126
+ * `package:`, or one routed to a system pack whose system it says nothing
127
+ * about. Counted as errors, never as skips.
131
128
  */
132
129
 
133
130
  /**
@@ -524,14 +521,14 @@ export class BasePackCompiler {
524
521
  * A refusal only this pass can make, because its subject is the note's
525
522
  * **type**.
526
523
  *
527
- * The `assertNo*Field` family above it in the walk is type-agnostic by
528
- * construction: it runs before `selects`, so that a note declaring a
529
- * retired field is answered whichever pass would have claimed it. A rule
524
+ * The `assertNoDeclaredPackage` check above it in the walk is type-agnostic
525
+ * by construction: it runs before `selects`, so that a note declaring
526
+ * `package:` is answered whichever pass would have claimed it. A rule
530
527
  * about what a *`trauma`* may write cannot live there — it needs the type's
531
528
  * field declaration, which only the pass that compiles the type can reach.
532
529
  *
533
530
  * So it is a hook, called once the note is known to be this pass's, and its
534
- * throw is counted and located exactly as the family's is: the note is
531
+ * throw is counted and located exactly as that check's is: the note is
535
532
  * declined rather than skipped, and the build fails naming the line.
536
533
  *
537
534
  * The default refuses nothing, which is the honest position for a pass
@@ -832,12 +829,10 @@ export class BasePackCompiler {
832
829
  /**
833
830
  * The Foundry path one authored art address names, or `null`.
834
831
  *
835
- * **The two empties survive.** `null` and an absent key mean *no art named*,
836
- * so the caller's own default applies with nullish coalescing; `""` means
837
- * *ship blank on purpose* and no default may replace it. That is
838
- * {@link module:engine/helpers.resolveImg}'s rule, reached through it rather
839
- * than restated, which is also what puts an art address and a body image
840
- * through one ownership rule.
832
+ * `null` and an absent key mean *no art named*, so the caller's own default
833
+ * applies with nullish coalescing. The address goes through
834
+ * {@link module:engine/helpers.resolveImg}, which puts an art address and a
835
+ * body image through one ownership rule.
841
836
  *
842
837
  * An address nothing answers is reported against the note and treated as
843
838
  * unnamed, so the document takes its default rather than shipping a path
@@ -849,8 +844,8 @@ export class BasePackCompiler {
849
844
  * @param {string} key - The key it was authored at, for the message.
850
845
  * @param {string} type - The asset type a bare value takes.
851
846
  * @param {Iterable<string>} [accepts] - The types this slot accepts.
852
- * @returns {string|null} The Foundry-relative path, `""` for a deliberate
853
- * blank, or `null` where the note names none.
847
+ * @returns {string|null} The Foundry-relative path, or `null` where the
848
+ * note names none.
854
849
  */
855
850
  artPathOf(value, key, type, accepts) {
856
851
  const {
@@ -1216,7 +1211,7 @@ export class BasePackCompiler {
1216
1211
  // A note the index could not record is a note this pass declines —
1217
1212
  // counted and reported exactly as the loop below does for the same
1218
1213
  // refusal, because it *is* the same refusal: the index defers to
1219
- // `assertNoDeclaredPackage` for a retired `package:`, which is the
1214
+ // `assertNoDeclaredPackage` for a declared `package:`, which is the
1220
1215
  // check this loop makes a few lines further down. All that changed is
1221
1216
  // which pass sees the note first.
1222
1217
  if (this.reportsCorpusProblems) {
@@ -1240,9 +1235,8 @@ export class BasePackCompiler {
1240
1235
  // this pass's own. Each note is then read for its **prose**: the
1241
1236
  // index carries what is *about* a note and deliberately not its text,
1242
1237
  // nor the `bodyLine`/`bodyColumn` a diagnostic needs, and this pass has
1243
- // to have the body anyway. So the read is one this pass was already
1244
- // making; what it no longer does is decide for itself which files to
1245
- // make it over.
1238
+ // to have the body anyway. So the read is one this pass makes anyway;
1239
+ // the index decides which files it covers.
1246
1240
  const dates = reckoningContext({ notes: this.corpus.records });
1247
1241
  for (const record of this.corpus.records) {
1248
1242
  if (!isNoteRecord(record)) continue;
@@ -1258,44 +1252,20 @@ export class BasePackCompiler {
1258
1252
  continue;
1259
1253
  }
1260
1254
  applyComputedBeingAge(fm, this.worldPresent, dates);
1261
- // The retired frontmatter fields, refused before `selects` so a
1262
- // note is answered whichever pass would have claimed it — and
1263
- // whatever the declared value says.
1264
- //
1265
- // - `package:`: a note's package is the repository's
1266
- // configured one, so declaring it restates a constant.
1267
- // - `draft:`: it excluded the note from the packs, the
1268
- // manifest and the site, and no checker reported the links that
1269
- // left dangling.
1270
- // - `aliases:`: it fed the alias index, which the bare
1271
- // `[[Alias]]` form was looked up in; the form is retired, so the
1272
- // list has no reader left. The nested `name.aliases` is a
1273
- // different field: the infobox displays it, and the note name
1274
- // check validates it.
1275
- //
1276
- // Both are reported and counted — never skipped, which is how a
1277
- // tree naming a package nothing answers to used to compile zero
1278
- // notes and exit 0. The file comes from the diagnostic locator, so
1279
- // neither message may repeat it.
1255
+ // `package:` is refused before `selects`, so a note is answered
1256
+ // whichever pass would have claimed it: a note's package is the
1257
+ // repository's configured one, so declaring it restates a
1258
+ // constant. It is reported and counted, never skipped. The file
1259
+ // comes from the diagnostic locator, so the message must not
1260
+ // repeat it.
1280
1261
  try {
1281
1262
  assertNoDeclaredPackage(fm, { absPath });
1282
- assertNoDeclaredFolder(fm, { absPath });
1283
- assertNoDraftField(fm, { absPath });
1284
- assertNoAliasesField(fm, { absPath });
1285
- assertNoSectionField(fm, { absPath });
1286
- assertNoTraitsField(fm, { absPath });
1287
1263
  } catch (err) {
1288
1264
  stats.declined++;
1289
1265
  this.errorCount++;
1290
1266
  this.noteError(err.message, err.position);
1291
1267
  continue;
1292
1268
  }
1293
- // Checked before `selects`, and therefore for every note this
1294
- // package owns rather than only the ones some pass claims. A
1295
- // retired type is claimed by no pass, so the alternative is not a
1296
- // wrong document — it is no document, skipped as quietly as the
1297
- // thousands of notes that legitimately belong to another pass.
1298
- assertTypeNotRetired(fm.type, absPath);
1299
1269
  if (!this.selects(fm)) {
1300
1270
  stats.skippedOther++;
1301
1271
  continue;
@@ -1313,7 +1283,7 @@ export class BasePackCompiler {
1313
1283
  // `shortcode`. It is not addressable, so there is nothing to
1314
1284
  // derive from and nothing for a link to point at; the message
1315
1285
  // names the reason rather than the missing field, because the
1316
- // field is no longer something an author writes.
1286
+ // field is not something an author writes.
1317
1287
  if (this.constructor.requiresId) {
1318
1288
  throw new Error(
1319
1289
  `${Label} note has no address, so it has no document id: ` +
@@ -1350,11 +1320,10 @@ export class BasePackCompiler {
1350
1320
  stats.skippedOther++;
1351
1321
  continue;
1352
1322
  }
1353
- // The type-specific half of the retired-field family:
1354
- // what a note of *this* type may not write, which needs the
1323
+ // What a note of *this* type may not write, which needs the
1355
1324
  // type's own field declaration and so cannot be asked before
1356
1325
  // `selects`. Counted as a declined note for the same reason
1357
- // they are — the alternative is a tree that compiles fewer
1326
+ // a declared `package:` is — the alternative is a tree that compiles fewer
1358
1327
  // documents than it has notes and exits 0.
1359
1328
  this.assertAuthorable(fm);
1360
1329
  } catch (err) {
@@ -59,11 +59,6 @@ import { worldInvariants } from "./calendar-notes.mjs";
59
59
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
60
60
  import { parseNoteDate } from "./note-dates.mjs";
61
61
  import { reckoningContext } from "./reckoning-markers.mjs";
62
- // `born`'s retired spelling is still read during its retirement window — see
63
- // `docs/content-format.md`'s "`birthday:` is the retired spelling of `born:`"
64
- // — so a being on the old spelling still computes an age rather than reading
65
- // as never born.
66
- import { readAliasedField } from "./retired-fields.mjs";
67
62
 
68
63
  /** The note type this module's derivation and check apply to. */
69
64
  const BEING_TYPE = "being";
@@ -170,7 +165,7 @@ export function applyComputedBeingAge(fm, presentRaw, dateContext = {}) {
170
165
  return;
171
166
  }
172
167
 
173
- const born = readAliasedField(fm, "born", { inData: true });
168
+ const born = data?.born;
174
169
  const computed = computeAge(born, presentRaw, dateContext);
175
170
  if (computed !== null) data.age = computed;
176
171
  }
@@ -237,7 +232,7 @@ export function checkBeingAge(note, { index } = {}) {
237
232
  const data = dataOf(note?.fm);
238
233
  if (!data) return [];
239
234
 
240
- const bornRaw = readAliasedField(note?.fm, "born", { inData: true });
235
+ const bornRaw = data.born;
241
236
  const ageRaw = data.age;
242
237
  if (ageRaw === undefined || ageRaw === null || ageRaw === "") return [];
243
238
  if (!index) return [];
@@ -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
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
4
15
 
@@ -2,6 +2,12 @@
2
2
  * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
3
  * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
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
+ *
5
11
  * SPDX-License-Identifier: GPL-3.0-or-later
6
12
  */
7
13
 
@@ -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
  import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
4
15
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
@@ -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
  import { dayOfYear } from "./calendars.mjs";
4
15
 
@@ -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
  import { calendarStructure } from "./calendars.mjs";
4
15
  import {
@@ -73,15 +73,17 @@ export async function buildCompileCorpus({
73
73
  }) {
74
74
  const resolved = config ?? loadPackConfig();
75
75
  const collected = problems ?? [];
76
+ const parsed = new Map();
76
77
  const records = indexRecordsFor({
77
78
  contentBase,
78
79
  assetsBase,
79
80
  config: resolved,
80
81
  skipDirectories,
81
82
  problems: collected,
83
+ parsed,
82
84
  });
83
85
  const corpusOptions = { config: resolved, records };
84
- const sqlOptions = { ...corpusOptions, skipDirectories };
86
+ const sqlOptions = { ...corpusOptions, skipDirectories, parsed };
85
87
  return {
86
88
  records,
87
89
  problems: collected,
@@ -148,12 +148,10 @@ export function addressSlug(fm) {
148
148
  * docs, neither of which contains a hyphen or names a type.
149
149
  *
150
150
  * **It is a pure function of the frontmatter**, and takes no options. Nothing
151
- * about the file the note was read from reaches it: the `README.md` convention
152
- * that made one note address a whole section is retired with the section itself,
153
- * so every note is addressed alike and there is one rule and no branch.
154
- * It takes no address scheme; the `landing` rule it once validated is
155
- * discarded; with that key retired, `prefix` was the only thing left in the
156
- * scheme and the paragraph above is the reason it never applied.
151
+ * about the file the note was read from reaches it, so every note is addressed
152
+ * alike and there is one rule and no branch. It takes no address scheme:
153
+ * `prefix` is the whole scheme and the paragraph above is the reason it never
154
+ * applies here.
157
155
  *
158
156
  * @param {object} fm - Parsed frontmatter.
159
157
  * @returns {string} The package-relative address, with a trailing slash and no
@@ -288,13 +286,12 @@ export const DOCUMENT_ID_NAMESPACE = "document";
288
286
  * The Foundry `_id` of the document a note compiles into, derived from its
289
287
  * canonical address.
290
288
  *
291
- * A note used to author this — an opaque 16-character string, one per note,
292
- * that said nothing its address did not and that no check guaranteed. The
293
- * address is the identity that *is* guaranteed: `content-lint` refuses a
289
+ * An authored id would be an opaque string that says nothing its address
290
+ * does not and that no check guarantees. The address is the identity that
291
+ * *is* guaranteed: `content-lint` refuses a
294
292
  * duplicate `(type, shortcode)` across every pack of a document type, which is
295
293
  * exactly the scope a primary document's id must be unique within. So the
296
- * derived id inherits a guard that already exists, where the authored one had
297
- * none.
294
+ * derived id inherits that guard.
298
295
  *
299
296
  * **The coupling this creates, stated plainly.** The address carries the
300
297
  * shortcode, so renaming a shortcode moves the document's id — where an
@@ -303,7 +300,7 @@ export const DOCUMENT_ID_NAMESPACE = "document";
303
300
  * note, so it is a breaking change either way; and a note that must keep its
304
301
  * identity across a rename pins an `id`, which is what the pin is for. One
305
302
  * thing genuinely degrades — `engine/address-diff.mjs` tells a rename from a
306
- * withdrawal by matching document ids, and can no longer do so for a note that
303
+ * withdrawal by matching document ids, and cannot do so for a note that
307
304
  * authors none. Its module note records that.
308
305
  *
309
306
  * @param {string} pkg - The owning **content** package.
@@ -1,4 +1,16 @@
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
+ */
13
+
2
14
  import MarkdownIt from "markdown-it";
3
15
  import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
4
16
  import { slugify } from "./content-slug.mjs";
@@ -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
  * General divs and the `:::secret` disclosure fence.
@@ -30,8 +41,8 @@
30
41
  * whole page, or Hugo's of the whole site page — read the body as part of its
31
42
  * own document rather than as an isolated fragment. A fragment rendered on its
32
43
  * own has no access to the note's footnote definitions, which live at the top
33
- * level outside every block, so a reference inside one used to fall through as
34
- * literal `[^id]` text; reached by the one parse that also sees the
44
+ * level outside every block, so a reference inside one would fall through as
45
+ * literal `[^id]` text if rendered alone; reached by the one parse that also sees the
35
46
  * definitions, it resolves exactly as a reference inside a list item or a
36
47
  * block quote already does.
37
48
  *
@@ -153,8 +153,8 @@ export function embedsIn(body, context) {
153
153
  * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
154
154
  * or the equivalent the site and the book build.
155
155
  * @param {ParsedEmbed} embed - The embed.
156
- * @returns {{pathname: string}|{reason: string, target: string, type?: string}}
157
- * The authored pathname the picture is at, or the finding.
156
+ * @returns {{pathname: string}|{reason: string, target: string, type?: string,
157
+ * shortcode?: string, declared?: string}} The authored pathname the picture is at, or the finding.
158
158
  */
159
159
  export function resolveEmbed(index, embed) {
160
160
  // **Every link carries a label**, and an embed is a link. Without one there
@@ -170,7 +170,13 @@ export function resolveEmbed(index, embed) {
170
170
  if (read.record) {
171
171
  return { pathname: read.pathname };
172
172
  }
173
- return { reason: read.reason, target: embed.written, type: read.type };
173
+ return {
174
+ reason: read.reason,
175
+ target: embed.written,
176
+ type: read.type,
177
+ ...(read.shortcode ? { shortcode: read.shortcode } : {}),
178
+ ...(read.declared ? { declared: read.declared } : {}),
179
+ };
174
180
  }
175
181
 
176
182
  /**
@@ -288,6 +294,8 @@ export function resolveEmbeds(body, { index }) {
288
294
  offset: embed.index,
289
295
  reason: resolved.reason,
290
296
  ...(resolved.type ? { type: resolved.type } : {}),
297
+ ...(resolved.shortcode ? { shortcode: resolved.shortcode } : {}),
298
+ ...(resolved.declared ? { declared: resolved.declared } : {}),
291
299
  });
292
300
  continue;
293
301
  }
@@ -1,4 +1,13 @@
1
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
+ *
2
11
  * SPDX-License-Identifier: GPL-3.0-or-later
3
12
  */
4
13
 
@@ -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
  * Where a footnote's two halves belong, and the two findings that hold the