@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
@@ -109,13 +109,11 @@ different lines or columns instead of collapsing onto the first.
109
109
  frontmatter key** by searching the fence rather than the whole file — a bare
110
110
  search for a key like `name` or `type` would routinely match a line of prose
111
111
  first. Passing `value` narrows the match to the entry that carries it, which
112
- is what a list-valued key (`aliases`) needs: the finding belongs on the
112
+ is what a list-valued key (`tags`) needs: the finding belongs on the
113
113
  wrong entry, not on the key that introduces the list. `opts.topLevel`
114
114
  requires the match at column 1, for a key name that is legal both at the top
115
- level and nested (`aliases` is both a retired top-level field and a
116
- permitted `name.aliases`) — without it, a finding about the retired field
117
- could resolve to the nested one and tell an author to delete a field they are
118
- allowed to write.
115
+ level and nested — without it, a finding about the top-level key could
116
+ resolve to the nested one.
119
117
 
120
118
  **`positionOfYamlPath(text, keyPath, opts)`** and
121
119
  **`positionOfFrontmatterPath(raw, keyPath, opts)`** locate a node by
@@ -214,6 +212,48 @@ and the summary carries no position because it has none.
214
212
  thalorna: 2572 notes — 2089 full, 464 draft, 19 stub.
215
213
  ```
216
214
 
215
+ ## A worked case: a note's culture
216
+
217
+ `data.culture` has four findings, all errors. Three come from `package-build
218
+ lint` reading what a note wrote, and each sits on the line that makes the note
219
+ wrong. A setting guide with no culture is located at its `subType:` line, the key
220
+ whose value made it a setting guide:
221
+
222
+ ```text
223
+ assets/content/Guides/G1.md:5:1: error: a setting guide introduces one culture, so this note must state `data.culture`, naming a culture lore note
224
+ ```
225
+
226
+ The field on a `doc` of any other subType is located at the `culture:` key:
227
+
228
+ ```text
229
+ assets/content/Guides/G2.md:8:3: error: `data.culture` is stated by a setting guide, and this note's `subType` is not `settingguide`
230
+ ```
231
+
232
+ A `lore` note of `subType: culture` is its own culture. It may leave the field
233
+ out or name itself, by its shortcode or by any longer form of its Address; a
234
+ value naming any other note is located at the value, and the message gives the
235
+ Address the value names:
236
+
237
+ ```text
238
+ assets/content/Lore/Hillfolk.md:8:12: error: data.culture reedflats-note-lore-reedfolk names another note; a culture note's data.culture names the note itself
239
+ ```
240
+
241
+ The fourth comes from resolving cultures into the content index. A place or
242
+ affiliation with no culture of its own whose `data.parents` resolve to different
243
+ cultures is located at its `parents:` key, and names each culture with the
244
+ parent it came through:
245
+
246
+ ```text
247
+ assets/content/Places/Causeway.md:8:3: error: data.parents resolve to different cultures (reedflats-note-lore-reedfolk via reedflats, reedflats-note-lore-hillfolk via stonecrest); state data.culture
248
+ ```
249
+
250
+ A polity whose seat and governed places disagree is located at `seat:`, or at
251
+ `data:` when it states no seat. Because the index is read by every build, the
252
+ conflict fails the index and each command that reads it without collecting
253
+ problems, listing every conflict at once. Stating `data.culture` on the note
254
+ resolves it. The rules are under
255
+ [Culture in the index](reference/format-details.md#culture-in-the-index).
256
+
217
257
  ## Summary counts and prose are not findings
218
258
 
219
259
  A run's progress and summary lines — `42 address(es) across 10 note(s).`,
@@ -256,18 +296,14 @@ all fails the run.
256
296
 
257
297
  **A corpus problem always fails, regardless of severity or a `--strict`
258
298
  flag.** When the content tree itself cannot be indexed — a note the walk
259
- cannot record, most often a retired `package:` key — the note is absent from
299
+ cannot record, most often a declared `package:` key — the note is absent from
260
300
  every other answer a command gives. Reporting it and still exiting `0` would
261
301
  call the tree clean while silently omitting a note, so every command that
262
- indexes a corpus (`lint`, `links`, `reachability`, `content-format notes`,
263
- `addresses diff`) fails on a corpus problem unconditionally, before it
302
+ indexes a corpus (`lint`, `links`, `reachability`, `addresses diff`) fails on a corpus problem unconditionally, before it
264
303
  evaluates anything else.
265
304
 
266
305
  **A few commands report advisory findings that never fail the run on their
267
- own**, and say so where they emit them: `content-format notes` measures a
268
- content tree against the format specification as a progress report during a
269
- migration, printing every finding as a `warning` and passing `--strict` to
270
- promote a class to `error` only once that class has reached zero; `format`
306
+ own**, and say so where they emit them: `format`
271
307
  reports where this repository's Prettier configuration diverges from the
272
308
  shared convention as a `warning`, because a repository's own configuration
273
309
  is allowed to win on purpose; `addresses diff` reports a withdrawn or renamed
@@ -283,7 +319,6 @@ over one would teach people to stop reading the report.
283
319
  | command | fails when |
284
320
  | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
285
321
  | `package-build lint [root]` | an address, frontmatter, schema, charset, icon or HTML finding is `error`-severity, or the corpus cannot be indexed |
286
- | `package-build content-format notes [root]` | `--strict` was passed and it reported any finding (all findings are `error`-severity only under `--strict`), or the corpus cannot be indexed |
287
322
  | `package-build content-format schema` | it reports any finding at all |
288
323
  | `package-build content-format fields` | it reports any finding at all |
289
324
  | `package-build links [root]` | it reports any finding at all, an index is unusable (a stale or unaddressable foreign manifest), or the corpus cannot be indexed |
@@ -9,7 +9,7 @@ subType: howto
9
9
 
10
10
  `package-build pdf` selects notes from a content package and sets them as a
11
11
  PDF. The website can publish the whole content tree; the book follows the
12
- ordered document tree named by `pdf.document`. This makes the book's contents
12
+ ordered document tree each book declares. This makes the book's contents
13
13
  an editorial choice. A note outside every selection remains in the content
14
14
  package and on its site, but it has no book entry.
15
15
 
@@ -18,23 +18,36 @@ produce the PDF. Typst must be available on `PATH`, or named with `pdf.binary`.
18
18
  The [command reference](../commands.md) lists the CLI options; the
19
19
  [configuration reference](../configuration.md) lists the `pdf` keys.
20
20
 
21
- ## Declare the book
21
+ ## Declare the books
22
22
 
23
- Give the book a title and document tree:
23
+ `pdf.books` lists one entry per PDF. A package that publishes one book lists
24
+ one; a package that publishes a world book and a book for each culture lists
25
+ them all, and `package-build pdf` builds every one. Each book needs an `id`, a
26
+ `title` and its tree of contents:
24
27
 
25
28
  ```yaml
