@heroiclands/package-build 20.4.0 → 20.6.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 (78) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/CONTENT.md +213 -20
  3. package/README.md +19 -1
  4. package/bin/content-build.mjs +135 -32
  5. package/bin/package-build.mjs +46 -13
  6. package/content-config.mjs +345 -101
  7. package/docs/api.md +1352 -0
  8. package/docs/commands.md +1609 -0
  9. package/docs/configuration.md +1432 -0
  10. package/docs/content-format.md +16 -6
  11. package/docs/diagnostics.md +356 -0
  12. package/docs/getting-started.md +813 -0
  13. package/docs/project-setup.md +469 -0
  14. package/engine/actor-compiler.mjs +30 -27
  15. package/engine/address-diff.mjs +45 -41
  16. package/engine/base-compiler.mjs +6 -0
  17. package/engine/bundles.mjs +9 -0
  18. package/engine/content-address.mjs +9 -9
  19. package/engine/content-index.mjs +44 -23
  20. package/engine/content-links.mjs +44 -11
  21. package/engine/content-lint.mjs +44 -10
  22. package/engine/content-tables.mjs +32 -27
  23. package/engine/folder-notes.mjs +4 -2
  24. package/engine/frontmatter-lint.mjs +35 -38
  25. package/engine/generate.mjs +5 -0
  26. package/engine/helpers.mjs +86 -32
  27. package/engine/index.mjs +12 -2
  28. package/engine/journals.mjs +9 -0
  29. package/engine/note-claims.mjs +18 -10
  30. package/engine/note-schemas.mjs +0 -5
  31. package/engine/note-vocabulary.mjs +32 -31
  32. package/engine/pack-config.mjs +26 -12
  33. package/engine/pack-router.mjs +0 -0
  34. package/engine/pdf-build.mjs +464 -0
  35. package/engine/pdf-fonts.mjs +420 -0
  36. package/engine/pdf-render.mjs +876 -0
  37. package/engine/pdf-toc.mjs +525 -0
  38. package/engine/scenes.mjs +14 -5
  39. package/engine/schema-check.mjs +1 -1
  40. package/engine/site-build.mjs +21 -3
  41. package/engine/web-wikilinks.mjs +6 -3
  42. package/engine/wikilinks.mjs +2 -4
  43. package/hm3/actors.mjs +8 -0
  44. package/hm3/items.mjs +8 -0
  45. package/package.json +1 -1
  46. package/release.mjs +63 -3
  47. package/sohl/actors.mjs +8 -0
  48. package/sohl/items.mjs +8 -0
  49. package/sohl/note-schemas.mjs +5 -5
  50. package/types/content-config.d.mts +66 -15
  51. package/types/engine/actor-compiler.d.mts +34 -30
  52. package/types/engine/address-diff.d.mts +57 -3
  53. package/types/engine/base-compiler.d.mts +10 -2
  54. package/types/engine/bundles.d.mts +9 -0
  55. package/types/engine/content-address.d.mts +9 -9
  56. package/types/engine/content-index.d.mts +57 -13
  57. package/types/engine/content-lint.d.mts +6 -4
  58. package/types/engine/content-tables.d.mts +49 -18
  59. package/types/engine/frontmatter-lint.d.mts +3 -2
  60. package/types/engine/helpers.d.mts +105 -31
  61. package/types/engine/index.d.mts +4 -0
  62. package/types/engine/journals.d.mts +9 -0
  63. package/types/engine/note-claims.d.mts +17 -10
  64. package/types/engine/note-vocabulary.d.mts +23 -196
  65. package/types/engine/pack-config.d.mts +4 -4
  66. package/types/engine/pdf-build.d.mts +42 -0
  67. package/types/engine/pdf-fonts.d.mts +30 -0
  68. package/types/engine/pdf-render.d.mts +156 -0
  69. package/types/engine/pdf-toc.d.mts +114 -0
  70. package/types/engine/scenes.d.mts +10 -1
  71. package/types/engine/schema-check.d.mts +2 -2
  72. package/types/engine/site-build.d.mts +34 -6
  73. package/types/engine/wikilinks.d.mts +2 -3
  74. package/types/hm3/actors.d.mts +8 -0
  75. package/types/hm3/items.d.mts +8 -0
  76. package/types/release.d.mts +15 -4
  77. package/types/sohl/actors.d.mts +10 -2
  78. package/types/sohl/items.d.mts +8 -0
@@ -0,0 +1,1432 @@
1
+ ## Configuration: `package-build.config.yaml`, key by key
2
+
3
+ Every repository built by `@heroiclands/package-build` declares itself in one
4
+ file at its root: `package-build.config.yaml` (`.yml` works identically). The
5
+ whole contract is `defineConfig`, exported from `content-config.mjs`: it
6
+ validates the object, fills every optional key with its default, and returns a
7
+ deeply frozen copy. It performs no I/O and knows nothing about any particular
8
+ package's content — the configuration is data, and the compilers read it.
9
+
10
+ The reserved `packageBuild:` section belongs to the packaging half of the
11
+ toolchain and is validated separately, by `resolvePackageBuildConfig` in
12
+ `config.mjs`. `content-config.mjs` checks only that the section is a mapping
13
+ and hands it back frozen; its own key-by-key reference is
14
+ [below](#the-packagebuild-section).
15
+
16
+ This document is a transcription of both validators. Where a rule is stated in
17
+ prose here, the validator states it in code; where a message is quoted, it is
18
+ quoted **verbatim** — search the error text you hit against this page and you
19
+ will find the row that produced it.
20
+
21
+ ### Two ways to write it
22
+
23
+ Every configuration ends at the same `defineConfig`, but reaches it by one of
24
+ two routes, and three keys behave differently depending on which:
25
+
26
+ | | `package-build.config.yaml` (data) | `package-build.config.mjs` (code) |
27
+ | ---------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
28
+ | Loaded by | `engine/pack-config.mjs`, which parses the YAML and derives three keys before calling `defineConfig` | `require()`, which loads the module and reads its default export — already the result of the file calling `defineConfig` itself |
29
+ | `rootDir` | Forbidden — always the directory the file sits in | Authored, typically `import.meta.dirname` |
30
+ | `foundryPackage` | Forbidden — always the adjacent `package.json` `name` | Authored |
31
+ | `itemBuilders` | A **name** (`sohl`, `hm3`) or list of names, resolved against the registries this package ships | The registry object itself — real builder functions, which only code can carry |
32
+
33
+ A file is chosen by its extension: `package-build.config.yaml`, then
34
+ `.yml`, then `.mjs`, resolved by walking up from the working directory. Two of
35
+ them in one directory is a hard error — a repository declares its build in
36
+ exactly one file. The `.mjs` form exists as an escape hatch for a repository
37
+ whose `itemBuilders` registry is its own code, not one of the two this package
38
+ ships; everything else about the two forms is identical, because both are
39
+ validated and frozen by the same `defineConfig`.
40
+
41
+ An `.mjs` configuration imports `defineConfig` from
42
+ `@heroiclands/package-build/content-config`, never from the package root
43
+ barrel — the barrel pulls in the compilers, which read this module, which
44
+ loads the config file, so a config reaching for the barrel closes a cycle
45
+ around its own evaluation.
46
+
47
+ ### Quick reference
48
+
49
+ 18 top-level keys. `rootDir` is not one of them — a data configuration never
50
+ writes it — and is documented under [Derived values](#derived-values) instead,
51
+ alongside `foundryPackage` and `itemBuilders`, whose data-configuration
52
+ behaviour is also derivation rather than ordinary authoring.
53
+
54
+ | Key | Type | Required | Default |
55
+ | ------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------- |
56
+ | [`contentPackage`](#contentpackage) | string | yes | — |
57
+ | [`foundryPackage`](#foundrypackage) | string | yes (`.mjs` only — derived in YAML); refused in a `documentation` package | — |
58
+ | [`packageKind`](#packagekind) | `"systems"` \| `"modules"` \| `"documentation"` | yes | — |
59
+ | [`stats`](#stats) | object | yes; refused in a `documentation` package | — |
60
+ | [`itemBuilders`](#itembuilders) | object, or list of `{system, builders}` (or a name/list of names, in YAML) | no; refused in a `documentation` package | `{}` |
61
+ | [`paths`](#paths) | object | no | see [`paths`](#paths) |
62
+ | [`skipDirectories`](#skipdirectories) | string[] | no | `[]` |
63
+ | [`icons`](#icons) | object, or a path to a file holding one | no | empty registry |
64
+ | [`packs`](#packs) | array | yes, at least one entry, in a `systems` or `modules` package; refused in a `documentation` package | — |
65
+ | [`docs`](#docs) | object | no; refused in a `documentation` package | `{}` |
66
+ | [`site`](#site) | object | no | see [`site`](#site) |
67
+ | [`pdf`](#pdf) | object | no | `null` |
68
+ | [`compatibility`](#compatibility) | object | no; refused in a `documentation` package | `null` |
69
+ | [`relationships`](#relationships) | object | no; refused in a `documentation` package | `{}` |
70
+ | [`systems`](#systems) | object | no; refused in a `documentation` package | `{}` |
71
+ | [`requiresSystem`](#requiressystem) | string | no; refused in a `documentation` package | `null` |
72
+ | [`packageBuild`](#the-packagebuild-section) | object | no | `{}` |
73
+ | [`publish`](#publish) | object | no; **required**, with `site: content`, in a `documentation` package | `{site: "homepage", address: {prefix: ""}}` |
74
+
75
+ Any key outside this list is refused:
76
+
77
+ > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, packageBuild, publish). ``
78
+
79
+ (`rootDir` appears in that list because it is a key `defineConfig` itself
80
+ accepts — an `.mjs` configuration authors it directly. A YAML configuration
81
+ never sees this particular message about it, because `engine/pack-config.mjs`
82
+ refuses an authored `rootDir` earlier, with its own message — see
83
+ [Derived values](#derived-values).)
84
+
85
+ ### Derived values
86
+
87
+ Five values in the resolved configuration are never transcribed by an author
88
+ — they are computed from where the file sits, from the adjacent
89
+ `package.json`, from the package kind, or from a name naming a table this
90
+ package already ships. Authoring `rootDir`, `foundryPackage` or
91
+ `stats.systemVersion` yourself is an **error**, not an override: a
92
+ transcribed copy is free to drift from what it copied, which is exactly how
93
+ `stats.systemVersion` once sat at a stale version for four releases while
94
+ nothing said so. `assetRoot` has no author-facing spelling to refuse in the
95
+ first place — it is never a key at all, only ever a computed value.
96
+
97
+ #### `rootDir`
98
+
99
+ The configuration's own directory, always. Every configured path in `paths`
100
+ is resolved against it, so the build reads the same tree whatever directory
101
+ it was launched from.
102
+
103
+ - In a **YAML** configuration, writing `rootDir:` is refused by the loader
104
+ before `defineConfig` ever runs:
105
+
106
+ > ``package-build: <config file> declares `rootDir`, which a data configuration may not: it is always the directory the file sits in. An absolute path written here would be one machine's; remove the key.``
107
+
108
+ - In an **`.mjs`** configuration, `rootDir` is an ordinary required key of
109
+ `defineConfig` — a non-empty, absolute string, typically
110
+ `import.meta.dirname`. A relative or missing value is refused:
111
+
112
+ > ``package-build config: `rootDir` must be a non-empty string.``
113
+
114
+ > ``package-build config: `rootDir` must be an absolute path — it is what makes the build independent of the directory it was launched from (pass `import.meta.dirname`).``
115
+
116
+ #### `foundryPackage`
117
+
118
+ The Foundry package id — what appears as `id` in the generated
119
+ `system.json` / `module.json`, and the value `assetRoot` and the packaging
120
+ half's `packageId` are built from. `null` in the resolved configuration of a
121
+ `documentation` package — see [`packageKind`](#packagekind) for the refusal,
122
+ which applies before either loader form gets a chance to derive anything.
123
+
124
+ - In a **YAML** configuration, writing `foundryPackage:` is refused; the
125
+ loader reads it from the adjacent `package.json` `name` instead, verbatim,
126
+ with no normalisation and no legality check (every consumer is an
127
+ unscoped, private npm package, so the case of a scoped name never arises):
128
+
129
+ > ``package-build: <config file> declares `foundryPackage`, which a data configuration may not: it is the `name` of the `package.json` beside it. Remove the key.``
130
+
131
+ If the adjacent `package.json` cannot be read, or declares no `name`:
132
+
133
+ > `package-build: <package.json path> could not be read, and the configuration derives both the Foundry package id and the system version from it.`
134
+
135
+ > ``package-build: <package.json path> declares no `name`, which is what the Foundry package id is derived from.``
136
+
137
+ A `documentation` package derives nothing here: it has no Foundry package
138
+ id, so the loader never reads `package.json` `name` for one.
139
+
140
+ - In an **`.mjs`** configuration, `foundryPackage` is an ordinary required
141
+ key — a non-empty string, checked the same way as every other required
142
+ string field — for a `systems` or `modules` package:
143
+
144
+ > ``package-build config: `foundryPackage` must be a non-empty string.``
145
+
146
+ #### `assetRoot`
147
+
148
+ The served Foundry asset root a compiled document's `img:` is resolved
149
+ against — the one reader of it is `resolveImg`
150
+ (`engine/helpers.mjs`). Never a key an author writes; always computed from
151
+ `packageKind` and `foundryPackage`.
152
+
153
+ - For a `systems` or `modules` package, `<packageKind>/<foundryPackage>/assets`.
154
+ - For a `documentation` package, `null` — Foundry serves no files for a
155
+ package of this kind, and `documentation/null/assets` would be an address
156
+ that resolves nowhere. `resolveImg` refuses rather than rooting a path
157
+ against nothing.
158
+
159
+ #### `stats.systemVersion`
160
+
161
+ The version of the game system the packs were built against, stamped into
162
+ every compiled document's `_stats.systemVersion`. `stats.systemId` is
163
+ derived the same way, from the same block — see [`stats`](#stats) for both.
164
+ Both are `null` in the resolved configuration of a `documentation` package,
165
+ which refuses `stats` outright — see [`stats`](#stats) — and so has neither
166
+ to derive.
167
+
168
+ - **A system package** (`packageKind: systems`) is its own system, so its
169
+ `systemVersion` is its own `package.json` `version`:
170
+
171
+ > ``package-build: <package.json path> declares no `version`, which is what a system's stats.systemVersion is derived from.``
172
+
173
+ - **A module** ships content _for_ another package's system. Its own
174
+ `package.json` version is the module's, not the system's, so deriving from
175
+ it would stamp a version that never existed. The honest source is
176
+ `systems:` (preferred) or `relationships.systems` (the older form) — see
177
+ [`systems`](#systems). Declaring neither, on a module whose packs carry no
178
+ `system:` either, is system-agnostic on purpose and stamps no version at
179
+ all. Declaring one without a usable `compatibility.verified` is refused:
180
+
181
+ > ``package-build: a module's stats.systemVersion is derived from the system it declares a relationship with, and this configuration declares none usable. Add `relationships.systems` naming <system id, or "the system"> with a `compatibility.verified` version. It is not taken from this package's own `package.json` version — that is the module's version, and stamping it would claim a system version that never existed.``
182
+
183
+ - Authoring `stats.systemVersion` directly is refused by `defineConfig`
184
+ itself, in both configuration forms — see [`stats`](#stats) for the exact
185
+ message.
186
+
187
+ - **In an `.mjs` configuration**, this derivation is the author's own
188
+ responsibility: `defineConfig` performs no I/O, so a code configuration
189
+ that wants `stats.systemVersion` populated reads its own `package.json` (or
190
+ its own `relationships.systems`) and supplies the result under the
191
+ `DERIVED_SYSTEM_VERSION` symbol exported by `content-config.mjs`, the same
192
+ channel `engine/pack-config.mjs` uses for a YAML configuration:
193
+
194
+ ```js
195
+ import { defineConfig, DERIVED_SYSTEM_VERSION } from "@heroiclands/package-build/content-config";
196
+
197
+ export default defineConfig({
198
+ // …
199
+ stats: {
200
+ lastModifiedBy: "acmebuilder00000",
201
+ [DERIVED_SYSTEM_VERSION]: "1.6.3",
202
+ },
203
+ });
204
+ ```
205
+
206
+ A symbol, deliberately — a string key would be a second, forgeable spelling
207
+ of the `stats.systemId` / `stats.systemVersion` keys `defineConfig` refuses
208
+ to let an author write, reachable from plain YAML. A symbol cannot be
209
+ written in YAML at all and does not appear in `Object.keys`, so the
210
+ refusal has no back door. Supplying nothing here, and declaring no
211
+ resolvable `systems:` entry either, leaves `stats.systemVersion` `null` —
212
+ `defineConfig` raises nothing for it; a document compiled that way simply
213
+ stamps `_stats.systemVersion: null`.
214
+
215
+ #### `itemBuilders`
216
+
217
+ Unlike the first three, authoring `itemBuilders` is not an error — it is
218
+ **translated**, in the YAML form only:
219
+
220
+ - In a **YAML** configuration, the value is a _name_ — `sohl`, `hm3` — or a
221
+ list of names, each resolved against the registries this package ships
222
+ (`sohl/item-builders.mjs`'s `ITEM_BUILDERS`, `hm3/item-builders.mjs`'s
223
+ `HM3_ITEM_BUILDERS`) before `defineConfig` ever sees the result. A name
224
+ this package does not ship is refused:
225
+
226
+ > ``package-build: <config file> names the `itemBuilders` registry "<name>", which this package does not ship. Known registries: sohl, hm3. To supply your own, declare it in package-build.config.mjs.``
227
+
228
+ A non-string entry — something that is not a registry name at all — is
229
+ refused the same way:
230
+
231
+ > ``package-build: <config file> must name its `itemBuilders` registry as a string — the registry is code, and data cannot carry it. Known registries: sohl, hm3; a registry of your own goes in package-build.config.mjs.``
232
+
233
+ - In an **`.mjs`** configuration, `itemBuilders` is the registry itself —
234
+ real builder functions — since only code can carry a function. See
235
+ [`itemBuilders`](#itembuilders-1) below for the shape both forms end at.
236
+
237
+ ---
238
+
239
+ ## The 18 keys
240
+
241
+ ### `contentPackage`
242
+
243
+ **Type:** string · **Required** · no default.
244
+
245
+ The address namespace every note in this repository is published under —
246
+ the first segment of every canonical address (`contentPackage-system-type-shortcode`,
247
+ so `sohl-none-doc-gear`). Read wherever an address is built or parsed
248
+ (`engine/content-address.mjs` and everything downstream of it).
249
+
250
+ Two rules apply, both enforced here rather than assumed, because an address
251
+ is read by counting hyphen-separated segments:
252
+
253
+ - It must be **lowercase alphanumeric** — the hyphen stays purely a separator.
254
+ A value containing one is refused:
255
+
256
+ > ``package-build config: `contentPackage` is `harn-adventures`, which is not lowercase alphanumeric (^[a-z0-9]+$). It is the first segment of every address this package publishes (`harn-adventures-<system>-<type>-<shortcode>`), and an address is read by counting hyphen-separated segments — so anything outside that here makes those addresses unreadable rather than merely ugly. `harn-adventures` became `harnadventures`.``
257
+
258
+ - It must **not also be a note type** — the two vocabularies are disjoint,
259
+ because a written address may drop its leading segments and
260
+ `contentPackage-shortcode` has to read unambiguously as one or the other:
261
+
262
+ > ``package-build config: `contentPackage` is `macro`, which is also a note type — `macro-<shortcode>` already addresses one. A written address may omit its leading segments, so `macro-<shortcode>` reads as a type and a shortcode and nothing but the two vocabularies being disjoint says which slot the name is filling. Rename the package.``
263
+
264
+ The vocabulary checked against is the union of every note type this
265
+ toolchain knows about — the closed format vocabulary, every consumer's
266
+ configured `itemBuilders` types, `macro`, the map types, and the
267
+ `doc`-prefixed form of each. `sohl` colliding with itself (the system
268
+ package's own id is also a system id, `sohl-sohl-skill-clmb` being the
269
+ honest result) is structural and cannot be avoided; this check exists to
270
+ prevent every other collision, which can be.
271
+
272
+ An empty or non-string value is refused generically:
273
+
274
+ > ``package-build config: `contentPackage` must be a non-empty string.``
275
+
276
+ ### `foundryPackage`
277
+
278
+ See [Derived values](#derived-values) — forbidden in a YAML configuration,
279
+ required (a non-empty string) in an `.mjs` one. Refused in either form for a
280
+ `documentation` package, which is not a Foundry package and has no Foundry
281
+ package id:
282
+
283
+ > ``package-build config: `foundryPackage` is refused in a `documentation` package, which is not a Foundry package, so it has no Foundry package id.``
284
+
285
+ ### `packageKind`
286
+
287
+ **Type:** `"systems"` \| `"modules"` \| `"documentation"` · **Required** · no default.
288
+
289
+ Which kind of package this repository builds. `systems` and `modules` are the
290
+ two Foundry answers — also the directory Foundry installs the package under,
291
+ and what `assetRoot` and the packaging half's `artifact` (`system` or
292
+ `module`) are derived from. `documentation` is the answer "not a Foundry
293
+ package at all": it publishes a site and a book from its notes, installs into
294
+ no Foundry data directory and compiles no compendium.
295
+
296
+ > ``package-build config: `packageKind` must be one of: systems, modules, documentation.``
297
+
298
+ `compilesFoundryDocuments(config)`, exported from `content-config.mjs`
299
+ alongside `DOCUMENTATION_KIND` and [`publishesContentPages`](#publish), is the
300
+ one question every Foundry-side reader asks — the manifest writer, to decide
301
+ whether there is a package for Foundry to install, and the pack compilers, to
302
+ decide whether there is anything to compile. It returns
303
+ `config.packageKind !== DOCUMENTATION_KIND`.
304
+
305
+ ### `stats`
306
+
307
+ **Type:** object · **Required** in a `systems` or `modules` package · refused
308
+ in a `documentation` package, which compiles no documents and so has no
309
+ `_stats` block to stamp:
310
+
311
+ > ``package-build config: `stats` is refused in a `documentation` package, which compiles no documents, so there is no `_stats` block to stamp.``
312
+
313
+ The identity stamped into every compiled document's `_stats` block.
314
+ `coreVersion` is **not** here — that is the top-level `compatibility.minimum`,
315
+ stamped from one place rather than duplicated.
316
+
317
+ | Key | Type | Required | Default |
318
+ | ---------------------- | ------ | ------------- | ------- |
319
+ | `stats.lastModifiedBy` | string | yes | — |
320
+ | `stats.systemId` | — | **forbidden** | derived |
321
+ | `stats.systemVersion` | — | **forbidden** | derived |
322
+
323
+ `stats.lastModifiedBy` is the 16-character id every compiled document is
324
+ stamped as authored by. An empty or missing value is refused generically:
325
+
326
+ > ``package-build config: `stats.lastModifiedBy` must be a non-empty string.``
327
+
328
+ `stats.systemId` and `stats.systemVersion` are **derived and may not be
329
+ authored**, in both configuration forms — `defineConfig` itself refuses them,
330
+ before either loader has a chance to supply its own derivation:
331
+
332
+ > ``package-build config: `stats.systemId` is derived and may not be authored. A system package is its own system; a module takes it from `requiresSystem`, or from `systems:` when it declares exactly one. Remove the key.``
333
+
334
+ > ``package-build config: `stats.systemVersion` is derived and may not be authored. It is the `compatibility.verified` of the system in `systems:`, or a system package's own `package.json` version. Remove the key.``
335
+
336
+ See [`stats.systemVersion`](#statssystemversion) under Derived values for how
337
+ each is actually resolved.
338
+
339
+ Any other key under `stats` is refused:
340
+
341
+ > ``package-build config: `stats.<key>` is not a recognized option (expected one of: lastModifiedBy).``
342
+
343
+ ### `itemBuilders`
344
+
345
+ **Type:** object (`{type: builder}`), or a list of `{system, builders}`
346
+ registries — or, in YAML only, a registry **name** (`sohl`, `hm3`) or list of
347
+ names · **Optional** · default `{}`. Refused in a `documentation` package,
348
+ which compiles no items and so has no item-type registry to name:
349
+
350
+ > ``package-build config: `itemBuilders` is refused in a `documentation` package, which compiles no items, so there is no item-type registry to name.``
351
+
352
+ The consumer's item-type registry: each content `type` that compiles into an
353
+ Item, paired with the builder function producing its `system` block. A
354
+ content module that ships no items declares none.
355
+
356
+ **Single-registry form** — what almost every configuration declares. Each
357
+ entry is either a bare builder function, or that function paired with the
358
+ type's default art and the frontmatter fields it declares:
359
+
360
+ ```yaml
361
+ itemBuilders: sohl # YAML: a name naming a shipped registry
362
+ ```
363
+
364
+ ```js
365
+ itemBuilders: { // .mjs: the registry itself
366
+ relic: { system: buildRelic, img: "icons/relic.svg", fields: [...] },
367
+ charm: buildCharm, // a bare builder — every note of the type needs its own `img:`
368
+ }
369
+ ```
370
+
371
+ | Key (under `itemBuilders.<type>`) | Type | Required | Default |
372
+ | --------------------------------- | --------------------------- | -------- | --------------------- |
373
+ | `itemBuilders.<type>.system` | function | yes | — |
374
+ | `itemBuilders.<type>.img` | string | no | none (no default art) |
375
+ | `itemBuilders.<type>.fields` | array of field declarations | no | none |
376
+
377
+ An entry that is neither a function nor an object with a `system` builder is
378
+ refused:
379
+
380
+ > ``package-build config: `itemBuilders.<type>` must be a builder function, or an object with a `system` builder.``
381
+
382
+ > ``package-build config: `itemBuilders.<type>.system` must be a function.``
383
+
384
+ Each entry of `itemBuilders.<type>.fields` must be an object declaring a
385
+ `to`:
386
+
387
+ > ``package-build config: `itemBuilders.<type>.fields[<index>]` must be a field declaration object.``
388
+
389
+ > ``package-build config: `itemBuilders.<type>.fields[<index>].to` must be a non-empty string.``
390
+
391
+ Any other key under one entry is refused:
392
+
393
+ > ``package-build config: `itemBuilders.<type>.<key>` is not a recognized option (expected one of: system, img, fields).``
394
+
395
+ **List-of-registries form** — for a repository feeding more than one system
396
+ (`harn-ensemble` ships an HM3 pack, a SoHL pack, and a system-neutral one).
397
+ The accepted type vocabulary is the **union** of the registries' keys; a
398
+ type more than one registry declares keeps a builder per system rather than
399
+ one winning in silence, and is tracked separately as
400
+ `itemTypesBySeveralSystems` in the resolved configuration.
401
+
402
+ ```yaml
403
+ itemBuilders: [sohl, hm3] # YAML
404
+ ```
405
+
406
+ ```js
407
+ itemBuilders: [
408
+ // .mjs
409
+ { system: "sohl", builders: { skill: buildSohlSkill } },
410
+ { system: "hm3", builders: { skill: buildHm3Skill } },
411
+ ];
412
+ ```
413
+
414
+ | Key (under `itemBuilders[]`) | Type | Required | Default |
415
+ | ---------------------------- | ---------------------------------------------- | -------- | ------- |
416
+ | `itemBuilders[].system` | string | yes | — |
417
+ | `itemBuilders[].builders` | object, same shape as the single-registry form | yes | — |
418
+
419
+ Declaring the same system twice is refused:
420
+
421
+ > ``package-build config: `itemBuilders[<index>]` declares a second registry for `<system>` — a system has one item vocabulary, so merge them at their source.``
422
+
423
+ An entry that is not `{system, builders}` is refused:
424
+
425
+ > ``package-build config: `itemBuilders[<index>]` must be `{ system, builders }` — a registry and the system it belongs to.``
426
+
427
+ Any other key on one list entry is refused:
428
+
429
+ > ``package-build config: `itemBuilders[<index>].<key>` is not a recognized option (expected one of: system, builders).``
430
+
431
+ See [`itemBuilders`](#itembuilders) under Derived values for how the YAML
432
+ name form resolves before reaching here.
433
+
434
+ ### `paths`
435
+
436
+ **Type:** object · **Optional** · every key defaults to the conventional
437
+ HeroicLands layout, resolved against `rootDir`:
438
+
439
+ | Key | Default | What it is |
440
+ | --------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
441
+ | `paths.content` | `assets/content` | The content tree root. |
442
+ | `paths.contentIndex` | `build/content-index` | Where `content-index` writes this package's note index. Derived and disposable. |
443
+ | `paths.packJson` | `build/packs-json` | Build-only per-entry JSON intermediate. |
444
+ | `paths.stage` | `build/stage/packs` | Compiled LevelDB packs. |
445
+ | `paths.unpack` | `build/tmp/packs` | Where `unpack` extracts JSON back to. |
446
+ | `paths.foreignCache` | `build/cache/foreign` | Where a dependency declaring `itemCatalog: true` is unpacked. |
447
+ | `paths.metadataCache` | `build/cache/metadata` | Where a dependency's published content index is fetched to, for every declared dependency. |
448
+
449
+ Every configured path must be **relative** — an absolute one would escape
450
+ the repository the config anchors:
451
+
452
+ > ``package-build config: `paths.<key>` must be relative to rootDir, so a consumer's layout travels with its repository.``
453
+
454
+ An empty value is refused generically:
455
+
456
+ > ``package-build config: `paths.<key>` must be a non-empty string.``
457
+
458
+ Any other key is refused:
459
+
460
+ > ``package-build config: `paths.<key>` is not a recognized option (expected one of: content, contentIndex, packJson, stage, unpack, foreignCache, metadataCache).``
461
+
462
+ ### `skipDirectories`
463
+
464
+ **Type:** string[] · **Optional** · default `[]`.
465
+
466
+ Directory names the content walk ignores wherever they appear — Obsidian's
467
+ `Templates`, for instance.
468
+
469
+ ```yaml
470
+ skipDirectories: [Templates]
471
+ ```
472
+
473
+ > ``package-build config: `skipDirectories` must be an array.``
474
+
475
+ > ``package-build config: `skipDirectories[<index>]` must be a non-empty string.``
476
+
477
+ ### `icons`
478
+
479
+ **Type:** object (`{families, icons, defaultFamily?}`), or a string naming a
480
+ file holding one, relative to the configuration · **Optional** · default: an
481
+ empty registry (`{families: {}, defaultFamily: undefined, icons: {}}`) —
482
+ nothing is supplied by default, because a registry entry is a promise that a
483
+ glyph will render and only the package shipping the font can keep it.
484
+
485
+ ```yaml
486
+ # inline
487
+ icons:
488
+ families:
489
+ fontawesome: { class: fa, styles: [solid, regular, brands], describe: Font Awesome Free }
490
+ icons:
491
+ being: { style: solid, icon: user, label: being }
492
+ ```
493
+
494
+ ```yaml
495
+ # or a path to a generated file — the shape a real package wants, since the
496
+ # registry is derived from what the interface actually draws
497
+ icons: assets/icon-registry.yaml
498
+ ```
499
+
500
+ A bare string that is empty is refused:
501
+
502
+ > ``package-build config: `icons` is empty — name a file, or write the registry inline.``
503
+
504
+ A named file that cannot be read, or does not parse as YAML, or parses to
505
+ nothing, is refused:
506
+
507
+ > ``package-build config: `icons` names <file>, which cannot be read from <rootDir>.``
508
+
509
+ > ``package-build config: `icons` names <file>, which is not readable YAML: <parser error>.``
510
+
511
+ > ``package-build config: `icons` names <file>, which is empty.``
512
+
513
+ A value that is neither a registry nor a file path is refused:
514
+
515
+ > ``package-build config: `icons` must be a registry — `families` and `icons` — or a path to a file holding one.``
516
+
517
+ Within the registry (inline or loaded from file):
518
+
519
+ | Key | Type | Required | Default |
520
+ | --------------------- | -------------------------------- | -------- | ------- |
521
+ | `icons.families` | object (name → family) | no | `{}` |
522
+ | `icons.icons` | object (name → icon entry) | no | `{}` |
523
+ | `icons.defaultFamily` | string, naming a declared family | no | none |
524
+
525
+ > ``package-build config: `icons.families` must be a mapping of name to family.``
526
+
527
+ > ``package-build config: `icons.icons` must be a mapping of name to icon entry.``
528
+
529
+ Every icon name is checked against the charset a note may write between the
530
+ colons — lowercase letters, digits and hyphens, the same charset an address
531
+ segment uses:
532
+
533
+ > ``package-build config: `icons.icons.Bad_Name` is not a name a note can write — `:icon-…:` takes lowercase letters, digits and hyphens, the charset an address segment uses.``
534
+
535
+ `defaultFamily`, if set, must name a family the registry actually declares:
536
+
537
+ > ``package-build config: `icons.defaultFamily` names `<name>`, which is not one of the declared families.``
538
+
539
+ The registry is then checked by `engine/content-icons.mjs`'s
540
+ `checkIconRegistry` — elsewhere in the toolchain its findings are warnings;
541
+ here, because this is the table every note in the package is read against,
542
+ any finding it reports is a refusal, joined into one message:
543
+
544
+ > ``package-build config: `icons` <findings, semicolon-joined>.``
545
+
546
+ ### `packs`
547
+
548
+ **Type:** array of pack specs · **Required, at least one entry**, in a
549
+ `systems` or `modules` package. Refused in a `documentation` package, which
550
+ compiles no compendium and so has no packs to declare:
551
+
552
+ > ``package-build config: `packs` is refused in a `documentation` package, which compiles no compendium, so there are no packs to declare.``
553
+
554
+ The compendium packs the build compiles, named exactly as declared in the
555
+ package manifest's `packs` array. Several packs may share a `type` — a
556
+ note's `pack:` frontmatter then names which one it belongs to, and at most
557
+ one pack of a type is marked `default: true` to receive the notes that name
558
+ none.
559
+
560
+ ```yaml
561
+ packs:
562
+ - { name: items, type: Item }
563
+ - { name: journals, type: JournalEntry, label: Journals }
564
+ ```
565
+
566
+ | Key (under `packs[]`) | Type | Required | Default |
567
+ | --------------------- | --------------------------------------------------------------------- | -------- | --------------------------------------- |
568
+ | `packs[].name` | string | yes | — |
569
+ | `packs[].type` | one of `Actor`, `Adventure`, `Item`, `JournalEntry`, `Macro`, `Scene` | yes | — |
570
+ | `packs[].label` | string | no | `packs[].name` |
571
+ | `packs[].private` | boolean | no | `false` |
572
+ | `packs[].companions` | array, same shape, one level only | no | `[]` |
573
+ | `packs[].mayBeEmpty` | boolean | no | `false` |
574
+ | `packs[].default` | boolean | no | `false` |
575
+ | `packs[].prebuilt` | string (directory) | no | `null` |
576
+ | `packs[].system` | string | no | `null` — falls back to `stats.systemId` |
577
+
578
+ `packs` itself:
579
+
580
+ > ``package-build config: `packs` must be an array.``
581
+
582
+ > ``package-build config: `packs` must declare at least one pack.``
583
+
584
+ Two packs (including companions, anywhere in the tree) may not share a name
585
+ — two packs both named `x` produce:
586
+
587
+ > ``package-build config: `packs` declares the pack `x` more than once.``
588
+
589
+ At most one pack of a given `type` may be marked default — two Item packs
590
+ `a` and `b` both marked `default: true` produce:
591
+
592
+ > ``package-build config: `packs` marks both `a` and `b` as the default Item pack; a note declaring no `pack:` must have one destination.``
593
+
594
+ `packs[].type` is checked against the closed set of document types this
595
+ toolchain compiles:
596
+
597
+ > ``package-build config: `packs[<index>].type` must be one of: Actor, Adventure, Item, JournalEntry, Macro, Scene.``
598
+
599
+ `folders` is retired — a folder is a note (`type: folder`) now, materialised
600
+ by the pack whose documents reference it through `packFolder`:
601
+
602
+ > ``package-build config: `packs[<index>].folders` is retired — delete it. A folder is a note (`type: folder`) now, and a pack materialises the folders its documents reference through `packFolder`, so there is no per-pack hierarchy file to name.``
603
+
604
+ A **companion** — a pack written by its parent pack's own compile pass — may
605
+ not declare `default` (no note is ever routed into one), and may not nest
606
+ further companions of its own:
607
+
608
+ > ``package-build config: `packs[<index>].companions[<index>].default` may not be declared on a companion: a companion is written by another pack's pass, so no note is ever routed into one.``
609
+
610
+ > ``package-build config: `packs[<index>].companions[<index>].companions` may not nest: a companion is written by another pack's pass, and that pass is the only level of indirection the build has.``
611
+
612
+ A **prebuilt** pack (its per-document JSON already exists) has no compile
613
+ pass, so it may not also be a companion, may not declare companions of its
614
+ own, and may not be `default`:
615
+
616
+ > ``package-build config: `packs[<index>].prebuilt` may not be declared on a companion: a companion is written by another pack's pass, and a prebuilt pack has no pass.``
617
+
618
+ > ``package-build config: `packs[<index>].companions` may not accompany `prebuilt`: a companion is written by this pack's pass, and a prebuilt pack has none.``
619
+
620
+ > ``package-build config: `packs[<index>].default` may not accompany `prebuilt`: the default pack receives notes declaring no `pack:`, and no note is routed into a prebuilt one.``
621
+
622
+ `packs[].system` — every document in a pack is stamped `_stats.systemId` and
623
+ `systemVersion`, and Foundry hides a whole package from any world whose
624
+ system `requiresSystem` does not name, so a pack's declared `system:` must
625
+ resolve to something real. A pack `x` naming `system: sohl` in a package
626
+ with no `systems:` block, whose own `foundryPackage` is `acme`, produces:
627
+
628
+ > ``package-build config: `packs.x.system` names `sohl`, which `systems:` does not declare — the `systems:` block is empty or absent, and which is not this package's own system `acme`. Every document in the pack is stamped `_stats.systemId` and `systemVersion` from one of those two, so with neither it would be stamped null. Add `systems:` naming `sohl` with a `compatibility.verified` version.``
629
+
630
+ A pack naming a `system:` other than the one `requiresSystem` names — the
631
+ pack could never be seen, since Foundry hides the whole package from any
632
+ world whose system `requiresSystem` does not name:
633
+
634
+ > ``package-build config: `packs.<name>.system` names `<system>` while `requiresSystem` is `<other>`, so this pack could never be seen — Foundry hides the whole package from any world whose system `requiresSystem` does not name. Drop `requiresSystem`, or correct the pack.``
635
+
636
+ Any other key on a pack entry is refused:
637
+
638
+ > ``package-build config: `packs[<index>].<key>` is not a recognized option (expected one of: name, type, label, private, companions, mayBeEmpty, default, prebuilt, system).``
639
+
640
+ ### `docs`
641
+
642
+ **Type:** object · **Optional** · default `{}`. Refused in a `documentation`
643
+ package, which compiles no items and so has no item-field reference pages to
644
+ frame:
645
+
646
+ > ``package-build config: `docs` is refused in a `documentation` package, which compiles no items, so there are no item-field reference pages to frame.``
647
+
648
+ How this repository frames the documentation pages it generates.
649
+
650
+ | Key | Type | Required | Default |
651
+ | ----------------- | ------ | -------- | ------- |
652
+ | `docs.itemFields` | object | no | `{}` |
653
+
654
+ > ``package-build config: `docs` must be a mapping.``
655
+
656
+ > ``package-build config: `docs.<key>` is not a recognized option (expected one of: itemFields).``
657
+
658
+ `docs.itemFields` frames the item-frontmatter reference rendered by
659
+ `content-build docs item-fields` — the tables come from the `itemBuilders`
660
+ registry and are the same wherever rendered; everything here is the
661
+ consumer's: heading, orientation, where the page is filed.
662
+
663
+ | Key | Type | Required | Default |
664
+ | -------------------------- | -------- | -------- | ---------------------------------------------------------------------- |
665
+ | `docs.itemFields.title` | string | no | none — the page's H1 |
666
+ | `docs.itemFields.out` | string | no | none — without it, the page goes to stdout |
667
+ | `docs.itemFields.preamble` | string[] | no | none — markdown lines between the generated banner and the first table |
668
+
669
+ > ``package-build config: `docs.itemFields` must be a mapping.``
670
+
671
+ > ``package-build config: `docs.itemFields.title` must be a non-empty string.``
672
+
673
+ `preamble` is a list of lines, and a blank entry is a meaningful blank line
674
+ — markdown's paragraph separator — so the check is on type, not content:
675
+
676
+ > ``package-build config: `docs.itemFields.preamble` must be a list of lines — a blank entry is a blank line, which is how paragraphs are separated in markdown.``
677
+
678
+ > ``package-build config: `docs.itemFields.preamble[<index>]` must be a string.``
679
+
680
+ Any other key under `docs.itemFields` is refused:
681
+
682
+ > ``package-build config: `docs.itemFields.<key>` is not a recognized option (expected one of: title, out, preamble).``
683
+
684
+ ### `site`
685
+
686
+ **Type:** object · **Optional** · every key defaults to nothing published:
687
+
688
+ | Key | Type | Default |
689
+ | ----------------------- | -------- | ------- |
690
+ | `site.out` | string | `""` |
691
+ | `site.base` | string | `""` |
692
+ | `site.packages` | string[] | `[]` |
693
+ | `site.sections` | object | `{}` |
694
+ | `site.readmeSections` | object | `{}` |
695
+ | `site.landing` | object | `null` |
696
+ | `site.trees` | array | `[]` |
697
+ | `site.pass` | string | `""` |
698
+ | `site.passOptions` | object | `{}` |
699
+ | `site.backfillSections` | boolean | `false` |
700
+
701
+ How much of a package reaches the web at all is **not** here — it is
702
+ [`publish.site`](#publish). `site` is framing: where the Hugo tree is
703
+ written, what a section is called, which extra trees are published beside
704
+ the content, which named pass bundle supplies the repository's own body
705
+ rewrites.
706
+
707
+ > ``package-build config: `site` must be a mapping.``
708
+
709
+ > ``package-build config: `site.<key>` is not a recognized option (expected one of: out, base, packages, sections, readmeSections, landing, trees, pass, passOptions, backfillSections).``
710
+
711
+ `site.out` is the output root, resolved by `engine/site-build.mjs`; unset,
712
+ it is refused **at build time** rather than by `defineConfig` (an unset
713
+ value would otherwise resolve to `rootDir` itself, and the tree the build
714
+ wipes on every run would be the working tree):
715
+
716
+ > `site.out is not set, so there is nowhere to write the site. Refusing to continue: the output directory is wiped on every run, and an unset one resolves to the repository root.`
717
+
718
+ `site.packages` names which content packages' notes the site walks, beyond
719
+ this one's own; `site.pass` names a repository's own body-rewrite bundle
720
+ (the one part of the site contract that is code, exactly as `itemBuilders`
721
+ names a registry):
722
+
723
+ > ``package-build config: `site.packages` must be a list.``
724
+
725
+ > ``package-build config: `site.packages[<index>]` must be a non-empty string.``
726
+
727
+ `site.trees` are the extra trees published beside the content, each naming
728
+ where it comes from and which section it publishes into:
729
+
730
+ | Key (under `site.trees[]`) | Type | Required |
731
+ | -------------------------- | ------ | -------- |
732
+ | `site.trees[].from` | string | yes |
733
+ | `site.trees[].section` | string | yes |
734
+
735
+ > ``package-build config: `site.trees` must be a list.``
736
+
737
+ > ``package-build config: `site.trees[<index>]` must be a mapping.``
738
+
739
+ > ``package-build config: `site.trees[<index>].from` must be a non-empty string.``
740
+
741
+ > ``package-build config: `site.trees[<index>].<key>` is not a recognized option (expected one of: from, section).``
742
+
743
+ `site.sections` (and `site.readmeSections`, the same shape) is a closed
744
+ vocabulary — a section's _only_ place to speak, since it exists solely as
745
+ the generated `_index.md` this build writes for it:
746
+
747
+ | Key (under `site.sections.<name>`) | Type | Required | Default |
748
+ | ---------------------------------- | -------------------------- | -------- | ------- |
749
+ | `site.sections.<name>.title` | string | yes | — |
750
+ | `site.sections.<name>.banner` | string | no | none |
751
+ | `site.sections.<name>.description` | string | no | none |
752
+ | `site.sections.<name>.listType` | string, an address segment | no | none |
753
+ | `site.sections.<name>.listSubType` | string, an address segment | no | none |
754
+
755
+ > ``package-build config: `site.sections.<name>` must be a mapping.``
756
+
757
+ > ``package-build config: `site.sections.<name>.title` must be a non-empty string.``
758
+
759
+ `listType` / `listSubType` say what the section **lists** — a content type
760
+ and subType, which are address segments and therefore checked against
761
+ `^[a-z0-9]+$`, deliberately distinct from the section's own name (a URL
762
+ this site chose; `user-guide` is the section, `userguide` the subType):
763
+
764
+ > ``package-build config: `site.sections.<name>.listType` is `Not Ok`, which is not lowercase alphanumeric. It names a content type or subType, and those are address segments (^[a-z0-9]+$) — not the section's own name, which is a URL this site chose and need not match (`user-guide` is the section, `userguide` the subType). A value no page carries selects nothing and leaves the landing empty.``
765
+
766
+ `listSubType` alone, without `listType`, names no query — a subType only
767
+ distinguishes _within_ a type:
768
+
769
+ > ``package-build config: `site.sections.<name>.listSubType` is declared without a `listType`. A subType tells pages apart only within a type — `rules`, `userguide` and `reference` are all `doc` — so on its own it names no query for a layout to run.``
770
+
771
+ Any other key on a section entry is refused:
772
+
773
+ > ``package-build config: `site.sections.<name>.<key>` is not a recognized option (expected one of: title, banner, description, listType, listSubType).``
774
+
775
+ `site.landing` is different from a section entry — it is the mount's own
776
+ landing page frontmatter, passed through verbatim to Hugo rather than
777
+ validated field by field, since its vocabulary is the theme's and not this
778
+ package's:
779
+
780
+ > ``package-build config: `site.landing` must be a mapping.``
781
+
782
+ `site.passOptions` and `site.backfillSections` have no further shape of
783
+ their own — `passOptions` is passed to the resolved `site.pass` bundle
784
+ unchanged, and `backfillSections` is a plain boolean:
785
+
786
+ > ``package-build config: `site.backfillSections` must be a boolean.``
787
+
788
+ ### `pdf`
789
+
790
+ **Type:** object · **Optional** · default `null` (no book is built).
791
+
792
+ The content tree published as a book — a **selection**, not a rendering of
793
+ everything: `pdf.document` names the tree that says which notes the volume
794
+ carries and in what order, parsed by `engine/pdf-toc.mjs`'s
795
+ `parseDocumentTree` and not validated here. Whether a book is built at all
796
+ is [`publish.site`](#publish), the same switch that gates the website —
797
+ `content` builds one, `homepage` does not, so a package cannot end up with
798
+ two switches that disagree about whether it publishes its content tree.
799
+
800
+ ```yaml
801
+ pdf:
802
+ title: The Hârn Ensemble
803
+ document: book.yaml
804
+ fonts:
805
+ serif: Libertinus Serif
806
+ mono: DejaVu Sans Mono
807
+ ```
808
+
809
+ | Key | Type | Required | Default |
810
+ | --------------- | --------------------------- | -------- | ---------------------- |
811
+ | `pdf.title` | string | yes | — |
812
+ | `pdf.document` | string (path) | yes | — |
813
+ | `pdf.subtitle` | string | no | `""` |
814
+ | `pdf.out` | string | no | `""` |
815
+ | `pdf.front` | string[] (markdown files) | no | `[]` |
816
+ | `pdf.fonts` | object | no | all empty |
817
+ | `pdf.iconFonts` | object (family → font file) | no | `{}` |
818
+ | `pdf.binary` | string | no | `""` — found on `PATH` |
819
+
820
+ `pdf.title` and `pdf.document` are required **together** — a document with
821
+ no title produces a file whose name and cover say nothing about what a
822
+ reader downloaded, and a title with no document has nothing to print:
823
+
824
+ > ``package-build config: `pdf.title` must be a non-empty string.``
825
+
826
+ > ``package-build config: `pdf.document` must be a non-empty string.``
827
+
828
+ > ``package-build config: `pdf` must be a mapping.``
829
+
830
+ > ``package-build config: `pdf.front` must be a list of markdown files.``
831
+
832
+ `pdf.fonts` names font **families**, not files — the renderer asks the font
833
+ stack for a family by name:
834
+
835
+ | Key (under `pdf.fonts`) | Type | Required | Default |
836
+ | ----------------------- | ------------- | -------- | ---------------------------------------------------------------- |
837
+ | `pdf.fonts.serif` | string | no | `""` |
838
+ | `pdf.fonts.sans` | string | no | `""` |
839
+ | `pdf.fonts.mono` | string | no | `""` |
840
+ | `pdf.fonts.path` | string (path) | no | `""` — where the renderer may look beyond the system's own fonts |
841
+
842
+ > ``package-build config: `pdf.fonts` must be a mapping.``
843
+
844
+ > ``package-build config: `pdf.fonts.<key>` is not a recognized option (expected one of: serif, sans, mono, path).``
845
+
846
+ `pdf.iconFonts` maps an icon family to the **font file** carrying its
847
+ glyphs — a file rather than a codepoint, since the font's own tables are
848
+ the only trustworthy source of which glyph a name resolves to:
849
+
850
+ > ``package-build config: `pdf.iconFonts` must be a mapping of icon family to font file.``
851
+
852
+ `pdf.binary` names the Typst binary when it is not simply `typst` on
853
+ `PATH` — bundling a native compiler would put a platform-specific artefact
854
+ in the dependency tree of every repository, most of which do not build
855
+ books.
856
+
857
+ Any other key on `pdf` is refused:
858
+
859
+ > ``package-build config: `pdf.<key>` is not a recognized option (expected one of: title, subtitle, document, out, front, fonts, iconFonts, binary).``
860
+
861
+ ### `compatibility`
862
+
863
+ **Type:** object · **Optional** · default `null`. Refused in a
864
+ `documentation` package, which installs into no Foundry data directory and so
865
+ has no Foundry core range to support:
866
+
867
+ > ``package-build config: `compatibility` is refused in a `documentation` package, which installs into no Foundry data directory, so there is no Foundry core range to support.``
868
+
869
+ The **Foundry core** version range this package supports — not to be
870
+ confused with `relationships.systems[].compatibility` or
871
+ `systems.<id>.compatibility`, which are a _game system's_ range. Same key
872
+ name, different subject. `compatibility.minimum` is stamped into every
873
+ compiled document as `_stats.coreVersion`, so a document never claims to
874
+ predate the migrations that would rewrite it; `compatibility.verified`
875
+ names the newest build the full suite has actually passed, never an
876
+ aspiration.
877
+
878
+ ```yaml
879
+ compatibility: { minimum: "14.359", verified: "14.364" }
880
+ ```
881
+
882
+ | Key | Type | Required | Default |
883
+ | ------------------------ | ------ | -------------------------------------- | ------- |
884
+ | `compatibility.minimum` | string | yes, once `compatibility:` is declared | — |
885
+ | `compatibility.verified` | string | no | none |
886
+
887
+ > ``package-build config: `compatibility` must be a mapping.``
888
+
889
+ > ``package-build config: `compatibility.minimum` must be a non-empty string.``
890
+
891
+ > ``package-build config: `compatibility.<key>` is not a recognized option (expected one of: minimum, verified).``
892
+
893
+ `compatibility` itself is optional at this validation layer — absent for a
894
+ content-only consumer, which has none to invent — but a repository that
895
+ compiles any pack needs one in practice: reading the floor throws, at
896
+ compile time rather than at configuration time, without it:
897
+
898
+ > ``package-build: the configuration declares no `compatibility.minimum`, so compiled documents have no honest core version to stamp. Declare it at the top level of package-build.config.yaml.``
899
+
900
+ ### `relationships`
901
+
902
+ **Type:** object · **Optional** · default `{}`. Refused in a `documentation`
903
+ package, which is not a Foundry package and so stands in no relationship to
904
+ one:
905
+
906
+ > ``package-build config: `relationships` is refused in a `documentation` package, which is not a Foundry package, so it stands in no relationship to one.``
907
+
908
+ What this package declares about other packages, in Foundry's own shape.
909
+ Passed through to the shipped manifest, and read here for one derivation: a
910
+ module's `_stats.systemVersion` can come from the `verified` field of the
911
+ system relationship it declares — see [`stats.systemVersion`](#statssystemversion).
912
+
913
+ | Key | Type | Required |
914
+ | -------------------------- | ----- | -------- |
915
+ | `relationships.systems` | array | no |
916
+ | `relationships.requires` | array | no |
917
+ | `relationships.recommends` | array | no |
918
+ | `relationships.conflicts` | array | no |
919
+
920
+ > ``package-build config: `relationships` must be a mapping.``
921
+
922
+ > ``package-build config: `relationships.<kind>` must be a list.``
923
+
924
+ > ``package-build config: `relationships.<kind>` is not a recognized option (expected one of: systems, requires, recommends, conflicts).``
925
+
926
+ Each entry, in any of the four lists:
927
+
928
+ | Key (under `relationships.<kind>[]`) | Type | Required | Default |
929
+ | --------------------------------------- | ------------------------------- | -------- | ------- |
930
+ | `relationships.systems[].id` | string | yes | — |
931
+ | `relationships.systems[].type` | string | no | none |
932
+ | `relationships.systems[].manifest` | string | no | none |
933
+ | `relationships.systems[].compatibility` | object, `{minimum?, verified?}` | no | none |
934
+ | `relationships.systems[].itemCatalog` | boolean | no | `false` |
935
+
936
+ (the same four keys apply under `requires[]`, `recommends[]` and
937
+ `conflicts[]`.)
938
+
939
+ > ``package-build config: `relationships.<kind>[<index>]` must be a mapping.``
940
+
941
+ > ``package-build config: `relationships.<kind>[<index>].id` must be a non-empty string.``
942
+
943
+ > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, type, manifest, compatibility, itemCatalog).``
944
+
945
+ `itemCatalog` opts into extracting the named package's Item packs so the
946
+ actors pass can resolve embedded items this repository does not hold — off
947
+ by default, since depending on a package is not the same as needing its
948
+ item catalogue at build time. It requires a `manifest`:
949
+
950
+ > ``package-build config: `relationships.<kind>[<index>].itemCatalog` must be true or false.``
951
+
952
+ > ``package-build config: `relationships.<kind>[<index>].itemCatalog` needs a `manifest` naming the package to fetch.``
953
+
954
+ ### `systems`
955
+
956
+ **Type:** object (`{id: spec}`) · **Optional** · default `{}`. Refused in a
957
+ `documentation` package, which compiles no documents and so ships content for
958
+ no game system:
959
+
960
+ > ``package-build config: `systems` is refused in a `documentation` package, which compiles no documents, so it ships content for no game system.``
961
+
962
+ The systems this package can stamp content against — **declaration only**,
963
+ not a restriction. Declaring a system here does not narrow which worlds can
964
+ load the package; only [`requiresSystem`](#requiressystem) does that. A
965
+ repository shipping content for two systems declares both here; a system
966
+ package needs no entry, since it is its own system by construction.
967
+
968
+ ```yaml
969
+ systems:
970
+ sohl:
971
+ compatibility: { verified: "1.6.3" }
972
+ ```
973
+
974
+ | Key (under `systems.<id>`) | Type | Required | Default |
975
+ | ------------------------------------- | ------ | -------- | ------- |
976
+ | `systems.<id>.manifest` | string | no | `null` |
977
+ | `systems.<id>.compatibility` | object | yes | — |
978
+ | `systems.<id>.compatibility.minimum` | string | no | `null` |
979
+ | `systems.<id>.compatibility.verified` | string | yes | — |
980
+
981
+ `compatibility.verified` is required here — unlike the top-level
982
+ `compatibility` block, where `minimum` is the required half — because this
983
+ is the value a pack actually **stamps**; a declaration that cannot answer
984
+ "which version was this built against" is the gap `systems:` exists to
985
+ close.
986
+
987
+ > ``package-build config: `systems` must be a mapping of id to spec.``
988
+
989
+ > ``package-build config: `systems` declares an empty system id.``
990
+
991
+ > ``package-build config: `systems.<id>` must be a mapping.``
992
+
993
+ > ``package-build config: `systems.<id>.compatibility` must be a mapping.``
994
+
995
+ > ``package-build config: `systems.<id>.compatibility.verified` must be a non-empty string.``
996
+
997
+ > ``package-build config: `systems.<id>.<key>` is not a recognized option (expected one of: manifest, compatibility).``
998
+
999
+ > ``package-build config: `systems.<id>.compatibility.<key>` is not a recognized option (expected one of: minimum, verified).``
1000
+
1001
+ ### `requiresSystem`
1002
+
1003
+ **Type:** string · **Optional** · default `null`. Refused in a
1004
+ `documentation` package, which compiles no documents and so has no game
1005
+ system to gate its packs on:
1006
+
1007
+ > ``package-build config: `requiresSystem` is refused in a `documentation` package, which compiles no documents, so there is no game system to gate its packs on.``
1008
+
1009
+ The one system this package refuses to load without — the **gate** half of
1010
+ the systems split. Naming one here emits `relationships.systems` for it,
1011
+ which Foundry's `supportsSystem` reads, making the package unavailable
1012
+ under any other system. Omitted, no relationship is emitted and each pack
1013
+ stamps whatever its own `system:` names.
1014
+
1015
+ It must name a system `systems:` actually declares. With no `systems:`
1016
+ block at all:
1017
+
1018
+ > ``package-build config: `requiresSystem` names `sohl`, which `systems:` does not declare — the `systems:` block is empty or absent.``
1019
+
1020
+ With one, naming what it does declare instead:
1021
+
1022
+ > ``package-build config: `requiresSystem` names `<name>`, which `systems:` does not declare. Declared: <list>.``
1023
+
1024
+ > ``package-build config: `requiresSystem` must be a non-empty string.``
1025
+
1026
+ ### `packageBuild`
1027
+
1028
+ Reserved for `@heroiclands/package-build`'s own packaging half — see
1029
+ [The `packageBuild` section](#the-packagebuild-section) below for its full
1030
+ key-by-key reference. `content-config.mjs` checks only that the value is a
1031
+ mapping:
1032
+
1033
+ > ``package-build config: `packageBuild` must be a mapping — it is the section @heroiclands/package-build reads, and that package validates what is inside it.``
1034
+
1035
+ ### `publish`
1036
+
1037
+ **Type:** object · **Optional** for a `systems` or `modules` package, default
1038
+ `{site: "homepage", address: {prefix: ""}}`. **Required** for a
1039
+ `documentation` package, with `site: content` — publishing the content tree is
1040
+ the whole of what that kind does:
1041
+
1042
+ > ``package-build config: `publish` is required in a `documentation` package: publishing the content tree is the whole of what it does. Write `publish: {site: content}`.``
1043
+
1044
+ > ``package-build config: `publish.site` must be `content` in a `documentation` package — `homepage` fences the content surfaces off, and a package that compiles nothing and publishes nothing from its tree would produce a single authored page and no book.``
1045
+
1046
+ Publishing switches — how much of this package reaches the web, and where
1047
+ its content tree's addresses mount inside the package.
1048
+
1049
+ | Key | Type | Required | Default |
1050
+ | ------------------------ | --------------------------- | -------- | -------------- |
1051
+ | `publish.site` | `"homepage"` \| `"content"` | no | `"homepage"` |
1052
+ | `publish.address` | object | no | `{prefix: ""}` |
1053
+ | `publish.address.prefix` | string | no | `""` |
1054
+
1055
+ > ``package-build config: `publish` must be an object.``
1056
+
1057
+ > ``package-build config: `publish.<key>` is not a recognized option (expected one of: site, address).``
1058
+
1059
+ Every HeroicLands package publishes at least an authored homepage at
1060
+ `https://www.heroiclands.org/<contentPackage>/` — there is no value meaning
1061
+ _no web presence at all_. `homepage` is the floor: the authored homepage
1062
+ and nothing else, no content-tree walk, no `site.sections` / `site.trees` /
1063
+ `site.landing` output. `content` is the homepage plus every page the
1064
+ content tree publishes. `publishesContentPages(config)`, exported from
1065
+ `content-config.mjs` alongside [`compilesFoundryDocuments`](#packagekind),
1066
+ answers the one question every reader of the mode actually asks — the site
1067
+ build, to decide whether to walk the tree at all, and the content index, to
1068
+ decide whether an entry carries a web `path`. It returns
1069
+ `config.publish.site === "content"`.
1070
+
1071
+ This was a boolean before `5.0.0`, and both spellings are refused rather
1072
+ than silently mapped, because a value reinterpreted reads to its author as
1073
+ though it still means what it said:
1074
+
1075
+ > ``package-build config: `publish.site` is no longer a boolean — write `site: content`. Every package publishes an authored homepage at /<contentPackage>/, so no value means "no web presence": `homepage` publishes that page and nothing else, and `content` publishes it plus every page the content tree compiles to.``
1076
+
1077
+ > ``package-build config: `publish.site` must be one of homepage, content (got "public").``
1078
+
1079
+ `publish.address.prefix` is where the content tree mounts _inside the
1080
+ package_ — `"kb/"` for a repository whose knowledgebase is one surface
1081
+ among several, `""` for one whose site is nothing but its content. It must
1082
+ end in a slash when set (a missing one would silently fuse the prefix to
1083
+ the first section) and must not begin with one (which would make the
1084
+ recorded address package-absolute):
1085
+
1086
+ > ``package-build config: `publish.address` must be an object.``
1087
+
1088
+ > ``package-build config: `publish.address.prefix` must end in a slash when it is set.``
1089
+
1090
+ > ``package-build config: `publish.address.prefix` must not begin with a slash.``
1091
+
1092
+ `publish.address.landing` is **retired** — it named which note addressed a
1093
+ whole section rather than a page within one, and there are no sections to
1094
+ address:
1095
+
1096
+ > ``package-build config: `publish.address.landing` is a retired option — delete it. It named which note addressed a whole section rather than a page within one, and there are no sections to address: a section is a Hugo content directory the note format does not carry, so no note lands one and every page is addressed `<type>-<shortcode>`. Nothing replaces it.``
1097
+
1098
+ Any other key under `publish.address` is refused:
1099
+
1100
+ > ``package-build config: `publish.address.<key>` is not a recognized option (expected one of: prefix).``
1101
+
1102
+ ---
1103
+
1104
+ ## The `packageBuild` section
1105
+
1106
+ Validated by `resolvePackageBuildConfig` in `config.mjs`, not by
1107
+ `content-config.mjs`. The two halves split by **input** — the content half
1108
+ reads the content tree, this one reads `lang/`, `styles/`, `src/`, the
1109
+ assets and the manifest template — and neither validates the other's keys.
1110
+ `packageBuild` is an ordinary section of the one configuration file; the
1111
+ split exists only to stop one key being checked twice against two
1112
+ disagreeing ideas of what it means.
1113
+
1114
+ ```yaml
1115
+ packageBuild:
1116
+ assets:
1117
+ - { from: lang, to: lang }
1118
+ - { from: assets/icons, to: assets/icons }
1119
+ assetTransform: ./utils/svg-theme.mjs
1120
+ stageDir: build/stage
1121
+ clean:
1122
+ extra: [site/content, site/public]
1123
+ lang:
1124
+ sources: lang/*.json
1125
+ deploy:
1126
+ envPrefix: SOHL
1127
+ ```
1128
+
1129
+ | Key | Type | Required | Default |
1130
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------- | -------- | --------------------------------- |
1131
+ | [`packageBuild.stageDir`](#packagebuildstagedir-and-packagebuildassets) | string | no | `build/stage` |
1132
+ | [`packageBuild.assets`](#packagebuildstagedir-and-packagebuildassets) | array | no | `[]` |
1133
+ | [`packageBuild.assetTransform`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
1134
+ | [`packageBuild.manifest`](#packagebuildmanifest) | object, pass-through | no | `{}` |
1135
+ | [`packageBuild.manifestFlags`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
1136
+ | [`packageBuild.schema`](#packagebuildschema) | object (`{documentType: {from, registry}}`) | no | `[]` |
1137
+ | [`packageBuild.clean`](#packagebuildclean) | object | no | `{extra: []}` |
1138
+ | [`packageBuild.lang`](#packagebuildlang) | object | no | see below |
1139
+ | [`packageBuild.deploy`](#packagebuilddeploy) | object | no | `{envPrefix: "SOHL"}` |
1140
+ | [`packageBuild.release`](#packagebuildrelease) | object | no | `{artifact: <from packageKind>}` |
1141
+ | [`packageBuild.bundle`](#packagebuildbundle) | object | no | `{entry: "<foundryPackage>.mjs"}` |
1142
+ | [`packageBuild.container`](#packagebuildcontainer) | object | no | see below |
1143
+ | [`packageBuild.e2e`](#packagebuilde2e) | object | no | see below |
1144
+
1145
+ > ``package-build config: `packageBuild.<key>` is not a recognised key (expected one of: stageDir, assets, assetTransform, manifest, manifestFlags, schema, clean, lang, deploy, release, bundle, container, e2e).``
1146
+
1147
+ ### `packageBuild.stageDir` and `packageBuild.assets`
1148
+
1149
+ `stageDir` is where the package is assembled before it is zipped or
1150
+ deployed — every asset destination is relative to it, so a repository's
1151
+ table says `lang`, not `build/stage/lang`.
1152
+
1153
+ `assets` is the table of staging copies: source path in the repository,
1154
+ destination under the staged package root.
1155
+
1156
+ | Key | Type | Required |
1157
+ | ---------------------------- | ------ | -------- |
1158
+ | `packageBuild.assets[].from` | string | yes |
1159
+ | `packageBuild.assets[].to` | string | yes |
1160
+
1161
+ > ``package-build config: `packageBuild.stageDir` must be a non-empty string.``
1162
+
1163
+ > ``package-build config: `packageBuild.assets` must be a list.``
1164
+
1165
+ > ``package-build config: `packageBuild.assets[<index>]` must be a mapping.``
1166
+
1167
+ > ``package-build config: `packageBuild.assets[<index>].from` must be a non-empty string.``
1168
+
1169
+ > ``package-build config: `packageBuild.assets[<index>].<key>` is not a recognised key (expected one of: from, to).``
1170
+
1171
+ ### `packageBuild.assetTransform` and `packageBuild.manifestFlags`
1172
+
1173
+ Both name a module, resolved against `rootDir`: `assetTransform` exports a
1174
+ `transform` a repository needs beyond verbatim asset copying, and
1175
+ `manifestFlags` exports a `flags` function for namespaced manifest flags a
1176
+ repository has to compute. Neither declares anything, both default to
1177
+ `null`:
1178
+
1179
+ > ``package-build config: `packageBuild.assetTransform` must be a non-empty string.``
1180
+
1181
+ > ``package-build config: `packageBuild.manifestFlags` must be a non-empty string.``
1182
+
1183
+ ### `packageBuild.manifest`
1184
+
1185
+ Everything declared here is emitted into the generated manifest unchanged —
1186
+ **deliberately not key-checked**, so a key Foundry adds in a later version
1187
+ can be declared without waiting on a release of this package. The one rule
1188
+ that has a wrong answer rather than an unknown one: a key the build
1189
+ **derives** must not also be authored, since the authored value would be
1190
+ silently overwritten and the two would be free to disagree with nothing to
1191
+ say so.
1192
+
1193
+ | Forbidden key | Derived from |
1194
+ | ------------------------------------- | ---------------------------------------------------------------- |
1195
+ | `packageBuild.manifest.id` | `foundryPackage`, itself derived from `package.json` `name` |
1196
+ | `packageBuild.manifest.version` | `package.json` `version` |
1197
+ | `packageBuild.manifest.url` | `package.json` `repository` |
1198
+ | `packageBuild.manifest.bugs` | `package.json` `repository` |
1199
+ | `packageBuild.manifest.manifest` | `package.json` `repository` and the release tag |
1200
+ | `packageBuild.manifest.download` | `package.json` `repository` and the release tag |
1201
+ | `packageBuild.manifest.compatibility` | the top level of `package-build.config.yaml` |
1202
+ | `packageBuild.manifest.relationships` | the top level of `package-build.config.yaml` |
1203
+ | `packageBuild.manifest.packs` | the `packs` list at the top level of `package-build.config.yaml` |
1204
+
1205
+ > ``package-build config: `packageBuild.manifest.version` is derived from package.json `version` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
1206
+
1207
+ > ``package-build config: `packageBuild.manifest` must be a mapping.``
1208
+
1209
+ ### `packageBuild.schema`
1210
+
1211
+ Registers the DataModel bindings `package-build schema` reads. The document
1212
+ type is the key, so a package that models only Items declares only that
1213
+ entry:
1214
+
1215
+ ```yaml
1216
+ packageBuild:
1217
+ schema:
1218
+ Item: { from: module/data/item-models.mjs, registry: ITEM_DM_DEF }
1219
+ ```
1220
+
1221
+ | Key (under `packageBuild.schema.<DocumentType>`) | Type | Required |
1222
+ | ------------------------------------------------ | ----------------------------- | -------- |
1223
+ | `packageBuild.schema.<DocumentType>.from` | string (path to a module) | yes |
1224
+ | `packageBuild.schema.<DocumentType>.registry` | string (the exported binding) | yes |
1225
+
1226
+ > ``package-build config: `packageBuild.schema` must be a mapping.``
1227
+
1228
+ > ``package-build config: `packageBuild.schema.<DocumentType>` must be a mapping.``
1229
+
1230
+ > ``package-build config: `packageBuild.schema.<DocumentType>.from` must be a non-empty string.``
1231
+
1232
+ ### `packageBuild.clean`
1233
+
1234
+ | Key | Type | Required | Default |
1235
+ | -------------------------- | -------- | -------- | ------- |
1236
+ | `packageBuild.clean.extra` | string[] | no | `[]` |
1237
+
1238
+ Directories to remove beyond the conventional build artifacts.
1239
+
1240
+ > ``package-build config: `packageBuild.clean` must be a mapping.``
1241
+
1242
+ > ``package-build config: `packageBuild.clean.extra` must be a list.``
1243
+
1244
+ > ``package-build config: `packageBuild.clean.extra[<index>]` must be a non-empty string.``
1245
+
1246
+ ### `packageBuild.lang`
1247
+
1248
+ Localization coverage settings, read by the `lang` checks.
1249
+
1250
+ | Key | Type | Required | Default |
1251
+ | ------------------------------ | ---------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
1252
+ | `packageBuild.lang.sources` | string or string[] (glob) | no | `lang/*.json` |
1253
+ | `packageBuild.lang.help` | string | no | `null` — extra guidance printed after a failure |
1254
+ | `packageBuild.lang.primary` | string | no | `lang/en.json` — the file coverage is measured against |
1255
+ | `packageBuild.lang.scripts` | string or string[] (glob) | no | `src/**/*.{ts,mjs}` |
1256
+ | `packageBuild.lang.templates` | string or string[] (glob) | no | `templates/**/*.hbs` |
1257
+ | `packageBuild.lang.keyRoots` | string or string[] (glob) | no | `null` — derived from the primary file's own keys unless a repository references a root the file does not yet declare |
1258
+ | `packageBuild.lang.references` | string (path to a module) | no | `null` — a module exporting a `references` function |
1259
+ | `packageBuild.lang.retained` | array of `{prefix, reason}` | no | `[]` — key prefixes exempt from the unreferenced advisory |
1260
+ | `packageBuild.lang.allow` | array of `{literal, reason}` | no | `[]` — template literals that are deliberately not localization keys |
1261
+
1262
+ > ``package-build config: `packageBuild.lang` must be a mapping.``
1263
+
1264
+ Each `retained` / `allow` entry states its own reason, which is what keeps
1265
+ the escape hatch from becoming a place unexplained exceptions accumulate:
1266
+
1267
+ > ``package-build config: `packageBuild.lang.retained[<index>]` must be a mapping.``
1268
+
1269
+ > ``package-build config: `packageBuild.lang.retained[<index>].reason` must be a non-empty string.``
1270
+
1271
+ > ``package-build config: `packageBuild.lang.retained[<index>].<key>` is not a recognised key (expected one of: prefix, reason).``
1272
+
1273
+ ### `packageBuild.deploy`
1274
+
1275
+ | Key | Type | Required | Default |
1276
+ | ------------------------------- | ------ | -------- | ------- |
1277
+ | `packageBuild.deploy.envPrefix` | string | no | `SOHL` |
1278
+
1279
+ Prefix of the deploy environment variables.
1280
+
1281
+ > ``package-build config: `packageBuild.deploy` must be a mapping.``
1282
+
1283
+ > ``package-build config: `packageBuild.deploy.envPrefix` must be a non-empty string.``
1284
+
1285
+ ### `packageBuild.release`
1286
+
1287
+ | Key | Type | Required | Default |
1288
+ | ------------------------------- | ------ | -------- | ------------------------------------------------------------------------ |
1289
+ | `packageBuild.release.artifact` | string | no | `system` for `packageKind: systems`, `module` for `packageKind: modules` |
1290
+
1291
+ Foundry installs a system from `system.json` and a module from
1292
+ `module.json`; the kind already decides which, so this is stated only when
1293
+ a repository genuinely needs the other answer.
1294
+
1295
+ > ``package-build config: `packageBuild.release` must be a mapping.``
1296
+
1297
+ > ``package-build config: `packageBuild.release.artifact` must be a non-empty string.``
1298
+
1299
+ ### `packageBuild.bundle`
1300
+
1301
+ | Key | Type | Required | Default |
1302
+ | --------------------------- | ------ | -------- | ---------------------- |
1303
+ | `packageBuild.bundle.entry` | string | no | `<foundryPackage>.mjs` |
1304
+
1305
+ The bundle file Foundry loads, as the manifest spells it. Named after the
1306
+ package by convention; stated only when a repository's bundler emits
1307
+ something else. Deliberately not read back out of the generated manifest —
1308
+ that would let the check agree with itself by construction.
1309
+
1310
+ > ``package-build config: `packageBuild.bundle` must be a mapping.``
1311
+
1312
+ > ``package-build config: `packageBuild.bundle.entry` must be a non-empty string.``
1313
+
1314
+ ### `packageBuild.container`
1315
+
1316
+ | Key | Type | Required | Default |
1317
+ | ------------------------------- | ------------------------------------------------- | -------- | --------------------------------- |
1318
+ | `packageBuild.container.image` | string | no | `null` — override for every stage |
1319
+ | `packageBuild.container.name` | string, matching `/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/` | no | `null` — named after the package |
1320
+ | `packageBuild.container.stages` | object (`{stage: spec}`) | no | `{}` |
1321
+
1322
+ `name` is checked against what `docker run` accepts, since the stage is
1323
+ appended to it and a rejected name would otherwise surface as a failure to
1324
+ create a container whose name was never written down:
1325
+
1326
+ > ``package-build config: `packageBuild.container.name` must be a container name docker accepts — a letter or digit, then letters, digits, underscores, periods or hyphens.``
1327
+
1328
+ > ``package-build config: `packageBuild.container` must be a mapping.``
1329
+
1330
+ > ``package-build config: `packageBuild.container.stages` must be a mapping.``
1331
+
1332
+ `packageBuild.container.stages.<name>` is for a stage that is genuinely
1333
+ one repository's own — an older Foundry serving a previous generation of
1334
+ the package. The four every HeroicLands package deploys to (dev, qa, prod,
1335
+ test) need no entry:
1336
+
1337
+ | Key (under `packageBuild.container.stages.<name>`) | Type | Required | Default |
1338
+ | -------------------------------------------------- | ----------------------------------- | -------- | ------- |
1339
+ | `packageBuild.container.stages.<name>.port` | number | no | `null` |
1340
+ | `packageBuild.container.stages.<name>.world` | string (`""` forces no auto-launch) | no | `null` |
1341
+ | `packageBuild.container.stages.<name>.version` | string | no | `null` |
1342
+
1343
+ > ``package-build config: `packageBuild.container.stages.<name>` must be a mapping.``
1344
+
1345
+ > ``package-build config: `packageBuild.container.stages.<name>.port` must be a number.``
1346
+
1347
+ > ``package-build config: `packageBuild.container.stages.<name>.world` must be a string ("" forces no auto-launch).``
1348
+
1349
+ ### `packageBuild.e2e`
1350
+
1351
+ The Cypress suite a repository runs against the served world — the one
1352
+ thing the harness does not own, since what runs against a standing world is
1353
+ entirely the repository's.
1354
+
1355
+ | Key | Type | Required | Default |
1356
+ | ---------------------------- | -------------------------------------- | -------- | ------- |
1357
+ | `packageBuild.e2e.stage` | string | no | `test` |
1358
+ | `packageBuild.e2e.suite` | object | no | `null` |
1359
+ | `packageBuild.e2e.results` | string or string[] (glob) | no | `[]` |
1360
+ | `packageBuild.e2e.build` | object (`{target: spec}`) | no | `{}` |
1361
+ | `packageBuild.e2e.world` | object | no | `{}` |
1362
+ | `packageBuild.e2e.gm` | object | no | `{}` |
1363
+ | `packageBuild.e2e.documents` | object (collection → source directory) | no | `{}` |
1364
+
1365
+ > ``package-build config: `packageBuild.e2e` must be a mapping.``
1366
+
1367
+ `packageBuild.e2e.suite`:
1368
+
1369
+ | Key | Type | Required |
1370
+ | ----------------------------- | -------------------------------- | -------- |
1371
+ | `packageBuild.e2e.suite.run` | string[] (program and arguments) | yes |
1372
+ | `packageBuild.e2e.suite.open` | string[] | no |
1373
+
1374
+ > ``package-build config: `packageBuild.e2e.suite` must be a mapping.``
1375
+
1376
+ > ``package-build config: `packageBuild.e2e.suite.run` must be a non-empty list naming a program to run.``
1377
+
1378
+ `packageBuild.e2e.build.<name>` — a bare string is the script name; the
1379
+ mapping form adds `recreate`, for a target that writes something Foundry
1380
+ reads only at world launch (the manifest), where deploying it into a
1381
+ running world deploys a file nothing will look at:
1382
+
1383
+ | Key | Type | Required | Default |
1384
+ | ---------------------------------------- | ------- | ------------------ | ------- |
1385
+ | `packageBuild.e2e.build.<name>.script` | string | yes (mapping form) | — |
1386
+ | `packageBuild.e2e.build.<name>.recreate` | boolean | no | `false` |
1387
+
1388
+ > ``package-build config: `packageBuild.e2e.build.<name>` must be a script name or a mapping.``
1389
+
1390
+ > ``package-build config: `packageBuild.e2e.build.<name>.recreate` must be a boolean.``
1391
+
1392
+ `packageBuild.e2e.world` — declared world identity:
1393
+
1394
+ | Key | Type |
1395
+ | ------------------------------------ | ------ |
1396
+ | `packageBuild.e2e.world.id` | string |
1397
+ | `packageBuild.e2e.world.title` | string |
1398
+ | `packageBuild.e2e.world.description` | string |
1399
+
1400
+ `packageBuild.e2e.gm` — declared GM credentials:
1401
+
1402
+ | Key | Type |
1403
+ | ------------------------------ | ------ |
1404
+ | `packageBuild.e2e.gm.name` | string |
1405
+ | `packageBuild.e2e.gm.password` | string |
1406
+
1407
+ Every value in both mappings must be a non-empty string:
1408
+
1409
+ > ``package-build config: `packageBuild.e2e.world.<key>` must be a non-empty string.``
1410
+
1411
+ `packageBuild.e2e.documents` is a mapping too, but an **open** one — unlike
1412
+ `world` and `gm`, it declares no fixed key list, since a collection name is
1413
+ the repository's own. Every value must still be a non-empty string, naming
1414
+ the source directory:
1415
+
1416
+ > ``package-build config: `packageBuild.e2e.documents.<key>` must be a non-empty string.``
1417
+
1418
+ ---
1419
+
1420
+ ## Every retired or forbidden key, in one place
1421
+
1422
+ | Key | Why |
1423
+ | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1424
+ | `publish.address.landing` | Retired — named a whole-section landing, and there are no sections to address. |
1425
+ | `packs[].folders` | Retired — a folder is a note (`type: folder`), materialised by the pack whose documents reference it. |
1426
+ | `rootDir` | Forbidden in a YAML configuration — always the file's own directory. |
1427
+ | `foundryPackage` | Forbidden in a YAML configuration — always the adjacent `package.json` `name`. |
1428
+ | `stats.systemId` | Forbidden in every configuration — derived from `packageKind`, `requiresSystem` or a lone declared system. |
1429
+ | `stats.systemVersion` | Forbidden in every configuration — derived from `package.json` (a system) or `systems:` / `relationships.systems` (a module). |
1430
+ | `packageBuild.manifest.id`, `.version`, `.url`, `.bugs`, `.manifest`, `.download`, `.compatibility`, `.relationships`, `.packs` | Forbidden — each is derived from `package.json` or the top level of `package-build.config.yaml`; see [`packageBuild.manifest`](#packagebuildmanifest). |
1431
+ | `publish.site: true` / `publish.site: false` | Refused rather than mapped — write `homepage` or `content`. |
1432
+ | `packs`, `itemBuilders`, `docs`, `compatibility`, `relationships`, `systems`, `requiresSystem`, `stats`, `foundryPackage` | Forbidden in a `documentation` package — each describes a Foundry package this kind is not; see the key's own section for its located refusal message. |