26
29
  pdf:
27
- title: The Setting Guide
28
- document: book.yaml
29
30
  out: build/pdf
31
+ books:
32
+ - id: guide
33
+ title: The Setting Guide
34
+ subtitle: A Reader's Companion
35
+ document: book.yaml
36
+ out: setting
30
37
  ```
31
38
 
32
39
  A tree containing only a homepage has no content pages for a book. With no
33
- `pdf` block or no content tree, the command has nothing to build. `pdf.title`
34
- and `pdf.document` are required together when `pdf` is declared.
40
+ `pdf` block or no content tree, the command has nothing to build and exits 0.
35
41
 
36
- `book.yaml` contains an ordered list of sections. Each section contains prose
37
- files, note filters, or nested sections:
42
+ ### The tree: `document` or `contents`
43
+
44
+ The tree is an ordered list of sections. Each section contains prose files,
45
+ note filters, or nested sections. A book gives its tree in exactly one of two
46
+ ways: `document` names a file that holds it, and `contents` writes it inline
47
+ in the configuration. A file suits a long tree; inline suits a short book that
48
+ reads best beside the other books it shares a package with.
49
+
50
+ `book.yaml` holds the tree under a `contents:` key:
38
51
 
39
52
  ```yaml
40
53
  contents:
@@ -49,17 +62,299 @@ contents:
49
62
  - filter: "type = 'place' AND subType = 'settlement'"
50
63
  ```
51
64
 
65
+ The same tree written inline sits under the book's own `contents`, without the
66
+ outer key:
67
+
68
+ ```yaml
69
+ pdf:
70
+ books:
71
+ - id: guide
72
+ title: The Setting Guide
73
+ contents:
74
+ - sectionName: Introduction
75
+ contents:
76
+ - file: prose/introduction.md
77
+ - sectionName: Places
78
+ contents:
79
+ - filter: "type = 'place'"
80
+ ```
81
+
82
+ A finding about an inline tree names `package-build.config.yaml` at the line
83
+ of the offending key; a finding about a tree file names that file.
84
+
52
85
  The build keeps the order of prose and subsections as written. Notes selected
53
86
  by a filter are ordered by their folded display name. The filter is a SQL
54
87
  `WHERE` clause over this package's `notes` table; the build supplies
55
- `SELECT * FROM notes`. A filter cannot choose another package's notes. It is
56
- an error when a filter selects no printable note, while notes that no filter
57
- selects are simply outside this volume. A note selected more than once prints
58
- at each selected position; inbound book links reach its first occurrence.
88
+ `SELECT * FROM notes`. A filter cannot choose another package's notes. A
89
+ book's `front` lists Markdown files placed before its contents.
90
+
91
+ ### The file stem and `--book`
92
+
93
+ A book's PDF is named `<out>-<version>.pdf`, where `out` is the book's **file
94
+ stem**. Without an `out`, the stem is `<package>-<id>`. A stem is one path
95
+ segment with no directory, and two books may not resolve to the same stem. The
96
+ version comes from `--book-version`; with none, the file is `<out>.pdf`. Every
97
+ book is written into the directory `pdf.out` names. A package that publishes
98
+ one book and keeps a file name of the form `<package>-<version>.pdf` declares
99
+ `out: <package>`.
100
+
101
+ `package-build pdf` builds every declared book in the order declared. `--book
102
+ <id>` builds the one book with that `id`:
103
+
104
+ ```bash
105
+ package-build pdf --book vedyara
106
+ ```
107
+
108
+ Coverage and home books, described below, are computed over every declared
109
+ book even when `--book` renders only one.
110
+
111
+ ### Sections that select nothing
112
+
113
+ A section whose filters select no printable note is left out of the book
114
+ without a finding. A tree can therefore carry a section for a culture or a
115
+ period before any note belongs to it, and the book prints nothing for it.
116
+
117
+ A section that holds prose and no selected notes, such as a preface, prints
118
+ its prose. A **book** whose filters select no note at all, whatever prose it
119
+ holds, is an error at its `id`, and that book is not built while the others
120
+ are:
121
+
122
+ ```text
123
+ package-build.config.yaml:9:15: error: book `sinale` selects no note
124
+ ```
125
+
126
+ ## Coverage and home books
127
+
128
+ Several books draw on one content package, so the question "is every note in
129
+ some book?" belongs to the set of books, not to any one of them. After every
130
+ book's filters have run against the one corpus, the build reports each
131
+ printable note that no book selects. A **printable** note is a note that is
132
+ neither a stub nor tagged `gm`, and that `pdf.unprinted` does not name.
133
+
134
+ Two shared keys under `pdf` control the report:
135
+
136
+ - `pdf.unprinted` is a `WHERE` clause over `notes` that names notes belonging
137
+ in no book, such as folder notes: `unprinted: "type = 'folder'"`. Coverage
138
+ skips them. It has no default, so set it for any note type that has no place
139
+ in a book.
140
+ - `pdf.coverage` is `warning` (the default), `error` or `off`. A printable note
141
+ no book selects is reported at that severity, at the note's `type:` line.
142
+ `off` reports nothing. `error` makes the command exit 1.
143
+
144
+ The finding carries the standard shape, `file:line:column: severity: message`,
145
+ with the path of the note relative to the working directory:
146
+
147
+ ```text
148
+ assets/content/Guides/Fieldcraft.md:2:1: warning: no book selects this note; add it to a book's query or to `pdf.unprinted`
149
+ ```
150
+
151
+ To clear the finding, add the note to a book's query or name it in
152
+ `pdf.unprinted`.
153
+
154
+ ### A note several books select
155
+
156
+ A note may be selected by more than one book, and it prints in each. Its
157
+ **home book** is the first book in `pdf.books` that selects it. A note a book
158
+ selects twice counts as that one book's and prints at each position.
159
+
160
+ The home is a property of the order the books are listed in and the queries
161
+ they hold. To move a note's home, reorder the books, or narrow the query of the
162
+ book that currently holds it so that it does not select the note. Nothing
163
+ else sets a home.
164
+
165
+ ### A reference to a note in another book
166
+
167
+ A PDF link cannot reach into another file, so a wikilink to a note that another
168
+ book is home to is not a link. It prints the words the link carried, then the
169
+ home book's title and the page the note's entry starts on there:
170
+
171
+ ```text
172
+ the Lyrebird Inn (The Tenebrae Volume, p. 214)
173
+ ```
174
+
175
+ Which form a wikilink takes depends on where its target is printed:
176
+
177
+ | The target is | The link prints |
178
+ | ------------------------------------------------------------- | ----------------------------------------- |
179
+ | An entry of the same book, including a note the book reprints | An internal destination |
180
+ | An entry of another book, and not of this one | `Name (_Book title_, p. N)`, with no link |
181
+ | A note in no book | The web address of the note's page |
182
+
183
+ The page is the page the PDF itself numbers, which is the number printed in the
184
+ footer and used by the contents and the index, so the printed `p. N` is the
185
+ physical page of the PDF. A note the book reprints, or prints in two places,
186
+ has one page in its home book: a reference from a third book names the home
187
+ book and the page of the home book's first entry for it. Infobox links and
188
+ generated sections follow the same table.
189
+
190
+ The page is read from the laid-out book, so the build makes two passes:
191
+
192
+ 1. **Pass one** renders every declared book, with `p. ?` in place of each
193
+ cross-book page, lays each out with `typst eval`, and writes
194
+ `pages.json` beside the books in `pdf.out`:
195
+
196
+ ```json
197
+ {
198
+ "sohl-thalorna-place-lyrebird-inn": { "book": "tenebrae", "page": 214 }
199
+ }
200
+ ```
201
+
202
+ The keys are the notes' canonical addresses in sorted order; the file is
203
+ identical across builds of unchanged sources.
204
+
205
+ 2. **Pass two** renders every book again with those pages and lays them out
206
+ again. The words `(The Tenebrae Volume, p. 214)` are wider than `p. ?`, so a
207
+ page can move. When no page moved, pass two is the output. When one did, the
208
+ build renders once more with the new pages and reports every note whose page
209
+ still differs from the page printed for it, as a warning at the note's file.
59
210
 
60
- The document tree can include a section with prose and no selected notes, for
61
- example a preface. Empty sections do not print. `pdf.front` lists Markdown
62
- files placed before the book's contents.
211
+ A build that prints no cross-book reference, such as a package with one book,
212
+ is laid out once. Rendering and laying out every book happens for each build
213
+ with `--book` too, because a page in one book is read from another; only the
214
+ requested books are compiled. `--no-compile` runs neither pass: the source
215
+ prints `p. ?` for each cross-book reference and no `pages.json` is written.
216
+
217
+ Two builds produce identical PDFs only when Typst embeds the same creation
218
+ time. Set `SOURCE_DATE_EPOCH` for a reproducible build.
219
+
220
+ ### Worked example: two culture books and a World book
221
+
222
+ A package publishes a book for each of two cultures and a World book that
223
+ gathers every region:
224
+
225
+ ```yaml
226
+ pdf:
227
+ out: build/pdf
228
+ unprinted: "type = 'folder'"
229
+ books:
230
+ - id: vedyara
231
+ title: Vedyara
232
+ contents:
233
+ - sectionName: Vedyara
234
+ contents:
235
+ - filter: "data.culture = 'thalorna-note-lore-vedyariclt'"
236
+ - id: okharin
237
+ title: Okharin
238
+ contents:
239
+ - sectionName: Okharin
240
+ contents:
241
+ - filter: "data.culture = 'thalorna-note-lore-okharinclt'"
242
+ - id: world
243
+ title: The World of Thalorna
244
+ contents:
245
+ - sectionName: Regions
246
+ contents:
247
+ - filter: "type = 'place' AND subType = 'region'"
248
+ ```
249
+
250
+ The content tree holds these notes:
251
+
252
+ | Note | Type and subtype | Culture | Selected by |
253
+ | ---------------------- | ----------------- | ------- | ---------------- |
254
+ | `Places/Ashmere.md` | place, region | Vedyara | `vedyara`, world |
255
+ | `Places/Fordholm.md` | place, settlement | Vedyara | `vedyara` |
256
+ | `Places/Stormhaven.md` | place, settlement | Okharin | `okharin` |
257
+ | `Places/The Reach.md` | place, region | none | `world` |
258
+ | `Guides/Fieldcraft.md` | doc, howto | none | no book |
259
+ | `Folders/Places.md` | folder | none | `pdf.unprinted` |
260
+
261
+ `Ashmere` is a region of the Vedyara culture, so both the `vedyara` book and
262
+ the `world` book select it. `vedyara` is listed first, so it is Ashmere's home.
263
+ `Fieldcraft` matches no filter, so it is the one coverage finding. The folder
264
+ note is named by `pdf.unprinted` and is not a candidate at all.
265
+
266
+ Running `package-build pdf` first reports one finding on stderr:
267
+
268
+ ```text
269
+ assets/content/Guides/Fieldcraft.md:2:1: warning: no book selects this note; add it to a book's query or to `pdf.unprinted`
270
+ ```
271
+
272
+ then, for each book, a line counting its entries, prose files and sections and
273
+ the path of its Typst source (and of its PDF when Typst compiles it), and then
274
+ the summary:
275
+
276
+ ```text
277
+ vedyara: 2 entries, 2 notes, 0 without a culture
278
+ okharin: 1 entry, 1 note, 0 without a culture
279
+ world: 2 entries, 2 notes, 1 without a culture
280
+ 5 printable notes, 1 in no book, 1 in pdf.unprinted
281
+ Ashmere (Places/Ashmere.md) is printed by vedyara, world; home: vedyara
282
+ ```
283
+
284
+ Each book line gives its entries (a note printed at two positions is two
285
+ entries), its distinct notes, and how many of those carry no `data.culture`.
286
+ The `world` book's count of one is `The Reach`. The note path in the summary is
287
+ relative to the content root, while a finding's path is relative to the working
288
+ directory. The totals line counts the
289
+ printable notes that are not in `pdf.unprinted`, the ones among them that no
290
+ book selects, and the notes `pdf.unprinted` names. A final line appears for
291
+ every note that several books select, in book order and then path order,
292
+ naming the books that print it and its home.
293
+
294
+ To make `world` the home of Ashmere, list it before `vedyara`. To keep
295
+ `vedyara` first and still have `world` own the region, narrow the `vedyara`
296
+ filter with `AND NOT (type = 'place' AND subType = 'region')`; Ashmere is then
297
+ selected by `world` alone, its home is `world`, and its summary line
298
+ disappears.
299
+
300
+ ## The cover page and the title page
301
+
302
+ A book opens on a **cover page** when it declares a `coverImage`, a path
303
+ resolved against the directory the configuration sits in:
304
+
305
+ ```yaml
306
+ books:
307
+ - id: world
308
+ title: The World of Thalorna
309
+ subtitle: A Song of Heroic Lands Sourcebook
310
+ coverImage: assets/images/covers/world.webp
311
+ document: world.yaml
312
+ ```
313
+
314
+ The image fills the page, cropped to fit, and is page 1. The book's title and
315
+ subtitle are centred in white in the upper third, over a translucent dark band
316
+ so they read over any art. The cover carries no running footer and no page
317
+ number. A book without `coverImage` begins on its title page.
318
+
319
+ The **title page** shows the book's title, subtitle and version, then the
320
+ package's people, one per line. The people come from the top-level `authors`
321
+ list, shared by every book, each entry a `name` and an optional `role`:
322
+
323
+ ```yaml
324
+ authors:
325
+ - name: Tom Rodriguez
326
+ role: author
327
+ - name: Tom Rodriguez
328
+ role: artist
329
+ - name: Ada Quill
330
+ ```
331
+
332
+ A person with a role prints as `Name, Role` with the role in title case, and
333
+ one without prints as `Name`. The role is one of `author`, `designer`,
334
+ `developer`, `editor`, `proofreader`, `artist`, `cartographer`, `layout`,
335
+ `playtester` and `translator`. A person who holds two roles is listed twice.
336
+
337
+ ```text
338
+ Tom Rodriguez, Author
339
+ Tom Rodriguez, Artist
340
+ Ada Quill
341
+ ```
342
+
343
+ At the foot of the title page sit the copyright and licence statements, set
344
+ under `pdf` and shared by every book:
345
+
346
+ ```yaml
347
+ pdf:
348
+ copyright: © 2026 Tom Rodriguez
349
+ license: |
350
+ Text is licensed under CC BY-SA 4.0.
351
+ Code is licensed under GPL-3.0-or-later.
352
+ ```
353
+
354
+ Both are optional, and the foot prints whichever is given. `copyright` is one
355
+ line; `license` may run to several. The [configuration
356
+ reference](../configuration.md#authors) lists the `authors` keys and the
357
+ closed role list.
63
358
 
64
359
  ## Control a section's page
65
360
 
@@ -123,6 +418,25 @@ the book prints adds the page it appears on. A term mentioned several times on
123
418
  one page lists that page once, and a note printed under more than one section
124
419
  is listed once, with every page it occupies.
125
420
 
421
+ **An index lists the notes its own book is home to.** A note's home is the
422
+ first book in `pdf.books` that selects it. In a package of several books:
423
+
424
+ - A note a book reprints from another book's home has no entry in the
425
+ reprinting book. Its term, with its aliases and event names, is in its home
426
+ book's index, and a link to it from the reprinting book adds nothing there.
427
+ - The pages an index lists are those of its own book, taken from the same table
428
+ as the cross-book references (`pages.json`), so the page a reference prints
429
+ for a note is the page its home book's index lists for the note's own entry.
430
+ - A mention of a note another book prints is not a term of the mentioning
431
+ book. The mention reads `Name (_Book title_, p. N)` in the text, and the note's
432
+ entry and its page are in the other book's index.
433
+
434
+ Set [`pdf.indexSeeAlso`](../configuration.md) to `true` to list those mentions
435
+ too. Each note another book prints that this book mentions in authored prose
436
+ then has a line, filed under its name:
437
+
438
+ > Sword: _Book A_, p. 12
439
+
126
440
  Two kinds of link add nothing:
127
441
 
128
442
  - **Links in generated sections** such as "Within" or "Governed by". They name
@@ -155,8 +469,10 @@ for them. A package can choose families and add a font directory:
155
469
 
156
470
  ```yaml
157
471
  pdf:
158
- title: The Setting Guide
159
- document: book.yaml
472
+ books:
473
+ - id: guide
474
+ title: The Setting Guide
475
+ document: book.yaml
160
476
  fonts:
161
477
  serif: Libertinus Serif
162
478
  sans: Libertinus Sans
@@ -181,10 +497,11 @@ the font registry.
181
497
  package-build pdf
182
498
  package-build pdf --no-compile
183
499
  package-build pdf --out build/book
500
+ package-build pdf --book vedyara
184
501
  ```
185
502
 
186
503
  `--no-compile` emits the Typst source and staged images without invoking
187
- Typst. The source and assets sit under the output directory, because Typst
504
+ Typst, and so without measuring pages. The source and assets sit under the output directory, because Typst
188
505
  reads paths relative to its compilation root. An authored image from this
189
506
  package can be staged into the book. A remote image or one owned by another
190
507
  package cannot be read by this build; the build reports that limitation.
@@ -203,7 +520,10 @@ so their links follow their own rule, because a book is often a selection:
203
520
  - A name whose note is in the book is an internal link followed by the page
204
521
  that note's entry starts on, printed as the contents prints it:
205
522
  `Ford (p. 12)`. The number is Typst's own, read after layout.
206
- - A name whose note is not in the book is printed as plain text, with no link,
207
- no page and no finding.
523
+ - A name whose note is printed by another book is followed by that book and
524
+ page, `Ford (_The Tenebrae Volume_, p. 12)`, with no link; see
525
+ [a reference to a note in another book](#a-reference-to-a-note-in-another-book).
526
+ - A name whose note is in no book is printed as plain text, with no link, no
527
+ page and no finding.
208
528
 
209
529
  Links an author wrote keep the rules above and carry no page number.
@@ -57,6 +57,9 @@ this harness and a Foundry license available to the test container:
57
57
  FOUNDRYVTT_TEST_DATA=/path/to/isolated-foundry-test-data
58
58
  FOUNDRYVTT_TEST_LICENSE_KEY=replace-with-test-license-key
59
59
  FOUNDRYVTT_TEST_PORT=30003
60
+ FOUNDRYVTT_CACHE=/path/to/foundry-release-cache
61
+ FOUNDRY_USERNAME=your-foundryvtt-com-account
62
+ FOUNDRY_PASSWORD=replace-with-account-password
60
63
  # FOUNDRYVTT_TEST_VERSION=14.367
61
64
  ```
62
65
 
@@ -82,8 +85,16 @@ own `.env.local` because that ignored file is not copied with the branch.
82
85
  The test data path is a Foundry data root, beneath which the harness creates
83
86
  `Data/worlds/` and installs the package. Use an absolute path; `~` and shell
84
87
  variables are not expanded in `.env.local`. Set `FOUNDRYVTT_TEST_VERSION` to
85
- select a specific Foundry build for routine runs. With the local settings in
86
- place, seed the world:
88
+ select a specific Foundry build for routine runs; the container then runs the
89
+ image made for that build. The first run downloads the release, which needs
90
+ `FOUNDRY_USERNAME` and `FOUNDRY_PASSWORD`, and the image keeps the archive in
91
+ `FOUNDRYVTT_CACHE`, or in `container_cache` beneath the data root when that is
92
+ unset. Once the archive is cached and the stage has a licence, later runs
93
+ start without signing in to foundryvtt.com, so its rate limit never stops a
94
+ run. Under rootless Docker, set
95
+ `FOUNDRYVTT_CONTAINER_USER=0:0`: container root is then the host user, so every
96
+ file under the data root belongs to them, where the image would otherwise run
97
+ Foundry as user 1000. With the local settings in place, seed the world:
87
98
 
88
99
  ```bash
89
100
  package-build e2e seed
@@ -49,6 +49,11 @@ slots. A pathname with slashes or an extension is not an art-slot address.
49
49
  | `data.tokenIcon` | Actor token art; follows `data.icon` when unset |
50
50
  | `data.banner` | Page hero image for the site |
51
51
 
52
+ A map's picture in Foundry's Scene lists is not an art slot: a `map` note names
53
+ it with `data.thumbnail`, an `image` Address the build writes into the Scene's
54
+ `thumb`. The [map reference](../reference/format-details.md#the-thumbnail)
55
+ describes it.
56
+
52
57
  An Item without `data.icon` uses its builder's configured default art. An
53
58
  Actor without `data.icon` uses its compiled default art. Write `data.icon`
54
59
  when the note needs a particular image. A portrait within the prose is an
@@ -58,7 +63,10 @@ it does not set a document's profile art. The
58
63
  slot's type and document destination.
59
64
 
60
65
  A being's `subType` selects its compiled default art: `character` and `npc`
61
- use person art, and `creature` uses creature art.
66
+ use person art, and `creature` uses creature art. HM3's built-in Actor and Item
67
+ defaults point into the system's `assets/images/` and `assets/icons/` trees,
68
+ introduced by [HM3's asset-path migration](https://git.pupluppy.org/HeroicLands/HarnMaster-3-FoundryVTT/pulls/60).
69
+ Use these defaults with an HM3 release that includes those assets.
62
70
 
63
71
  ## Write an inline icon in prose
64
72
 
@@ -98,8 +106,10 @@ book:
98
106
 
99
107
  ```yaml
100
108
  pdf:
101
- title: The Setting Guide
102
- document: book.yaml
109
+ books:
110
+ - id: guide
111
+ title: The Setting Guide
112
+ document: book.yaml
103
113
  iconFonts:
104
114
  fontawesome: assets/fonts/fa-solid-900.ttf
105
115
  ```
@@ -123,6 +133,6 @@ The content index assigns addresses to image assets. The lint reports unknown
123
133
  inline names and invalid art addresses. A documentation package has no packs,
124
134
  so it skips `package compile`. Inspect a compiled Actor or Item's
125
135
  `img`, an Actor's `prototypeToken.texture.src`, and a map's `background.src`
126
- when checking document art. Inspect the site page and emitted Typst source for
136
+ and `thumb` when checking document art. Inspect the site page and emitted Typst source for
127
137
  inline icons and banners. The [command reference](../commands.md) gives the
128
138
  available build and lint options.
@@ -366,7 +366,9 @@ order.
366
366
  gives a usable overview of areas, peoples, institutions, and daily life, then
367
367
  links to the detailed notes. Use `subType: settingguide` for that entry point;
368
368
  `concept` explains one subject, `reference` serves lookup, and `userguide`
369
- explains the Foundry controls. For example, a guide at
369
+ explains the Foundry controls. A setting guide is a culture's guide to the
370
+ setting, so it names its culture in `data.culture`, the Address of a `lore` note
371
+ of `subType: culture`; a setting guide without one is an error. For example, a guide at
370
372
  `assets/content/Guides/Reed_Flats.md` can begin:
371
373
 
372
374
  ```markdown
@@ -376,6 +378,8 @@ name: { full: The Reed Flats Guide }
376
378
  type: doc
377
379
  subType: settingguide
378
380
  description: A starting point for adventures in the Reed Flats.
381
+ data:
382
+ culture: lore-reedfolk
379
383
  ---
380
384
 
381
385
  The river trade links the settlements of the Reed Flats. Their routes and
@@ -630,9 +634,7 @@ packageBuild:
630
634
 
631
635
  manifest:
632
636
  title: Acme Bestiary
633
- description: Creatures of the reed flats, as compendium journals.
634
- authors:
635
- - name: Acme
637
+ descriptionHtml: Creatures of the reed flats, as compendium journals.
636
638
  license: LICENSE.md
637
639
  readme: README.md
638
640
  languages:
@@ -649,14 +651,21 @@ packageBuild:
649
651
  `assets:` is a copy list, from the repository into the stage. `manifest:` is
650
652
  everything about the manifest that cannot be derived from `package.json` or from
651
653
  the keys above — the human-facing identity, and the files Foundry links to.
652
- `title` is required by Foundry and has no derivation; supply it.
654
+ `title` is required by Foundry and has no derivation; supply it. The manifest's
655
+ `authors` is derived too, from the top-level `authors` list, which names the
656
+ package's people once for the manifest, the site and the book:
657
+
658
+ ```yaml
659
+ authors:
660
+ - name: Acme
661
+ ```
653
662
 
654
663
  `packFolders` may name only packs this package ships, and the build compares the
655
664
  two lists rather than trusting them to agree:
656
665
 
657
666
  ```console
658
667
  $ npx package-build manifest
659
- package-build.config.yaml:36:21: error: packFolders: folder "Acme Bestiary" names pack "items", which this package does not ship (packs: journals)
668
+ package-build.config.yaml:34:21: error: packFolders: folder "Acme Bestiary" names pack "items", which this package does not ship (packs: journals)
660
669
  package-build: packFolders names 1 pack this package does not ship (reported above). Foundry skips a name it cannot resolve, so the folder would ship missing those packs — correct `packageBuild.manifest.packFolders`.
661
670
  ```
662
671
 
@@ -862,8 +871,9 @@ None of them is needed to build a package.
862
871
 
863
872
  **A website.** Every package publishes one — at the least, the homepage note
864
873
  from step 5 — at `https://www.heroiclands.org/<contentPackage>/`. Add that
865
- address to `package.json` as `homepage`, with a `description` and an `author`
866
- beside it, and add a `packageBuild.manifest.title`. The theme the site renders
874
+ address to `package.json` as `homepage`, with a `description` beside it, list
875
+ the package's people in `authors` in `package-build.config.yaml`, and add a
876
+ `packageBuild.manifest.title`. The theme the site renders
867
877
  through ships with this package, so nothing else is installed. Then `package-build deps fetch` caches the
868
878
  organisation's navigation, and `package-build site` writes the whole Hugo
869
879
  source tree under `build/hugo/` — the configuration generated from those